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:
parent
41429040b0
commit
2c059b71ba
16 changed files with 1160 additions and 563 deletions
BIN
docs/pictures/diagrams-provider-aws-ssm-parameter-store.png
Normal file
BIN
docs/pictures/diagrams-provider-aws-ssm-parameter-store.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 52 KiB |
File diff suppressed because one or more lines are too long
|
@ -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"
|
||||
|
|
|
@ -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
docs/snippets/aws-parameter-store.yaml
Normal file
21
docs/snippets/aws-parameter-store.yaml
Normal 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
|
39
pkg/provider/aws/parameterstore/fake/fake.go
Normal file
39
pkg/provider/aws/parameterstore/fake/fake.go
Normal 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
|
||||
}
|
||||
}
|
|
@ -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
pkg/provider/aws/parameterstore/parameterstore_test.go
Normal file
262
pkg/provider/aws/parameterstore/parameterstore_test.go
Normal 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)
|
||||
}
|
|
@ -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() {
|
||||
|
|
|
@ -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: ¶meterstore.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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
24
pkg/provider/aws/session/fake/assumeroler.go
Normal file
24
pkg/provider/aws/session/fake/assumeroler.go
Normal 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)
|
||||
}
|
|
@ -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 (
|
||||
|
|
|
@ -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{
|
||||
|
|
Loading…
Reference in a new issue