Skip to content

Commit 199f60a

Browse files
committed
Add support for multiple keys per secret
Signed-off-by: Patrick Assuied <[email protected]>
1 parent d8ac01b commit 199f60a

File tree

3 files changed

+155
-14
lines changed

3 files changed

+155
-14
lines changed

secretstores/aws/secretmanager/metadata.yaml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,10 @@ metadata:
1616
description: |
1717
The Secrets manager endpoint. The AWS SDK will generate a default endpoint if not specified. Useful for local testing with AWS LocalStack
1818
example: '"http://localhost:4566"'
19-
type: string
19+
type: string
20+
- name: multipleKeysPerSecret
21+
required: false
22+
description: |
23+
A boolean value to indicate if the secrets with multiple keys should break keys out.
24+
example: "true"
25+
type: bool

secretstores/aws/secretmanager/secretmanager.go

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -40,16 +40,18 @@ func NewSecretManager(logger logger.Logger) secretstores.SecretStore {
4040
}
4141

4242
type SecretManagerMetaData struct {
43-
Region string `json:"region" mapstructure:"region" mdignore:"true"`
44-
AccessKey string `json:"accessKey" mapstructure:"accessKey" mdignore:"true"`
45-
SecretKey string `json:"secretKey" mapstructure:"secretKey" mdignore:"true"`
46-
SessionToken string `json:"sessionToken" mapstructure:"sessionToken" mdignore:"true"`
47-
Endpoint string `json:"endpoint" mapstructure:"endpoint"`
43+
Region string `json:"region" mapstructure:"region" mdignore:"true"`
44+
AccessKey string `json:"accessKey" mapstructure:"accessKey" mdignore:"true"`
45+
SecretKey string `json:"secretKey" mapstructure:"secretKey" mdignore:"true"`
46+
SessionToken string `json:"sessionToken" mapstructure:"sessionToken" mdignore:"true"`
47+
Endpoint string `json:"endpoint" mapstructure:"endpoint"`
48+
MultipleKeysPerSecret bool `json:"multipleKeysPerSecret" mapstructure:"multipleKeysPerSecret"`
4849
}
4950

5051
type smSecretStore struct {
51-
authProvider awsAuth.Provider
52-
logger logger.Logger
52+
authProvider awsAuth.Provider
53+
logger logger.Logger
54+
multipleKeysPerSecret bool
5355
}
5456

5557
// Init creates an AWS secret manager client.
@@ -67,6 +69,7 @@ func (s *smSecretStore) Init(ctx context.Context, metadata secretstores.Metadata
6769
SessionToken: meta.SessionToken,
6870
Endpoint: meta.Endpoint,
6971
}
72+
s.multipleKeysPerSecret = meta.MultipleKeysPerSecret
7073

7174
provider, err := awsAuth.NewProvider(ctx, opts, awsAuth.GetConfig(opts))
7275
if err != nil {
@@ -76,6 +79,28 @@ func (s *smSecretStore) Init(ctx context.Context, metadata secretstores.Metadata
7679
return nil
7780
}
7881

82+
func (s *smSecretStore) formatSecret(output *secretsmanager.GetSecretValueOutput) map[string]string {
83+
result := map[string]string{}
84+
85+
if output.Name != nil && output.SecretString != nil {
86+
if s.multipleKeysPerSecret {
87+
data := map[string]string{}
88+
err := json.Unmarshal([]byte(*output.SecretString), &data)
89+
if err == nil {
90+
for k, v := range data {
91+
result[*output.Name+":"+k] = v
92+
}
93+
} else {
94+
result[*output.Name] = *output.SecretString
95+
}
96+
} else {
97+
result[*output.Name] = *output.SecretString
98+
}
99+
}
100+
101+
return result
102+
}
103+
79104
// GetSecret retrieves a secret using a key and returns a map of decrypted string/string values.
80105
func (s *smSecretStore) GetSecret(ctx context.Context, req secretstores.GetSecretRequest) (secretstores.GetSecretResponse, error) {
81106
var versionID *string
@@ -98,9 +123,7 @@ func (s *smSecretStore) GetSecret(ctx context.Context, req secretstores.GetSecre
98123
resp := secretstores.GetSecretResponse{
99124
Data: map[string]string{},
100125
}
101-
if output.Name != nil && output.SecretString != nil {
102-
resp.Data[*output.Name] = *output.SecretString
103-
}
126+
resp.Data = s.formatSecret(output)
104127

105128
return resp, nil
106129
}
@@ -131,9 +154,7 @@ func (s *smSecretStore) BulkGetSecret(ctx context.Context, req secretstores.Bulk
131154
return secretstores.BulkGetSecretResponse{Data: nil}, fmt.Errorf("couldn't get secret: %s", *entry.Name)
132155
}
133156

134-
if entry.Name != nil && secrets.SecretString != nil {
135-
resp.Data[*entry.Name] = map[string]string{*entry.Name: *secrets.SecretString}
136-
}
157+
resp.Data[*entry.Name] = s.formatSecret(secrets)
137158
}
138159

139160
nextToken = output.NextToken

secretstores/aws/secretmanager/secretmanager_test.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,120 @@ func TestGetSecret(t *testing.T) {
162162
require.NoError(t, e)
163163
assert.Equal(t, secretValue, output.Data[req.Name])
164164
})
165+
166+
t.Run("with multiple keys per secret", func(t *testing.T) {
167+
mockSSM := &awsAuth.MockSecretManager{
168+
GetSecretValueFn: func(ctx context.Context, input *secretsmanager.GetSecretValueInput, option ...request.Option) (*secretsmanager.GetSecretValueOutput, error) {
169+
assert.Nil(t, input.VersionId)
170+
assert.Nil(t, input.VersionStage)
171+
secret := `{"key1":"value1","key2":"value2"}`
172+
173+
return &secretsmanager.GetSecretValueOutput{
174+
Name: input.SecretId,
175+
SecretString: &secret,
176+
}, nil
177+
},
178+
}
179+
180+
secret := awsAuth.SecretManagerClients{
181+
Manager: mockSSM,
182+
}
183+
184+
mockedClients := awsAuth.Clients{
185+
Secret: &secret,
186+
}
187+
mockAuthProvider := &awsAuth.StaticAuth{}
188+
mockAuthProvider.WithMockClients(&mockedClients)
189+
s := smSecretStore{
190+
authProvider: mockAuthProvider,
191+
multipleKeysPerSecret: true,
192+
}
193+
194+
req := secretstores.GetSecretRequest{
195+
Name: "/aws/secret/testing",
196+
Metadata: map[string]string{},
197+
}
198+
output, e := s.GetSecret(t.Context(), req)
199+
require.NoError(t, e)
200+
assert.Len(t, output.Data, 2)
201+
assert.Equal(t, "value1", output.Data["/aws/secret/testing:key1"])
202+
assert.Equal(t, "value2", output.Data["/aws/secret/testing:key2"])
203+
})
204+
205+
t.Run("with multiple keys per secret and option disabled", func(t *testing.T) {
206+
mockSSM := &awsAuth.MockSecretManager{
207+
GetSecretValueFn: func(ctx context.Context, input *secretsmanager.GetSecretValueInput, option ...request.Option) (*secretsmanager.GetSecretValueOutput, error) {
208+
assert.Nil(t, input.VersionId)
209+
assert.Nil(t, input.VersionStage)
210+
secret := `{"key1":"value1","key2":"value2"}`
211+
212+
return &secretsmanager.GetSecretValueOutput{
213+
Name: input.SecretId,
214+
SecretString: &secret,
215+
}, nil
216+
},
217+
}
218+
219+
secret := awsAuth.SecretManagerClients{
220+
Manager: mockSSM,
221+
}
222+
223+
mockedClients := awsAuth.Clients{
224+
Secret: &secret,
225+
}
226+
mockAuthProvider := &awsAuth.StaticAuth{}
227+
mockAuthProvider.WithMockClients(&mockedClients)
228+
s := smSecretStore{
229+
authProvider: mockAuthProvider,
230+
}
231+
232+
req := secretstores.GetSecretRequest{
233+
Name: "/aws/secret/testing",
234+
Metadata: map[string]string{},
235+
}
236+
output, e := s.GetSecret(t.Context(), req)
237+
require.NoError(t, e)
238+
assert.Len(t, output.Data, 1)
239+
assert.Equal(t, `{"key1":"value1","key2":"value2"}`, output.Data["/aws/secret/testing"])
240+
})
241+
242+
t.Run("with multiple keys per secret and secret is NOT json", func(t *testing.T) {
243+
mockSSM := &awsAuth.MockSecretManager{
244+
GetSecretValueFn: func(ctx context.Context, input *secretsmanager.GetSecretValueInput, option ...request.Option) (*secretsmanager.GetSecretValueOutput, error) {
245+
assert.Nil(t, input.VersionId)
246+
assert.Nil(t, input.VersionStage)
247+
secret := "not json"
248+
249+
return &secretsmanager.GetSecretValueOutput{
250+
Name: input.SecretId,
251+
SecretString: &secret,
252+
}, nil
253+
},
254+
}
255+
256+
secret := awsAuth.SecretManagerClients{
257+
Manager: mockSSM,
258+
}
259+
260+
mockedClients := awsAuth.Clients{
261+
Secret: &secret,
262+
}
263+
mockAuthProvider := &awsAuth.StaticAuth{}
264+
mockAuthProvider.WithMockClients(&mockedClients)
265+
s := smSecretStore{
266+
authProvider: mockAuthProvider,
267+
multipleKeysPerSecret: true,
268+
}
269+
270+
req := secretstores.GetSecretRequest{
271+
Name: "/aws/secret/testing",
272+
Metadata: map[string]string{},
273+
}
274+
output, e := s.GetSecret(t.Context(), req)
275+
require.NoError(t, e)
276+
assert.Len(t, output.Data, 1)
277+
assert.Equal(t, "not json", output.Data["/aws/secret/testing"])
278+
})
165279
})
166280

167281
t.Run("unsuccessfully retrieve secret", func(t *testing.T) {

0 commit comments

Comments
 (0)