Browse Source

feat: add prefix definition to all secret keys for aws parameter store (#3718)

* feat: add prefix definition to all secret keys for aws parameter store

Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>

* added a push secret test to verify called parameter has a prefix

Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>

---------

Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>
Gergely Brautigam 1 year ago
parent
commit
8c709cfa43

+ 4 - 0
apis/externalsecrets/v1beta1/secretstore_aws_types.go

@@ -124,4 +124,8 @@ type AWSProvider struct {
 	// AWS STS assume role transitive session tags. Required when multiple rules are used with the provider
 	// +optional
 	TransitiveTagKeys []*string `json:"transitiveTagKeys,omitempty"`
+
+	// Prefix adds a prefix to all retrieved values.
+	// +optional
+	Prefix string `json:"prefix,omitempty"`
 }

+ 3 - 0
config/crds/bases/external-secrets.io_clustersecretstores.yaml

@@ -2084,6 +2084,9 @@ spec:
                       externalID:
                         description: AWS External ID set on assumed IAM roles
                         type: string
+                      prefix:
+                        description: Prefix adds a prefix to all retrieved values.
+                        type: string
                       region:
                         description: AWS Region to be used for the provider
                         type: string

+ 3 - 0
config/crds/bases/external-secrets.io_secretstores.yaml

@@ -2084,6 +2084,9 @@ spec:
                       externalID:
                         description: AWS External ID set on assumed IAM roles
                         type: string
+                      prefix:
+                        description: Prefix adds a prefix to all retrieved values.
+                        type: string
                       region:
                         description: AWS Region to be used for the provider
                         type: string

+ 6 - 0
deploy/crds/bundle.yaml

@@ -2613,6 +2613,9 @@ spec:
                         externalID:
                           description: AWS External ID set on assumed IAM roles
                           type: string
+                        prefix:
+                          description: Prefix adds a prefix to all retrieved values.
+                          type: string
                         region:
                           description: AWS Region to be used for the provider
                           type: string
@@ -8244,6 +8247,9 @@ spec:
                         externalID:
                           description: AWS External ID set on assumed IAM roles
                           type: string
+                        prefix:
+                          description: Prefix adds a prefix to all retrieved values.
+                          type: string
                         region:
                           description: AWS Region to be used for the provider
                           type: string

+ 12 - 0
docs/api/spec.md

@@ -281,6 +281,18 @@ SecretsManager
 <p>AWS STS assume role transitive session tags. Required when multiple rules are used with the provider</p>
 </td>
 </tr>
+<tr>
+<td>
+<code>prefix</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>Prefix adds a prefix to all retrieved values.</p>
+</td>
+</tr>
 </tbody>
 </table>
 <h3 id="external-secrets.io/v1beta1.AWSServiceType">AWSServiceType

+ 9 - 7
pkg/provider/aws/parameterstore/fake/fake.go

@@ -26,13 +26,14 @@ import (
 
 // Client implements the aws parameterstore interface.
 type Client struct {
-	GetParameterWithContextFn        GetParameterWithContextFn
-	GetParametersByPathWithContextFn GetParametersByPathWithContextFn
-	PutParameterWithContextFn        PutParameterWithContextFn
-	PutParameterWithContextCalledN   int
-	DeleteParameterWithContextFn     DeleteParameterWithContextFn
-	DescribeParametersWithContextFn  DescribeParametersWithContextFn
-	ListTagsForResourceWithContextFn ListTagsForResourceWithContextFn
+	GetParameterWithContextFn           GetParameterWithContextFn
+	GetParametersByPathWithContextFn    GetParametersByPathWithContextFn
+	PutParameterWithContextFn           PutParameterWithContextFn
+	PutParameterWithContextCalledN      int
+	PutParameterWithContextFnCalledWith [][]*ssm.PutParameterInput
+	DeleteParameterWithContextFn        DeleteParameterWithContextFn
+	DescribeParametersWithContextFn     DescribeParametersWithContextFn
+	ListTagsForResourceWithContextFn    ListTagsForResourceWithContextFn
 }
 
 type GetParameterWithContextFn func(aws.Context, *ssm.GetParameterInput, ...request.Option) (*ssm.GetParameterOutput, error)
@@ -88,6 +89,7 @@ func NewDescribeParametersWithContextFn(output *ssm.DescribeParametersOutput, er
 
 func (sm *Client) PutParameterWithContext(ctx aws.Context, input *ssm.PutParameterInput, options ...request.Option) (*ssm.PutParameterOutput, error) {
 	sm.PutParameterWithContextCalledN++
+	sm.PutParameterWithContextFnCalledWith = append(sm.PutParameterWithContextFnCalledWith, []*ssm.PutParameterInput{input})
 	return sm.PutParameterWithContextFn(ctx, input, options...)
 }
 

+ 9 - 7
pkg/provider/aws/parameterstore/parameterstore.go

@@ -60,6 +60,7 @@ type ParameterStore struct {
 	sess         *session.Session
 	client       PMInterface
 	referentAuth bool
+	prefix       string
 }
 
 // PMInterface is a subset of the parameterstore api.
@@ -79,11 +80,12 @@ const (
 )
 
 // New constructs a ParameterStore Provider that is specific to a store.
-func New(sess *session.Session, cfg *aws.Config, referentAuth bool) (*ParameterStore, error) {
+func New(sess *session.Session, cfg *aws.Config, prefix string, referentAuth bool) (*ParameterStore, error) {
 	return &ParameterStore{
 		sess:         sess,
 		referentAuth: referentAuth,
 		client:       ssm.New(sess, cfg),
+		prefix:       prefix,
 	}, nil
 }
 
@@ -105,7 +107,7 @@ func (pm *ParameterStore) getTagsByName(ctx aws.Context, ref *ssm.GetParameterOu
 }
 
 func (pm *ParameterStore) DeleteSecret(ctx context.Context, remoteRef esv1beta1.PushSecretRemoteRef) error {
-	secretName := remoteRef.GetRemoteKey()
+	secretName := pm.prefix + remoteRef.GetRemoteKey()
 	secretValue := ssm.GetParameterInput{
 		Name: &secretName,
 	}
@@ -179,7 +181,7 @@ func (pm *ParameterStore) PushSecret(ctx context.Context, secret *corev1.Secret,
 	}
 
 	stringValue := string(value)
-	secretName := data.GetRemoteKey()
+	secretName := pm.prefix + data.GetRemoteKey()
 
 	secretRequest := ssm.PutParameterInput{
 		Name:      &secretName,
@@ -466,7 +468,7 @@ func (pm *ParameterStore) GetSecret(ctx context.Context, ref esv1beta1.ExternalS
 func (pm *ParameterStore) getParameterTags(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) (*ssm.GetParameterOutput, error) {
 	param := ssm.GetParameterOutput{
 		Parameter: &ssm.Parameter{
-			Name: parameterNameWithVersion(ref),
+			Name: pm.parameterNameWithVersion(ref),
 		},
 	}
 	tags, err := pm.getTagsByName(ctx, &param)
@@ -487,7 +489,7 @@ func (pm *ParameterStore) getParameterTags(ctx context.Context, ref esv1beta1.Ex
 
 func (pm *ParameterStore) getParameterValue(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) (*ssm.GetParameterOutput, error) {
 	out, err := pm.client.GetParameterWithContext(ctx, &ssm.GetParameterInput{
-		Name:           parameterNameWithVersion(ref),
+		Name:           pm.parameterNameWithVersion(ref),
 		WithDecryption: aws.Bool(true),
 	})
 
@@ -518,8 +520,8 @@ func (pm *ParameterStore) GetSecretMap(ctx context.Context, ref esv1beta1.Extern
 	return secretData, nil
 }
 
-func parameterNameWithVersion(ref esv1beta1.ExternalSecretDataRemoteRef) *string {
-	name := ref.Key
+func (pm *ParameterStore) parameterNameWithVersion(ref esv1beta1.ExternalSecretDataRemoteRef) *string {
+	name := pm.prefix + ref.Key
 	if ref.Version != "" {
 		// see docs: https://docs.aws.amazon.com/systems-manager/latest/userguide/sysman-paramstore-versions.html#reference-parameter-version
 		name += ":" + ref.Version

+ 56 - 4
pkg/provider/aws/parameterstore/parameterstore_test.go

@@ -42,6 +42,11 @@ const (
 	invalidProp        = "INVALPROP"
 )
 
+var (
+	fakeSecretKey = "fakeSecretKey"
+	fakeValue     = "fakeValue"
+)
+
 type parameterstoreTestCase struct {
 	fakeClient     *fakeps.Client
 	apiInput       *ssm.GetParameterInput
@@ -51,6 +56,7 @@ type parameterstoreTestCase struct {
 	expectError    string
 	expectedSecret string
 	expectedData   map[string][]byte
+	prefix         string
 }
 
 func makeValidParameterStoreTestCase() *parameterstoreTestCase {
@@ -60,6 +66,7 @@ func makeValidParameterStoreTestCase() *parameterstoreTestCase {
 		apiOutput:      makeValidAPIOutput(),
 		remoteRef:      makeValidRemoteRef(),
 		apiErr:         nil,
+		prefix:         "",
 		expectError:    "",
 		expectedSecret: "",
 		expectedData:   make(map[string][]byte),
@@ -270,8 +277,6 @@ const remoteKey = "fake-key"
 func TestPushSecret(t *testing.T) {
 	invalidParameters := errors.New(ssm.ErrCodeInvalidParameters)
 	alreadyExistsError := errors.New(ssm.ErrCodeAlreadyExistsException)
-	fakeSecretKey := "fakeSecretKey"
-	fakeValue := "fakeValue"
 	fakeSecret := &corev1.Secret{
 		Data: map[string][]byte{
 			fakeSecretKey: []byte(fakeValue),
@@ -518,9 +523,43 @@ func TestPushSecret(t *testing.T) {
 	}
 }
 
+func TestPushSecretWithPrefix(t *testing.T) {
+	fakeSecret := &corev1.Secret{
+		Data: map[string][]byte{
+			fakeSecretKey: []byte(fakeValue),
+		},
+	}
+	managedByESO := ssm.Tag{
+		Key:   &managedBy,
+		Value: &externalSecrets,
+	}
+	putParameterOutput := &ssm.PutParameterOutput{}
+	getParameterOutput := &ssm.GetParameterOutput{}
+	describeParameterOutput := &ssm.DescribeParametersOutput{}
+	validListTagsForResourceOutput := &ssm.ListTagsForResourceOutput{
+		TagList: []*ssm.Tag{&managedByESO},
+	}
+
+	client := fakeps.Client{
+		PutParameterWithContextFn:        fakeps.NewPutParameterWithContextFn(putParameterOutput, nil),
+		GetParameterWithContextFn:        fakeps.NewGetParameterWithContextFn(getParameterOutput, nil),
+		DescribeParametersWithContextFn:  fakeps.NewDescribeParametersWithContextFn(describeParameterOutput, nil),
+		ListTagsForResourceWithContextFn: fakeps.NewListTagsForResourceWithContextFn(validListTagsForResourceOutput, nil),
+	}
+
+	psd := fake.PushSecretData{SecretKey: fakeSecretKey, RemoteKey: remoteKey}
+	ps := ParameterStore{
+		client: &client,
+		prefix: "/test/this/thing/",
+	}
+	err := ps.PushSecret(context.TODO(), fakeSecret, psd)
+	require.NoError(t, err)
+
+	input := client.PutParameterWithContextFnCalledWith[0][0]
+	assert.Equal(t, "/test/this/thing/fake-key", *input.Name)
+}
+
 func TestPushSecretCalledOnlyOnce(t *testing.T) {
-	fakeSecretKey := "fakeSecretKey"
-	fakeValue := "fakeValue"
 	fakeSecret := &corev1.Secret{
 		Data: map[string][]byte{
 			fakeSecretKey: []byte(fakeValue),
@@ -569,6 +608,17 @@ func TestGetSecret(t *testing.T) {
 		pstc.expectedSecret = "RRRRR"
 	}
 
+	// good case: key is passed in and prefix is set, output is sent back
+	setSecretStringWithPrefix := func(pstc *parameterstoreTestCase) {
+		pstc.apiInput = &ssm.GetParameterInput{
+			Name:           aws.String("/test/this/baz"),
+			WithDecryption: aws.Bool(true),
+		}
+		pstc.prefix = "/test/this"
+		pstc.apiOutput.Parameter.Value = aws.String("RRRRR")
+		pstc.expectedSecret = "RRRRR"
+	}
+
 	// good case: extract property
 	setExtractProperty := func(pstc *parameterstoreTestCase) {
 		pstc.apiOutput.Parameter.Value = aws.String(`{"/shmoo": "bang"}`)
@@ -649,6 +699,7 @@ func TestGetSecret(t *testing.T) {
 	}
 
 	successCases := []*parameterstoreTestCase{
+		makeValidParameterStoreTestCaseCustom(setSecretStringWithPrefix),
 		makeValidParameterStoreTestCaseCustom(setSecretString),
 		makeValidParameterStoreTestCaseCustom(setExtractProperty),
 		makeValidParameterStoreTestCaseCustom(setMissingProperty),
@@ -665,6 +716,7 @@ func TestGetSecret(t *testing.T) {
 	ps := ParameterStore{}
 	for k, v := range successCases {
 		ps.client = v.fakeClient
+		ps.prefix = v.prefix
 		out, err := ps.GetSecret(context.Background(), *v.remoteRef)
 		if !ErrorContains(err, v.expectError) {
 			t.Errorf("[%d] unexpected error: %s, expected: '%s'", k, err.Error(), v.expectError)

+ 2 - 2
pkg/provider/aws/provider.go

@@ -156,7 +156,7 @@ func newClient(ctx context.Context, store esv1beta1.GenericStore, kube client.Cl
 		case esv1beta1.AWSServiceSecretsManager:
 			return secretsmanager.New(sess, cfg, prov.SecretsManager, true)
 		case esv1beta1.AWSServiceParameterStore:
-			return parameterstore.New(sess, cfg, true)
+			return parameterstore.New(sess, cfg, storeSpec.Provider.AWS.Prefix, true)
 		}
 		return nil, fmt.Errorf(errUnknownProviderService, prov.Service)
 	}
@@ -195,7 +195,7 @@ func newClient(ctx context.Context, store esv1beta1.GenericStore, kube client.Cl
 	case esv1beta1.AWSServiceSecretsManager:
 		return secretsmanager.New(sess, cfg, prov.SecretsManager, false)
 	case esv1beta1.AWSServiceParameterStore:
-		return parameterstore.New(sess, cfg, false)
+		return parameterstore.New(sess, cfg, storeSpec.Provider.AWS.Prefix, false)
 	}
 	return nil, fmt.Errorf(errUnknownProviderService, prov.Service)
 }