Support AWS secret manager as secret store (#142)

* Initial commit

* Initial commit

* Update Readme.md

* Update godoc comment

* Added session token

* Incorporate review comment

* Added the header

* Corrected godoc
This commit is contained in:
sayboras 2019-11-27 02:36:11 +11:00 committed by Yaron Schneider
parent 08362d9e6b
commit eabe18a79f
4 changed files with 295 additions and 0 deletions

View File

@ -6,6 +6,7 @@ Currently supported secret stores are:
* Kubernetes
* Azure KeyVault
* AWS Secret manager
## Implementing a new Secret Store

View File

@ -0,0 +1,113 @@
// ------------------------------------------------------------
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
// ------------------------------------------------------------
package secretmanager
import (
"encoding/json"
"fmt"
"github.com/aws/aws-sdk-go/service/secretsmanager"
"github.com/aws/aws-sdk-go/service/secretsmanager/secretsmanageriface"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/dapr/components-contrib/secretstores"
)
const (
VersionID = "VersionID"
VersionStage = "VersionStage"
)
// NewSecretManager returns a new secret manager store
func NewSecretManager() secretstores.SecretStore {
return &smSecretStore{}
}
type secretManagerMetaData struct {
Region string `json:"region"`
AccessKey string `json:"accessKey"`
SecretKey string `json:"secretKey"`
SessionToken string `json:"sessionToken"`
}
type smSecretStore struct {
client secretsmanageriface.SecretsManagerAPI
}
// Init creates a AWS secret manager client
func (s *smSecretStore) Init(metadata secretstores.Metadata) error {
meta, err := s.getSecretManagerMetadata(metadata)
if err != nil {
return err
}
client, err := s.getClient(meta)
if err != nil {
return err
}
s.client = client
return nil
}
// GetSecret retrieves a secret using a key and returns a map of decrypted string/string values
func (s *smSecretStore) GetSecret(req secretstores.GetSecretRequest) (secretstores.GetSecretResponse, error) {
var versionID *string
if value, ok := req.Metadata[VersionID]; ok {
versionID = &value
}
var versionStage *string
if value, ok := req.Metadata[VersionStage]; ok {
versionStage = &value
}
output, err := s.client.GetSecretValue(&secretsmanager.GetSecretValueInput{
SecretId: &req.Name,
VersionId: versionID,
VersionStage: versionStage,
})
if err != nil {
return secretstores.GetSecretResponse{Data: nil}, fmt.Errorf("couldn't get secret: %s", err)
}
resp := secretstores.GetSecretResponse{
Data: map[string]string{},
}
if output.Name != nil && output.SecretString != nil {
resp.Data[*output.Name] = *output.SecretString
}
return resp, nil
}
func (s *smSecretStore) getClient(metadata *secretManagerMetaData) (*secretsmanager.SecretsManager, error) {
sess, err := session.NewSession(aws.NewConfig().
WithRegion(*aws.String(metadata.Region)).
WithCredentials(credentials.NewStaticCredentials(metadata.AccessKey, metadata.SecretKey, metadata.SessionToken)))
if err != nil {
return nil, err
}
return secretsmanager.New(sess), nil
}
func (s *smSecretStore) getSecretManagerMetadata(spec secretstores.Metadata) (*secretManagerMetaData, error) {
b, err := json.Marshal(spec.Properties)
if err != nil {
return nil, err
}
var meta secretManagerMetaData
err = json.Unmarshal(b, &meta)
if err != nil {
return nil, err
}
if meta.SecretKey == "" || meta.AccessKey == "" || meta.Region == "" || meta.SessionToken == "" {
return nil, fmt.Errorf("missing aws credentials in metadata")
}
return &meta, nil
}

View File

@ -0,0 +1,36 @@
// ------------------------------------------------------------
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
// ------------------------------------------------------------
// +build integration
package secretmanager
import (
"github.com/dapr/components-contrib/secretstores"
"github.com/stretchr/testify/assert"
"os"
"testing"
)
// TestIntegrationGetSecret requires AWS specific environments for authentication AWS_DEFAULT_REGION AWS_ACCESS_KEY_ID,
// AWS_SECRET_ACCESS_KkEY and AWS_SESSION_TOKEN
func TestIntegrationGetSecret(t *testing.T) {
secretName := "/aws/secret/testing"
sm := NewSecretManager()
err := sm.Init(secretstores.Metadata{
Properties: map[string]string{
"Region": os.Getenv("AWS_DEFAULT_REGION"),
"AccessKey": os.Getenv("AWS_ACCESS_KEY_ID"),
"SecretKey": os.Getenv("AWS_SECRET_ACCESS_KEY"),
"SessionToken": os.Getenv("AWS_SESSION_TOKEN"),
},
})
assert.Nil(t, err)
response, err := sm.GetSecret(secretstores.GetSecretRequest{
Name: secretName,
Metadata: map[string]string{},
})
assert.Nil(t, err)
assert.NotNil(t, response)
}

View File

@ -0,0 +1,145 @@
// ------------------------------------------------------------
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
// ------------------------------------------------------------
package secretmanager
import (
"fmt"
"testing"
"github.com/aws/aws-sdk-go/service/secretsmanager"
"github.com/aws/aws-sdk-go/service/secretsmanager/secretsmanageriface"
"github.com/dapr/components-contrib/secretstores"
"github.com/stretchr/testify/assert"
)
const secretValue = "secret"
type mockedSM struct {
GetSecretValueFn func(*secretsmanager.GetSecretValueInput) (*secretsmanager.GetSecretValueOutput, error)
secretsmanageriface.SecretsManagerAPI
}
func (m *mockedSM) GetSecretValue(input *secretsmanager.GetSecretValueInput) (*secretsmanager.GetSecretValueOutput, error) {
return m.GetSecretValueFn(input)
}
func TestInit(t *testing.T) {
m := secretstores.Metadata{}
s := NewSecretManager()
t.Run("Init with valid metadata", func(t *testing.T) {
m.Properties = map[string]string{
"AccessKey": "a",
"Region": "a",
"SecretKey": "a",
"SessionToken": "a",
}
err := s.Init(m)
assert.Nil(t, err)
})
t.Run("Init with missing metadata", func(t *testing.T) {
m.Properties = map[string]string{
"Dummy": "a",
}
err := s.Init(m)
assert.NotNil(t, err)
assert.Equal(t, err, fmt.Errorf("missing aws credentials in metadata"))
})
}
func TestGetSecret(t *testing.T) {
t.Run("successfully retrieve secret", func(t *testing.T) {
t.Run("without version id and version stage", func(t *testing.T) {
s := smSecretStore{
client: &mockedSM{
GetSecretValueFn: func(input *secretsmanager.GetSecretValueInput) (*secretsmanager.GetSecretValueOutput, error) {
assert.Nil(t, input.VersionId)
assert.Nil(t, input.VersionStage)
secret := secretValue
return &secretsmanager.GetSecretValueOutput{
Name: input.SecretId,
SecretString: &secret,
}, nil
},
},
}
req := secretstores.GetSecretRequest{
Name: "/aws/secret/testing",
Metadata: map[string]string{},
}
output, e := s.GetSecret(req)
assert.Nil(t, e)
assert.Equal(t, "secret", output.Data[req.Name])
})
t.Run("with version id", func(t *testing.T) {
s := smSecretStore{
client: &mockedSM{
GetSecretValueFn: func(input *secretsmanager.GetSecretValueInput) (*secretsmanager.GetSecretValueOutput, error) {
assert.NotNil(t, input.VersionId)
secret := secretValue
return &secretsmanager.GetSecretValueOutput{
Name: input.SecretId,
SecretString: &secret,
}, nil
},
},
}
req := secretstores.GetSecretRequest{
Name: "/aws/secret/testing",
Metadata: map[string]string{
VersionID: "1",
},
}
output, e := s.GetSecret(req)
assert.Nil(t, e)
assert.Equal(t, secretValue, output.Data[req.Name])
})
t.Run("with version stage", func(t *testing.T) {
s := smSecretStore{
client: &mockedSM{
GetSecretValueFn: func(input *secretsmanager.GetSecretValueInput) (*secretsmanager.GetSecretValueOutput, error) {
assert.NotNil(t, input.VersionStage)
secret := secretValue
return &secretsmanager.GetSecretValueOutput{
Name: input.SecretId,
SecretString: &secret,
}, nil
},
},
}
req := secretstores.GetSecretRequest{
Name: "/aws/secret/testing",
Metadata: map[string]string{
VersionStage: "dev",
},
}
output, e := s.GetSecret(req)
assert.Nil(t, e)
assert.Equal(t, secretValue, output.Data[req.Name])
})
})
t.Run("unsuccessfully retrieve secret", func(t *testing.T) {
s := smSecretStore{
client: &mockedSM{
GetSecretValueFn: func(input *secretsmanager.GetSecretValueInput) (*secretsmanager.GetSecretValueOutput, error) {
return nil, fmt.Errorf("failed due to any reason")
},
},
}
req := secretstores.GetSecretRequest{
Name: "/aws/secret/testing",
Metadata: map[string]string{},
}
_, err := s.GetSecret(req)
assert.NotNil(t, err)
})
}