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
|
||||
* Azure KeyVault
|
||||
* AWS Secret manager
|
||||
|
||||
## 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