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:
parent
08362d9e6b
commit
eabe18a79f
|
|
@ -6,6 +6,7 @@ Currently supported secret stores are:
|
||||||
|
|
||||||
* Kubernetes
|
* Kubernetes
|
||||||
* Azure KeyVault
|
* Azure KeyVault
|
||||||
|
* AWS Secret manager
|
||||||
|
|
||||||
## Implementing a new Secret Store
|
## Implementing a new Secret Store
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue