From 16cef773033e77d97f42fa5655c13bace6b9edc7 Mon Sep 17 00:00:00 2001 From: xibz Date: Thu, 11 Oct 2018 17:31:23 -0700 Subject: [PATCH] aws/session: Adding support for credential_source (#2201) * Enable getting profile credentials from EC2 instance metadata * Adding support for credential source types: EcsContainer, Ec2InstanceMetadata, and Environment * fixing setAssumeRoleSource to return source collision error --- aws/defaults/defaults.go | 6 +- aws/defaults/defaults_test.go | 3 +- aws/session/session.go | 117 +++++++-- aws/session/session_test.go | 223 ++++++++++++++++++ aws/session/shared_config.go | 44 ++-- aws/session/testdata/credential_source_config | 16 ++ aws/session/testdata/shared_config | 2 +- internal/shareddefaults/ecs_container.go | 12 + 8 files changed, 381 insertions(+), 42 deletions(-) create mode 100644 aws/session/testdata/credential_source_config create mode 100644 internal/shareddefaults/ecs_container.go diff --git a/aws/defaults/defaults.go b/aws/defaults/defaults.go index 6cd84cd96de..23bb639e018 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/shareddefaults" ) // A Defaults provides a collection of default values for SDK clients. @@ -114,7 +115,6 @@ 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" ) // RemoteCredProvider returns a credentials provider for the default remote @@ -124,8 +124,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(shareddefaults.ECSCredsProviderEnvVar); len(uri) > 0 { + u := fmt.Sprintf("%s%s", shareddefaults.ECSContainerCredentialsURI, uri) return httpCredProvider(cfg, handlers, u) } diff --git a/aws/defaults/defaults_test.go b/aws/defaults/defaults_test.go index 8a0e491f276..8c1319c43bb 100644 --- a/aws/defaults/defaults_test.go +++ b/aws/defaults/defaults_test.go @@ -10,6 +10,7 @@ import ( "github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds" "github.com/aws/aws-sdk-go/aws/credentials/endpointcreds" "github.com/aws/aws-sdk-go/aws/request" + "github.com/aws/aws-sdk-go/internal/shareddefaults" ) func TestHTTPCredProvider(t *testing.T) { @@ -90,7 +91,7 @@ func TestHTTPCredProvider(t *testing.T) { func TestECSCredProvider(t *testing.T) { defer os.Clearenv() - os.Setenv(ecsCredsProviderEnvVar, "/abc/123") + os.Setenv(shareddefaults.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 51f30556301..5d7b289501b 100644 --- a/aws/session/session.go +++ b/aws/session/session.go @@ -19,8 +19,26 @@ import ( "github.com/aws/aws-sdk-go/aws/defaults" "github.com/aws/aws-sdk-go/aws/endpoints" "github.com/aws/aws-sdk-go/aws/request" + "github.com/aws/aws-sdk-go/internal/shareddefaults" ) +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,6 +454,57 @@ 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(shareddefaults.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, @@ -445,32 +514,14 @@ func mergeConfigSrcs(cfg, userCfg *aws.Config, envCfg envConfig, sharedCfg share 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, @@ -493,6 +544,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 9612b315003..bd0f54e0052 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/shareddefaults" "github.com/aws/aws-sdk-go/service/s3" ) @@ -437,6 +438,228 @@ func TestSessionAssumeRole_InvalidSourceProfile(t *testing.T) { assert.Nil(t, s) } +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" + }` + + 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" : "access-key", + "SecretAccessKey" : "secret-key", + "Token" : "token", + "Expiration" : "2100-01-01T00:00:00Z", + "LastUpdated" : "2009-11-23T0:00:00Z" + }` + + 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("")) + } + })) + + shareddefaults.ECSContainerCredentialsURI = 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 + } + }, + }, + } + + 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) + } + + if c.expectedError != nil { + continue + } + + creds, err := sess.Config.Credentials.Get() + if err != nil { + t.Errorf("expected no error, but received %v", err) + } + + if e, a := c.expectedAccessKey, creds.AccessKeyID; e != a { + t.Errorf("expected %v, but received %v", e, a) + } + + if e, a := c.expectedSecretKey, creds.SecretAccessKey; e != a { + t.Errorf("expected %v, but received %v", e, a) + } + + if err := clean(); err != nil { + t.Errorf("expected no error, but received %v", err) + } + } +} + func initSessionTestEnv() (oldEnv []string) { oldEnv = awstesting.StashEnv() os.Setenv("AWS_CONFIG_FILE", "file_not_exists") diff --git a/aws/session/shared_config.go b/aws/session/shared_config.go index 09c8e5bc7ab..565a0b79508 100644 --- a/aws/session/shared_config.go +++ b/aws/session/shared_config.go @@ -16,11 +16,12 @@ const ( sessionTokenKey = `aws_session_token` // optional // Assume Role Credentials group - roleArnKey = `role_arn` // group required - sourceProfileKey = `source_profile` // group required - externalIDKey = `external_id` // optional - mfaSerialKey = `mfa_serial` // optional - roleSessionNameKey = `role_session_name` // optional + roleArnKey = `role_arn` // group required + sourceProfileKey = `source_profile` // group required (or credential_source) + credentialSourceKey = `credential_source` // group required (or source_profile) + externalIDKey = `external_id` // optional + mfaSerialKey = `mfa_serial` // optional + roleSessionNameKey = `role_session_name` // optional // Additional Config fields regionKey = `region` @@ -32,11 +33,12 @@ const ( ) type assumeRoleConfig struct { - RoleARN string - SourceProfile string - ExternalID string - MFASerial string - RoleSessionName string + RoleARN string + SourceProfile string + CredentialSource string + ExternalID string + MFASerial string + RoleSessionName string } // sharedConfig represents the configuration fields of the SDK config files. @@ -127,6 +129,13 @@ func loadSharedConfigIniFiles(filenames []string) ([]sharedConfigFile, error) { func (cfg *sharedConfig) setAssumeRoleSource(origProfile string, files []sharedConfigFile) error { var assumeRoleSrc sharedConfig + if len(cfg.AssumeRole.CredentialSource) > 0 { + // setAssumeRoleSource is only called when source_profile is found. + // If both source_profile and credential_source are set, then + // ErrSharedConfigSourceCollision will be returned + return ErrSharedConfigSourceCollision + } + // Multiple level assume role chains are not support if cfg.AssumeRole.SourceProfile == origProfile { assumeRoleSrc = *cfg @@ -195,13 +204,16 @@ func (cfg *sharedConfig) setFromIniFile(profile string, file sharedConfigFile) e // Assume Role roleArn := section.Key(roleArnKey).String() srcProfile := section.Key(sourceProfileKey).String() - if len(roleArn) > 0 && len(srcProfile) > 0 { + credentialSource := section.Key(credentialSourceKey).String() + hasSource := len(srcProfile) > 0 || len(credentialSource) > 0 + if len(roleArn) > 0 && hasSource { cfg.AssumeRole = assumeRoleConfig{ - RoleARN: roleArn, - SourceProfile: srcProfile, - ExternalID: section.Key(externalIDKey).String(), - MFASerial: section.Key(mfaSerialKey).String(), - RoleSessionName: section.Key(roleSessionNameKey).String(), + RoleARN: roleArn, + SourceProfile: srcProfile, + CredentialSource: credentialSource, + ExternalID: section.Key(externalIDKey).String(), + MFASerial: section.Key(mfaSerialKey).String(), + RoleSessionName: section.Key(roleSessionNameKey).String(), } } 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 8705608e10a..fe816fe201b 100644 --- a/aws/session/testdata/shared_config +++ b/aws/session/testdata/shared_config @@ -1,5 +1,5 @@ [default] -s3 = +s3 = unsupported_key=123 other_unsupported=abc diff --git a/internal/shareddefaults/ecs_container.go b/internal/shareddefaults/ecs_container.go new file mode 100644 index 00000000000..b63e4c2639b --- /dev/null +++ b/internal/shareddefaults/ecs_container.go @@ -0,0 +1,12 @@ +package shareddefaults + +const ( + // ECSCredsProviderEnvVar is an environmental variable key used to + // determine which path needs to be hit. + ECSCredsProviderEnvVar = "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI" +) + +// ECSContainerCredentialsURI is the endpoint to retrieve container +// credentials. This can be overriden to test to ensure the credential process +// is behaving correctly. +var ECSContainerCredentialsURI = "http://169.254.170.2"