// Copyright 2015-2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"). You may // not use this file except in compliance with the License. A copy of the // License is located at // // http://aws.amazon.com/apache2.0/ // // or in the "license" file accompanying this file. This file 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 regcreds import ( "encoding/json" "fmt" "testing" "github.com/aws/amazon-ecs-cli/ecs-cli/modules/clients/aws/iam/mock" "github.com/aws/amazon-ecs-cli/ecs-cli/modules/clients/aws/kms/mock" "github.com/aws/amazon-ecs-cli/ecs-cli/modules/clients/aws/secretsmanager/mock" "github.com/aws/amazon-ecs-cli/ecs-cli/modules/clients/aws/tagging/mock" "github.com/aws/amazon-ecs-cli/ecs-cli/modules/utils/regcredio" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/kms" taggingSDK "github.com/aws/aws-sdk-go/service/resourcegroupstaggingapi" secretsmanager "github.com/aws/aws-sdk-go/service/secretsmanager" "github.com/golang/mock/gomock" "github.com/pkg/errors" "github.com/stretchr/testify/assert" ) func TestGetOrCreateRegistryCredentials_WithCredPair(t *testing.T) { testUsername := "someUser1" testPassword := "someP4$$w0rd" testRegistryName := "examplereg.net" testContainers := []string{"logging", "web"} testRegistryCreds := map[string]regcredio.RegistryCredEntry{ testRegistryName: getTestCredsEntry("", testUsername, testPassword, "", testContainers), } expectedCreateInput := secretsmanager.CreateSecretInput{ Name: generateECSResourceName(testRegistryName), SecretString: generateSecretString(testUsername, testPassword), Description: generateSecretDescription(testRegistryName), } responseARN := "arn:aws:secretsmanager:examplereg.net-123" mocks := setupTestController(t) gomock.InOrder( mocks.MockSM.EXPECT().DescribeSecret(gomock.Any()).Return(nil, nil), mocks.MockSM.EXPECT().CreateSecret(expectedCreateInput).Return(&secretsmanager.CreateSecretOutput{ARN: aws.String(responseARN)}, nil), ) credsOutput, err := getOrCreateRegistryCredentials(testRegistryCreds, mocks.MockSM, false) assert.NoError(t, err, "Expected no error when creating secret with cred pair") actualCredEntry := credsOutput[testRegistryName] assert.NotEmpty(t, actualCredEntry) assert.Equal(t, responseARN, actualCredEntry.CredentialARN) assert.ElementsMatch(t, testContainers, actualCredEntry.ContainerNames) } func TestGetOrCreateRegistryCredentials_WithCredPairAndKmsKey(t *testing.T) { testUsername := "someUser1" testPassword := "someP4$$w0rd" testKmsKeyID := "my-fav-key" testRegistryName := "examplereg.net" testContainers := []string{"logging", "web"} testRegistryCreds := map[string]regcredio.RegistryCredEntry{ testRegistryName: getTestCredsEntry("", testUsername, testPassword, testKmsKeyID, testContainers), } expectedCreateInput := secretsmanager.CreateSecretInput{ Name: generateECSResourceName(testRegistryName), SecretString: generateSecretString(testUsername, testPassword), KmsKeyId: aws.String(testKmsKeyID), Description: generateSecretDescription(testRegistryName), } responseARN := "arn:aws:secretsmanager:examplereg.net-123" mocks := setupTestController(t) gomock.InOrder( mocks.MockSM.EXPECT().DescribeSecret(gomock.Any()).Return(nil, nil), mocks.MockSM.EXPECT().CreateSecret(expectedCreateInput).Return(&secretsmanager.CreateSecretOutput{ARN: aws.String(responseARN)}, nil), ) credsOutput, err := getOrCreateRegistryCredentials(testRegistryCreds, mocks.MockSM, false) assert.NoError(t, err, "Expected no error when creating secret with cred pair") actualCredEntry := credsOutput[testRegistryName] assert.NotEmpty(t, actualCredEntry) assert.Equal(t, responseARN, actualCredEntry.CredentialARN) assert.ElementsMatch(t, testContainers, actualCredEntry.ContainerNames) } func TestGetOrCreateRegistryCredentials_WithCredPairAndExistingFound(t *testing.T) { testUsername := "someUser1" testPassword := "someP4$$w0rd" testRegistryName := "examplereg.net" testContainers := []string{"logging", "web"} testRegistryCreds := map[string]regcredio.RegistryCredEntry{ testRegistryName: getTestCredsEntry("", testUsername, testPassword, "", testContainers), } responseARN := "arn:aws:secretsmanager:examplereg.net-123" mocks := setupTestController(t) mocks.MockSM.EXPECT().DescribeSecret(gomock.Any()).Return(&secretsmanager.DescribeSecretOutput{ARN: aws.String(responseARN)}, nil) credsOutput, err := getOrCreateRegistryCredentials(testRegistryCreds, mocks.MockSM, false) assert.NoError(t, err, "Expected no error when creating secret with cred pair") actualCredEntry := credsOutput[testRegistryName] assert.NotEmpty(t, actualCredEntry) assert.Equal(t, responseARN, actualCredEntry.CredentialARN) assert.ElementsMatch(t, testContainers, actualCredEntry.ContainerNames) } func TestGetOrCreateRegistryCredentials_WithSecretArnOnly(t *testing.T) { testSecretARN := "arn:aws:secretsmanager:examplereg.net-123" testRegistryName := "examplereg.net" testContainers := []string{"logging", "web"} testRegistryCreds := map[string]regcredio.RegistryCredEntry{ testRegistryName: getTestCredsEntry(testSecretARN, "", "", "", testContainers), } mocks := setupTestController(t) mocks.MockSM.EXPECT().DescribeSecret(gomock.Any()).Return(&secretsmanager.DescribeSecretOutput{}, nil) credsOutput, err := getOrCreateRegistryCredentials(testRegistryCreds, mocks.MockSM, false) assert.NoError(t, err, "Expected no error when using existing secren ARN") actualCredEntry := credsOutput[testRegistryName] assert.NotEmpty(t, actualCredEntry) assert.Equal(t, testSecretARN, actualCredEntry.CredentialARN) assert.ElementsMatch(t, testContainers, actualCredEntry.ContainerNames) } func TestGetOrCreateRegistryCredentials_WithExistingAndCredsUpdateOk(t *testing.T) { testSecretARN := "arn:aws:secretsmanager:examplereg.net-123" testUsername := "someUser1" testPassword := "someP4$$w0rd" testRegistryName := "examplereg.net" testContainers := []string{"logging", "web"} testRegistryCreds := map[string]regcredio.RegistryCredEntry{ testRegistryName: getTestCredsEntry(testSecretARN, testUsername, testPassword, "", testContainers), } expectedPutSecretValueInput := secretsmanager.PutSecretValueInput{ SecretId: aws.String(testSecretARN), SecretString: generateSecretString(testUsername, testPassword), } mocks := setupTestController(t) gomock.InOrder( mocks.MockSM.EXPECT().PutSecretValue(expectedPutSecretValueInput).Return(&secretsmanager.PutSecretValueOutput{}, nil), mocks.MockSM.EXPECT().DescribeSecret(gomock.Any()).Return(&secretsmanager.DescribeSecretOutput{}, nil), ) // call with updateAllowed = true credsOutput, err := getOrCreateRegistryCredentials(testRegistryCreds, mocks.MockSM, true) assert.NoError(t, err, "Expected no error when updating existing secren ARN") actualCredEntry := credsOutput[testRegistryName] assert.NotEmpty(t, actualCredEntry) assert.ElementsMatch(t, testContainers, actualCredEntry.ContainerNames) } func TestGetOrCreateRegistryCredentials_WithExistingAndCredsNoUpdate(t *testing.T) { testSecretARN := "arn:aws:secretsmanager:examplereg.net-123" testUsername := "someUser1" testPassword := "someP4$$w0rd" testRegistryName := "examplereg.net" testContainers := []string{"logging", "web"} testRegistryCreds := map[string]regcredio.RegistryCredEntry{ testRegistryName: getTestCredsEntry(testSecretARN, testUsername, testPassword, "", testContainers), } // call with updateAllowed = false mocks := setupTestController(t) mocks.MockSM.EXPECT().DescribeSecret(gomock.Any()).Return(&secretsmanager.DescribeSecretOutput{}, nil) credsOutput, err := getOrCreateRegistryCredentials(testRegistryCreds, mocks.MockSM, false) assert.NoError(t, err, "Expected no error when using existing secren ARN") actualCredEntry := credsOutput[testRegistryName] assert.NotEmpty(t, actualCredEntry) assert.ElementsMatch(t, testContainers, actualCredEntry.ContainerNames) } func TestGetOrCreateRegistryCredentials_ErrorOnCreate(t *testing.T) { testRegistryCreds := map[string]regcredio.RegistryCredEntry{ "testRegistry": getTestCredsEntry("", "testUsername", "testPassword", "", []string{"test"}), } mocks := setupTestController(t) gomock.InOrder( mocks.MockSM.EXPECT().DescribeSecret(gomock.Any()).Return(nil, nil), mocks.MockSM.EXPECT().CreateSecret(gomock.Any()).Return(nil, errors.New("something went wrong")), ) _, err := getOrCreateRegistryCredentials(testRegistryCreds, mocks.MockSM, false) assert.Error(t, err) } func TestGetOrCreateRegistryCredentials_ErrorOnUpdate(t *testing.T) { testRegistryCreds := map[string]regcredio.RegistryCredEntry{ "testRegistry": getTestCredsEntry("arn:aws:secretsmanager:secret:test", "testUsername", "testPassword", "", []string{"test"}), } mocks := setupTestController(t) mocks.MockSM.EXPECT().PutSecretValue(gomock.Any()).Return(nil, errors.New("something went wrong")) _, err := getOrCreateRegistryCredentials(testRegistryCreds, mocks.MockSM, true) assert.Error(t, err) } func TestTagRegistryCredentials(t *testing.T) { creds := map[string]regcredio.CredsOutputEntry{ "the-who-registry.com": regcredio.CredsOutputEntry{ CredentialARN: "arn:aws:secretsmanager:eu-west-1:111111111111:secret:path/whoareyou-1978", }, } tags := map[string]*string{ "Baba": aws.String("O'riley"), "Eminence": aws.String("Front"), "My": aws.String("Generation"), } ctrl := gomock.NewController(t) mockTagging := mock_tagging.NewMockClient(ctrl) gomock.InOrder( mockTagging.EXPECT().TagResources(gomock.Any()).Do(func(x interface{}) { input := x.(*taggingSDK.TagResourcesInput) assert.Equal(t, tags, input.Tags, "Expected tags to match") }).Return(&taggingSDK.TagResourcesOutput{}, nil), ) err := tagRegistryCredentials(creds, tags, mockTagging) assert.NoError(t, err, "Unexpected error calling tagRegistryCredentials") } func TestTagRegistryCredentialsError(t *testing.T) { creds := map[string]regcredio.CredsOutputEntry{ "the-who-registry.com": regcredio.CredsOutputEntry{ CredentialARN: "arn:aws:secretsmanager:eu-west-1:111111111111:secret:path/whoareyou-1978", }, } tags := map[string]*string{ "Baba": aws.String("O'riley"), "Eminence": aws.String("Front"), "My": aws.String("Generation"), } ctrl := gomock.NewController(t) mockTagging := mock_tagging.NewMockClient(ctrl) gomock.InOrder( mockTagging.EXPECT().TagResources(gomock.Any()).Do(func(x interface{}) { input := x.(*taggingSDK.TagResourcesInput) assert.Equal(t, tags, input.Tags, "Expected tags to match") }).Return(nil, fmt.Errorf("Some API error")), ) err := tagRegistryCredentials(creds, tags, mockTagging) assert.Error(t, err, "Expected error calling tagRegistryCredentials") } func TestTagRegistryCredentialsFailedResources(t *testing.T) { creds := map[string]regcredio.CredsOutputEntry{ "the-who-registry.com": regcredio.CredsOutputEntry{ CredentialARN: "arn:aws:secretsmanager:eu-west-1:111111111111:secret:path/whoareyou-1978", }, } tags := map[string]*string{ "Baba": aws.String("O'riley"), "Eminence": aws.String("Front"), "My": aws.String("Generation"), } ctrl := gomock.NewController(t) mockTagging := mock_tagging.NewMockClient(ctrl) gomock.InOrder( mockTagging.EXPECT().TagResources(gomock.Any()).Do(func(x interface{}) { input := x.(*taggingSDK.TagResourcesInput) assert.Equal(t, tags, input.Tags, "Expected tags to match") }).Return(&taggingSDK.TagResourcesOutput{ FailedResourcesMap: map[string]*taggingSDK.FailureInfo{ "arn:aws:secretsmanager:eu-west-1:111111111111:secret:path/whoareyou-1978": &taggingSDK.FailureInfo{ ErrorMessage: aws.String("Auth Error: who are you"), }, }, }, nil), ) err := tagRegistryCredentials(creds, tags, mockTagging) assert.Error(t, err, "Expected error calling tagRegistryCredentials") } func TestValidateCredsInput_ErrorEmptyCreds(t *testing.T) { emptyCredMap := make(map[string]regcredio.RegistryCredEntry) emptyCredsInput := regcredio.ECSRegCredsInput{ Version: "1", RegistryCredentials: emptyCredMap, } _, err := validateCredsInput(emptyCredsInput, nil) assert.Error(t, err, "Expected empty creds to return error") } func TestValidateCredsInput_ErrorOnMissingReqFields(t *testing.T) { mapWithEmptyCredEntry := map[string]regcredio.RegistryCredEntry{ "example.com": regcredio.RegistryCredEntry{}, } testCredsInput := regcredio.ECSRegCredsInput{ Version: "1", RegistryCredentials: mapWithEmptyCredEntry, } _, err := validateCredsInput(testCredsInput, nil) assert.Error(t, err, "Expected creds with empty entry to return error") } func TestValidateCredsInput_ErrorOnDuplicateContainers(t *testing.T) { duplicateContainer := "http" regCreds := map[string]regcredio.RegistryCredEntry{ "registry-1.net": regcredio.RegistryCredEntry{ SecretManagerARN: "arn:aws:secretsmanager:some-secret", ContainerNames: []string{duplicateContainer, "logging"}, }, "registry-2.net": regcredio.RegistryCredEntry{ SecretManagerARN: "arn:aws:secretsmanager:some-other-secret", ContainerNames: []string{"metrics", duplicateContainer}, }, } testCredsInput := regcredio.ECSRegCredsInput{ Version: "1", RegistryCredentials: regCreds, } _, err := validateCredsInput(testCredsInput, nil) assert.Error(t, err, "Expected creds with duplicate containers to return error") } func TestValidateCredsInput_KeyAliasDescribed(t *testing.T) { mocks := setupTestController(t) testRegName := "testRegistry" regCreds := map[string]regcredio.RegistryCredEntry{ testRegName: getTestCredsEntry("", "testuser", "testPassword", "alias/someKey", []string{"test"}), } testCredsInput := regcredio.ECSRegCredsInput{ Version: "1", RegistryCredentials: regCreds, } expectedKeyARN := "arn:aws:kms:key/56yrtgf-4etrfgd-34erfd" expectKeyMetadata := kms.KeyMetadata{ Arn: aws.String(expectedKeyARN), } gomock.InOrder( mocks.MockKMS.EXPECT().GetValidKeyARN("alias/someKey").Return(expectedKeyARN, nil), mocks.MockKMS.EXPECT().DescribeKey("alias/someKey").Return(&kms.DescribeKeyOutput{KeyMetadata: &expectKeyMetadata}, nil), ) validatedOutput, err := validateCredsInput(testCredsInput, mocks.MockKMS) assert.NoError(t, err, "Unexpected error on Describe Key") assert.Equal(t, expectedKeyARN, validatedOutput[testRegName].KmsKeyID) } func TestValidateCredsInput_NoDescribeOnKeyARN(t *testing.T) { mocks := setupTestController(t) testKeyARN := "arn:aws:kms:key/7457r6ythfg-5rythgf" regCreds := map[string]regcredio.RegistryCredEntry{ "testRegistry": getTestCredsEntry("", "testuser", "testPassword", testKeyARN, []string{"test"}), } testCredsInput := regcredio.ECSRegCredsInput{ Version: "1", RegistryCredentials: regCreds, } mocks.MockKMS.EXPECT().GetValidKeyARN(testKeyARN).Return(testKeyARN, nil) validatedOutput, err := validateCredsInput(testCredsInput, mocks.MockKMS) assert.NoError(t, err, "Unexpected error when validating reg creds") assert.Equal(t, testKeyARN, validatedOutput["testRegistry"].KmsKeyID) } func TestValidateCredsInput_ErrorOnDescribeFail(t *testing.T) { mocks := setupTestController(t) regCreds := map[string]regcredio.RegistryCredEntry{ "testRegistry": getTestCredsEntry("", "testuser", "testPassword", "alias/someKey", []string{"test"}), } testCredsInput := regcredio.ECSRegCredsInput{ Version: "1", RegistryCredentials: regCreds, } gomock.InOrder( mocks.MockKMS.EXPECT().GetValidKeyARN(gomock.Any()).Return("", errors.New("something went wrong")), mocks.MockKMS.EXPECT().DescribeKey(gomock.Any()).Return(nil, errors.New("something went wrong")), ) _, err := validateCredsInput(testCredsInput, mocks.MockKMS) assert.Error(t, err, "Expected error when Describe Key fails") } func TestValidateCredsInput_ErrorOnRegionMismatch(t *testing.T) { testKeyARN := "arn:aws:kms:us-east-1:1234567:key/765ythgf-45erfd" regCreds := map[string]regcredio.RegistryCredEntry{ "testRegistry": getTestCredsEntry("arn:aws:secretsmanager:us-west-2:1234567:secret/some-secret", "testuser", "testPassword", testKeyARN, []string{"test"}), } testCredsInput := regcredio.ECSRegCredsInput{ Version: "1", RegistryCredentials: regCreds, } mocks := setupTestController(t) mocks.MockKMS.EXPECT().GetValidKeyARN(testKeyARN).Return(testKeyARN, nil) _, err := validateCredsInput(testCredsInput, mocks.MockKMS) assert.Error(t, err, "Expected error when secret and key regions don't match") } func TestGenerateSecretString(t *testing.T) { type ECSRegistrySecret struct { Username string `json:"username"` Password string `json:"password"` } testCases := []struct { inputUsername string inputPassword string expectedSecret ECSRegistrySecret }{ {"user1", "l33tp4$$w0rd", ECSRegistrySecret{"user1", "l33tp4$$w0rd"}}, {"someUserNameThatIsVeryLong0987654321", "*3G7nMl6W*Pi#*erjm", ECSRegistrySecret{"someUserNameThatIsVeryLong0987654321", "*3G7nMl6W*Pi#*erjm"}}, {"myemail@example.com", "some-dashed-psswrd-64", ECSRegistrySecret{"myemail@example.com", "some-dashed-psswrd-64"}}, } for _, test := range testCases { t.Run(fmt.Sprintf("Parse registry secret %s", test.inputUsername), func(t *testing.T) { actualSecretString := generateSecretString(test.inputUsername, test.inputPassword) assert.NotEmpty(t, *actualSecretString) regSecret := &ECSRegistrySecret{} err := json.Unmarshal([]byte(*actualSecretString), regSecret) assert.NoError(t, err, "Unexpected error when unmarshalling registry secret") assert.Equal(t, test.expectedSecret.Username, regSecret.Username, "Expected username to match") assert.Equal(t, test.expectedSecret.Password, regSecret.Password, "Expected password to match") }) } } func getTestCredsEntry(secretARN, username, password, kmsKey string, containers []string) regcredio.RegistryCredEntry { return regcredio.RegistryCredEntry{ SecretManagerARN: secretARN, Username: username, Password: password, KmsKeyID: kmsKey, ContainerNames: containers, } } type testClients struct { MockIAM *mock_iam.MockClient MockKMS *mock_kms.MockClient MockSM *mock_secretsmanager.MockSMClient } func setupTestController(t *testing.T) testClients { ctrl := gomock.NewController(t) clients := testClients{ MockIAM: mock_iam.NewMockClient(ctrl), MockKMS: mock_kms.NewMockClient(ctrl), MockSM: mock_secretsmanager.NewMockSMClient(ctrl), } return clients }