Browse Source

feat: add ssm parameter store support (#59)

* feat: add parameter store implementation
Moritz Johner 5 years ago
parent
commit
2c059b71ba

BIN
docs/pictures/diagrams-provider-aws-ssm-parameter-store.png


File diff suppressed because it is too large
+ 1 - 1
docs/pictures/diagrams.drawio


+ 71 - 4
docs/provider-aws-parameter-store.md

@@ -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"

+ 39 - 5
docs/provider-aws-secrets-manager.md

@@ -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"

+ 21 - 0
docs/snippets/aws-parameter-store.yaml

@@ -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

+ 39 - 0
pkg/provider/aws/parameterstore/fake/fake.go

@@ -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
+	}
+}

+ 43 - 13
pkg/provider/aws/parameterstore/parameterstore.go

@@ -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
 }

+ 262 - 0
pkg/provider/aws/parameterstore/parameterstore_test.go

@@ -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)
+}

+ 105 - 13
pkg/provider/aws/provider.go

@@ -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) {
+	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
+	}
+	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("store is nil")
+		return nil, fmt.Errorf("found nil store")
 	}
-	spec := store.GetSpec()
-	if spec == nil {
+	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")
-	}
-	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)
+	prov := spc.Provider.AWS
+	if prov == nil {
+		return nil, fmt.Errorf("invalid provider spec. Missing AWS field in store %s", store.GetObjectMeta().String())
 	}
-	return nil, fmt.Errorf("unknown AWS Provider Service: %s", spec.Provider.AWS.Service)
+	return prov, nil
 }
 
 func init() {

+ 515 - 6
pkg/provider/aws/provider_test.go

@@ -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,15 +68,63 @@ 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{
+						AWS: &esv1alpha1.AWSProvider{
+							Service: esv1alpha1.AWSServiceParameterStore,
+						},
+					},
+				},
+			},
+		},
+		{
+			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"),
+									},
+								},
+							},
 						},
 					},
 				},
@@ -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)
+}

+ 0 - 9
pkg/provider/aws/secretsmanager/fake/fake.go

@@ -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)
-}

+ 7 - 79
pkg/provider/aws/secretsmanager/secretsmanager.go

@@ -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.

+ 4 - 430
pkg/provider/aws/secretsmanager/secretsmanager_test.go

@@ -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)
-	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, "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
-	})
+	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, "3333")
-	assert.Equal(t, creds.SecretAccessKey, "4444")
+	assert.NotNil(t, c.client)
 }
 
 // test the sm<->aws interface

+ 24 - 0
pkg/provider/aws/session/fake/assumeroler.go

@@ -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)
+}

+ 13 - 0
pkg/provider/aws/session/session.go

@@ -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 (

+ 15 - 2
pkg/provider/aws/session/session_test.go

@@ -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{