From 84cbcb679489c568090cdc7aba61ce424d2e1c77 Mon Sep 17 00:00:00 2001 From: Janario Oliveira Date: Thu, 13 Jun 2019 00:05:02 +0200 Subject: [PATCH] aws/session: Add support for chaining assume IAM role from shared config (#2579) Adds support chaining assume role credentials from the shared config/credentials files. This change allows you to create an assume role chain of multiple levels of assumed IAM roles. The config profile the deepest in the chain must use static credentials, or `credential_source`. If the deepest profile doesn't have either of these the session will fail to load. Fixes the SDK's shared config credential source not assuming a role with environment and ECS credentials. EC2 credentials were already supported. Fix #2528 Fix #2385 Also adds the ability to specify the Handlers the SDK should use at the SessionWithOptions. This allows the a set of handlers to be provided at the very beginning of the session credential chain. --- aws/credentials/credentials.go | 3 +- .../stscreds/assume_role_provider.go | 3 +- aws/request/handlers.go | 45 ++ aws/session/credentials.go | 202 ++++++++ aws/session/credentials_test.go | 416 +++++++++++++++ aws/session/session.go | 179 +------ aws/session/session_test.go | 490 ++---------------- aws/session/shared_config.go | 14 +- aws/session/shared_config_test.go | 75 ++- aws/session/shared_test.go | 15 + aws/session/testdata/credential_source_config | 12 +- aws/session/testdata/shared_config | 16 + 12 files changed, 851 insertions(+), 619 deletions(-) create mode 100644 aws/session/credentials.go create mode 100644 aws/session/credentials_test.go create mode 100644 aws/session/shared_test.go diff --git a/aws/credentials/credentials.go b/aws/credentials/credentials.go index 894bbc7f82c..83bbc311b4d 100644 --- a/aws/credentials/credentials.go +++ b/aws/credentials/credentials.go @@ -50,9 +50,10 @@ package credentials import ( "fmt" - "github.com/aws/aws-sdk-go/aws/awserr" "sync" "time" + + "github.com/aws/aws-sdk-go/aws/awserr" ) // AnonymousCredentials is an empty Credential object that can be used as diff --git a/aws/credentials/stscreds/assume_role_provider.go b/aws/credentials/stscreds/assume_role_provider.go index b6dbfd2467d..2e528d130d4 100644 --- a/aws/credentials/stscreds/assume_role_provider.go +++ b/aws/credentials/stscreds/assume_role_provider.go @@ -200,7 +200,7 @@ type AssumeRoleProvider struct { // by a random percentage between 0 and MaxJitterFraction. MaxJitterFrac must // have a value between 0 and 1. Any other value may lead to expected behavior. // With a MaxJitterFrac value of 0, default) will no jitter will be used. - // + // // For example, with a Duration of 30m and a MaxJitterFrac of 0.1, the // AssumeRole call will be made with an arbitrary Duration between 27m and // 30m. @@ -258,7 +258,6 @@ func NewCredentialsWithClient(svc AssumeRoler, roleARN string, options ...func(* // Retrieve generates a new set of temporary credentials using STS. func (p *AssumeRoleProvider) Retrieve() (credentials.Value, error) { - // Apply defaults where parameters are not set. if p.RoleSessionName == "" { // Try to work out a role name that will hopefully end up unique. diff --git a/aws/request/handlers.go b/aws/request/handlers.go index 8ef8548a96d..627ec722c05 100644 --- a/aws/request/handlers.go +++ b/aws/request/handlers.go @@ -59,6 +59,51 @@ func (h *Handlers) Clear() { h.Complete.Clear() } +// IsEmpty returns if there are no handlers in any of the handlerlists. +func (h *Handlers) IsEmpty() bool { + if h.Validate.Len() != 0 { + return false + } + if h.Build.Len() != 0 { + return false + } + if h.Send.Len() != 0 { + return false + } + if h.Sign.Len() != 0 { + return false + } + if h.Unmarshal.Len() != 0 { + return false + } + if h.UnmarshalStream.Len() != 0 { + return false + } + if h.UnmarshalMeta.Len() != 0 { + return false + } + if h.UnmarshalError.Len() != 0 { + return false + } + if h.ValidateResponse.Len() != 0 { + return false + } + if h.Retry.Len() != 0 { + return false + } + if h.AfterRetry.Len() != 0 { + return false + } + if h.CompleteAttempt.Len() != 0 { + return false + } + if h.Complete.Len() != 0 { + return false + } + + return true +} + // A HandlerListRunItem represents an entry in the HandlerList which // is being run. type HandlerListRunItem struct { diff --git a/aws/session/credentials.go b/aws/session/credentials.go new file mode 100644 index 00000000000..9e2652c5596 --- /dev/null +++ b/aws/session/credentials.go @@ -0,0 +1,202 @@ +package session + +import ( + "fmt" + "os" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/aws/credentials/processcreds" + "github.com/aws/aws-sdk-go/aws/credentials/stscreds" + "github.com/aws/aws-sdk-go/aws/defaults" + "github.com/aws/aws-sdk-go/aws/request" + "github.com/aws/aws-sdk-go/internal/shareddefaults" +) + +// valid credential source values +const ( + credSourceEc2Metadata = "Ec2InstanceMetadata" + credSourceEnvironment = "Environment" + credSourceECSContainer = "EcsContainer" +) + +func resolveCredentials(cfg *aws.Config, + envCfg envConfig, sharedCfg sharedConfig, + handlers request.Handlers, + sessOpts Options, +) (*credentials.Credentials, error) { + // Credentials from Assume Role with specific credentials source. + if envCfg.EnableSharedConfig && len(sharedCfg.AssumeRole.CredentialSource) > 0 { + return resolveCredsFromSource(cfg, envCfg, sharedCfg, handlers, sessOpts) + } + + // Credentials from environment variables + if len(envCfg.Creds.AccessKeyID) > 0 { + return credentials.NewStaticCredentialsFromCreds(envCfg.Creds), nil + } + + // Fallback to the "default" credential resolution chain. + return resolveCredsFromProfile(cfg, envCfg, sharedCfg, handlers, sessOpts) +} + +func resolveCredsFromProfile(cfg *aws.Config, + envCfg envConfig, sharedCfg sharedConfig, + handlers request.Handlers, + sessOpts Options, +) (*credentials.Credentials, error) { + + if envCfg.EnableSharedConfig && len(sharedCfg.AssumeRole.RoleARN) > 0 && sharedCfg.AssumeRoleSource != nil { + // Assume IAM role with credentials source from a different profile. + cred, err := resolveCredsFromProfile(cfg, envCfg, *sharedCfg.AssumeRoleSource, handlers, sessOpts) + if err != nil { + return nil, err + } + + cfgCp := *cfg + cfgCp.Credentials = cred + return credsFromAssumeRole(cfgCp, handlers, sharedCfg, sessOpts) + + } else if len(sharedCfg.Creds.AccessKeyID) > 0 { + // Static Credentials from Shared Config/Credentials file. + return credentials.NewStaticCredentialsFromCreds( + sharedCfg.Creds, + ), nil + + } else if len(sharedCfg.CredentialProcess) > 0 { + // Credential Process credentials from Shared Config/Credentials file. + return processcreds.NewCredentials( + sharedCfg.CredentialProcess, + ), nil + + } else if envCfg.EnableSharedConfig && len(sharedCfg.AssumeRole.CredentialSource) > 0 { + // Assume IAM Role with specific credential source. + return resolveCredsFromSource(cfg, envCfg, sharedCfg, handlers, sessOpts) + } + + // Fallback to default credentials provider, include mock errors + // for the credential chain so user can identify why credentials + // failed to be retrieved. + return credentials.NewCredentials(&credentials.ChainProvider{ + VerboseErrors: aws.BoolValue(cfg.CredentialsChainVerboseErrors), + Providers: []credentials.Provider{ + &credProviderError{ + Err: awserr.New("EnvAccessKeyNotFound", + "failed to find credentials in the environment.", nil), + }, + &credProviderError{ + Err: awserr.New("SharedCredsLoad", + fmt.Sprintf("failed to load profile, %s.", envCfg.Profile), nil), + }, + defaults.RemoteCredProvider(*cfg, handlers), + }, + }), nil +} + +func resolveCredsFromSource(cfg *aws.Config, + envCfg envConfig, sharedCfg sharedConfig, + handlers request.Handlers, + sessOpts Options, +) (*credentials.Credentials, error) { + // if both credential_source and source_profile have been set, return an + // error as this is undefined behavior. Only one can be used at a time + // within a profile. + if len(sharedCfg.AssumeRole.SourceProfile) > 0 { + return nil, ErrSharedConfigSourceCollision + } + + cfgCp := *cfg + switch sharedCfg.AssumeRole.CredentialSource { + case credSourceEc2Metadata: + p := defaults.RemoteCredProvider(cfgCp, handlers) + cfgCp.Credentials = credentials.NewCredentials(p) + + case credSourceEnvironment: + cfgCp.Credentials = credentials.NewStaticCredentialsFromCreds(envCfg.Creds) + + case credSourceECSContainer: + if len(os.Getenv(shareddefaults.ECSCredsProviderEnvVar)) == 0 { + return nil, ErrSharedConfigECSContainerEnvVarEmpty + } + + p := defaults.RemoteCredProvider(cfgCp, handlers) + cfgCp.Credentials = credentials.NewCredentials(p) + + default: + return nil, ErrSharedConfigInvalidCredSource + } + + return credsFromAssumeRole(cfgCp, handlers, sharedCfg, sessOpts) +} + +func credsFromAssumeRole(cfg aws.Config, + handlers request.Handlers, + sharedCfg sharedConfig, + sessOpts Options, +) (*credentials.Credentials, error) { + if len(sharedCfg.AssumeRole.MFASerial) > 0 && sessOpts.AssumeRoleTokenProvider == nil { + // AssumeRole Token provider is required if doing Assume Role + // with MFA. + return nil, AssumeRoleTokenProviderNotSetError{} + } + + 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 + } + }, + ), nil +} + +// 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. +type AssumeRoleTokenProviderNotSetError struct{} + +// Code is the short id of the error. +func (e AssumeRoleTokenProviderNotSetError) Code() string { + return "AssumeRoleTokenProviderNotSetError" +} + +// Message is the description of the error +func (e AssumeRoleTokenProviderNotSetError) Message() string { + return fmt.Sprintf("assume role with MFA enabled, but AssumeRoleTokenProvider session option not set.") +} + +// OrigErr is the underlying error that caused the failure. +func (e AssumeRoleTokenProviderNotSetError) OrigErr() error { + return nil +} + +// Error satisfies the error interface. +func (e AssumeRoleTokenProviderNotSetError) Error() string { + return awserr.SprintError(e.Code(), e.Message(), "", nil) +} + +type credProviderError struct { + Err error +} + +var emptyCreds = credentials.Value{} + +func (c credProviderError) Retrieve() (credentials.Value, error) { + return credentials.Value{}, c.Err +} +func (c credProviderError) IsExpired() bool { + return true +} diff --git a/aws/session/credentials_test.go b/aws/session/credentials_test.go new file mode 100644 index 00000000000..ad6a4edf50a --- /dev/null +++ b/aws/session/credentials_test.go @@ -0,0 +1,416 @@ +// +build go1.7 + +package session + +import ( + "fmt" + "net/http" + "net/http/httptest" + "os" + "reflect" + "strings" + "testing" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/credentials" + "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/awstesting" + "github.com/aws/aws-sdk-go/internal/shareddefaults" + "github.com/aws/aws-sdk-go/service/sts" +) + +func setupCredentialsEndpoints(t *testing.T) (endpoints.Resolver, func()) { + origECSEndpoint := shareddefaults.ECSContainerCredentialsURI + + ecsMetadataServer := httptest.NewServer(http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/ECS" { + w.Write([]byte(ecsResponse)) + } else { + w.Write([]byte("")) + } + })) + shareddefaults.ECSContainerCredentialsURI = ecsMetadataServer.URL + + ec2MetadataServer := httptest.NewServer(http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/meta-data/iam/security-credentials/RoleName" { + 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")))) + })) + + resolver := endpoints.ResolverFunc( + func(service, region string, opts ...func(*endpoints.Options)) (endpoints.ResolvedEndpoint, error) { + switch service { + case "ec2metadata": + return endpoints.ResolvedEndpoint{ + URL: ec2MetadataServer.URL, + }, nil + case "sts": + return endpoints.ResolvedEndpoint{ + URL: stsServer.URL, + }, nil + default: + return endpoints.ResolvedEndpoint{}, + fmt.Errorf("unknown service endpoint, %s", service) + } + }) + + return resolver, func() { + shareddefaults.ECSContainerCredentialsURI = origECSEndpoint + ecsMetadataServer.Close() + ec2MetadataServer.Close() + stsServer.Close() + } +} + +func TestSharedConfigCredentialSource(t *testing.T) { + const configFile = "testdata/credential_source_config" + + cases := []struct { + name string + profile string + expectedError error + expectedAccessKey string + expectedSecretKey string + expectedChain []string + init func() + }{ + { + name: "credential source and source profile", + profile: "invalid_source_and_credential_source", + expectedError: ErrSharedConfigSourceCollision, + init: func() { + os.Setenv("AWS_ACCESS_KEY", "access_key") + os.Setenv("AWS_SECRET_KEY", "secret_key") + }, + }, + { + name: "env var credential source", + profile: "env_var_credential_source", + expectedAccessKey: "AKID", + expectedSecretKey: "SECRET", + expectedChain: []string{ + "assume_role_w_creds_role_arn_env", + }, + init: func() { + os.Setenv("AWS_ACCESS_KEY", "access_key") + os.Setenv("AWS_SECRET_KEY", "secret_key") + }, + }, + { + name: "ec2metadata credential source", + profile: "ec2metadata", + expectedChain: []string{ + "assume_role_w_creds_role_arn_ec2", + }, + expectedAccessKey: "AKID", + expectedSecretKey: "SECRET", + }, + { + name: "ecs container credential source", + profile: "ecscontainer", + expectedAccessKey: "AKID", + expectedSecretKey: "SECRET", + expectedChain: []string{ + "assume_role_w_creds_role_arn_ecs", + }, + init: func() { + os.Setenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", "/ECS") + }, + }, + { + name: "chained assume role with env creds", + profile: "chained_assume_role", + expectedAccessKey: "AKID", + expectedSecretKey: "SECRET", + expectedChain: []string{ + "assume_role_w_creds_role_arn_chain", + "assume_role_w_creds_role_arn_ec2", + }, + }, + } + + for i, c := range cases { + t.Run(fmt.Sprintf("%d %s", i, c.name), + func(t *testing.T) { + env := awstesting.StashEnv() + defer awstesting.PopEnv(env) + + os.Setenv("AWS_REGION", "us-east-1") + os.Setenv("AWS_SDK_LOAD_CONFIG", "1") + os.Setenv("AWS_CONFIG_FILE", configFile) + os.Setenv("AWS_PROFILE", c.profile) + + endpointResolver, cleanupFn := setupCredentialsEndpoints(t) + defer cleanupFn() + + if c.init != nil { + c.init() + } + + var credChain []string + handlers := defaults.Handlers() + handlers.Sign.PushBack(func(r *request.Request) { + if r.Config.Credentials == credentials.AnonymousCredentials { + return + } + params := r.Params.(*sts.AssumeRoleInput) + credChain = append(credChain, *params.RoleArn) + }) + + sess, err := NewSessionWithOptions(Options{ + Config: aws.Config{ + Logger: t, + // LogLevel: aws.LogLevel(aws.LogDebugWithHTTPBody), + EndpointResolver: endpointResolver, + }, + Handlers: handlers, + }) + if e, a := c.expectedError, err; e != a { + t.Errorf("expected %v, but received %v", e, a) + } + + if c.expectedError != nil { + return + } + + creds, err := sess.Config.Credentials.Get() + if err != nil { + t.Fatalf("expected no error, but received %v", err) + } + + if e, a := c.expectedChain, credChain; !reflect.DeepEqual(e, a) { + t.Errorf("expected %v, but received %v", e, a) + } + + 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) + } + }) + } +} + +const ecsResponse = `{ + "Code": "Success", + "Type": "AWS-HMAC", + "AccessKeyId" : "ecs-access-key", + "SecretAccessKey" : "ecs-secret-key", + "Token" : "token", + "Expiration" : "2100-01-01T00:00:00Z", + "LastUpdated" : "2009-11-23T0:00:00Z" + }` + +const ec2MetadataResponse = `{ + "Code": "Success", + "Type": "AWS-HMAC", + "AccessKeyId" : "ec2-access-key", + "SecretAccessKey" : "ec2-secret-key", + "Token" : "token", + "Expiration" : "2100-01-01T00:00:00Z", + "LastUpdated" : "2009-11-23T0:00:00Z" + }` + +const assumeRoleRespMsg = ` + + + + arn:aws:sts::account_id:assumed-role/role/session_name + AKID:session_name + + + AKID + SECRET + SESSION_TOKEN + %s + + + + request-id + + +` + +func TestSesisonAssumeRole(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_w_creds") + + server := 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")))) + })) + + s, err := NewSession(&aws.Config{Endpoint: aws.String(server.URL), DisableSSL: aws.Bool(true)}) + + creds, err := s.Config.Credentials.Get() + if err != nil { + t.Errorf("expect nil, %v", err) + } + if e, a := "AKID", creds.AccessKeyID; e != a { + t.Errorf("expect %v, got %v", e, a) + } + if e, a := "SECRET", creds.SecretAccessKey; e != a { + t.Errorf("expect %v, got %v", e, a) + } + if e, a := "SESSION_TOKEN", creds.SessionToken; e != a { + t.Errorf("expect %v, got %v", e, a) + } + if e, a := "AssumeRoleProvider", creds.ProviderName; !strings.Contains(a, e) { + t.Errorf("expect %v, to contain %v", e, a) + } +} + +func TestSessionAssumeRole_WithMFA(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_w_creds") + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if e, a := r.FormValue("SerialNumber"), "0123456789"; e != a { + t.Errorf("expect %v, got %v", e, a) + } + if e, a := r.FormValue("TokenCode"), "tokencode"; e != a { + t.Errorf("expect %v, got %v", e, a) + } + + w.Write([]byte(fmt.Sprintf(assumeRoleRespMsg, time.Now().Add(15*time.Minute).Format("2006-01-02T15:04:05Z")))) + })) + + customProviderCalled := false + sess, err := NewSessionWithOptions(Options{ + Profile: "assume_role_w_mfa", + Config: aws.Config{ + Region: aws.String("us-east-1"), + Endpoint: aws.String(server.URL), + DisableSSL: aws.Bool(true), + }, + SharedConfigState: SharedConfigEnable, + AssumeRoleTokenProvider: func() (string, error) { + customProviderCalled = true + + return "tokencode", nil + }, + }) + if err != nil { + t.Errorf("expect nil, %v", err) + } + + creds, err := sess.Config.Credentials.Get() + if err != nil { + t.Errorf("expect nil, %v", err) + } + if !customProviderCalled { + t.Errorf("expect true") + } + + if e, a := "AKID", creds.AccessKeyID; e != a { + t.Errorf("expect %v, got %v", e, a) + } + if e, a := "SECRET", creds.SecretAccessKey; e != a { + t.Errorf("expect %v, got %v", e, a) + } + if e, a := "SESSION_TOKEN", creds.SessionToken; e != a { + t.Errorf("expect %v, got %v", e, a) + } + if e, a := "AssumeRoleProvider", creds.ProviderName; !strings.Contains(a, e) { + t.Errorf("expect %v, to contain %v", e, a) + } +} + +func TestSessionAssumeRole_WithMFA_NoTokenProvider(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_w_creds") + + _, err := NewSessionWithOptions(Options{ + Profile: "assume_role_w_mfa", + SharedConfigState: SharedConfigEnable, + }) + if e, a := (AssumeRoleTokenProviderNotSetError{}), err; e != a { + t.Errorf("expect %v, got %v", e, a) + } +} + +func TestSessionAssumeRole_DisableSharedConfig(t *testing.T) { + // Backwards compatibility with Shared config disabled + // assume role should not be built into the config. + oldEnv := initSessionTestEnv() + defer awstesting.PopEnv(oldEnv) + + os.Setenv("AWS_SDK_LOAD_CONFIG", "0") + os.Setenv("AWS_SHARED_CREDENTIALS_FILE", testConfigFilename) + os.Setenv("AWS_PROFILE", "assume_role_w_creds") + + s, err := NewSession() + if err != nil { + t.Errorf("expect nil, %v", err) + } + + creds, err := s.Config.Credentials.Get() + if err != nil { + t.Errorf("expect nil, %v", err) + } + if e, a := "assume_role_w_creds_akid", creds.AccessKeyID; e != a { + t.Errorf("expect %v, got %v", e, a) + } + if e, a := "assume_role_w_creds_secret", creds.SecretAccessKey; e != a { + t.Errorf("expect %v, got %v", e, a) + } + if e, a := "SharedConfigCredentials", creds.ProviderName; !strings.Contains(a, e) { + t.Errorf("expect %v, to contain %v", e, a) + } +} + +func TestSessionAssumeRole_InvalidSourceProfile(t *testing.T) { + // Backwards compatibility with Shared config disabled + // assume role should not be built into the config. + oldEnv := initSessionTestEnv() + defer awstesting.PopEnv(oldEnv) + + os.Setenv("AWS_SDK_LOAD_CONFIG", "1") + os.Setenv("AWS_SHARED_CREDENTIALS_FILE", testConfigFilename) + os.Setenv("AWS_PROFILE", "assume_role_invalid_source_profile") + + s, err := NewSession() + if err == nil { + t.Errorf("expect error") + } + if e, a := "SharedConfigAssumeRoleError: failed to load assume role", err.Error(); !strings.Contains(a, e) { + t.Errorf("expect %v, to contain %v", e, a) + } + if s != nil { + t.Errorf("expect nil, %v", err) + } +} diff --git a/aws/session/session.go b/aws/session/session.go index be4b5f07772..dab8fea3998 100644 --- a/aws/session/session.go +++ b/aws/session/session.go @@ -3,7 +3,6 @@ package session import ( "crypto/tls" "crypto/x509" - "fmt" "io" "io/ioutil" "net/http" @@ -14,13 +13,10 @@ import ( "github.com/aws/aws-sdk-go/aws/client" "github.com/aws/aws-sdk-go/aws/corehandlers" "github.com/aws/aws-sdk-go/aws/credentials" - "github.com/aws/aws-sdk-go/aws/credentials/processcreds" - "github.com/aws/aws-sdk-go/aws/credentials/stscreds" "github.com/aws/aws-sdk-go/aws/csm" "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 ( @@ -224,6 +220,12 @@ type Options struct { // to also enable this feature. CustomCABundle session option field has priority // over the AWS_CA_BUNDLE environment variable, and will be used if both are set. CustomCABundle io.Reader + + // The handlers that the session and all API clients will be created with. + // This must be a complete set of handlers. Use the defaults.Handlers() + // function to initialize this value before changing the handlers to be + // used by the SDK. + Handlers request.Handlers } // NewSessionWithOptions returns a new Session created from SDK defaults, config files, @@ -344,7 +346,11 @@ func enableCSM(handlers *request.Handlers, clientID string, port string, logger func newSession(opts Options, envCfg envConfig, cfgs ...*aws.Config) (*Session, error) { cfg := defaults.Config() - handlers := defaults.Handlers() + + handlers := opts.Handlers + if handlers.IsEmpty() { + handlers = defaults.Handlers() + } // Get a merged version of the user provided config to determine if // credentials were. @@ -443,7 +449,11 @@ func loadCertPool(r io.Reader) (*x509.CertPool, error) { return p, nil } -func mergeConfigSrcs(cfg, userCfg *aws.Config, envCfg envConfig, sharedCfg sharedConfig, handlers request.Handlers, sessOpts Options) error { +func mergeConfigSrcs(cfg, userCfg *aws.Config, + envCfg envConfig, sharedCfg sharedConfig, + handlers request.Handlers, + sessOpts Options, +) error { // Merge in user provided configuration cfg.MergeIn(userCfg) @@ -464,164 +474,19 @@ func mergeConfigSrcs(cfg, userCfg *aws.Config, envCfg envConfig, sharedCfg share } } - // Configure credentials if not already set + // Configure credentials if not already set by the user when creating the + // Session. 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, - ) - } else if envCfg.EnableSharedConfig && len(sharedCfg.AssumeRole.RoleARN) > 0 && sharedCfg.AssumeRoleSource != nil { - cfgCp := *cfg - 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 = assumeRoleCredentials(cfgCp, handlers, sharedCfg, sessOpts) - } else if len(sharedCfg.Creds.AccessKeyID) > 0 { - cfg.Credentials = credentials.NewStaticCredentialsFromCreds( - sharedCfg.Creds, - ) - } else if len(sharedCfg.CredentialProcess) > 0 { - cfg.Credentials = processcreds.NewCredentials( - sharedCfg.CredentialProcess, - ) - } else { - // Fallback to default credentials provider, include mock errors - // for the credential chain so user can identify why credentials - // failed to be retrieved. - cfg.Credentials = credentials.NewCredentials(&credentials.ChainProvider{ - VerboseErrors: aws.BoolValue(cfg.CredentialsChainVerboseErrors), - Providers: []credentials.Provider{ - &credProviderError{Err: awserr.New("EnvAccessKeyNotFound", "failed to find credentials in the environment.", nil)}, - &credProviderError{Err: awserr.New("SharedCredsLoad", fmt.Sprintf("failed to load profile, %s.", envCfg.Profile), nil)}, - defaults.RemoteCredProvider(*cfg, handlers), - }, - }) + creds, err := resolveCredentials(cfg, envCfg, sharedCfg, handlers, sessOpts) + if err != nil { + return err } + cfg.Credentials = creds } 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. -type AssumeRoleTokenProviderNotSetError struct{} - -// Code is the short id of the error. -func (e AssumeRoleTokenProviderNotSetError) Code() string { - return "AssumeRoleTokenProviderNotSetError" -} - -// Message is the description of the error -func (e AssumeRoleTokenProviderNotSetError) Message() string { - return fmt.Sprintf("assume role with MFA enabled, but AssumeRoleTokenProvider session option not set.") -} - -// OrigErr is the underlying error that caused the failure. -func (e AssumeRoleTokenProviderNotSetError) OrigErr() error { - return nil -} - -// Error satisfies the error interface. -func (e AssumeRoleTokenProviderNotSetError) Error() string { - return awserr.SprintError(e.Code(), e.Message(), "", nil) -} - -type credProviderError struct { - Err error -} - -var emptyCreds = credentials.Value{} - -func (c credProviderError) Retrieve() (credentials.Value, error) { - return credentials.Value{}, c.Err -} -func (c credProviderError) IsExpired() bool { - return true -} - func initHandlers(s *Session) { // Add the Validate parameter handler if it is not disabled. s.Handlers.Validate.Remove(corehandlers.ValidateParametersHandler) diff --git a/aws/session/session_test.go b/aws/session/session_test.go index 61a1fd2f5fb..af63d85ca22 100644 --- a/aws/session/session_test.go +++ b/aws/session/session_test.go @@ -1,21 +1,21 @@ +// +build go1.7 + package session import ( "bytes" "fmt" "net/http" - "net/http/httptest" "os" + "strconv" "strings" "testing" - "time" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/credentials" "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" ) @@ -367,454 +367,42 @@ func TestNewSessionWithOptions_Overrides(t *testing.T) { }, } - for _, c := range cases { - oldEnv := initSessionTestEnv() - defer awstesting.PopEnv(oldEnv) - - for k, v := range c.InEnvs { - os.Setenv(k, v) - } - - s, err := NewSessionWithOptions(Options{ - Profile: c.InProfile, - SharedConfigState: SharedConfigEnable, + for i, c := range cases { + t.Run(strconv.Itoa(i), func(t *testing.T) { + oldEnv := initSessionTestEnv() + defer awstesting.PopEnv(oldEnv) + + for k, v := range c.InEnvs { + os.Setenv(k, v) + } + + s, err := NewSessionWithOptions(Options{ + Profile: c.InProfile, + SharedConfigState: SharedConfigEnable, + }) + if err != nil { + t.Errorf("expect nil, %v", err) + } + + creds, err := s.Config.Credentials.Get() + if err != nil { + t.Errorf("expect nil, %v", err) + } + if e, a := c.OutRegion, *s.Config.Region; e != a { + t.Errorf("expect %v, got %v", e, a) + } + if e, a := c.OutCreds.AccessKeyID, creds.AccessKeyID; e != a { + t.Errorf("expect %v, got %v", e, a) + } + if e, a := c.OutCreds.SecretAccessKey, creds.SecretAccessKey; e != a { + t.Errorf("expect %v, got %v", e, a) + } + if e, a := c.OutCreds.SessionToken, creds.SessionToken; e != a { + t.Errorf("expect %v, got %v", e, a) + } + if e, a := c.OutCreds.ProviderName, creds.ProviderName; !strings.Contains(a, e) { + t.Errorf("expect %v, to contain %v", e, a) + } }) - if err != nil { - t.Errorf("expect nil, %v", err) - } - - creds, err := s.Config.Credentials.Get() - if err != nil { - t.Errorf("expect nil, %v", err) - } - if e, a := c.OutRegion, *s.Config.Region; e != a { - t.Errorf("expect %v, got %v", e, a) - } - if e, a := c.OutCreds.AccessKeyID, creds.AccessKeyID; e != a { - t.Errorf("expect %v, got %v", e, a) - } - if e, a := c.OutCreds.SecretAccessKey, creds.SecretAccessKey; e != a { - t.Errorf("expect %v, got %v", e, a) - } - if e, a := c.OutCreds.SessionToken, creds.SessionToken; e != a { - t.Errorf("expect %v, got %v", e, a) - } - if e, a := c.OutCreds.ProviderName, creds.ProviderName; !strings.Contains(a, e) { - t.Errorf("expect %v, to contain %v", e, a) - } - } -} - -const assumeRoleRespMsg = ` - - - - arn:aws:sts::account_id:assumed-role/role/session_name - AKID:session_name - - - AKID - SECRET - SESSION_TOKEN - %s - - - - request-id - - -` - -func TestSesisonAssumeRole(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_w_creds") - - server := 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")))) - })) - - s, err := NewSession(&aws.Config{Endpoint: aws.String(server.URL), DisableSSL: aws.Bool(true)}) - - creds, err := s.Config.Credentials.Get() - if err != nil { - t.Errorf("expect nil, %v", err) } - if e, a := "AKID", creds.AccessKeyID; e != a { - t.Errorf("expect %v, got %v", e, a) - } - if e, a := "SECRET", creds.SecretAccessKey; e != a { - t.Errorf("expect %v, got %v", e, a) - } - if e, a := "SESSION_TOKEN", creds.SessionToken; e != a { - t.Errorf("expect %v, got %v", e, a) - } - if e, a := "AssumeRoleProvider", creds.ProviderName; !strings.Contains(a, e) { - t.Errorf("expect %v, to contain %v", e, a) - } -} - -func TestSessionAssumeRole_WithMFA(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_w_creds") - - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if e, a := r.FormValue("SerialNumber"), "0123456789"; e != a { - t.Errorf("expect %v, got %v", e, a) - } - if e, a := r.FormValue("TokenCode"), "tokencode"; e != a { - t.Errorf("expect %v, got %v", e, a) - } - - w.Write([]byte(fmt.Sprintf(assumeRoleRespMsg, time.Now().Add(15*time.Minute).Format("2006-01-02T15:04:05Z")))) - })) - - customProviderCalled := false - sess, err := NewSessionWithOptions(Options{ - Profile: "assume_role_w_mfa", - Config: aws.Config{ - Region: aws.String("us-east-1"), - Endpoint: aws.String(server.URL), - DisableSSL: aws.Bool(true), - }, - SharedConfigState: SharedConfigEnable, - AssumeRoleTokenProvider: func() (string, error) { - customProviderCalled = true - - return "tokencode", nil - }, - }) - if err != nil { - t.Errorf("expect nil, %v", err) - } - - creds, err := sess.Config.Credentials.Get() - if err != nil { - t.Errorf("expect nil, %v", err) - } - if !customProviderCalled { - t.Errorf("expect true") - } - - if e, a := "AKID", creds.AccessKeyID; e != a { - t.Errorf("expect %v, got %v", e, a) - } - if e, a := "SECRET", creds.SecretAccessKey; e != a { - t.Errorf("expect %v, got %v", e, a) - } - if e, a := "SESSION_TOKEN", creds.SessionToken; e != a { - t.Errorf("expect %v, got %v", e, a) - } - if e, a := "AssumeRoleProvider", creds.ProviderName; !strings.Contains(a, e) { - t.Errorf("expect %v, to contain %v", e, a) - } -} - -func TestSessionAssumeRole_WithMFA_NoTokenProvider(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_w_creds") - - _, err := NewSessionWithOptions(Options{ - Profile: "assume_role_w_mfa", - SharedConfigState: SharedConfigEnable, - }) - if e, a := (AssumeRoleTokenProviderNotSetError{}), err; e != a { - t.Errorf("expect %v, got %v", e, a) - } -} - -func TestSessionAssumeRole_DisableSharedConfig(t *testing.T) { - // Backwards compatibility with Shared config disabled - // assume role should not be built into the config. - oldEnv := initSessionTestEnv() - defer awstesting.PopEnv(oldEnv) - - os.Setenv("AWS_SDK_LOAD_CONFIG", "0") - os.Setenv("AWS_SHARED_CREDENTIALS_FILE", testConfigFilename) - os.Setenv("AWS_PROFILE", "assume_role_w_creds") - - s, err := NewSession() - if err != nil { - t.Errorf("expect nil, %v", err) - } - - creds, err := s.Config.Credentials.Get() - if err != nil { - t.Errorf("expect nil, %v", err) - } - if e, a := "assume_role_w_creds_akid", creds.AccessKeyID; e != a { - t.Errorf("expect %v, got %v", e, a) - } - if e, a := "assume_role_w_creds_secret", creds.SecretAccessKey; e != a { - t.Errorf("expect %v, got %v", e, a) - } - if e, a := "SharedConfigCredentials", creds.ProviderName; !strings.Contains(a, e) { - t.Errorf("expect %v, to contain %v", e, a) - } -} - -func TestSessionAssumeRole_InvalidSourceProfile(t *testing.T) { - // Backwards compatibility with Shared config disabled - // assume role should not be built into the config. - oldEnv := initSessionTestEnv() - defer awstesting.PopEnv(oldEnv) - - os.Setenv("AWS_SDK_LOAD_CONFIG", "1") - os.Setenv("AWS_SHARED_CREDENTIALS_FILE", testConfigFilename) - os.Setenv("AWS_PROFILE", "assume_role_invalid_source_profile") - - s, err := NewSession() - if err == nil { - t.Errorf("expect error") - } - if e, a := "SharedConfigAssumeRoleError: failed to load assume role", err.Error(); !strings.Contains(a, e) { - t.Errorf("expect %v, to contain %v", e, a) - } - if s != nil { - t.Errorf("expect nil, %v", err) - } -} - -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") - os.Setenv("AWS_SHARED_CREDENTIALS_FILE", "file_not_exists") - - return oldEnv } diff --git a/aws/session/shared_config.go b/aws/session/shared_config.go index 7cb44021b3f..e0102363ddd 100644 --- a/aws/session/shared_config.go +++ b/aws/session/shared_config.go @@ -156,10 +156,20 @@ func (cfg *sharedConfig) setAssumeRoleSource(origProfile string, files []sharedC if err != nil { return err } + + // Chain if profile depends of other profiles + if len(assumeRoleSrc.AssumeRole.SourceProfile) > 0 { + err := assumeRoleSrc.setAssumeRoleSource(cfg.AssumeRole.SourceProfile, files) + if err != nil { + return err + } + } } - if len(assumeRoleSrc.Creds.AccessKeyID) == 0 { - return SharedConfigAssumeRoleError{RoleARN: cfg.AssumeRole.RoleARN} + if cfg.AssumeRole.SourceProfile == origProfile || len(assumeRoleSrc.AssumeRole.SourceProfile) == 0 { + if len(assumeRoleSrc.AssumeRole.CredentialSource) == 0 && len(assumeRoleSrc.Creds.AccessKeyID) == 0 { + return SharedConfigAssumeRoleError{RoleARN: cfg.AssumeRole.RoleARN} + } } cfg.AssumeRoleSource = &assumeRoleSrc diff --git a/aws/session/shared_config_test.go b/aws/session/shared_config_test.go index b92a09b6007..2c93a2d3699 100644 --- a/aws/session/shared_config_test.go +++ b/aws/session/shared_config_test.go @@ -125,6 +125,77 @@ func TestLoadSharedConfig(t *testing.T) { Profile: "profile_name", Err: SharedConfigLoadError{Filename: filepath.Join("testdata", "shared_config_invalid_ini")}, }, + { + Filenames: []string{testConfigOtherFilename, testConfigFilename}, + Profile: "assume_role_with_credential_source", + Expected: sharedConfig{ + AssumeRole: assumeRoleConfig{ + RoleARN: "assume_role_with_credential_source_role_arn", + CredentialSource: credSourceEc2Metadata, + }, + }, + }, + { + Filenames: []string{testConfigOtherFilename, testConfigFilename}, + Profile: "multiple_assume_role", + Expected: sharedConfig{ + AssumeRole: assumeRoleConfig{ + RoleARN: "multiple_assume_role_role_arn", + SourceProfile: "assume_role", + }, + AssumeRoleSource: &sharedConfig{ + AssumeRole: assumeRoleConfig{ + RoleARN: "assume_role_role_arn", + SourceProfile: "complete_creds", + }, + AssumeRoleSource: &sharedConfig{ + Creds: credentials.Value{ + AccessKeyID: "complete_creds_akid", + SecretAccessKey: "complete_creds_secret", + ProviderName: fmt.Sprintf("SharedConfigCredentials: %s", testConfigFilename), + }, + }, + }, + }, + }, + { + Filenames: []string{testConfigOtherFilename, testConfigFilename}, + Profile: "multiple_assume_role_with_credential_source", + Expected: sharedConfig{ + AssumeRole: assumeRoleConfig{ + RoleARN: "multiple_assume_role_with_credential_source_role_arn", + SourceProfile: "assume_role_with_credential_source", + }, + AssumeRoleSource: &sharedConfig{ + AssumeRole: assumeRoleConfig{ + RoleARN: "assume_role_with_credential_source_role_arn", + CredentialSource: credSourceEc2Metadata, + }, + }, + }, + }, + { + Filenames: []string{testConfigOtherFilename, testConfigFilename}, + Profile: "multiple_assume_role_with_credential_source2", + Expected: sharedConfig{ + AssumeRole: assumeRoleConfig{ + RoleARN: "multiple_assume_role_with_credential_source2_role_arn", + SourceProfile: "multiple_assume_role_with_credential_source", + }, + AssumeRoleSource: &sharedConfig{ + AssumeRole: assumeRoleConfig{ + RoleARN: "multiple_assume_role_with_credential_source_role_arn", + SourceProfile: "assume_role_with_credential_source", + }, + AssumeRoleSource: &sharedConfig{ + AssumeRole: assumeRoleConfig{ + RoleARN: "assume_role_with_credential_source_role_arn", + CredentialSource: credSourceEc2Metadata, + }, + }, + }, + }, + }, } for i, c := range cases { @@ -139,7 +210,7 @@ func TestLoadSharedConfig(t *testing.T) { if err != nil { t.Errorf("%d, expect nil, %v", i, err) } - if e, a := c.Expected, cfg; !reflect.DeepEqual(e,a) { + if e, a := c.Expected, cfg; !reflect.DeepEqual(e, a) { t.Errorf("%d, expect %v, got %v", i, e, a) } } @@ -249,7 +320,7 @@ func TestLoadSharedConfigFromFile(t *testing.T) { if err != nil { t.Errorf("%d, expect nil, %v", i, err) } - if e, a := c.Expected, cfg; e != a { + if e, a := c.Expected, cfg; !reflect.DeepEqual(e, a) { t.Errorf("%d, expect %v, got %v", i, e, a) } } diff --git a/aws/session/shared_test.go b/aws/session/shared_test.go new file mode 100644 index 00000000000..13f01677f1a --- /dev/null +++ b/aws/session/shared_test.go @@ -0,0 +1,15 @@ +package session + +import ( + "os" + + "github.com/aws/aws-sdk-go/awstesting" +) + +func initSessionTestEnv() (oldEnv []string) { + oldEnv = awstesting.StashEnv() + os.Setenv("AWS_CONFIG_FILE", "file_not_exists") + os.Setenv("AWS_SHARED_CREDENTIALS_FILE", "file_not_exists") + + return oldEnv +} diff --git a/aws/session/testdata/credential_source_config b/aws/session/testdata/credential_source_config index 58b66614ad3..4379eb84df9 100644 --- a/aws/session/testdata/credential_source_config +++ b/aws/session/testdata/credential_source_config @@ -1,16 +1,20 @@ [env_var_credential_source] -role_arn = arn +role_arn = assume_role_w_creds_role_arn_env credential_source = Environment [invalid_source_and_credential_source] -role_arn = arn +role_arn = assume_role_w_creds_role_arn_bad credential_source = Environment source_profile = env_var_credential_source [ec2metadata] -role_arn = assume_role_w_creds_role_arn +role_arn = assume_role_w_creds_role_arn_ec2 credential_source = Ec2InstanceMetadata [ecscontainer] -role_arn = assume_role_w_creds_role_arn +role_arn = assume_role_w_creds_role_arn_ecs credential_source = EcsContainer + +[chained_assume_role] +role_arn = assume_role_w_creds_role_arn_chain +source_profile = ec2metadata diff --git a/aws/session/testdata/shared_config b/aws/session/testdata/shared_config index fe816fe201b..7d645d0c1cf 100644 --- a/aws/session/testdata/shared_config +++ b/aws/session/testdata/shared_config @@ -63,3 +63,19 @@ 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_with_credential_source] +role_arn = assume_role_with_credential_source_role_arn +credential_source = Ec2InstanceMetadata + +[multiple_assume_role] +role_arn = multiple_assume_role_role_arn +source_profile = assume_role + +[multiple_assume_role_with_credential_source] +role_arn = multiple_assume_role_with_credential_source_role_arn +source_profile = assume_role_with_credential_source + +[multiple_assume_role_with_credential_source2] +role_arn = multiple_assume_role_with_credential_source2_role_arn +source_profile = multiple_assume_role_with_credential_source