diff --git a/go.mod b/go.mod index c20ad76..2286f64 100644 --- a/go.mod +++ b/go.mod @@ -3,17 +3,17 @@ module github.com/hashicorp/boundary-plugin-host-aws go 1.16 require ( - github.com/aws/aws-sdk-go v1.40.44 - github.com/fatih/color v1.12.0 // indirect + github.com/aws/aws-sdk-go v1.40.55 + github.com/fatih/color v1.13.0 // indirect github.com/google/go-cmp v0.5.6 // indirect github.com/hashicorp/boundary/sdk v0.0.11-0.20210926154843-a834a147850b - github.com/hashicorp/go-secure-stdlib/awsutil v0.1.4 + github.com/hashicorp/go-secure-stdlib/awsutil v0.1.6-0.20211004211126-d07ba63494a2 github.com/hashicorp/terraform-json v0.12.0 - github.com/mattn/go-isatty v0.0.13 // indirect + github.com/mattn/go-colorable v0.1.11 // indirect github.com/rogpeppe/go-internal v1.6.2 // indirect github.com/stretchr/testify v1.7.0 golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d // indirect - golang.org/x/sys v0.0.0-20210917161153-d61c044b1678 // indirect + golang.org/x/sys v0.0.0-20211004093028-2c5d950f24ef // indirect google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83 // indirect google.golang.org/protobuf v1.27.1 gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index d677e03..4b66c38 100644 --- a/go.sum +++ b/go.sum @@ -65,8 +65,8 @@ github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/aws/aws-sdk-go v1.25.37/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.30.27/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= -github.com/aws/aws-sdk-go v1.40.44 h1:kECaYybTWYZY5IKHvQMxbE6Wi5Qrb+7hbkV7zQV3Sg8= -github.com/aws/aws-sdk-go v1.40.44/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q= +github.com/aws/aws-sdk-go v1.40.55 h1:l5xNBmafEAfdJIIe32QbTLG3j/JscH8fzZqYyMlntuI= +github.com/aws/aws-sdk-go v1.40.55/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q= github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f/go.mod h1:AuiFmCCPBSrqvVMvuqFuk0qogytodnVFVSN5CeJB8Gc= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= @@ -115,8 +115,8 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.m github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fatih/color v1.12.0 h1:mRhaKNwANqRgUBGKmnI5ZxEk7QXmjQeCcuYFMX2bfcc= -github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= +github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/frankban/quicktest v1.10.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= @@ -224,8 +224,8 @@ github.com/hashicorp/go-retryablehttp v0.6.6/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER github.com/hashicorp/go-rootcerts v1.0.1/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= github.com/hashicorp/go-secure-stdlib/awsutil v0.1.2/go.mod h1:QRJZ7siKie+SZJB9jLbfKrs0Gd0yPWMtbneg0iU1PrY= -github.com/hashicorp/go-secure-stdlib/awsutil v0.1.4 h1:aCPwC2WPyomaw5gk5ZZ7DPevN8d9DS3igkmq6DxGMjU= -github.com/hashicorp/go-secure-stdlib/awsutil v0.1.4/go.mod h1:MpCPSPGLDILGb4JMm94/mMi3YysIqsXzGCzkEZjcjXg= +github.com/hashicorp/go-secure-stdlib/awsutil v0.1.6-0.20211004211126-d07ba63494a2 h1:P5iG1hox8kzIetqnXn/VgrARsAtooV7jK0McTkIOqQI= +github.com/hashicorp/go-secure-stdlib/awsutil v0.1.6-0.20211004211126-d07ba63494a2/go.mod h1:MpCPSPGLDILGb4JMm94/mMi3YysIqsXzGCzkEZjcjXg= github.com/hashicorp/go-secure-stdlib/configutil v0.1.2/go.mod h1:EN1DJMjv9y5e/uRAuP2WPibAox5KHEiq2BjZl00aNPQ= github.com/hashicorp/go-secure-stdlib/listenerutil v0.1.1/go.mod h1:irE9ILwhE8VooWdhHhBy0qXzOMRlHKoWoMlr31mAFCE= github.com/hashicorp/go-secure-stdlib/parseutil v0.1.1/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8= @@ -285,14 +285,15 @@ github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LE github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= -github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.11 h1:nQ+aFkoE2TMGc0b68U2OKSexC+eq46+XwZzWXHRmPYs= +github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.13 h1:qdl+GuBjcsKKDco5BsxPJlId98mSWNKqYA+Co0SC1yA= -github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/cli v1.1.2/go.mod h1:6iaV0fGdElS6dPBx0EApTxHrcWvmJphyh2n8YBLPPZ4= @@ -519,8 +520,10 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210917161153-d61c044b1678 h1:J27LZFQBFoihqXoegpscI10HpjZ7B5WQLLKL2FZXQKw= -golang.org/x/sys v0.0.0-20210917161153-d61c044b1678/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211004093028-2c5d950f24ef h1:fPxZ3Umkct3LZ8gK9nbk+DWDJ9fstZa2grBn+lWVKPs= +golang.org/x/sys v0.0.0-20211004093028-2c5d950f24ef/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/plugin/helper_test.go b/plugin/helper_test.go new file mode 100644 index 0000000..174bcbc --- /dev/null +++ b/plugin/helper_test.go @@ -0,0 +1,40 @@ +package plugin + +import ( + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/iam" + "github.com/aws/aws-sdk-go/service/iam/iamiface" + "github.com/hashicorp/go-secure-stdlib/awsutil" +) + +type testMockIAMState struct { + DeleteAccessKeyCalled bool +} + +func (s *testMockIAMState) Reset() { + s.DeleteAccessKeyCalled = false +} + +type testMockIAM struct { + iamiface.IAMAPI + + State *testMockIAMState +} + +func newTestMockIAM(state *testMockIAMState, opts ...awsutil.MockIAMOption) awsutil.IAMAPIFunc { + return func(sess *session.Session) (iamiface.IAMAPI, error) { + m := &testMockIAM{ + State: state, + } + f := awsutil.NewMockIAM(opts...) + var err error + + m.IAMAPI, err = f(sess) + return m, err + } +} + +func (m *testMockIAM) DeleteAccessKey(input *iam.DeleteAccessKeyInput) (*iam.DeleteAccessKeyOutput, error) { + m.State.DeleteAccessKeyCalled = true + return m.IAMAPI.DeleteAccessKey(input) +} diff --git a/plugin/state.go b/plugin/state.go index 2f2d7e6..44b5faf 100644 --- a/plugin/state.go +++ b/plugin/state.go @@ -19,6 +19,9 @@ type awsCatalogPersistedState struct { AccessKeyId string SecretAccessKey string CredsLastRotatedTime time.Time + + // testOpts are options that should be used for testing only + testOpts []awsutil.Option } func awsCatalogPersistedStateFromProto(in *pb.HostCatalogPersisted) (*awsCatalogPersistedState, error) { @@ -63,15 +66,15 @@ func (s *awsCatalogPersistedState) ToProto() (*pb.HostCatalogPersisted, error) { } func (s *awsCatalogPersistedState) ValidateCreds() error { - c, err := awsutil.NewCredentialsConfig( + c, err := awsutil.NewCredentialsConfig(append([]awsutil.Option{ awsutil.WithAccessKey(s.AccessKeyId), awsutil.WithSecretKey(s.SecretAccessKey), - ) + }, s.testOpts...)...) if err != nil { return fmt.Errorf("error loading credentials: %w", err) } - if _, err := c.GetCallerIdentity(); err != nil { + if _, err := c.GetCallerIdentity(s.testOpts...); err != nil { return fmt.Errorf("error validating credentials: %w", err) } @@ -79,15 +82,17 @@ func (s *awsCatalogPersistedState) ValidateCreds() error { } func (s *awsCatalogPersistedState) RotateCreds() error { - c, err := awsutil.NewCredentialsConfig( + c, err := awsutil.NewCredentialsConfig(append([]awsutil.Option{ awsutil.WithAccessKey(s.AccessKeyId), awsutil.WithSecretKey(s.SecretAccessKey), - ) + }, s.testOpts...)...) if err != nil { return fmt.Errorf("error loading credentials: %w", err) } - if err := c.RotateKeys(awsutil.WithValidityCheckTimeout(rotationWaitTimeout)); err != nil { + if err := c.RotateKeys(append([]awsutil.Option{ + awsutil.WithValidityCheckTimeout(rotationWaitTimeout), + }, s.testOpts...)...); err != nil { return fmt.Errorf("error rotating credentials: %w", err) } @@ -134,22 +139,25 @@ func (s *awsCatalogPersistedState) ReplaceCreds(accessKeyId, secretAccessKey str // ID, secret access key, and rotation time fields are zeroed out in // the state just to ensure that they cannot be re-used after. func (s *awsCatalogPersistedState) DeleteCreds() error { - c, err := awsutil.NewCredentialsConfig( + c, err := awsutil.NewCredentialsConfig(append([]awsutil.Option{ awsutil.WithAccessKey(s.AccessKeyId), awsutil.WithSecretKey(s.SecretAccessKey), - ) + }, s.testOpts...)...) if err != nil { return fmt.Errorf("error loading credentials: %w", err) } - if err := c.DeleteAccessKey(s.AccessKeyId); err != nil { + if err := c.DeleteAccessKey(s.AccessKeyId, s.testOpts...); err != nil { // Determine if the deletion error was due to a missing // resource. If it was, just pass it. - if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == iam.ErrCodeNoSuchEntityException { - s.AccessKeyId = "" - s.SecretAccessKey = "" - s.CredsLastRotatedTime = time.Time{} - return nil + var awsErr awserr.Error + if errors.As(err, &awsErr) { + if awsErr.Code() == iam.ErrCodeNoSuchEntityException { + s.AccessKeyId = "" + s.SecretAccessKey = "" + s.CredsLastRotatedTime = time.Time{} + return nil + } } // Otherwise treat it as an actual error. @@ -165,10 +173,10 @@ func (s *awsCatalogPersistedState) DeleteCreds() error { // GetSession returns a configured AWS session for the credentials in // the state. func (s *awsCatalogPersistedState) GetSession() (*session.Session, error) { - c, err := awsutil.NewCredentialsConfig( + c, err := awsutil.NewCredentialsConfig(append([]awsutil.Option{ awsutil.WithAccessKey(s.AccessKeyId), awsutil.WithSecretKey(s.SecretAccessKey), - ) + }, s.testOpts...)...) if err != nil { return nil, err } diff --git a/plugin/state_test.go b/plugin/state_test.go index 3840ced..86bfa64 100644 --- a/plugin/state_test.go +++ b/plugin/state_test.go @@ -1,13 +1,24 @@ package plugin import ( + "errors" + "fmt" "testing" "time" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/iam" pb "github.com/hashicorp/boundary/sdk/pbs/plugin" + "github.com/hashicorp/go-secure-stdlib/awsutil" "github.com/stretchr/testify/require" ) +const testOptionErr = "test option error" +const testGetCallerIdentityErr = "test error for GetCallerIdentity" +const testGetUserErr = "test error for GetUser" +const testDeleteAccessKeyErr = "test error for DeleteAccessKey" + func TestAwsCatalogPersistedStateFromProto(t *testing.T) { cases := []struct { name string @@ -108,3 +119,403 @@ func TestAwsCatalogPersistedStateFromProto(t *testing.T) { }) } } + +func TestAwsCatalogPersistedStateToProto(t *testing.T) { + cases := []struct { + name string + in *awsCatalogPersistedState + expected *pb.HostCatalogPersisted + expectedErr string // NOTE: Here for scaffolding but hard to test deep structpb err right now + }{ + { + name: "good with non-zero timestamp", + in: &awsCatalogPersistedState{ + AccessKeyId: "foobar", + SecretAccessKey: "bazqux", + CredsLastRotatedTime: func() time.Time { + t, err := time.Parse(time.RFC3339, "2006-01-02T15:04:05+07:00") + if err != nil { + panic(err) + } + + return t + }(), + }, + expected: &pb.HostCatalogPersisted{ + Secrets: mustStruct(map[string]interface{}{ + constAccessKeyId: "foobar", + constSecretAccessKey: "bazqux", + constCredsLastRotatedTime: "2006-01-02T15:04:05+07:00", + }), + }, + }, + { + name: "good with zero timestamp", + in: &awsCatalogPersistedState{ + AccessKeyId: "foobar", + SecretAccessKey: "bazqux", + CredsLastRotatedTime: time.Time{}, + }, + expected: &pb.HostCatalogPersisted{ + Secrets: mustStruct(map[string]interface{}{ + constAccessKeyId: "foobar", + constSecretAccessKey: "bazqux", + constCredsLastRotatedTime: (time.Time{}).Format(time.RFC3339Nano), + }), + }, + }, + } + + for _, tc := range cases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + require := require.New(t) + actual, err := tc.in.ToProto() + if tc.expectedErr != "" { + require.EqualError(err, tc.expectedErr) + return + } + + require.NoError(err) + require.Equal(tc.expected, actual) + }) + } +} + +func TestAwsCatalogPersistedStateValidateCreds(t *testing.T) { + cases := []struct { + name string + in *awsCatalogPersistedState + expectedErr string + }{ + { + name: "could not load credentials", + in: &awsCatalogPersistedState{ + testOpts: []awsutil.Option{awsutil.MockOptionErr(errors.New(testOptionErr))}, + }, + expectedErr: fmt.Sprintf("error loading credentials: error reading options in NewCredentialsConfig: %s", testOptionErr), + }, + { + name: "validation error", + in: &awsCatalogPersistedState{ + testOpts: []awsutil.Option{awsutil.WithSTSAPIFunc(awsutil.NewMockSTS(awsutil.WithGetCallerIdentityError(errors.New(testGetCallerIdentityErr))))}, + }, + expectedErr: fmt.Sprintf("error validating credentials: %s", testGetCallerIdentityErr), + }, + { + name: "good", + in: &awsCatalogPersistedState{ + AccessKeyId: "foobar", + SecretAccessKey: "bazqux", + testOpts: []awsutil.Option{awsutil.WithSTSAPIFunc(awsutil.NewMockSTS())}, + }, + }, + } + + for _, tc := range cases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + require := require.New(t) + err := tc.in.ValidateCreds() + if tc.expectedErr != "" { + require.EqualError(err, tc.expectedErr) + return + } + + require.NoError(err) + }) + } +} + +func TestAwsCatalogPersistedStateRotateCreds(t *testing.T) { + cases := []struct { + name string + in *awsCatalogPersistedState + expectedAccessKeyId string + expectedSecretAccessKey string + expectedNonZeroRotatedTime bool + expectedErr string + }{ + { + name: "could not load credentials", + in: &awsCatalogPersistedState{ + testOpts: []awsutil.Option{awsutil.MockOptionErr(errors.New(testOptionErr))}, + }, + expectedErr: fmt.Sprintf("error loading credentials: error reading options in NewCredentialsConfig: %s", testOptionErr), + }, + { + name: "rotation error", + in: &awsCatalogPersistedState{ + AccessKeyId: "foobar", + SecretAccessKey: "bazqux", + testOpts: []awsutil.Option{ + awsutil.WithIAMAPIFunc( + awsutil.NewMockIAM( + awsutil.WithGetUserError(errors.New(testGetUserErr)), + ), + ), + }, + }, + expectedErr: fmt.Sprintf("error rotating credentials: error calling CreateAccessKey: error calling aws.GetUser: %s", testGetUserErr), + }, + { + name: "good", + in: &awsCatalogPersistedState{ + AccessKeyId: "foobar", + SecretAccessKey: "bazqux", + testOpts: []awsutil.Option{ + awsutil.WithSTSAPIFunc( + awsutil.NewMockSTS(), + ), + awsutil.WithIAMAPIFunc( + awsutil.NewMockIAM( + awsutil.WithGetUserOutput( + &iam.GetUserOutput{ + User: &iam.User{ + Arn: aws.String("arn:aws:iam::123456789012:user/JohnDoe"), + UserId: aws.String("AIDAJQABLZS4A3QDU576Q"), + UserName: aws.String("JohnDoe"), + }, + }, + ), + awsutil.WithCreateAccessKeyOutput( + &iam.CreateAccessKeyOutput{ + AccessKey: &iam.AccessKey{ + AccessKeyId: aws.String("one"), + SecretAccessKey: aws.String("two"), + UserName: aws.String("JohnDoe"), + }, + }, + ), + ), + ), + }, + }, + expectedAccessKeyId: "one", + expectedSecretAccessKey: "two", + expectedNonZeroRotatedTime: true, + }, + } + + for _, tc := range cases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + require := require.New(t) + err := tc.in.RotateCreds() + if tc.expectedErr != "" { + require.EqualError(err, tc.expectedErr) + return + } + + require.NoError(err) + require.Equal(tc.expectedAccessKeyId, tc.in.AccessKeyId) + require.Equal(tc.expectedSecretAccessKey, tc.in.SecretAccessKey) + require.Equal(tc.expectedNonZeroRotatedTime, !tc.in.CredsLastRotatedTime.IsZero()) + }) + } +} + +func TestAwsCatalogPersistedStateReplaceCreds(t *testing.T) { + // NOTE: Not safe to run this test in parallel + state := new(testMockIAMState) + cases := []struct { + name string + in *awsCatalogPersistedState + accessKeyId string + secretAccessKey string + expectedAccessKeyId string + expectedSecretAccessKey string + expectedDeleteCalled bool + expectedErr string + }{ + { + name: "missing access key id", + in: &awsCatalogPersistedState{}, + expectedErr: "access key id cannot be empty", + }, + { + name: "missing access key id", + in: &awsCatalogPersistedState{}, + accessKeyId: "one", + expectedErr: "secret access key cannot be empty", + }, + { + name: "identical access key id", + in: &awsCatalogPersistedState{ + AccessKeyId: "foobar", + SecretAccessKey: "bazqux", + }, + accessKeyId: "foobar", + secretAccessKey: "bazqux", + expectedErr: "attempting to replace access key with the same one", + }, + { + name: "deletion error", + in: &awsCatalogPersistedState{ + AccessKeyId: "foobar", + SecretAccessKey: "bazqux", + CredsLastRotatedTime: time.Now(), + testOpts: []awsutil.Option{ + awsutil.WithIAMAPIFunc( + awsutil.NewMockIAM( + awsutil.WithDeleteAccessKeyError(errors.New(testDeleteAccessKeyErr)), + ), + ), + }, + }, + accessKeyId: "one", + secretAccessKey: "two", + expectedErr: fmt.Sprintf("error deleting old access key: error deleting old access key: %s", testDeleteAccessKeyErr), + }, + { + name: "good with delete of old rotated", + in: &awsCatalogPersistedState{ + AccessKeyId: "foobar", + SecretAccessKey: "bazqux", + CredsLastRotatedTime: time.Now(), + testOpts: []awsutil.Option{ + awsutil.WithIAMAPIFunc( + newTestMockIAM(state), + ), + }, + }, + accessKeyId: "one", + secretAccessKey: "two", + expectedAccessKeyId: "one", + expectedSecretAccessKey: "two", + expectedDeleteCalled: true, + }, + { + name: "good without delete of old rotated", + in: &awsCatalogPersistedState{ + AccessKeyId: "foobar", + SecretAccessKey: "bazqux", + testOpts: []awsutil.Option{ + awsutil.WithIAMAPIFunc( + newTestMockIAM(state), + ), + }, + }, + accessKeyId: "one", + secretAccessKey: "two", + expectedAccessKeyId: "one", + expectedSecretAccessKey: "two", + expectedDeleteCalled: false, + }, + } + + for _, tc := range cases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + require := require.New(t) + state.Reset() + err := tc.in.ReplaceCreds(tc.accessKeyId, tc.secretAccessKey) + if tc.expectedErr != "" { + require.EqualError(err, tc.expectedErr) + return + } + + require.NoError(err) + require.Equal(tc.expectedAccessKeyId, tc.in.AccessKeyId) + require.Equal(tc.expectedSecretAccessKey, tc.in.SecretAccessKey) + require.Zero(tc.in.CredsLastRotatedTime) + require.Equal(tc.expectedDeleteCalled, state.DeleteAccessKeyCalled) + }) + } +} + +func TestAwsCatalogPersistedStateDeleteCreds(t *testing.T) { + cases := []struct { + name string + in *awsCatalogPersistedState + expectedErr string + }{ + { + name: "could not load credentials", + in: &awsCatalogPersistedState{ + testOpts: []awsutil.Option{awsutil.MockOptionErr(errors.New(testOptionErr))}, + }, + expectedErr: fmt.Sprintf("error loading credentials: error reading options in NewCredentialsConfig: %s", testOptionErr), + }, + { + name: "deletion error", + in: &awsCatalogPersistedState{ + AccessKeyId: "foobar", + SecretAccessKey: "bazqux", + testOpts: []awsutil.Option{ + awsutil.WithIAMAPIFunc( + awsutil.NewMockIAM( + awsutil.WithDeleteAccessKeyError(errors.New(testDeleteAccessKeyErr)), + ), + ), + }, + }, + expectedErr: fmt.Sprintf("error deleting old access key: error deleting old access key: %s", testDeleteAccessKeyErr), + }, + { + name: "deletion error, but OK because key was just gone", + in: &awsCatalogPersistedState{ + AccessKeyId: "foobar", + SecretAccessKey: "bazqux", + testOpts: []awsutil.Option{ + awsutil.WithIAMAPIFunc( + awsutil.NewMockIAM( + awsutil.WithDeleteAccessKeyError( + awserr.New(iam.ErrCodeNoSuchEntityException, "", nil), + ), + ), + ), + }, + }, + }, + { + name: "good", + in: &awsCatalogPersistedState{ + AccessKeyId: "foobar", + SecretAccessKey: "bazqux", + testOpts: []awsutil.Option{ + awsutil.WithIAMAPIFunc( + awsutil.NewMockIAM(), + ), + }, + }, + }, + } + + for _, tc := range cases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + require := require.New(t) + err := tc.in.DeleteCreds() + if tc.expectedErr != "" { + require.EqualError(err, tc.expectedErr) + return + } + + require.NoError(err) + require.Empty(tc.in.AccessKeyId) + require.Empty(tc.in.SecretAccessKey) + require.Zero(tc.in.CredsLastRotatedTime) + }) + } +} + +func TestAwsCatalogPersistedStateGetSessionErr(t *testing.T) { + require := require.New(t) + state := &awsCatalogPersistedState{ + testOpts: []awsutil.Option{awsutil.MockOptionErr(errors.New(testOptionErr))}, + } + + _, err := state.GetSession() + require.EqualError(err, fmt.Sprintf("error reading options in NewCredentialsConfig: %s", testOptionErr)) +} + +func TestAwsCatalogPersistedStateEC2ClientErr(t *testing.T) { + require := require.New(t) + state := &awsCatalogPersistedState{ + testOpts: []awsutil.Option{awsutil.MockOptionErr(errors.New(testOptionErr))}, + } + + _, err := state.EC2Client("someregion") + require.EqualError(err, fmt.Sprintf("error getting AWS session: error reading options in NewCredentialsConfig: %s", testOptionErr)) +}