1
0
Fork 0
mirror of https://github.com/external-secrets/external-secrets.git synced 2024-12-14 11:57:59 +00:00

feat: add ssm parameter store support (#59)

* feat: add parameter store implementation
This commit is contained in:
Moritz Johner 2021-03-22 19:14:19 +01:00 committed by GitHub
parent 41429040b0
commit 2c059b71ba
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 1160 additions and 563 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

File diff suppressed because one or more lines are too long

View file

@ -1,7 +1,74 @@
!!! bug "Not implemented"
This is currently **not yet** implemented. Feel free to contribute. Please see
[issue#27](https://github.com/external-secrets/external-secrets/issues/27)
for futher information.
![aws sm](./pictures/diagrams-provider-aws-ssm-parameter-store.png)
## Parameter Store
A `ParameterStore` points to AWS SSM Parameter Store in a certain account within a
defined region. You should define Roles that define fine-grained access to
individual secrets and pass them to ESO using `spec.provider.aws.role`. This
way users of the `SecretStore` can only access the secrets necessary.
``` yaml
{% include 'aws-parameter-store.yaml' %}
```
!!! warning "API Pricing & Throttling"
The SSM Parameter Store API is charged by throughput and
is available in different tiers, [see pricing](https://aws.amazon.com/systems-manager/pricing/#Parameter_Store).
Please estimate your costs before using ESO. Cost depends on the RefreshInterval of your ExternalSecrets.
### IAM Policy
Create a IAM Policy to pin down access to secrets matching `dev-*`, for futher information see [AWS Documentation](https://docs.aws.amazon.com/systems-manager/latest/userguide/sysman-paramstore-access.html):
``` json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Deny",
"Action": [
"ssm:GetParameter*"
],
"Resource": "arn:aws:ssm:us-east-2:123456789012:parameter/dev-*"
}
]
}
```
### JSON Secret Values
You can store JSON objects in a parameter. You can access nested values or arrays using [gjson syntax](https://github.com/tidwall/gjson/blob/master/SYNTAX.md):
Consider the following JSON object that is stored in the Parameter Store key `my-json-secret`:
``` json
{
"name": {"first": "Tom", "last": "Anderson"},
"friends": [
{"first": "Dale", "last": "Murphy"},
{"first": "Roger", "last": "Craig"},
{"first": "Jane", "last": "Murphy"}
]
}
```
This is an example on how you would look up nested keys in the above json object:
``` yaml
apiVersion: external-secrets.io/v1alpha1
kind: ExternalSecret
metadata:
name: example
spec:
# [omitted for brevity]
data:
- secretKey: firstname
remoteRef:
key: my-json-secret
property: name.first # Tom
- secretKey: first_friend
remoteRef:
key: my-json-secret
property: friends.1.first # Roger
```
--8<-- "snippets/provider-aws-access.md"

View file

@ -1,14 +1,10 @@
![aws sm](./pictures/eso-az-kv-aws-sm.png)
--8<-- "snippets/provider-aws-access.md"
## Secrets Manager
A `SecretStore` points to AWS Secrets Manager in a certain account within a
defined region. You should define Roles that allow fine-grained access to
defined region. You should define Roles that define fine-grained access to
individual secrets and pass them to ESO using `spec.provider.aws.role`. This
way users of the `SecretStore` can only access the secrets necessary.
@ -16,6 +12,7 @@ way users of the `SecretStore` can only access the secrets necessary.
{% include 'aws-sm-store.yaml' %}
```
### IAM Policy
Create a IAM Policy to pin down access to secrets matching `dev-*`.
@ -38,3 +35,40 @@ Create a IAM Policy to pin down access to secrets matching `dev-*`.
]
}
```
### JSON Secret Values
SecretsManager supports *simple* key/value pairs that are stored as json. If you use the API you can store more complex JSON objects. You can access nested values or arrays using [gjson syntax](https://github.com/tidwall/gjson/blob/master/SYNTAX.md):
Consider the following JSON object that is stored in the SecretsManager key `my-json-secret`:
``` json
{
"name": {"first": "Tom", "last": "Anderson"},
"friends": [
{"first": "Dale", "last": "Murphy"},
{"first": "Roger", "last": "Craig"},
{"first": "Jane", "last": "Murphy"}
]
}
```
This is an example on how you would look up nested keys in the above json object:
``` yaml
apiVersion: external-secrets.io/v1alpha1
kind: ExternalSecret
metadata:
name: example
spec:
# [omitted for brevity]
data:
- secretKey: firstname
remoteRef:
key: my-json-secret
property: name.first # Tom
- secretKey: first_friend
remoteRef:
key: my-json-secret
property: friends.1.first # Roger
```
--8<-- "snippets/provider-aws-access.md"

View file

@ -0,0 +1,21 @@
apiVersion: external-secrets.io/v1alpha1
kind: SecretStore
metadata:
name: secretstore-sample
spec:
controller: dev
provider:
aws:
service: ParameterStore
# define a specific role to limit access
# to certain secrets
role: iam-role
region: eu-central-1
auth:
secretRef:
accessKeyIDSecretRef:
name: awssm-secret
key: access-key
secretAccessKeySecretRef:
name: awssm-secret
key: secret-access-key

View file

@ -0,0 +1,39 @@
/*
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package fake
import (
"fmt"
"github.com/aws/aws-sdk-go/service/ssm"
"github.com/google/go-cmp/cmp"
)
// Client implements the aws parameterstore interface.
type Client struct {
valFn func(*ssm.GetParameterInput) (*ssm.GetParameterOutput, error)
}
func (sm *Client) GetParameter(in *ssm.GetParameterInput) (*ssm.GetParameterOutput, error) {
return sm.valFn(in)
}
func (sm *Client) WithValue(in *ssm.GetParameterInput, val *ssm.GetParameterOutput, err error) {
sm.valFn = func(paramIn *ssm.GetParameterInput) (*ssm.GetParameterOutput, error) {
if !cmp.Equal(paramIn, in) {
return nil, fmt.Errorf("unexpected test argument")
}
return val, err
}
}

View file

@ -15,21 +15,21 @@ package parameterstore
import (
"context"
"encoding/json"
"fmt"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/client"
"github.com/aws/aws-sdk-go/service/ssm"
"github.com/tidwall/gjson"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
"github.com/external-secrets/external-secrets/pkg/provider"
awssess "github.com/external-secrets/external-secrets/pkg/provider/aws/session"
)
// ParameterStore is a provider for AWS ParameterStore.
type ParameterStore struct {
stsProvider awssess.STSProvider
// session *session.Session
// client PMInterface
client PMInterface
}
// PMInterface is a subset of the parameterstore api.
@ -41,20 +41,50 @@ type PMInterface interface {
var log = ctrl.Log.WithName("provider").WithName("aws").WithName("parameterstore")
// New constructs a ParameterStore Provider that is specific to a store.
func New(ctx context.Context, store esv1alpha1.GenericStore, kube client.Client, namespace string, stsProvider awssess.STSProvider) (provider.SecretsClient, error) {
pm := &ParameterStore{
stsProvider: stsProvider,
}
return pm, nil
func New(sess client.ConfigProvider) (*ParameterStore, error) {
return &ParameterStore{
client: ssm.New(sess),
}, nil
}
// GetSecret returns a single secret from the provider.
func (pm *ParameterStore) GetSecret(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) ([]byte, error) {
log.Info("fetching secret value", "key", ref.Key)
return []byte("NOOP"), nil
out, err := pm.client.GetParameter(&ssm.GetParameterInput{
Name: &ref.Key,
WithDecryption: aws.Bool(true),
})
if err != nil {
return nil, fmt.Errorf("unable to get parameter: %w", err)
}
if ref.Property == "" {
if out.Parameter.Value != nil {
return []byte(*out.Parameter.Value), nil
}
return nil, fmt.Errorf("invalid secret received. parameter value is nil for key: %s", ref.Key)
}
val := gjson.Get(*out.Parameter.Value, ref.Property)
if !val.Exists() {
return nil, fmt.Errorf("key %s does not exist in secret %s", ref.Property, ref.Key)
}
return []byte(val.String()), nil
}
// GetSecretMap returns multiple k/v pairs from the provider.
func (pm *ParameterStore) GetSecretMap(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
return map[string][]byte{"NOOP": []byte("NOOP")}, nil
log.Info("fetching secret map", "key", ref.Key)
data, err := pm.GetSecret(ctx, ref)
if err != nil {
return nil, err
}
kv := make(map[string]string)
err = json.Unmarshal(data, &kv)
if err != nil {
return nil, fmt.Errorf("unable to unmarshal secret %s: %w", ref.Key, err)
}
secretData := make(map[string][]byte)
for k, v := range kv {
secretData[k] = []byte(v)
}
return secretData, nil
}

View file

@ -0,0 +1,262 @@
/*
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package parameterstore
import (
"context"
"fmt"
"strings"
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ssm"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
fake "github.com/external-secrets/external-secrets/pkg/provider/aws/parameterstore/fake"
sess "github.com/external-secrets/external-secrets/pkg/provider/aws/session"
)
func TestConstructor(t *testing.T) {
s, err := sess.New("1111", "2222", "foo", "", nil)
assert.Nil(t, err)
c, err := New(s)
assert.Nil(t, err)
assert.NotNil(t, c.client)
}
// test the ssm<->aws interface
// make sure correct values are passed and errors are handled accordingly.
func TestGetSecret(t *testing.T) {
f := &fake.Client{}
p := &ParameterStore{
client: f,
}
for i, row := range []struct {
apiInput *ssm.GetParameterInput
apiOutput *ssm.GetParameterOutput
rr esv1alpha1.ExternalSecretDataRemoteRef
apiErr error
expectError string
expectedSecret string
}{
{
// good case: key is passed in, output is sent back
apiInput: &ssm.GetParameterInput{
Name: aws.String("/baz"),
WithDecryption: aws.Bool(true),
},
rr: esv1alpha1.ExternalSecretDataRemoteRef{
Key: "/baz",
},
apiOutput: &ssm.GetParameterOutput{
Parameter: &ssm.Parameter{
Value: aws.String("RRRRR"),
},
},
apiErr: nil,
expectError: "",
expectedSecret: "RRRRR",
},
{
// good case: extract property
apiInput: &ssm.GetParameterInput{
Name: aws.String("/baz"),
WithDecryption: aws.Bool(true),
},
rr: esv1alpha1.ExternalSecretDataRemoteRef{
Key: "/baz",
Property: "/shmoo",
},
apiOutput: &ssm.GetParameterOutput{
Parameter: &ssm.Parameter{
Value: aws.String(`{"/shmoo": "bang"}`),
},
},
apiErr: nil,
expectError: "",
expectedSecret: "bang",
},
{
// bad case: missing property
apiInput: &ssm.GetParameterInput{
Name: aws.String("/baz"),
WithDecryption: aws.Bool(true),
},
rr: esv1alpha1.ExternalSecretDataRemoteRef{
Key: "/baz",
Property: "INVALPROP",
},
apiOutput: &ssm.GetParameterOutput{
Parameter: &ssm.Parameter{
Value: aws.String(`{"/shmoo": "bang"}`),
},
},
apiErr: nil,
expectError: "key INVALPROP does not exist in secret",
expectedSecret: "",
},
{
// bad case: extract property failure due to invalid json
apiInput: &ssm.GetParameterInput{
Name: aws.String("/baz"),
WithDecryption: aws.Bool(true),
},
rr: esv1alpha1.ExternalSecretDataRemoteRef{
Key: "/baz",
Property: "INVALPROP",
},
apiOutput: &ssm.GetParameterOutput{
Parameter: &ssm.Parameter{
Value: aws.String(`------`),
},
},
apiErr: nil,
expectError: "key INVALPROP does not exist in secret",
expectedSecret: "",
},
{
// case: parameter.Value may be nil but binary is set
apiInput: &ssm.GetParameterInput{
Name: aws.String("/baz"),
WithDecryption: aws.Bool(true),
},
rr: esv1alpha1.ExternalSecretDataRemoteRef{
Key: "/baz",
},
apiOutput: &ssm.GetParameterOutput{
Parameter: &ssm.Parameter{
Value: nil,
},
},
apiErr: nil,
expectError: "parameter value is nil for key",
expectedSecret: "",
},
{
// should return err
apiInput: &ssm.GetParameterInput{
Name: aws.String("/foo/bar"),
WithDecryption: aws.Bool(true),
},
rr: esv1alpha1.ExternalSecretDataRemoteRef{
Key: "/foo/bar",
},
apiOutput: &ssm.GetParameterOutput{},
apiErr: fmt.Errorf("oh no"),
expectError: "oh no",
},
} {
f.WithValue(row.apiInput, row.apiOutput, row.apiErr)
out, err := p.GetSecret(context.Background(), row.rr)
if !ErrorContains(err, row.expectError) {
t.Errorf("[%d] unexpected error: %s, expected: '%s'", i, err.Error(), row.expectError)
}
if string(out) != row.expectedSecret {
t.Errorf("[%d] unexpected secret: expected %s, got %s", i, row.expectedSecret, string(out))
}
}
}
func TestGetSecretMap(t *testing.T) {
f := &fake.Client{}
p := &ParameterStore{
client: f,
}
for i, row := range []struct {
apiInput *ssm.GetParameterInput
apiOutput *ssm.GetParameterOutput
rr esv1alpha1.ExternalSecretDataRemoteRef
expectedData map[string]string
apiErr error
expectError string
}{
{
// good case: default version & deserialization
apiInput: &ssm.GetParameterInput{
Name: aws.String("/baz"),
WithDecryption: aws.Bool(true),
},
apiOutput: &ssm.GetParameterOutput{
Parameter: &ssm.Parameter{
Value: aws.String(`{"foo":"bar"}`),
},
},
rr: esv1alpha1.ExternalSecretDataRemoteRef{
Key: "/baz",
},
expectedData: map[string]string{
"foo": "bar",
},
apiErr: nil,
expectError: "",
},
{
// bad case: api error returned
apiInput: &ssm.GetParameterInput{
Name: aws.String("/baz"),
WithDecryption: aws.Bool(true),
},
apiOutput: &ssm.GetParameterOutput{
Parameter: &ssm.Parameter{},
},
rr: esv1alpha1.ExternalSecretDataRemoteRef{
Key: "/baz",
},
expectedData: map[string]string{
"foo": "bar",
},
apiErr: fmt.Errorf("some api err"),
expectError: "some api err",
},
{
// bad case: invalid json
apiInput: &ssm.GetParameterInput{
Name: aws.String("/baz"),
WithDecryption: aws.Bool(true),
},
apiOutput: &ssm.GetParameterOutput{
Parameter: &ssm.Parameter{
Value: aws.String(`-----------------`),
},
},
rr: esv1alpha1.ExternalSecretDataRemoteRef{
Key: "/baz",
},
expectedData: map[string]string{},
apiErr: nil,
expectError: "unable to unmarshal secret",
},
} {
f.WithValue(row.apiInput, row.apiOutput, row.apiErr)
out, err := p.GetSecretMap(context.Background(), row.rr)
if !ErrorContains(err, row.expectError) {
t.Errorf("[%d] unexpected error: %s, expected: '%s'", i, err.Error(), row.expectError)
}
if cmp.Equal(out, row.expectedData) {
t.Errorf("[%d] unexpected secret data: expected %#v, got %#v", i, row.expectedData, out)
}
}
}
func ErrorContains(out error, want string) bool {
if out == nil {
return want == ""
}
if want == "" {
return false
}
return strings.Contains(out.Error(), want)
}

View file

@ -1,9 +1,25 @@
/*
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package aws
import (
"context"
"fmt"
"github.com/aws/aws-sdk-go/aws/session"
v1 "k8s.io/api/core/v1"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
@ -17,28 +33,104 @@ import (
// Provider satisfies the provider interface.
type Provider struct{}
var log = ctrl.Log.WithName("provider").WithName("aws")
// NewClient constructs a new secrets client based on the provided store.
func (p *Provider) NewClient(ctx context.Context, store esv1alpha1.GenericStore, kube client.Client, namespace string) (provider.SecretsClient, error) {
if store == nil {
return nil, fmt.Errorf("store is nil")
return newClient(ctx, store, kube, namespace, awssess.DefaultSTSProvider)
}
func newClient(ctx context.Context, store esv1alpha1.GenericStore, kube client.Client, namespace string, assumeRoler awssess.STSProvider) (provider.SecretsClient, error) {
prov, err := getAWSProvider(store)
if err != nil {
return nil, err
}
spec := store.GetSpec()
if spec == nil {
sess, err := newSession(ctx, store, kube, namespace, assumeRoler)
if err != nil {
return nil, fmt.Errorf("unable to create session: %w", err)
}
switch prov.Service {
case esv1alpha1.AWSServiceSecretsManager:
return secretsmanager.New(sess)
case esv1alpha1.AWSServiceParameterStore:
return parameterstore.New(sess)
}
return nil, fmt.Errorf("unknown AWS Provider Service: %s", prov.Service)
}
// newSession creates a new aws session based on a store
// it looks up credentials at the provided secrets.
func newSession(ctx context.Context, store esv1alpha1.GenericStore, kube client.Client, namespace string, assumeRoler awssess.STSProvider) (*session.Session, error) {
prov, err := getAWSProvider(store)
if err != nil {
return nil, err
}
var sak, aks string
// use provided credentials via secret reference
if prov.Auth != nil {
log.V(1).Info("fetching secrets for authentication")
ke := client.ObjectKey{
Name: prov.Auth.SecretRef.AccessKeyID.Name,
Namespace: namespace, // default to ExternalSecret namespace
}
// only ClusterStore is allowed to set namespace (and then it's required)
if store.GetObjectKind().GroupVersionKind().Kind == esv1alpha1.ClusterSecretStoreKind {
if prov.Auth.SecretRef.AccessKeyID.Namespace == nil {
return nil, fmt.Errorf("invalid ClusterSecretStore: missing AWS AccessKeyID Namespace")
}
ke.Namespace = *prov.Auth.SecretRef.AccessKeyID.Namespace
}
akSecret := v1.Secret{}
err := kube.Get(ctx, ke, &akSecret)
if err != nil {
return nil, fmt.Errorf("could not fetch accessKeyID secret: %w", err)
}
ke = client.ObjectKey{
Name: prov.Auth.SecretRef.SecretAccessKey.Name,
Namespace: namespace, // default to ExternalSecret namespace
}
// only ClusterStore is allowed to set namespace (and then it's required)
if store.GetObjectKind().GroupVersionKind().Kind == esv1alpha1.ClusterSecretStoreKind {
if prov.Auth.SecretRef.SecretAccessKey.Namespace == nil {
return nil, fmt.Errorf("invalid ClusterSecretStore: missing AWS SecretAccessKey Namespace")
}
ke.Namespace = *prov.Auth.SecretRef.SecretAccessKey.Namespace
}
sakSecret := v1.Secret{}
err = kube.Get(ctx, ke, &sakSecret)
if err != nil {
return nil, fmt.Errorf("could not fetch SecretAccessKey secret: %w", err)
}
sak = string(sakSecret.Data[prov.Auth.SecretRef.SecretAccessKey.Key])
aks = string(akSecret.Data[prov.Auth.SecretRef.AccessKeyID.Key])
if sak == "" {
return nil, fmt.Errorf("missing SecretAccessKey")
}
if aks == "" {
return nil, fmt.Errorf("missing AccessKeyID")
}
}
return awssess.New(sak, aks, prov.Region, prov.Role, assumeRoler)
}
// getAWSProvider does the necessary nil checks on the generic store
// it returns the aws provider or an error.
func getAWSProvider(store esv1alpha1.GenericStore) (*esv1alpha1.AWSProvider, error) {
if store == nil {
return nil, fmt.Errorf("found nil store")
}
spc := store.GetSpec()
if spc == nil {
return nil, fmt.Errorf("store is missing spec")
}
if spec.Provider == nil {
if spc.Provider == nil {
return nil, fmt.Errorf("storeSpec is missing provider")
}
if spec.Provider.AWS == nil {
return nil, fmt.Errorf("storeSpec is missing aws spec")
prov := spc.Provider.AWS
if prov == nil {
return nil, fmt.Errorf("invalid provider spec. Missing AWS field in store %s", store.GetObjectMeta().String())
}
switch spec.Provider.AWS.Service {
case esv1alpha1.AWSServiceSecretsManager:
return secretsmanager.New(ctx, store, kube, namespace, awssess.DefaultSTSProvider)
case esv1alpha1.AWSServiceParameterStore:
return parameterstore.New(ctx, store, kube, namespace, awssess.DefaultSTSProvider)
}
return nil, fmt.Errorf("unknown AWS Provider Service: %s", spec.Provider.AWS.Service)
return prov, nil
}
func init() {

View file

@ -1,13 +1,40 @@
/*
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package aws
import (
"context"
"os"
"strings"
"testing"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials/stscreds"
awssess "github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/sts"
"github.com/stretchr/testify/assert"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
clientfake "sigs.k8s.io/controller-runtime/pkg/client/fake"
esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
"github.com/external-secrets/external-secrets/pkg/provider/aws/parameterstore"
"github.com/external-secrets/external-secrets/pkg/provider/aws/secretsmanager"
session "github.com/external-secrets/external-secrets/pkg/provider/aws/session"
fakesess "github.com/external-secrets/external-secrets/pkg/provider/aws/session/fake"
)
func TestProvider(t *testing.T) {
@ -15,9 +42,10 @@ func TestProvider(t *testing.T) {
p := Provider{}
tbl := []struct {
test string
store esv1alpha1.GenericStore
expErr bool
test string
store esv1alpha1.GenericStore
expType interface{}
expErr bool
}{
{
test: "should not create provider due to nil store",
@ -40,10 +68,10 @@ func TestProvider(t *testing.T) {
},
},
},
{
test: "should create provider",
expErr: false,
test: "should create parameter store client",
expErr: false,
expType: &parameterstore.ParameterStore{},
store: &esv1alpha1.SecretStore{
Spec: esv1alpha1.SecretStoreSpec{
Provider: &esv1alpha1.SecretStoreProvider{
@ -54,6 +82,54 @@ func TestProvider(t *testing.T) {
},
},
},
{
test: "should create secretsmanager client",
expErr: false,
expType: &secretsmanager.SecretsManager{},
store: &esv1alpha1.SecretStore{
Spec: esv1alpha1.SecretStoreSpec{
Provider: &esv1alpha1.SecretStoreProvider{
AWS: &esv1alpha1.AWSProvider{
Service: esv1alpha1.AWSServiceSecretsManager,
},
},
},
},
},
{
test: "invalid service should return an error",
expErr: true,
store: &esv1alpha1.SecretStore{
Spec: esv1alpha1.SecretStoreSpec{
Provider: &esv1alpha1.SecretStoreProvider{
AWS: &esv1alpha1.AWSProvider{
Service: "HIHIHIHHEHEHEHEHEHE",
},
},
},
},
},
{
test: "newSession error should be returned",
expErr: true,
store: &esv1alpha1.SecretStore{
Spec: esv1alpha1.SecretStoreSpec{
Provider: &esv1alpha1.SecretStoreProvider{
AWS: &esv1alpha1.AWSProvider{
Service: esv1alpha1.AWSServiceParameterStore,
Auth: &esv1alpha1.AWSAuth{
SecretRef: esv1alpha1.AWSAuthSecretRef{
AccessKeyID: esmeta.SecretKeySelector{
Name: "foo",
Namespace: aws.String("NOOP"),
},
},
},
},
},
},
},
},
}
for i := range tbl {
row := tbl[i]
@ -65,7 +141,440 @@ func TestProvider(t *testing.T) {
} else {
assert.Nil(t, err)
assert.NotNil(t, sc)
assert.IsType(t, row.expType, sc)
}
})
}
}
func TestNewSession(t *testing.T) {
rows := []TestSessionRow{
{
name: "nil store",
expectErr: "found nil store",
store: nil,
},
{
name: "not store spec",
expectErr: "storeSpec is missing provider",
store: &esv1alpha1.SecretStore{},
},
{
name: "store spec has no provider",
expectErr: "storeSpec is missing provider",
store: &esv1alpha1.SecretStore{
Spec: esv1alpha1.SecretStoreSpec{},
},
},
{
name: "spec has no awssm field",
expectErr: "Missing AWS field",
store: &esv1alpha1.SecretStore{
Spec: esv1alpha1.SecretStoreSpec{
Provider: &esv1alpha1.SecretStoreProvider{},
},
},
},
{
name: "configure aws using environment variables",
store: &esv1alpha1.SecretStore{
Spec: esv1alpha1.SecretStoreSpec{
Provider: &esv1alpha1.SecretStoreProvider{
AWS: &esv1alpha1.AWSProvider{},
},
},
},
env: map[string]string{
"AWS_ACCESS_KEY_ID": "1111",
"AWS_SECRET_ACCESS_KEY": "2222",
},
expectProvider: true,
expectedKeyID: "1111",
expectedSecretKey: "2222",
},
{
name: "configure aws using environment variables + assume role",
stsProvider: func(*awssess.Session) stscreds.AssumeRoler {
return &fakesess.AssumeRoler{
AssumeRoleFunc: func(input *sts.AssumeRoleInput) (*sts.AssumeRoleOutput, error) {
assert.Equal(t, *input.RoleArn, "foo-bar-baz")
return &sts.AssumeRoleOutput{
AssumedRoleUser: &sts.AssumedRoleUser{
Arn: aws.String("1123132"),
AssumedRoleId: aws.String("xxxxx"),
},
Credentials: &sts.Credentials{
AccessKeyId: aws.String("3333"),
SecretAccessKey: aws.String("4444"),
Expiration: aws.Time(time.Now().Add(time.Hour)),
SessionToken: aws.String("6666"),
},
}, nil
},
}
},
store: &esv1alpha1.SecretStore{
Spec: esv1alpha1.SecretStoreSpec{
Provider: &esv1alpha1.SecretStoreProvider{
AWS: &esv1alpha1.AWSProvider{
Role: "foo-bar-baz",
},
},
},
},
env: map[string]string{
"AWS_ACCESS_KEY_ID": "1111",
"AWS_SECRET_ACCESS_KEY": "2222",
},
expectProvider: true,
expectedKeyID: "3333",
expectedSecretKey: "4444",
},
{
name: "error out when secret with credentials does not exist",
namespace: "foo",
store: &esv1alpha1.SecretStore{
Spec: esv1alpha1.SecretStoreSpec{
Provider: &esv1alpha1.SecretStoreProvider{
AWS: &esv1alpha1.AWSProvider{
Auth: &esv1alpha1.AWSAuth{
SecretRef: esv1alpha1.AWSAuthSecretRef{
AccessKeyID: esmeta.SecretKeySelector{
Name: "othersecret",
Key: "one",
},
SecretAccessKey: esmeta.SecretKeySelector{
Name: "othersecret",
Key: "two",
},
},
},
},
},
},
},
expectErr: `secrets "othersecret" not found`,
},
{
name: "use credentials from secret to configure aws",
namespace: "foo",
store: &esv1alpha1.SecretStore{
Spec: esv1alpha1.SecretStoreSpec{
Provider: &esv1alpha1.SecretStoreProvider{
AWS: &esv1alpha1.AWSProvider{
Auth: &esv1alpha1.AWSAuth{
SecretRef: esv1alpha1.AWSAuthSecretRef{
AccessKeyID: esmeta.SecretKeySelector{
Name: "onesecret",
// Namespace is not set
Key: "one",
},
SecretAccessKey: esmeta.SecretKeySelector{
Name: "onesecret",
// Namespace is not set
Key: "two",
},
},
},
},
},
},
},
secrets: []v1.Secret{
{
ObjectMeta: metav1.ObjectMeta{
Name: "onesecret",
Namespace: "foo",
},
Data: map[string][]byte{
"one": []byte("1111"),
"two": []byte("2222"),
},
},
},
expectProvider: true,
expectedKeyID: "1111",
expectedSecretKey: "2222",
},
{
name: "error out when secret key does not exist",
namespace: "foo",
store: &esv1alpha1.SecretStore{
Spec: esv1alpha1.SecretStoreSpec{
Provider: &esv1alpha1.SecretStoreProvider{
AWS: &esv1alpha1.AWSProvider{
Auth: &esv1alpha1.AWSAuth{
SecretRef: esv1alpha1.AWSAuthSecretRef{
AccessKeyID: esmeta.SecretKeySelector{
Name: "brokensecret",
Key: "one",
},
SecretAccessKey: esmeta.SecretKeySelector{
Name: "brokensecret",
Key: "two",
},
},
},
},
},
},
},
secrets: []v1.Secret{
{
ObjectMeta: metav1.ObjectMeta{
Name: "brokensecret",
Namespace: "foo",
},
Data: map[string][]byte{},
},
},
expectErr: "missing SecretAccessKey",
},
{
name: "should not be able to access secrets from different namespace",
namespace: "foo",
store: &esv1alpha1.SecretStore{
Spec: esv1alpha1.SecretStoreSpec{
Provider: &esv1alpha1.SecretStoreProvider{
AWS: &esv1alpha1.AWSProvider{
Auth: &esv1alpha1.AWSAuth{
SecretRef: esv1alpha1.AWSAuthSecretRef{
AccessKeyID: esmeta.SecretKeySelector{
Name: "onesecret",
Namespace: aws.String("evil"), // this should not be possible!
Key: "one",
},
SecretAccessKey: esmeta.SecretKeySelector{
Name: "onesecret",
Namespace: aws.String("evil"),
Key: "two",
},
},
},
},
},
},
},
secrets: []v1.Secret{
{
ObjectMeta: metav1.ObjectMeta{
Name: "onesecret",
Namespace: "evil",
},
Data: map[string][]byte{
"one": []byte("1111"),
"two": []byte("2222"),
},
},
},
expectErr: `secrets "onesecret" not found`,
},
{
name: "ClusterStore should use credentials from a specific namespace",
namespace: "es-namespace",
store: &esv1alpha1.ClusterSecretStore{
TypeMeta: metav1.TypeMeta{
APIVersion: esv1alpha1.ClusterSecretStoreKindAPIVersion,
Kind: esv1alpha1.ClusterSecretStoreKind,
},
Spec: esv1alpha1.SecretStoreSpec{
Provider: &esv1alpha1.SecretStoreProvider{
AWS: &esv1alpha1.AWSProvider{
Auth: &esv1alpha1.AWSAuth{
SecretRef: esv1alpha1.AWSAuthSecretRef{
AccessKeyID: esmeta.SecretKeySelector{
Name: "onesecret",
Namespace: aws.String("platform-team-ns"),
Key: "one",
},
SecretAccessKey: esmeta.SecretKeySelector{
Name: "onesecret",
Namespace: aws.String("platform-team-ns"),
Key: "two",
},
},
},
},
},
},
},
secrets: []v1.Secret{
{
ObjectMeta: metav1.ObjectMeta{
Name: "onesecret",
Namespace: "platform-team-ns",
},
Data: map[string][]byte{
"one": []byte("1111"),
"two": []byte("2222"),
},
},
},
expectProvider: true,
expectedKeyID: "1111",
expectedSecretKey: "2222",
},
{
name: "namespace is mandatory when using ClusterStore with SecretKeySelector",
namespace: "es-namespace",
store: &esv1alpha1.ClusterSecretStore{
TypeMeta: metav1.TypeMeta{
APIVersion: esv1alpha1.ClusterSecretStoreKindAPIVersion,
Kind: esv1alpha1.ClusterSecretStoreKind,
},
Spec: esv1alpha1.SecretStoreSpec{
Provider: &esv1alpha1.SecretStoreProvider{
AWS: &esv1alpha1.AWSProvider{
Auth: &esv1alpha1.AWSAuth{
SecretRef: esv1alpha1.AWSAuthSecretRef{
AccessKeyID: esmeta.SecretKeySelector{
Name: "onesecret",
Key: "one",
},
SecretAccessKey: esmeta.SecretKeySelector{
Name: "onesecret",
Key: "two",
},
},
},
},
},
},
},
expectErr: "invalid ClusterSecretStore: missing AWS AccessKeyID Namespace",
},
}
for i := range rows {
row := rows[i]
t.Run(row.name, func(t *testing.T) {
testRow(t, row)
})
}
}
type TestSessionRow struct {
name string
store esv1alpha1.GenericStore
secrets []v1.Secret
namespace string
stsProvider session.STSProvider
expectProvider bool
expectErr string
expectedKeyID string
expectedSecretKey string
env map[string]string
}
func testRow(t *testing.T, row TestSessionRow) {
kc := clientfake.NewClientBuilder().Build()
for i := range row.secrets {
err := kc.Create(context.Background(), &row.secrets[i])
assert.Nil(t, err)
}
for k, v := range row.env {
os.Setenv(k, v)
}
defer func() {
for k := range row.env {
os.Unsetenv(k)
}
}()
s, err := newSession(context.Background(), row.store, kc, row.namespace, row.stsProvider)
if !ErrorContains(err, row.expectErr) {
t.Errorf("expected error %s but found %s", row.expectErr, err.Error())
}
// pass test on expected error
if err != nil {
return
}
if row.expectProvider && s == nil {
t.Errorf("expected provider object, found nil")
return
}
creds, _ := s.Config.Credentials.Get()
assert.Equal(t, creds.AccessKeyID, row.expectedKeyID)
assert.Equal(t, creds.SecretAccessKey, row.expectedSecretKey)
}
func TestSMEnvCredentials(t *testing.T) {
k8sClient := clientfake.NewClientBuilder().Build()
os.Setenv("AWS_SECRET_ACCESS_KEY", "1111")
os.Setenv("AWS_ACCESS_KEY_ID", "2222")
defer os.Unsetenv("AWS_SECRET_ACCESS_KEY")
defer os.Unsetenv("AWS_ACCESS_KEY_ID")
s, err := newSession(context.Background(), &esv1alpha1.SecretStore{
Spec: esv1alpha1.SecretStoreSpec{
Provider: &esv1alpha1.SecretStoreProvider{
// defaults
AWS: &esv1alpha1.AWSProvider{},
},
},
}, k8sClient, "example-ns", session.DefaultSTSProvider)
assert.Nil(t, err)
assert.NotNil(t, s)
creds, err := s.Config.Credentials.Get()
assert.Nil(t, err)
assert.Equal(t, creds.AccessKeyID, "2222")
assert.Equal(t, creds.SecretAccessKey, "1111")
}
func TestSMAssumeRole(t *testing.T) {
k8sClient := clientfake.NewClientBuilder().Build()
sts := &fakesess.AssumeRoler{
AssumeRoleFunc: func(input *sts.AssumeRoleInput) (*sts.AssumeRoleOutput, error) {
// make sure the correct role is passed in
assert.Equal(t, *input.RoleArn, "my-awesome-role")
return &sts.AssumeRoleOutput{
AssumedRoleUser: &sts.AssumedRoleUser{
Arn: aws.String("1123132"),
AssumedRoleId: aws.String("xxxxx"),
},
Credentials: &sts.Credentials{
AccessKeyId: aws.String("3333"),
SecretAccessKey: aws.String("4444"),
Expiration: aws.Time(time.Now().Add(time.Hour)),
SessionToken: aws.String("6666"),
},
}, nil
},
}
os.Setenv("AWS_SECRET_ACCESS_KEY", "1111")
os.Setenv("AWS_ACCESS_KEY_ID", "2222")
defer os.Unsetenv("AWS_SECRET_ACCESS_KEY")
defer os.Unsetenv("AWS_ACCESS_KEY_ID")
s, err := newSession(context.Background(), &esv1alpha1.SecretStore{
Spec: esv1alpha1.SecretStoreSpec{
Provider: &esv1alpha1.SecretStoreProvider{
// do assume role!
AWS: &esv1alpha1.AWSProvider{
Role: "my-awesome-role",
},
},
},
}, k8sClient, "example-ns", func(se *awssess.Session) stscreds.AssumeRoler {
// check if the correct temporary credentials were used
creds, err := se.Config.Credentials.Get()
assert.Nil(t, err)
assert.Equal(t, creds.AccessKeyID, "2222")
assert.Equal(t, creds.SecretAccessKey, "1111")
return sts
})
assert.Nil(t, err)
assert.NotNil(t, s)
creds, err := s.Config.Credentials.Get()
assert.Nil(t, err)
assert.Equal(t, creds.AccessKeyID, "3333")
assert.Equal(t, creds.SecretAccessKey, "4444")
}
func ErrorContains(out error, want string) bool {
if out == nil {
return want == ""
}
if want == "" {
return false
}
return strings.Contains(out.Error(), want)
}

View file

@ -17,7 +17,6 @@ import (
"fmt"
awssm "github.com/aws/aws-sdk-go/service/secretsmanager"
"github.com/aws/aws-sdk-go/service/sts"
"github.com/google/go-cmp/cmp"
)
@ -38,11 +37,3 @@ func (sm *Client) WithValue(in *awssm.GetSecretValueInput, val *awssm.GetSecretV
return val, err
}
}
type AssumeRoler struct {
AssumeRoleFunc func(*sts.AssumeRoleInput) (*sts.AssumeRoleOutput, error)
}
func (f *AssumeRoler) AssumeRole(input *sts.AssumeRoleInput) (*sts.AssumeRoleOutput, error) {
return f.AssumeRoleFunc(input)
}

View file

@ -18,23 +18,17 @@ import (
"encoding/json"
"fmt"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/aws/client"
awssm "github.com/aws/aws-sdk-go/service/secretsmanager"
"github.com/tidwall/gjson"
v1 "k8s.io/api/core/v1"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
"github.com/external-secrets/external-secrets/pkg/provider"
awssess "github.com/external-secrets/external-secrets/pkg/provider/aws/session"
)
// SecretsManager is a provider for AWS SecretsManager.
type SecretsManager struct {
session *session.Session
stsProvider awssess.STSProvider
client SMInterface
client SMInterface
}
// SMInterface is a subset of the smiface api.
@ -45,77 +39,11 @@ type SMInterface interface {
var log = ctrl.Log.WithName("provider").WithName("aws").WithName("secretsmanager")
// New constructs a SecretsManager Provider that is specific to a store.
func New(ctx context.Context, store esv1alpha1.GenericStore, kube client.Client, namespace string, stsProvider awssess.STSProvider) (provider.SecretsClient, error) {
sm := &SecretsManager{
stsProvider: stsProvider,
}
if store == nil {
return nil, fmt.Errorf("found nil store")
}
spc := store.GetSpec()
if spc == nil {
return nil, fmt.Errorf("store is missing spec")
}
if spc.Provider == nil {
return nil, fmt.Errorf("storeSpec is missing provider")
}
smProvider := spc.Provider.AWS
if smProvider == nil {
return nil, fmt.Errorf("invalid provider spec. Missing AWSSM field in store %s", store.GetObjectMeta().String())
}
var sak, aks string
// use provided credentials via secret reference
if smProvider.Auth != nil {
log.V(1).Info("fetching secrets for authentication")
ke := client.ObjectKey{
Name: smProvider.Auth.SecretRef.AccessKeyID.Name,
Namespace: namespace, // default to ExternalSecret namespace
}
// only ClusterStore is allowed to set namespace (and then it's required)
if store.GetObjectKind().GroupVersionKind().Kind == esv1alpha1.ClusterSecretStoreKind {
if smProvider.Auth.SecretRef.AccessKeyID.Namespace == nil {
return nil, fmt.Errorf("invalid ClusterSecretStore: missing AWSSM AccessKeyID Namespace")
}
ke.Namespace = *smProvider.Auth.SecretRef.AccessKeyID.Namespace
}
akSecret := v1.Secret{}
err := kube.Get(ctx, ke, &akSecret)
if err != nil {
return nil, fmt.Errorf("could not fetch accessKeyID secret: %w", err)
}
ke = client.ObjectKey{
Name: smProvider.Auth.SecretRef.SecretAccessKey.Name,
Namespace: namespace, // default to ExternalSecret namespace
}
// only ClusterStore is allowed to set namespace (and then it's required)
if store.GetObjectKind().GroupVersionKind().Kind == esv1alpha1.ClusterSecretStoreKind {
if smProvider.Auth.SecretRef.SecretAccessKey.Namespace == nil {
return nil, fmt.Errorf("invalid ClusterSecretStore: missing AWSSM SecretAccessKey Namespace")
}
ke.Namespace = *smProvider.Auth.SecretRef.SecretAccessKey.Namespace
}
sakSecret := v1.Secret{}
err = kube.Get(ctx, ke, &sakSecret)
if err != nil {
return nil, fmt.Errorf("could not fetch SecretAccessKey secret: %w", err)
}
sak = string(sakSecret.Data[smProvider.Auth.SecretRef.SecretAccessKey.Key])
aks = string(akSecret.Data[smProvider.Auth.SecretRef.AccessKeyID.Key])
if sak == "" {
return nil, fmt.Errorf("missing SecretAccessKey")
}
if aks == "" {
return nil, fmt.Errorf("missing AccessKeyID")
}
}
sess, err := awssess.New(sak, aks, smProvider.Region, smProvider.Role, sm.stsProvider)
if err != nil {
return nil, err
}
sm.session = sess
sm.client = awssm.New(sess)
return sm, nil
// New creates a new SecretsManager client.
func New(sess client.ConfigProvider) (*SecretsManager, error) {
return &SecretsManager{
client: awssm.New(sess),
}, nil
}
// GetSecret returns a single secret from the provider.

View file

@ -16,451 +16,25 @@ package secretsmanager
import (
"context"
"fmt"
"os"
"strings"
"testing"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials/stscreds"
"github.com/aws/aws-sdk-go/aws/session"
awssm "github.com/aws/aws-sdk-go/service/secretsmanager"
"github.com/aws/aws-sdk-go/service/sts"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
clientfake "sigs.k8s.io/controller-runtime/pkg/client/fake"
esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
fakesm "github.com/external-secrets/external-secrets/pkg/provider/aws/secretsmanager/fake"
awssess "github.com/external-secrets/external-secrets/pkg/provider/aws/session"
sess "github.com/external-secrets/external-secrets/pkg/provider/aws/session"
)
func TestConstructor(t *testing.T) {
rows := []ConstructorRow{
{
name: "nil store",
expectErr: "found nil store",
store: nil,
},
{
name: "not store spec",
expectErr: "storeSpec is missing provider",
store: &esv1alpha1.SecretStore{},
},
{
name: "store spec has no provider",
expectErr: "storeSpec is missing provider",
store: &esv1alpha1.SecretStore{
Spec: esv1alpha1.SecretStoreSpec{},
},
},
{
name: "spec has no awssm field",
expectErr: "Missing AWSSM field",
store: &esv1alpha1.SecretStore{
Spec: esv1alpha1.SecretStoreSpec{
Provider: &esv1alpha1.SecretStoreProvider{},
},
},
},
{
name: "configure aws using environment variables",
store: &esv1alpha1.SecretStore{
Spec: esv1alpha1.SecretStoreSpec{
Provider: &esv1alpha1.SecretStoreProvider{
AWS: &esv1alpha1.AWSProvider{},
},
},
},
env: map[string]string{
"AWS_ACCESS_KEY_ID": "1111",
"AWS_SECRET_ACCESS_KEY": "2222",
},
expectProvider: true,
expectedKeyID: "1111",
expectedSecretKey: "2222",
},
{
name: "configure aws using environment variables + assume role",
stsProvider: func(*session.Session) stscreds.AssumeRoler {
return &fakesm.AssumeRoler{
AssumeRoleFunc: func(input *sts.AssumeRoleInput) (*sts.AssumeRoleOutput, error) {
assert.Equal(t, *input.RoleArn, "foo-bar-baz")
return &sts.AssumeRoleOutput{
AssumedRoleUser: &sts.AssumedRoleUser{
Arn: aws.String("1123132"),
AssumedRoleId: aws.String("xxxxx"),
},
Credentials: &sts.Credentials{
AccessKeyId: aws.String("3333"),
SecretAccessKey: aws.String("4444"),
Expiration: aws.Time(time.Now().Add(time.Hour)),
SessionToken: aws.String("6666"),
},
}, nil
},
}
},
store: &esv1alpha1.SecretStore{
Spec: esv1alpha1.SecretStoreSpec{
Provider: &esv1alpha1.SecretStoreProvider{
AWS: &esv1alpha1.AWSProvider{
Role: "foo-bar-baz",
},
},
},
},
env: map[string]string{
"AWS_ACCESS_KEY_ID": "1111",
"AWS_SECRET_ACCESS_KEY": "2222",
},
expectProvider: true,
expectedKeyID: "3333",
expectedSecretKey: "4444",
},
{
name: "error out when secret with credentials does not exist",
namespace: "foo",
store: &esv1alpha1.SecretStore{
Spec: esv1alpha1.SecretStoreSpec{
Provider: &esv1alpha1.SecretStoreProvider{
AWS: &esv1alpha1.AWSProvider{
Auth: &esv1alpha1.AWSAuth{
SecretRef: esv1alpha1.AWSAuthSecretRef{
AccessKeyID: esmeta.SecretKeySelector{
Name: "othersecret",
Key: "one",
},
SecretAccessKey: esmeta.SecretKeySelector{
Name: "othersecret",
Key: "two",
},
},
},
},
},
},
},
expectErr: `secrets "othersecret" not found`,
},
{
name: "use credentials from secret to configure aws",
namespace: "foo",
store: &esv1alpha1.SecretStore{
Spec: esv1alpha1.SecretStoreSpec{
Provider: &esv1alpha1.SecretStoreProvider{
AWS: &esv1alpha1.AWSProvider{
Auth: &esv1alpha1.AWSAuth{
SecretRef: esv1alpha1.AWSAuthSecretRef{
AccessKeyID: esmeta.SecretKeySelector{
Name: "onesecret",
// Namespace is not set
Key: "one",
},
SecretAccessKey: esmeta.SecretKeySelector{
Name: "onesecret",
// Namespace is not set
Key: "two",
},
},
},
},
},
},
},
secrets: []v1.Secret{
{
ObjectMeta: metav1.ObjectMeta{
Name: "onesecret",
Namespace: "foo",
},
Data: map[string][]byte{
"one": []byte("1111"),
"two": []byte("2222"),
},
},
},
expectProvider: true,
expectedKeyID: "1111",
expectedSecretKey: "2222",
},
{
name: "error out when secret key does not exist",
namespace: "foo",
store: &esv1alpha1.SecretStore{
Spec: esv1alpha1.SecretStoreSpec{
Provider: &esv1alpha1.SecretStoreProvider{
AWS: &esv1alpha1.AWSProvider{
Auth: &esv1alpha1.AWSAuth{
SecretRef: esv1alpha1.AWSAuthSecretRef{
AccessKeyID: esmeta.SecretKeySelector{
Name: "brokensecret",
Key: "one",
},
SecretAccessKey: esmeta.SecretKeySelector{
Name: "brokensecret",
Key: "two",
},
},
},
},
},
},
},
secrets: []v1.Secret{
{
ObjectMeta: metav1.ObjectMeta{
Name: "brokensecret",
Namespace: "foo",
},
Data: map[string][]byte{},
},
},
expectErr: "missing SecretAccessKey",
},
{
name: "should not be able to access secrets from different namespace",
namespace: "foo",
store: &esv1alpha1.SecretStore{
Spec: esv1alpha1.SecretStoreSpec{
Provider: &esv1alpha1.SecretStoreProvider{
AWS: &esv1alpha1.AWSProvider{
Auth: &esv1alpha1.AWSAuth{
SecretRef: esv1alpha1.AWSAuthSecretRef{
AccessKeyID: esmeta.SecretKeySelector{
Name: "onesecret",
Namespace: aws.String("evil"), // this should not be possible!
Key: "one",
},
SecretAccessKey: esmeta.SecretKeySelector{
Name: "onesecret",
Namespace: aws.String("evil"),
Key: "two",
},
},
},
},
},
},
},
secrets: []v1.Secret{
{
ObjectMeta: metav1.ObjectMeta{
Name: "onesecret",
Namespace: "evil",
},
Data: map[string][]byte{
"one": []byte("1111"),
"two": []byte("2222"),
},
},
},
expectErr: `secrets "onesecret" not found`,
},
{
name: "ClusterStore should use credentials from a specific namespace",
namespace: "es-namespace",
store: &esv1alpha1.ClusterSecretStore{
TypeMeta: metav1.TypeMeta{
APIVersion: esv1alpha1.ClusterSecretStoreKindAPIVersion,
Kind: esv1alpha1.ClusterSecretStoreKind,
},
Spec: esv1alpha1.SecretStoreSpec{
Provider: &esv1alpha1.SecretStoreProvider{
AWS: &esv1alpha1.AWSProvider{
Auth: &esv1alpha1.AWSAuth{
SecretRef: esv1alpha1.AWSAuthSecretRef{
AccessKeyID: esmeta.SecretKeySelector{
Name: "onesecret",
Namespace: aws.String("platform-team-ns"),
Key: "one",
},
SecretAccessKey: esmeta.SecretKeySelector{
Name: "onesecret",
Namespace: aws.String("platform-team-ns"),
Key: "two",
},
},
},
},
},
},
},
secrets: []v1.Secret{
{
ObjectMeta: metav1.ObjectMeta{
Name: "onesecret",
Namespace: "platform-team-ns",
},
Data: map[string][]byte{
"one": []byte("1111"),
"two": []byte("2222"),
},
},
},
expectProvider: true,
expectedKeyID: "1111",
expectedSecretKey: "2222",
},
{
name: "namespace is mandatory when using ClusterStore with SecretKeySelector",
namespace: "es-namespace",
store: &esv1alpha1.ClusterSecretStore{
TypeMeta: metav1.TypeMeta{
APIVersion: esv1alpha1.ClusterSecretStoreKindAPIVersion,
Kind: esv1alpha1.ClusterSecretStoreKind,
},
Spec: esv1alpha1.SecretStoreSpec{
Provider: &esv1alpha1.SecretStoreProvider{
AWS: &esv1alpha1.AWSProvider{
Auth: &esv1alpha1.AWSAuth{
SecretRef: esv1alpha1.AWSAuthSecretRef{
AccessKeyID: esmeta.SecretKeySelector{
Name: "onesecret",
Key: "one",
},
SecretAccessKey: esmeta.SecretKeySelector{
Name: "onesecret",
Key: "two",
},
},
},
},
},
},
},
expectErr: "invalid ClusterSecretStore: missing AWSSM AccessKeyID Namespace",
},
}
for i := range rows {
row := rows[i]
t.Run(row.name, func(t *testing.T) {
testRow(t, row)
})
}
}
type ConstructorRow struct {
name string
store esv1alpha1.GenericStore
secrets []v1.Secret
namespace string
stsProvider awssess.STSProvider
expectProvider bool
expectErr string
expectedKeyID string
expectedSecretKey string
env map[string]string
}
func testRow(t *testing.T, row ConstructorRow) {
kc := clientfake.NewClientBuilder().Build()
for i := range row.secrets {
err := kc.Create(context.Background(), &row.secrets[i])
assert.Nil(t, err)
}
for k, v := range row.env {
os.Setenv(k, v)
}
defer func() {
for k := range row.env {
os.Unsetenv(k)
}
}()
newsm, err := New(context.Background(), row.store, kc, row.namespace, row.stsProvider)
if !ErrorContains(err, row.expectErr) {
t.Errorf("expected error %s but found %s", row.expectErr, err.Error())
}
// pass test on expected error
if err != nil {
return
}
if row.expectProvider && newsm == nil {
t.Errorf("expected provider object, found nil")
return
}
creds, _ := newsm.(*SecretsManager).session.Config.Credentials.Get()
assert.Equal(t, creds.AccessKeyID, row.expectedKeyID)
assert.Equal(t, creds.SecretAccessKey, row.expectedSecretKey)
}
func TestSMEnvCredentials(t *testing.T) {
k8sClient := clientfake.NewClientBuilder().Build()
os.Setenv("AWS_SECRET_ACCESS_KEY", "1111")
os.Setenv("AWS_ACCESS_KEY_ID", "2222")
defer os.Unsetenv("AWS_SECRET_ACCESS_KEY")
defer os.Unsetenv("AWS_ACCESS_KEY_ID")
smi, err := New(context.Background(), &esv1alpha1.SecretStore{
Spec: esv1alpha1.SecretStoreSpec{
Provider: &esv1alpha1.SecretStoreProvider{
// defaults
AWS: &esv1alpha1.AWSProvider{},
},
},
}, k8sClient, "example-ns", awssess.DefaultSTSProvider)
s, err := sess.New("1111", "2222", "foo", "", nil)
assert.Nil(t, err)
assert.NotNil(t, smi)
sm, ok := smi.(*SecretsManager)
assert.True(t, ok)
creds, err := sm.session.Config.Credentials.Get()
c, err := New(s)
assert.Nil(t, err)
assert.Equal(t, creds.AccessKeyID, "2222")
assert.Equal(t, creds.SecretAccessKey, "1111")
}
func TestSMAssumeRole(t *testing.T) {
k8sClient := clientfake.NewClientBuilder().Build()
sts := &fakesm.AssumeRoler{
AssumeRoleFunc: func(input *sts.AssumeRoleInput) (*sts.AssumeRoleOutput, error) {
// make sure the correct role is passed in
assert.Equal(t, *input.RoleArn, "my-awesome-role")
return &sts.AssumeRoleOutput{
AssumedRoleUser: &sts.AssumedRoleUser{
Arn: aws.String("1123132"),
AssumedRoleId: aws.String("xxxxx"),
},
Credentials: &sts.Credentials{
AccessKeyId: aws.String("3333"),
SecretAccessKey: aws.String("4444"),
Expiration: aws.Time(time.Now().Add(time.Hour)),
SessionToken: aws.String("6666"),
},
}, nil
},
}
os.Setenv("AWS_SECRET_ACCESS_KEY", "1111")
os.Setenv("AWS_ACCESS_KEY_ID", "2222")
defer os.Unsetenv("AWS_SECRET_ACCESS_KEY")
defer os.Unsetenv("AWS_ACCESS_KEY_ID")
smi, err := New(context.Background(), &esv1alpha1.SecretStore{
Spec: esv1alpha1.SecretStoreSpec{
Provider: &esv1alpha1.SecretStoreProvider{
// do assume role!
AWS: &esv1alpha1.AWSProvider{
Role: "my-awesome-role",
},
},
},
}, k8sClient, "example-ns", func(se *session.Session) stscreds.AssumeRoler {
// check if the correct temporary credentials were used
creds, err := se.Config.Credentials.Get()
assert.Nil(t, err)
assert.Equal(t, creds.AccessKeyID, "2222")
assert.Equal(t, creds.SecretAccessKey, "1111")
return sts
})
assert.Nil(t, err)
assert.NotNil(t, smi)
sm, ok := smi.(*SecretsManager)
assert.True(t, ok)
creds, err := sm.session.Config.Credentials.Get()
assert.Nil(t, err)
assert.Equal(t, creds.AccessKeyID, "3333")
assert.Equal(t, creds.SecretAccessKey, "4444")
assert.NotNil(t, c.client)
}
// test the sm<->aws interface

View file

@ -0,0 +1,24 @@
/*
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package fake
import "github.com/aws/aws-sdk-go/service/sts"
type AssumeRoler struct {
AssumeRoleFunc func(*sts.AssumeRoleInput) (*sts.AssumeRoleOutput, error)
}
func (f *AssumeRoler) AssumeRole(input *sts.AssumeRoleInput) (*sts.AssumeRoleOutput, error) {
return f.AssumeRoleFunc(input)
}

View file

@ -1,3 +1,16 @@
/*
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package session
import (

View file

@ -1,3 +1,16 @@
/*
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package session
import (
@ -10,7 +23,7 @@ import (
"github.com/aws/aws-sdk-go/service/sts"
"github.com/stretchr/testify/assert"
fakesm "github.com/external-secrets/external-secrets/pkg/provider/aws/secretsmanager/fake"
fakesess "github.com/external-secrets/external-secrets/pkg/provider/aws/session/fake"
)
func TestSession(t *testing.T) {
@ -41,7 +54,7 @@ func TestSession(t *testing.T) {
region: "xxxxx",
role: "zzzzz",
sts: func(*session.Session) stscreds.AssumeRoler {
return &fakesm.AssumeRoler{
return &fakesess.AssumeRoler{
AssumeRoleFunc: func(input *sts.AssumeRoleInput) (*sts.AssumeRoleOutput, error) {
assert.Equal(t, *input.RoleArn, "zzzzz")
return &sts.AssumeRoleOutput{