From 4b39ef4074f80efe063aed04dcd704766234547a Mon Sep 17 00:00:00 2001 From: xibz Date: Thu, 11 Oct 2018 10:30:22 -0700 Subject: [PATCH] Adding support for credential source types: EcsContainer, Ec2InstanceMetadata, and Environment --- aws/defaults/defaults.go | 9 +- aws/defaults/defaults_test.go | 2 +- aws/session/session.go | 127 +++++++-- aws/session/session_test.go | 254 ++++++++++++++---- aws/session/shared_config_test.go | 11 - aws/session/testdata/credential_source_config | 16 ++ aws/session/testdata/shared_config | 4 - internal/container/uri.go | 6 + 8 files changed, 334 insertions(+), 95 deletions(-) create mode 100644 aws/session/testdata/credential_source_config create mode 100644 internal/container/uri.go diff --git a/aws/defaults/defaults.go b/aws/defaults/defaults.go index 6cd84cd96de..e3e7195cd8a 100644 --- a/aws/defaults/defaults.go +++ b/aws/defaults/defaults.go @@ -24,6 +24,7 @@ import ( "github.com/aws/aws-sdk-go/aws/ec2metadata" "github.com/aws/aws-sdk-go/aws/endpoints" "github.com/aws/aws-sdk-go/aws/request" + "github.com/aws/aws-sdk-go/internal/container" ) // A Defaults provides a collection of default values for SDK clients. @@ -114,7 +115,9 @@ func CredProviders(cfg *aws.Config, handlers request.Handlers) []credentials.Pro const ( httpProviderAuthorizationEnvVar = "AWS_CONTAINER_AUTHORIZATION_TOKEN" httpProviderEnvVar = "AWS_CONTAINER_CREDENTIALS_FULL_URI" - ecsCredsProviderEnvVar = "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI" + // EcsCredsProviderEnvVar is an environmental variable key used to + // determine which path needs to be hit. + EcsCredsProviderEnvVar = "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI" ) // RemoteCredProvider returns a credentials provider for the default remote @@ -124,8 +127,8 @@ func RemoteCredProvider(cfg aws.Config, handlers request.Handlers) credentials.P return localHTTPCredProvider(cfg, handlers, u) } - if uri := os.Getenv(ecsCredsProviderEnvVar); len(uri) > 0 { - u := fmt.Sprintf("http://169.254.170.2%s", uri) + if uri := os.Getenv(EcsCredsProviderEnvVar); len(uri) > 0 { + u := fmt.Sprintf("%s%s", container.URI, uri) return httpCredProvider(cfg, handlers, u) } diff --git a/aws/defaults/defaults_test.go b/aws/defaults/defaults_test.go index 8a0e491f276..139aaf00c0d 100644 --- a/aws/defaults/defaults_test.go +++ b/aws/defaults/defaults_test.go @@ -90,7 +90,7 @@ func TestHTTPCredProvider(t *testing.T) { func TestECSCredProvider(t *testing.T) { defer os.Clearenv() - os.Setenv(ecsCredsProviderEnvVar, "/abc/123") + os.Setenv(EcsCredsProviderEnvVar, "/abc/123") provider := RemoteCredProvider(aws.Config{}, request.Handlers{}) if provider == nil { diff --git a/aws/session/session.go b/aws/session/session.go index 97a46206cc7..9c00a635f72 100644 --- a/aws/session/session.go +++ b/aws/session/session.go @@ -21,6 +21,23 @@ import ( "github.com/aws/aws-sdk-go/aws/request" ) +const ( + // ErrCodeSharedConfig represents an error that occurs in the shared + // configuration logic + ErrCodeSharedConfig = "SharedConfigErr" +) + +// ErrSharedConfigSourceCollision will be returned if a section contains both +// source_profile and credential_source +var ErrSharedConfigSourceCollision = awserr.New(ErrCodeSharedConfig, "only source profile or credential source can be specified, not both", nil) + +// ErrSharedConfigECSContainerEnvVarEmpty will be returned if the environment +// variables are empty and Environment was set as the credential source +var ErrSharedConfigECSContainerEnvVarEmpty = awserr.New(ErrCodeSharedConfig, "EcsContainer was specified as the credential_source, but 'AWS_CONTAINER_CREDENTIALS_RELATIVE_URI' was not set", nil) + +// ErrSharedConfigInvalidCredSource will be returned if an invalid credential source was provided +var ErrSharedConfigInvalidCredSource = awserr.New(ErrCodeSharedConfig, "credential source values must be EcsContainer, Ec2InstanceMetadata, or Environment", nil) + // A Session provides a central location to create service clients from and // store configurations and request handlers for those services. // @@ -436,46 +453,74 @@ func mergeConfigSrcs(cfg, userCfg *aws.Config, envCfg envConfig, sharedCfg share // Configure credentials if not already set if cfg.Credentials == credentials.AnonymousCredentials && userCfg.Credentials == nil { + + // inspect the profile to see if a credential source has been specified. + if envCfg.EnableSharedConfig && len(sharedCfg.AssumeRole.CredentialSource) > 0 { + + // if both credential_source and source_profile have been set, return an error + // as this is undefined behavior. + if len(sharedCfg.AssumeRole.SourceProfile) > 0 { + return ErrSharedConfigSourceCollision + } + + // valid credential source values + const ( + credSourceEc2Metadata = "Ec2InstanceMetadata" + credSourceEnvironment = "Environment" + credSourceECSContainer = "EcsContainer" + ) + + switch sharedCfg.AssumeRole.CredentialSource { + case credSourceEc2Metadata: + cfgCp := *cfg + p := defaults.RemoteCredProvider(cfgCp, handlers) + cfgCp.Credentials = credentials.NewCredentials(p) + + if len(sharedCfg.AssumeRole.MFASerial) > 0 && sessOpts.AssumeRoleTokenProvider == nil { + // AssumeRole Token provider is required if doing Assume Role + // with MFA. + return AssumeRoleTokenProviderNotSetError{} + } + + cfg.Credentials = assumeRoleCredentials(cfgCp, handlers, sharedCfg, sessOpts) + case credSourceEnvironment: + cfg.Credentials = credentials.NewStaticCredentialsFromCreds( + envCfg.Creds, + ) + case credSourceECSContainer: + if len(os.Getenv(defaults.EcsCredsProviderEnvVar)) == 0 { + return ErrSharedConfigECSContainerEnvVarEmpty + } + + cfgCp := *cfg + p := defaults.RemoteCredProvider(cfgCp, handlers) + creds := credentials.NewCredentials(p) + + cfg.Credentials = creds + default: + return ErrSharedConfigInvalidCredSource + } + + return nil + } + if len(envCfg.Creds.AccessKeyID) > 0 { cfg.Credentials = credentials.NewStaticCredentialsFromCreds( envCfg.Creds, ) } else if envCfg.EnableSharedConfig && len(sharedCfg.AssumeRole.RoleARN) > 0 && sharedCfg.AssumeRoleSource != nil { cfgCp := *cfg - if sharedCfg.AssumeRole.CredentialSource == "Ec2InstanceMetadata" { - p := defaults.RemoteCredProvider(cfgCp, handlers) - cfgCp.Credentials = credentials.NewCredentials(p) - } else if len(sharedCfg.AssumeRole.SourceProfile) > 0 { - cfgCp.Credentials = credentials.NewStaticCredentialsFromCreds( - sharedCfg.AssumeRoleSource.Creds, - ) - } + cfgCp.Credentials = credentials.NewStaticCredentialsFromCreds( + sharedCfg.AssumeRoleSource.Creds, + ) + if len(sharedCfg.AssumeRole.MFASerial) > 0 && sessOpts.AssumeRoleTokenProvider == nil { // AssumeRole Token provider is required if doing Assume Role // with MFA. return AssumeRoleTokenProviderNotSetError{} } - cfg.Credentials = stscreds.NewCredentials( - &Session{ - Config: &cfgCp, - Handlers: handlers.Copy(), - }, - sharedCfg.AssumeRole.RoleARN, - func(opt *stscreds.AssumeRoleProvider) { - opt.RoleSessionName = sharedCfg.AssumeRole.RoleSessionName - - // Assume role with external ID - if len(sharedCfg.AssumeRole.ExternalID) > 0 { - opt.ExternalID = aws.String(sharedCfg.AssumeRole.ExternalID) - } - - // Assume role with MFA - if len(sharedCfg.AssumeRole.MFASerial) > 0 { - opt.SerialNumber = aws.String(sharedCfg.AssumeRole.MFASerial) - opt.TokenProvider = sessOpts.AssumeRoleTokenProvider - } - }, - ) + + cfg.Credentials = assumeRoleCredentials(cfgCp, handlers, sharedCfg, sessOpts) } else if len(sharedCfg.Creds.AccessKeyID) > 0 { cfg.Credentials = credentials.NewStaticCredentialsFromCreds( sharedCfg.Creds, @@ -498,6 +543,30 @@ func mergeConfigSrcs(cfg, userCfg *aws.Config, envCfg envConfig, sharedCfg share return nil } +func assumeRoleCredentials(cfg aws.Config, handlers request.Handlers, sharedCfg sharedConfig, sessOpts Options) *credentials.Credentials { + return stscreds.NewCredentials( + &Session{ + Config: &cfg, + Handlers: handlers.Copy(), + }, + sharedCfg.AssumeRole.RoleARN, + func(opt *stscreds.AssumeRoleProvider) { + opt.RoleSessionName = sharedCfg.AssumeRole.RoleSessionName + + // Assume role with external ID + if len(sharedCfg.AssumeRole.ExternalID) > 0 { + opt.ExternalID = aws.String(sharedCfg.AssumeRole.ExternalID) + } + + // Assume role with MFA + if len(sharedCfg.AssumeRole.MFASerial) > 0 { + opt.SerialNumber = aws.String(sharedCfg.AssumeRole.MFASerial) + opt.TokenProvider = sessOpts.AssumeRoleTokenProvider + } + }, + ) +} + // AssumeRoleTokenProviderNotSetError is an error returned when creating a session when the // MFAToken option is not set when shared config is configured load assume a // role with an MFA token. diff --git a/aws/session/session_test.go b/aws/session/session_test.go index 5663ed85de1..ddac974d909 100644 --- a/aws/session/session_test.go +++ b/aws/session/session_test.go @@ -16,6 +16,7 @@ import ( "github.com/aws/aws-sdk-go/aws/defaults" "github.com/aws/aws-sdk-go/aws/endpoints" "github.com/aws/aws-sdk-go/awstesting" + "github.com/aws/aws-sdk-go/internal/container" "github.com/aws/aws-sdk-go/service/s3" ) @@ -437,67 +438,226 @@ func TestSessionAssumeRole_InvalidSourceProfile(t *testing.T) { assert.Nil(t, s) } -func TestSessionAssumeRole_EC2CredentialSource(t *testing.T) { - oldEnv := initSessionTestEnv() - defer awstesting.PopEnv(oldEnv) - - os.Setenv("AWS_REGION", "us-east-1") - os.Setenv("AWS_SDK_LOAD_CONFIG", "1") - os.Setenv("AWS_SHARED_CREDENTIALS_FILE", testConfigFilename) - os.Setenv("AWS_PROFILE", "assume_role_ec2_instance_metadata") +func TestSharedConfigCredentialSource(t *testing.T) { + cases := []struct { + name string + profile string + expectedError error + expectedAccessKey string + expectedSecretKey string + init func(*aws.Config, string) func() error + }{ + { + name: "env var credential source", + profile: "env_var_credential_source", + expectedAccessKey: "access_key", + expectedSecretKey: "secret_key", + init: func(cfg *aws.Config, profile string) func() error { + os.Setenv("AWS_SDK_LOAD_CONFIG", "1") + os.Setenv("AWS_CONFIG_FILE", "testdata/credential_source_config") + os.Setenv("AWS_PROFILE", profile) + os.Setenv("AWS_ACCESS_KEY", "access_key") + os.Setenv("AWS_SECRET_KEY", "secret_key") + + return func() error { + os.Unsetenv("AWS_SDK_LOAD_CONFIG") + os.Unsetenv("AWS_CONFIG_FILE") + os.Unsetenv("AWS_PROFILE") + os.Unsetenv("AWS_ACCESS_KEY") + os.Unsetenv("AWS_SECRET_KEY") + + return nil + } + }, + }, + { + name: "credential source and source profile", + profile: "invalid_source_and_credential_source", + expectedError: ErrSharedConfigSourceCollision, + init: func(cfg *aws.Config, profile string) func() error { + os.Setenv("AWS_SDK_LOAD_CONFIG", "1") + os.Setenv("AWS_CONFIG_FILE", "testdata/credential_source_config") + os.Setenv("AWS_PROFILE", profile) + os.Setenv("AWS_ACCESS_KEY", "access_key") + os.Setenv("AWS_SECRET_KEY", "secret_key") + + return func() error { + os.Unsetenv("AWS_SDK_LOAD_CONFIG") + os.Unsetenv("AWS_CONFIG_FILE") + os.Unsetenv("AWS_PROFILE") + os.Unsetenv("AWS_ACCESS_KEY") + os.Unsetenv("AWS_SECRET_KEY") + + return nil + } + }, + }, + { + name: "ec2metadata credential source", + profile: "ec2metadata", + expectedAccessKey: "AKID", + expectedSecretKey: "SECRET", + init: func(cfg *aws.Config, profile string) func() error { + os.Setenv("AWS_REGION", "us-east-1") + os.Setenv("AWS_SDK_LOAD_CONFIG", "1") + os.Setenv("AWS_CONFIG_FILE", "testdata/credential_source_config") + os.Setenv("AWS_PROFILE", "ec2metadata") + + const ec2MetadataResponse = `{ + "Code": "Success", + "Type": "AWS-HMAC", + "AccessKeyId" : "access-key", + "SecretAccessKey" : "secret-key", + "Token" : "token", + "Expiration" : "2100-01-01T00:00:00Z", + "LastUpdated" : "2009-11-23T0:00:00Z" + }` - const ec2MetadataResponse = `{ + ec2MetadataCalled := false + ec2MetadataServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/meta-data/iam/security-credentials/RoleName" { + ec2MetadataCalled = true + w.Write([]byte(ec2MetadataResponse)) + } else if r.URL.Path == "/meta-data/iam/security-credentials/" { + w.Write([]byte("RoleName")) + } else { + w.Write([]byte("")) + } + })) + + stsServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(fmt.Sprintf(assumeRoleRespMsg, time.Now().Add(15*time.Minute).Format("2006-01-02T15:04:05Z")))) + })) + + cfg.EndpointResolver = endpoints.ResolverFunc( + func(service, region string, opts ...func(*endpoints.Options)) (endpoints.ResolvedEndpoint, error) { + if service == "ec2metadata" { + return endpoints.ResolvedEndpoint{ + URL: ec2MetadataServer.URL, + }, nil + } + + return endpoints.ResolvedEndpoint{ + URL: stsServer.URL, + }, nil + }, + ) + + return func() error { + os.Unsetenv("AWS_SDK_LOAD_CONFIG") + os.Unsetenv("AWS_CONFIG_FILE") + os.Unsetenv("AWS_PROFILE") + os.Unsetenv("AWS_REGION") + + ec2MetadataServer.Close() + stsServer.Close() + + if !ec2MetadataCalled { + return fmt.Errorf("expected ec2metadata to be called") + } + + return nil + } + }, + }, + { + name: "ecs container credential source", + profile: "ecscontainer", + expectedAccessKey: "access-key", + expectedSecretKey: "secret-key", + init: func(cfg *aws.Config, profile string) func() error { + os.Setenv("AWS_REGION", "us-east-1") + os.Setenv("AWS_SDK_LOAD_CONFIG", "1") + os.Setenv("AWS_CONFIG_FILE", "testdata/credential_source_config") + os.Setenv("AWS_PROFILE", "ecscontainer") + os.Setenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", "/ECS") + + const ecsResponse = `{ "Code": "Success", "Type": "AWS-HMAC", - "AccessKeyId" : "accessKey", - "SecretAccessKey" : "secret", + "AccessKeyId" : "access-key", + "SecretAccessKey" : "secret-key", "Token" : "token", "Expiration" : "2100-01-01T00:00:00Z", "LastUpdated" : "2009-11-23T0:00:00Z" }` - ec2MetadataCalled := false - ec2MetadataServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path == "/meta-data/iam/security-credentials/RoleName" { - ec2MetadataCalled = true - w.Write([]byte(ec2MetadataResponse)) - } else if r.URL.Path == "/meta-data/iam/security-credentials" { - w.Write([]byte("RoleName")) - } else { - w.Write([]byte("")) - } - })) + ecsCredsCalled := false + ecsMetadataServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/ECS" { + ecsCredsCalled = true + w.Write([]byte(ecsResponse)) + } else { + w.Write([]byte("")) + } + })) + + container.URI = ecsMetadataServer.URL + + stsServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(fmt.Sprintf(assumeRoleRespMsg, time.Now().Add(15*time.Minute).Format("2006-01-02T15:04:05Z")))) + })) + + cfg.Endpoint = aws.String(stsServer.URL) + + cfg.EndpointResolver = endpoints.ResolverFunc( + func(service, region string, opts ...func(*endpoints.Options)) (endpoints.ResolvedEndpoint, error) { + fmt.Println("SERVICE", service) + return endpoints.ResolvedEndpoint{ + URL: stsServer.URL, + }, nil + }, + ) + + return func() error { + os.Unsetenv("AWS_SDK_LOAD_CONFIG") + os.Unsetenv("AWS_CONFIG_FILE") + os.Unsetenv("AWS_PROFILE") + os.Unsetenv("AWS_REGION") + os.Unsetenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI") + + ecsMetadataServer.Close() + stsServer.Close() + + if !ecsCredsCalled { + return fmt.Errorf("expected ec2metadata to be called") + } + + return nil + } + }, + }, + } - defer ec2MetadataServer.Close() + for _, c := range cases { + cfg := &aws.Config{} + clean := c.init(cfg, c.profile) + sess, err := NewSession(cfg) + if e, a := c.expectedError, err; e != a { + t.Errorf("expected %v, but received %v", e, a) + } - stsServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte(fmt.Sprintf(assumeRoleRespMsg, time.Now().Add(15*time.Minute).Format("2006-01-02T15:04:05Z")))) - })) + if c.expectedError != nil { + continue + } - defer stsServer.Close() + creds, err := sess.Config.Credentials.Get() + if err != nil { + t.Errorf("expected no error, but received %v", err) + } - s, err := NewSession(&aws.Config{ - EndpointResolver: endpoints.ResolverFunc( - func(service, region string, opts ...func(*endpoints.Options)) (endpoints.ResolvedEndpoint, error) { - if service == "ec2metadata" { - return endpoints.ResolvedEndpoint{ - URL: ec2MetadataServer.URL, - }, nil - } + if e, a := c.expectedAccessKey, creds.AccessKeyID; e != a { + t.Errorf("expected %v, but received %v", e, a) + } - return endpoints.ResolvedEndpoint{ - URL: stsServer.URL, - }, nil - }, - ), - }) + if e, a := c.expectedSecretKey, creds.SecretAccessKey; e != a { + t.Errorf("expected %v, but received %v", e, a) + } - creds, err := s.Config.Credentials.Get() - assert.NoError(t, err) - assert.Equal(t, ec2MetadataCalled, true) - assert.Equal(t, "AKID", creds.AccessKeyID) - assert.Equal(t, "SECRET", creds.SecretAccessKey) - assert.Equal(t, "SESSION_TOKEN", creds.SessionToken) + if err := clean(); err != nil { + t.Errorf("expected no error, but received %v", err) + } + } } func initSessionTestEnv() (oldEnv []string) { diff --git a/aws/session/shared_config_test.go b/aws/session/shared_config_test.go index 84083e5f4d5..3a07b8d9700 100644 --- a/aws/session/shared_config_test.go +++ b/aws/session/shared_config_test.go @@ -108,17 +108,6 @@ func TestLoadSharedConfig(t *testing.T) { }, }, }, - { - Filenames: []string{testConfigOtherFilename, testConfigFilename}, - Profile: "assume_role_ec2_instance_metadata", - Expected: sharedConfig{ - AssumeRole: assumeRoleConfig{ - RoleARN: "assume_role_ec2_instance_metadata_role_arn", - CredentialSource: "Ec2InstanceMetadata", - }, - AssumeRoleSource: &sharedConfig{}, - }, - }, { Filenames: []string{testConfigOtherFilename, testConfigFilename}, Profile: "assume_role_wo_creds", diff --git a/aws/session/testdata/credential_source_config b/aws/session/testdata/credential_source_config new file mode 100644 index 00000000000..58b66614ad3 --- /dev/null +++ b/aws/session/testdata/credential_source_config @@ -0,0 +1,16 @@ +[env_var_credential_source] +role_arn = arn +credential_source = Environment + +[invalid_source_and_credential_source] +role_arn = arn +credential_source = Environment +source_profile = env_var_credential_source + +[ec2metadata] +role_arn = assume_role_w_creds_role_arn +credential_source = Ec2InstanceMetadata + +[ecscontainer] +role_arn = assume_role_w_creds_role_arn +credential_source = EcsContainer diff --git a/aws/session/testdata/shared_config b/aws/session/testdata/shared_config index c6d3185dd3c..fe816fe201b 100644 --- a/aws/session/testdata/shared_config +++ b/aws/session/testdata/shared_config @@ -63,7 +63,3 @@ aws_secret_access_key = assume_role_w_creds_secret [assume_role_wo_creds] role_arn = assume_role_wo_creds_role_arn source_profile = assume_role_wo_creds - -[assume_role_ec2_instance_metadata] -role_arn = assume_role_ec2_instance_metadata_role_arn -credential_source = Ec2InstanceMetadata diff --git a/internal/container/uri.go b/internal/container/uri.go new file mode 100644 index 00000000000..14ca42751bb --- /dev/null +++ b/internal/container/uri.go @@ -0,0 +1,6 @@ +package container + +// URI is the endpoint to retrieve container credentials. +// This can be overriden to test to ensure the credential +// process is behaving correctly. +var URI = "http://169.254.170.2"