From e8777b6237dace715062a845d7bb90e44870fc37 Mon Sep 17 00:00:00 2001 From: Sean McGrail Date: Wed, 27 Jan 2021 15:39:03 -0800 Subject: [PATCH] AWS Single Sign-On (SSO) Provider Support (#1072) * Implement AWS SSO Credential Provider * Add Support AWS SSO Provider in Credential Chain * Add Go Module Replace Tool for Local SDK Usage * Expand Credential Chain Tests --- .../config-feature-1611597600517230000.json | 9 + ...edentials-feature-1611597655218336000.json | 9 + config/config_test.go | 2 +- config/go.mod | 1 + config/go.sum | 2 + config/load_options.go | 29 +++ config/provider.go | 19 ++ config/resolve_credentials.go | 27 ++- config/resolve_credentials_test.go | 191 ++++++++++++++---- config/shared_config.go | 81 +++++++- config/shared_config_test.go | 59 +++++- config/shared_test.go | 14 ++ config/testdata/config_source_shared | 24 +++ config/testdata/shared_config | 34 ++++ credentials/go.mod | 2 + credentials/go.sum | 2 + credentials/ssocreds/doc.go | 63 ++++++ credentials/ssocreds/os.go | 9 + credentials/ssocreds/os_windows.go | 7 + credentials/ssocreds/provider.go | 184 +++++++++++++++++ credentials/ssocreds/provider_test.go | 154 ++++++++++++++ ...26f0eb29dc1310529dcc8fc178693e1493135.json | 4 + ...90cb535abf87a12eb4c57db2b1e837e229ea0.json | 4 + ...7ff326478d8c33d47eeb3408cf1c783cb611e.json | 6 + internal/sdk/time.go | 11 + local-mod-replace.sh | 39 ++++ 26 files changed, 935 insertions(+), 51 deletions(-) create mode 100644 .changes/next-release/config-feature-1611597600517230000.json create mode 100644 .changes/next-release/credentials-feature-1611597655218336000.json create mode 100644 credentials/ssocreds/doc.go create mode 100644 credentials/ssocreds/os.go create mode 100644 credentials/ssocreds/os_windows.go create mode 100644 credentials/ssocreds/provider.go create mode 100644 credentials/ssocreds/provider_test.go create mode 100644 credentials/ssocreds/testdata/00126f0eb29dc1310529dcc8fc178693e1493135.json create mode 100644 credentials/ssocreds/testdata/b5f90cb535abf87a12eb4c57db2b1e837e229ea0.json create mode 100644 credentials/ssocreds/testdata/f7f7ff326478d8c33d47eeb3408cf1c783cb611e.json create mode 100755 local-mod-replace.sh diff --git a/.changes/next-release/config-feature-1611597600517230000.json b/.changes/next-release/config-feature-1611597600517230000.json new file mode 100644 index 00000000000..40691b396de --- /dev/null +++ b/.changes/next-release/config-feature-1611597600517230000.json @@ -0,0 +1,9 @@ +{ + "ID": "config-feature-1611597600517230000", + "SchemaVersion": 1, + "Module": "config", + "Type": "feature", + "Description": "Add Support for AWS Single Sign-On (SSO) credential provider", + "MinVersion": "", + "AffectedModules": null +} \ No newline at end of file diff --git a/.changes/next-release/credentials-feature-1611597655218336000.json b/.changes/next-release/credentials-feature-1611597655218336000.json new file mode 100644 index 00000000000..87d4cf11123 --- /dev/null +++ b/.changes/next-release/credentials-feature-1611597655218336000.json @@ -0,0 +1,9 @@ +{ + "ID": "credentials-feature-1611597655218336000", + "SchemaVersion": 1, + "Module": "credentials", + "Type": "feature", + "Description": "Add AWS Single Sign-On (SSO) credential provider", + "MinVersion": "", + "AffectedModules": null +} \ No newline at end of file diff --git a/config/config_test.go b/config/config_test.go index 993fddd713c..ea65b58c999 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -2,11 +2,11 @@ package config import ( "context" - "github.com/google/go-cmp/cmp" "testing" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/credentials" + "github.com/google/go-cmp/cmp" ) func TestConfigs_SharedConfigOptions(t *testing.T) { diff --git a/config/go.mod b/config/go.mod index b7f376d73a2..24774d3364e 100644 --- a/config/go.mod +++ b/config/go.mod @@ -6,6 +6,7 @@ require ( github.com/aws/aws-sdk-go-v2 v1.0.1-0.20210122214637-6cf9ad2f8e2f github.com/aws/aws-sdk-go-v2/credentials v1.0.0 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.0.0 + github.com/aws/aws-sdk-go-v2/service/sso v1.0.0 github.com/aws/aws-sdk-go-v2/service/sts v1.0.0 github.com/aws/smithy-go v1.0.0 github.com/google/go-cmp v0.5.4 diff --git a/config/go.sum b/config/go.sum index 244464e39c2..a2dd5414150 100644 --- a/config/go.sum +++ b/config/go.sum @@ -1,3 +1,5 @@ +github.com/aws/aws-sdk-go-v2/service/sso v1.0.0 h1:eNwZL0deLt9ehrTpPAO/pvztJxa4RT6+E7sbDpgMGUQ= +github.com/aws/aws-sdk-go-v2/service/sso v1.0.0/go.mod h1:qNdDupP6xoM//zL1JmPl2XGbyPL5kKrlsoYVh8XZxzQ= github.com/aws/smithy-go v1.0.0 h1:hkhcRKG9rJ4Fn+RbfXY7Tz7b3ITLDyolBnLLBhwbg/c= github.com/aws/smithy-go v1.0.0/go.mod h1:EzMw8dbp/YJL4A5/sbhGddag+NPT7q084agLbB9LgIw= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/config/load_options.go b/config/load_options.go index bd12a1fde61..bdcddbbf768 100644 --- a/config/load_options.go +++ b/config/load_options.go @@ -8,6 +8,7 @@ import ( "github.com/aws/aws-sdk-go-v2/credentials/ec2rolecreds" "github.com/aws/aws-sdk-go-v2/credentials/endpointcreds" "github.com/aws/aws-sdk-go-v2/credentials/processcreds" + "github.com/aws/aws-sdk-go-v2/credentials/ssocreds" "github.com/aws/aws-sdk-go-v2/credentials/stscreds" "github.com/aws/aws-sdk-go-v2/feature/ec2/imds" "github.com/aws/smithy-go/logging" @@ -110,6 +111,10 @@ type LoadOptions struct { // stscreds.AssumeRoleOptions AssumeRoleCredentialOptions func(*stscreds.AssumeRoleOptions) + // SSOProviderOptions is a function for setting + // the ssocreds.Options + SSOProviderOptions func(options *ssocreds.Options) + // LogConfigurationWarnings when set to true, enables logging // configuration warnings LogConfigurationWarnings *bool @@ -592,3 +597,27 @@ func WithS3UseARNRegion(v bool) LoadOptionsFunc { return nil } } + + +// getSSOProviderOptions returns AssumeRoleCredentialOptions from LoadOptions +func (o LoadOptions) getSSOProviderOptions(context.Context) (func(options *ssocreds.Options), bool, error) { + if o.SSOProviderOptions == nil { + return nil, false, nil + } + + return o.SSOProviderOptions, true, nil +} + +// WithSSOProviderOptions is a helper function to construct +// functional options that sets a function to use ssocreds.Options +// on config's LoadOptions. If the SSO credential provider options is set to nil, +// the sso provider options value will be ignored. If multiple +// WithSSOProviderOptions calls are made, the last call overrides +// the previous call values. +func WithSSOProviderOptions(v func(*ssocreds.Options)) LoadOptionsFunc { + return func(o *LoadOptions) error { + o.SSOProviderOptions = v + return nil + } +} + diff --git a/config/provider.go b/config/provider.go index 68406c6b881..a4308368d03 100644 --- a/config/provider.go +++ b/config/provider.go @@ -9,6 +9,7 @@ import ( "github.com/aws/aws-sdk-go-v2/credentials/ec2rolecreds" "github.com/aws/aws-sdk-go-v2/credentials/endpointcreds" "github.com/aws/aws-sdk-go-v2/credentials/processcreds" + "github.com/aws/aws-sdk-go-v2/credentials/ssocreds" "github.com/aws/aws-sdk-go-v2/credentials/stscreds" "github.com/aws/smithy-go/logging" "github.com/aws/smithy-go/middleware" @@ -406,3 +407,21 @@ func getLogConfigurationWarnings(ctx context.Context, configs configs) (v bool, } return } + +// ssoCredentialOptionsProvider is an interface for retrieving a function for setting +// the ssocreds.Options. +type ssoCredentialOptionsProvider interface { + getSSOProviderOptions(context.Context) (func(*ssocreds.Options), bool, error) +} + +func getSSOProviderOptions(ctx context.Context, configs configs) (v func(options *ssocreds.Options), found bool, err error) { + for _, c := range configs { + if p, ok := c.(ssoCredentialOptionsProvider); ok { + v, found, err = p.getSSOProviderOptions(ctx) + if err != nil || found { + break + } + } + } + return +} diff --git a/config/resolve_credentials.go b/config/resolve_credentials.go index 35b48c4391c..a8fa8fcca59 100644 --- a/config/resolve_credentials.go +++ b/config/resolve_credentials.go @@ -11,8 +11,10 @@ import ( "github.com/aws/aws-sdk-go-v2/credentials/ec2rolecreds" "github.com/aws/aws-sdk-go-v2/credentials/endpointcreds" "github.com/aws/aws-sdk-go-v2/credentials/processcreds" + "github.com/aws/aws-sdk-go-v2/credentials/ssocreds" "github.com/aws/aws-sdk-go-v2/credentials/stscreds" "github.com/aws/aws-sdk-go-v2/feature/ec2/imds" + "github.com/aws/aws-sdk-go-v2/service/sso" "github.com/aws/aws-sdk-go-v2/service/sts" ) @@ -118,6 +120,9 @@ func resolveCredsFromProfile(ctx context.Context, cfg *aws.Config, envConfig *En Value: sharedConfig.Credentials, } + case sharedConfig.hasSSOConfiguration(): + err = resolveSSOCredentials(ctx, cfg, sharedConfig, configs) + case len(sharedConfig.CredentialProcess) != 0: // Get credentials from CredentialProcess err = processCredentials(ctx, cfg, sharedConfig, configs) @@ -151,6 +156,24 @@ func resolveCredsFromProfile(ctx context.Context, cfg *aws.Config, envConfig *En return nil } +func resolveSSOCredentials(ctx context.Context, cfg *aws.Config, sharedConfig *SharedConfig, configs configs) error { + var options []func(*ssocreds.Options) + v, found, err := getSSOProviderOptions(ctx, configs) + if err != nil { + return err + } + if found { + options = append(options, v) + } + + cfgCopy := cfg.Copy() + cfgCopy.Region = sharedConfig.SSORegion + + cfg.Credentials = ssocreds.New(sso.NewFromConfig(cfgCopy), sharedConfig.SSOAccountID, sharedConfig.SSORoleName, sharedConfig.SSOStartURL, options...) + + return nil +} + func ecsContainerURI(path string) string { return fmt.Sprintf("%s%s", ecsContainerEndpoint, path) } @@ -353,7 +376,7 @@ func assumeWebIdentity(ctx context.Context, cfg *aws.Config, filepath string, ro optFns = append(optFns, optFn) } - provider := stscreds.NewWebIdentityRoleProvider(sts.NewFromConfig(cfg.Copy()), roleARN, stscreds.IdentityTokenFile(filepath), optFns...) + provider := stscreds.NewWebIdentityRoleProvider(sts.NewFromConfig(*cfg), roleARN, stscreds.IdentityTokenFile(filepath), optFns...) cfg.Credentials = provider @@ -401,7 +424,7 @@ func credsFromAssumeRole(ctx context.Context, cfg *aws.Config, sharedCfg *Shared } } - cfg.Credentials = stscreds.NewAssumeRoleProvider(sts.NewFromConfig(cfg.Copy()), sharedCfg.RoleARN, optFns...) + cfg.Credentials = stscreds.NewAssumeRoleProvider(sts.NewFromConfig(*cfg), sharedCfg.RoleARN, optFns...) return nil } diff --git a/config/resolve_credentials_test.go b/config/resolve_credentials_test.go index c26703ad411..b8c5c0a6238 100644 --- a/config/resolve_credentials_test.go +++ b/config/resolve_credentials_test.go @@ -3,6 +3,7 @@ package config import ( "context" "fmt" + "io/ioutil" "net/http" "net/http/httptest" "os" @@ -15,6 +16,7 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/internal/awstesting" + "github.com/aws/aws-sdk-go-v2/service/sso" "github.com/aws/aws-sdk-go-v2/service/sts" "github.com/aws/smithy-go/middleware" ) @@ -66,6 +68,14 @@ func setupCredentialsEndpoints(t *testing.T) (aws.EndpointResolver, func()) { Format("2006-01-02T15:04:05Z")))) })) + ssoServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(fmt.Sprintf( + getRoleCredentialsResponse, + time.Now(). + Add(15*time.Minute). + UnixNano()/int64(time.Millisecond)))) + })) + resolver := aws.EndpointResolverFunc( func(service, region string) (aws.Endpoint, error) { switch service { @@ -73,6 +83,10 @@ func setupCredentialsEndpoints(t *testing.T) (aws.EndpointResolver, func()) { return aws.Endpoint{ URL: stsServer.URL, }, nil + case sso.ServiceID: + return aws.Endpoint{ + URL: ssoServer.URL, + }, nil default: return aws.Endpoint{}, fmt.Errorf("unknown service endpoint, %s", service) @@ -83,10 +97,49 @@ func setupCredentialsEndpoints(t *testing.T) (aws.EndpointResolver, func()) { resetECSEndpoint() ecsMetadataServer.Close() ec2MetadataServer.Close() + ssoServer.Close() stsServer.Close() } } +func ssoTestSetup() (func(), error) { + dir, err := ioutil.TempDir("", "sso-test") + if err != nil { + return nil, err + } + + cacheDir := filepath.Join(dir, ".aws", "sso", "cache") + err = os.MkdirAll(cacheDir, 0750) + if err != nil { + os.RemoveAll(dir) + return nil, err + } + + tokenFile, err := os.Create(filepath.Join(cacheDir, "eb5e43e71ce87dd92ec58903d76debd8ee42aefd.json")) + if err != nil { + os.RemoveAll(dir) + return nil, err + } + defer tokenFile.Close() + + _, err = tokenFile.WriteString(fmt.Sprintf(ssoTokenCacheFile, time.Now(). + Add(15*time.Minute). + Format(time.RFC3339))) + if err != nil { + os.RemoveAll(dir) + return nil, err + } + + if runtime.GOOS == "windows" { + os.Setenv("USERPROFILE", dir) + } else { + os.Setenv("HOME", dir) + } + + return func() { + }, nil +} + func TestSharedConfigCredentialSource(t *testing.T) { var configFileForWindows = filepath.Join("testdata", "config_source_shared_for_windows") var configFile = filepath.Join("testdata", "config_source_shared") @@ -95,34 +148,38 @@ func TestSharedConfigCredentialSource(t *testing.T) { var credFile = filepath.Join("testdata", "credentials_source_shared") cases := map[string]struct { - name string - envProfile string - configProfile string - expectedError string - expectedAccessKey string - expectedSecretKey string - expectedChain []string - init func() - dependentOnOS bool + name string + envProfile string + configProfile string + expectedError string + expectedAccessKey string + expectedSecretKey string + expectedSessionToken string + expectedChain []string + init func() (func(), error) + dependentOnOS bool }{ "credential source and source profile": { envProfile: "invalid_source_and_credential_source", - expectedError: "only source profile or credential source can be specified", - init: func() { + expectedError: "only one credential type may be specified per profile", + init: func() (func(), error) { os.Setenv("AWS_ACCESS_KEY", "access_key") os.Setenv("AWS_SECRET_KEY", "secret_key") + return func() {}, nil }, }, "env var credential source": { - configProfile: "env_var_credential_source", - expectedAccessKey: "AKID", - expectedSecretKey: "SECRET", + configProfile: "env_var_credential_source", + expectedAccessKey: "AKID", + expectedSecretKey: "SECRET", + expectedSessionToken: "SESSION_TOKEN", expectedChain: []string{ "assume_role_w_creds_role_arn_env", }, - init: func() { + init: func() (func(), error) { os.Setenv("AWS_ACCESS_KEY", "access_key") os.Setenv("AWS_SECRET_KEY", "secret_key") + return func() {}, nil }, }, "ec2metadata credential source": { @@ -130,24 +187,28 @@ func TestSharedConfigCredentialSource(t *testing.T) { expectedChain: []string{ "assume_role_w_creds_role_arn_ec2", }, - expectedAccessKey: "AKID", - expectedSecretKey: "SECRET", + expectedAccessKey: "AKID", + expectedSecretKey: "SECRET", + expectedSessionToken: "SESSION_TOKEN", }, "ecs container credential source": { - envProfile: "ecscontainer", - expectedAccessKey: "AKID", - expectedSecretKey: "SECRET", + envProfile: "ecscontainer", + expectedAccessKey: "AKID", + expectedSecretKey: "SECRET", + expectedSessionToken: "SESSION_TOKEN", expectedChain: []string{ "assume_role_w_creds_role_arn_ecs", }, - init: func() { + init: func() (func(), error) { os.Setenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", "/ECS") + return func() {}, nil }, }, "chained assume role with env creds": { - envProfile: "chained_assume_role", - expectedAccessKey: "AKID", - expectedSecretKey: "SECRET", + envProfile: "chained_assume_role", + expectedAccessKey: "AKID", + expectedSecretKey: "SECRET", + expectedSessionToken: "SESSION_TOKEN", expectedChain: []string{ "assume_role_w_creds_role_arn_chain", "assume_role_w_creds_role_arn_ec2", @@ -160,43 +221,79 @@ func TestSharedConfigCredentialSource(t *testing.T) { expectedSecretKey: "cred_proc_secret", }, "credential process with ARN set": { - envProfile: "cred_proc_arn_set", - dependentOnOS: true, - expectedAccessKey: "AKID", - expectedSecretKey: "SECRET", + envProfile: "cred_proc_arn_set", + dependentOnOS: true, + expectedAccessKey: "AKID", + expectedSecretKey: "SECRET", + expectedSessionToken: "SESSION_TOKEN", expectedChain: []string{ "assume_role_w_creds_proc_role_arn", }, }, "chained assume role with credential process": { - envProfile: "chained_cred_proc", - dependentOnOS: true, - expectedAccessKey: "AKID", - expectedSecretKey: "SECRET", + envProfile: "chained_cred_proc", + dependentOnOS: true, + expectedAccessKey: "AKID", + expectedSecretKey: "SECRET", + expectedSessionToken: "SESSION_TOKEN", expectedChain: []string{ "assume_role_w_creds_proc_source_prof", }, }, "credential source overrides config source": { - envProfile: "credentials_overide", - expectedAccessKey: "AKID", - expectedSecretKey: "SECRET", + envProfile: "credentials_overide", + expectedAccessKey: "AKID", + expectedSecretKey: "SECRET", + expectedSessionToken: "SESSION_TOKEN", expectedChain: []string{ "assume_role_w_creds_role_arn_ec2", }, - init: func() { + init: func() (func(), error) { os.Setenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", "/ECS") + return func() {}, nil }, }, "only credential source": { - envProfile: "only_credentials_source", - expectedAccessKey: "AKID", - expectedSecretKey: "SECRET", + envProfile: "only_credentials_source", + expectedAccessKey: "AKID", + expectedSecretKey: "SECRET", + expectedSessionToken: "SESSION_TOKEN", expectedChain: []string{ "assume_role_w_creds_role_arn_ecs", }, - init: func() { + init: func() (func(), error) { os.Setenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", "/ECS") + return func() {}, nil + }, + }, + "sso credentials": { + envProfile: "sso_creds", + expectedAccessKey: "SSO_AKID", + expectedSecretKey: "SSO_SECRET_KEY", + expectedSessionToken: "SSO_SESSION_TOKEN", + init: func() (func(), error) { + return ssoTestSetup() + }, + }, + "chained assume role with sso credentials": { + envProfile: "source_sso_creds", + expectedAccessKey: "AKID", + expectedSecretKey: "SECRET", + expectedSessionToken: "SESSION_TOKEN", + expectedChain: []string{ + "source_sso_creds_arn", + }, + init: func() (func(), error) { + return ssoTestSetup() + }, + }, + "chained assume role with sso and static credentials": { + envProfile: "assume_sso_and_static", + expectedAccessKey: "AKID", + expectedSecretKey: "SECRET", + expectedSessionToken: "SESSION_TOKEN", + expectedChain: []string{ + "assume_sso_and_static_arn", }, }, } @@ -222,8 +319,14 @@ func TestSharedConfigCredentialSource(t *testing.T) { endpointResolver, cleanupFn := setupCredentialsEndpoints(t) defer cleanupFn() + var cleanup func() if c.init != nil { - c.init() + var err error + cleanup, err = c.init() + if err != nil { + t.Fatalf("expect no error, got %v", err) + } + defer cleanup() } var credChain []string @@ -278,7 +381,11 @@ func TestSharedConfigCredentialSource(t *testing.T) { } if e, a := c.expectedSecretKey, creds.SecretAccessKey; e != a { - t.Errorf("expected %v, but received %v", e, a) + t.Errorf("expect %v, but received %v", e, a) + } + + if e, a := c.expectedSessionToken, creds.SessionToken; e != a { + t.Errorf("expect %v, got %v", e, a) } }) } diff --git a/config/shared_config.go b/config/shared_config.go index e6130714b14..a4fd7cec47e 100644 --- a/config/shared_config.go +++ b/config/shared_config.go @@ -33,6 +33,12 @@ const ( roleSessionNameKey = `role_session_name` // optional roleDurationSecondsKey = "duration_seconds" // optional + // AWS Single Sign-On (AWS SSO) group + ssoAccountIDKey = "sso_account_id" + ssoRegionKey = "sso_region" + ssoRoleNameKey = "sso_role_name" + ssoStartURL = "sso_start_url" + // Additional Config fields regionKey = `region` @@ -110,6 +116,11 @@ type SharedConfig struct { CredentialProcess string WebIdentityTokenFile string + SSOAccountID string + SSORegion string + SSORoleName string + SSOStartURL string + RoleARN string ExternalID string MFASerial string @@ -748,9 +759,9 @@ func (c *SharedConfig) setFromIniSections(profiles map[string]struct{}, profile c.clearAssumeRoleOptions() } else { // First time a profile has been seen, It must either be a assume role - // or credentials. Assert if the credential type requires a role ARN, - // the ARN is also set. - if err := c.validateCredentialsRequireARN(profile); err != nil { + // credentials, or SSO. Assert if the credential type requires a role ARN, + // the ARN is also set, or validate that the SSO configuration is complete. + if err := c.validateCredentialsConfig(profile); err != nil { return err } } @@ -835,6 +846,12 @@ func (c *SharedConfig) setFromIniSection(profile string, section ini.Section) er updateString(&c.CredentialSource, section, credentialSourceKey) updateString(&c.Region, section, regionKey) + // AWS Single Sign-On (AWS SSO) + updateString(&c.SSOAccountID, section, ssoAccountIDKey) + updateString(&c.SSORegion, section, ssoRegionKey) + updateString(&c.SSORoleName, section, ssoRoleNameKey) + updateString(&c.SSOStartURL, section, ssoStartURL) + if section.Has(roleDurationSecondsKey) { d := time.Duration(section.Int(roleDurationSecondsKey)) * time.Second c.RoleDurationSeconds = &d @@ -861,6 +878,18 @@ func (c *SharedConfig) setFromIniSection(profile string, section ini.Section) er return nil } +func (c *SharedConfig) validateCredentialsConfig(profile string) error { + if err := c.validateCredentialsRequireARN(profile); err != nil { + return err + } + + if err := c.validateSSOConfiguration(profile); err != nil { + return err + } + + return nil +} + func (c *SharedConfig) validateCredentialsRequireARN(profile string) error { var credSource string @@ -890,8 +919,39 @@ func (c *SharedConfig) validateCredentialType() error { len(c.CredentialSource) != 0, len(c.CredentialProcess) != 0, len(c.WebIdentityTokenFile) != 0, + c.hasSSOConfiguration(), ) { - return fmt.Errorf("only source profile or credential source can be specified, not both") + return fmt.Errorf("only one credential type may be specified per profile: source profile, credential source, credential process, web identity token, or sso") + } + + return nil +} + +func (c *SharedConfig) validateSSOConfiguration(profile string) error { + if !c.hasSSOConfiguration() { + return nil + } + + var missing []string + if len(c.SSOAccountID) == 0 { + missing = append(missing, ssoAccountIDKey) + } + + if len(c.SSORegion) == 0 { + missing = append(missing, ssoRegionKey) + } + + if len(c.SSORoleName) == 0 { + missing = append(missing, ssoRoleNameKey) + } + + if len(c.SSOStartURL) == 0 { + missing = append(missing, ssoStartURL) + } + + if len(missing) > 0 { + return fmt.Errorf("profile %q is configured to use SSO but is missing required configuration: %s", + profile, strings.Join(missing, ", ")) } return nil @@ -903,6 +963,7 @@ func (c *SharedConfig) hasCredentials() bool { case len(c.CredentialSource) != 0: case len(c.CredentialProcess) != 0: case len(c.WebIdentityTokenFile) != 0: + case c.hasSSOConfiguration(): case c.Credentials.HasKeys(): default: return false @@ -911,6 +972,18 @@ func (c *SharedConfig) hasCredentials() bool { return true } +func (c *SharedConfig) hasSSOConfiguration() bool { + switch { + case len(c.SSOAccountID) != 0: + case len(c.SSORegion) != 0: + case len(c.SSORoleName) != 0: + case len(c.SSOStartURL) != 0: + default: + return false + } + return true +} + func (c *SharedConfig) clearAssumeRoleOptions() { c.RoleARN = "" c.ExternalID = "" diff --git a/config/shared_config_test.go b/config/shared_config_test.go index cf41e70a99e..cfe4ca36033 100644 --- a/config/shared_config_test.go +++ b/config/shared_config_test.go @@ -14,6 +14,7 @@ import ( "github.com/aws/aws-sdk-go-v2/internal/ini" "github.com/aws/smithy-go/logging" "github.com/aws/smithy-go/ptr" + "github.com/google/go-cmp/cmp" ) var _ regionProvider = (*SharedConfig)(nil) @@ -218,6 +219,60 @@ func TestNewSharedConfig(t *testing.T) { }, }, }, + "AWS SSO Profile": { + Filenames: []string{testConfigFilename}, + Profile: "sso_creds", + Expected: SharedConfig{ + Profile: "sso_creds", + SSOAccountID: "012345678901", + SSORegion: "us-west-2", + SSORoleName: "TestRole", + SSOStartURL: "https://127.0.0.1/start", + }, + }, + "Assume Role with AWS SSO Credentials": { + Filenames: []string{testConfigFilename}, + Profile: "source_sso_creds", + Expected: SharedConfig{ + Profile: "source_sso_creds", + RoleARN: "source_sso_creds_arn", + SourceProfileName: "sso_creds", + Source: &SharedConfig{ + Profile: "sso_creds", + SSOAccountID: "012345678901", + SSORegion: "us-west-2", + SSORoleName: "TestRole", + SSOStartURL: "https://127.0.0.1/start", + }, + }, + }, + "AWS SSO Invalid Profile": { + Filenames: []string{testConfigFilename}, + Profile: "invalid_sso_creds", + Err: fmt.Errorf("profile \"invalid_sso_creds\" is configured to use SSO but is missing required configuration: sso_region, sso_role_name, sso_start_url"), + }, + "AWS SSO Profile and Static Credentials": { + Filenames: []string{testConfigFilename}, + Profile: "sso_and_static", + Expected: SharedConfig{ + Profile: "sso_and_static", + Credentials: aws.Credentials{ + AccessKeyID: "sso_and_static_akid", + SecretAccessKey: "sso_and_static_secret", + SessionToken: "sso_and_static_token", + Source: fmt.Sprintf("SharedConfigCredentials: %s", testConfigFilename), + }, + SSOAccountID: "012345678901", + SSORegion: "us-west-2", + SSORoleName: "TestRole", + SSOStartURL: "https://THIS_SHOULD_NOT_BE_IN_TESTDATA_CACHE/start", + }, + }, + "Assume Role with AWS SSO Configuration and Source Profile": { + Filenames: []string{testConfigFilename}, + Profile: "source_sso_and_assume", + Err: fmt.Errorf("only one credential type may be specified per profile"), + }, } for name, c := range cases { @@ -238,8 +293,8 @@ func TestNewSharedConfig(t *testing.T) { if c.Err != nil { t.Errorf("expect error: %v, got none", c.Err) } - if e, a := c.Expected, cfg; !reflect.DeepEqual(e, a) { - t.Errorf(" expect %v, got %v", e, a) + if diff := cmp.Diff(c.Expected, cfg); len(diff) > 0 { + t.Error(diff) } }) } diff --git a/config/shared_test.go b/config/shared_test.go index 2607e3440fe..ebe91923385 100644 --- a/config/shared_test.go +++ b/config/shared_test.go @@ -47,6 +47,20 @@ const assumeRoleRespMsg = ` ` +const getRoleCredentialsResponse = `{ + "roleCredentials": { + "accessKeyId": "SSO_AKID", + "secretAccessKey": "SSO_SECRET_KEY", + "sessionToken": "SSO_SESSION_TOKEN", + "expiration": %d + } +}` + +const ssoTokenCacheFile = `{ + "accessToken": "ssoAccessToken", + "expiresAt": "%s" +}` + type mockHTTPClient func(*http.Request) (*http.Response, error) func (m mockHTTPClient) Do(r *http.Request) (*http.Response, error) { diff --git a/config/testdata/config_source_shared b/config/testdata/config_source_shared index e94eb688a7a..132910bf54b 100644 --- a/config/testdata/config_source_shared +++ b/config/testdata/config_source_shared @@ -33,3 +33,27 @@ source_profile = cred_proc_no_arn_set [profile credentials_overide] role_arn = assume_role_w_creds_role_arn_ec2 credential_source = Ec2InstanceMetadata + +[profile sso_creds] +sso_account_id = 012345678901 +sso_region = us-west-2 +sso_role_name = TestRole +sso_start_url = https://127.0.0.1/start + +[profile source_sso_creds] +role_arn = source_sso_creds_arn +source_profile = sso_creds + +[profile assume_sso_and_static] +role_arn = assume_sso_and_static_arn +source_profile = sso_and_static + +[profile sso_and_static] +aws_access_key_id = sso_and_static_akid +aws_secret_access_key = sso_and_static_secret +aws_session_token = sso_and_static_token +sso_account_id = 012345678901 +sso_region = us-west-2 +sso_role_name = TestRole +sso_start_url = https://THIS_SHOULD_NOT_BE_IN_TESTDATA_CACHE/start + diff --git a/config/testdata/shared_config b/config/testdata/shared_config index 3896cfeaf99..6462b50be48 100644 --- a/config/testdata/shared_config +++ b/config/testdata/shared_config @@ -104,3 +104,37 @@ source_profile = assume_role_with_credential_source [profile multiple_assume_role_with_credential_source2] role_arn = multiple_assume_role_with_credential_source2_role_arn source_profile = multiple_assume_role_with_credential_source + +[profile sso_creds] +sso_account_id = 012345678901 +sso_region = us-west-2 +sso_role_name = TestRole +sso_start_url = https://127.0.0.1/start + +[profile source_sso_creds] +role_arn = source_sso_creds_arn +source_profile = sso_creds + +[profile invalid_sso_creds] +sso_account_id = 012345678901 + +[profile sso_and_static] +aws_access_key_id = sso_and_static_akid +aws_secret_access_key = sso_and_static_secret +aws_session_token = sso_and_static_token +sso_account_id = 012345678901 +sso_region = us-west-2 +sso_role_name = TestRole +sso_start_url = https://THIS_SHOULD_NOT_BE_IN_TESTDATA_CACHE/start + +[profile sso_and_assume] +sso_account_id = 012345678901 +sso_region = us-west-2 +sso_role_name = TestRole +sso_start_url = https://127.0.0.1/start +role_arn = sso_with_assume_role_arn +source_profile = multiple_assume_role_with_credential_source + +[profile source_sso_and_assume] +role_arn = source_sso_and_assume_arn +source_profile = sso_and_assume diff --git a/credentials/go.mod b/credentials/go.mod index 7f1d84595bc..312721c72f5 100644 --- a/credentials/go.mod +++ b/credentials/go.mod @@ -5,8 +5,10 @@ go 1.15 require ( github.com/aws/aws-sdk-go-v2 v1.0.1-0.20210122214637-6cf9ad2f8e2f github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.0.0 + github.com/aws/aws-sdk-go-v2/service/sso v1.0.0 github.com/aws/aws-sdk-go-v2/service/sts v1.0.0 github.com/aws/smithy-go v1.0.0 + github.com/google/go-cmp v0.5.4 ) replace ( diff --git a/credentials/go.sum b/credentials/go.sum index 8f7f803c058..fe76f6955f8 100644 --- a/credentials/go.sum +++ b/credentials/go.sum @@ -1,3 +1,5 @@ +github.com/aws/aws-sdk-go-v2/service/sso v1.0.0 h1:eNwZL0deLt9ehrTpPAO/pvztJxa4RT6+E7sbDpgMGUQ= +github.com/aws/aws-sdk-go-v2/service/sso v1.0.0/go.mod h1:qNdDupP6xoM//zL1JmPl2XGbyPL5kKrlsoYVh8XZxzQ= github.com/aws/smithy-go v1.0.0 h1:hkhcRKG9rJ4Fn+RbfXY7Tz7b3ITLDyolBnLLBhwbg/c= github.com/aws/smithy-go v1.0.0/go.mod h1:EzMw8dbp/YJL4A5/sbhGddag+NPT7q084agLbB9LgIw= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= diff --git a/credentials/ssocreds/doc.go b/credentials/ssocreds/doc.go new file mode 100644 index 00000000000..2f396c0a118 --- /dev/null +++ b/credentials/ssocreds/doc.go @@ -0,0 +1,63 @@ +// Package ssocreds provides a credential provider for retrieving temporary AWS credentials using an SSO access token. +// +// IMPORTANT: The provider in this package does not initiate or perform the AWS SSO login flow. The SDK provider +// expects that you have already performed the SSO login flow using AWS CLI using the "aws sso login" command, or by +// some other mechanism. The provider must find a valid non-expired access token for the AWS SSO user portal URL in +// ~/.aws/sso/cache. If a cached token is not found, it is expired, or the file is malformed an error will be returned. +// +// Loading AWS SSO credentials with the AWS shared configuration file +// +// You can use configure AWS SSO credentials from the AWS shared configuration file by +// providing the specifying the required keys in the profile: +// +// sso_account_id +// sso_region +// sso_role_name +// sso_start_url +// +// For example, the following defines a profile "devsso" and specifies the AWS SSO parameters that defines the target +// account, role, sign-on portal, and the region where the user portal is located. Note: all SSO arguments must be +// provided, or an error will be returned. +// +// [profile devsso] +// sso_start_url = https://my-sso-portal.awsapps.com/start +// sso_role_name = SSOReadOnlyRole +// sso_region = us-east-1 +// sso_account_id = 123456789012 +// +// Using the config module, you can load the AWS SDK shared configuration, and specify that this profile be used to +// retrieve credentials. For example: +// +// config, err := config.LoadDefaultConfig(context.TODO(), config.WithSharedConfigProfile("devsso")) +// if err != nil { +// return err +// } +// +// Programmatically loading AWS SSO credentials directly +// +// You can programmatically construct the AWS SSO Provider in your application, and provide the necessary information +// to load and retrieve temporary credentials using an access token from ~/.aws/sso/cache. +// +// client := sso.NewFromConfig(cfg) +// +// var provider aws.CredentialsProvider +// provider = ssocreds.New(client, "123456789012", "SSOReadOnlyRole", "us-east-1", "https://my-sso-portal.awsapps.com/start") +// +// // Wrap the provider with aws.CredentialsCache to cache the credentials until their expire time +// provider = aws.NewCredentialsCache(provider) +// +// credentials, err := provider.Retrieve(context.TODO()) +// if err != nil { +// return err +// } +// +// It is important that you wrap the Provider with aws.CredentialsCache if you are programmatically constructing the +// provider directly. This prevents your application from accessing the cached access token and requesting new +// credentials each time the credentials are used. +// +// Additional Resources +// +// Configuring the AWS CLI to use AWS Single Sign-On: https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-sso.html +// +// AWS Single Sign-On User Guide: https://docs.aws.amazon.com/singlesignon/latest/userguide/what-is.html +package ssocreds diff --git a/credentials/ssocreds/os.go b/credentials/ssocreds/os.go new file mode 100644 index 00000000000..ceca7dceecb --- /dev/null +++ b/credentials/ssocreds/os.go @@ -0,0 +1,9 @@ +// +build !windows + +package ssocreds + +import "os" + +func getHomeDirectory() string { + return os.Getenv("HOME") +} diff --git a/credentials/ssocreds/os_windows.go b/credentials/ssocreds/os_windows.go new file mode 100644 index 00000000000..eb48f61e5bc --- /dev/null +++ b/credentials/ssocreds/os_windows.go @@ -0,0 +1,7 @@ +package ssocreds + +import "os" + +func getHomeDirectory() string { + return os.Getenv("USERPROFILE") +} diff --git a/credentials/ssocreds/provider.go b/credentials/ssocreds/provider.go new file mode 100644 index 00000000000..279df7a1318 --- /dev/null +++ b/credentials/ssocreds/provider.go @@ -0,0 +1,184 @@ +package ssocreds + +import ( + "context" + "crypto/sha1" + "encoding/hex" + "encoding/json" + "fmt" + "io/ioutil" + "path/filepath" + "strings" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/internal/sdk" + "github.com/aws/aws-sdk-go-v2/service/sso" +) + +// ProviderName is the name of the provider used to specify the source of credentials. +const ProviderName = "SSOProvider" + +var defaultCacheLocation func() string + +func defaultCacheLocationImpl() string { + return filepath.Join(getHomeDirectory(), ".aws", "sso", "cache") +} + +func init() { + defaultCacheLocation = defaultCacheLocationImpl +} + +// GetRoleCredentialsAPIClient is a API client that implements the GetRoleCredentials operation. +type GetRoleCredentialsAPIClient interface { + GetRoleCredentials(ctx context.Context, params *sso.GetRoleCredentialsInput, optFns ...func(*sso.Options)) (*sso.GetRoleCredentialsOutput, error) +} + +// Options is the Provider options structure. +type Options struct { + // The Client which is configured for the AWS Region where the AWS SSO user portal is located. + Client GetRoleCredentialsAPIClient + + // The AWS account that is assigned to the user. + AccountID string + + // The role name that is assigned to the user. + RoleName string + + // The URL that points to the organization's AWS Single Sign-On (AWS SSO) user portal. + StartURL string +} + +// Provider is an AWS credential provider that retrieves temporary AWS credentials by exchanging an SSO login token. +type Provider struct { + options Options +} + +// New returns a new AWS Single Sign-On (AWS SSO) credential provider. The provided client is expected to be configured +// for the AWS Region where the AWS SSO user portal is located. +func New(client GetRoleCredentialsAPIClient, accountID, roleName, startURL string, optFns ...func(options *Options)) *Provider { + options := Options{ + Client: client, + AccountID: accountID, + RoleName: roleName, + StartURL: startURL, + } + + for _, fn := range optFns { + fn(&options) + } + + return &Provider{ + options: options, + } +} + +// Retrieve retrieves temporary AWS credentials from the configured Amazon Single Sign-On (AWS SSO) user portal +// by exchanging the accessToken present in ~/.aws/sso/cache. +func (p *Provider) Retrieve(ctx context.Context) (aws.Credentials, error) { + tokenFile, err := loadTokenFile(p.options.StartURL) + if err != nil { + return aws.Credentials{}, err + } + + output, err := p.options.Client.GetRoleCredentials(ctx, &sso.GetRoleCredentialsInput{ + AccessToken: &tokenFile.AccessToken, + AccountId: &p.options.AccountID, + RoleName: &p.options.RoleName, + }) + if err != nil { + return aws.Credentials{}, err + } + + return aws.Credentials{ + AccessKeyID: aws.ToString(output.RoleCredentials.AccessKeyId), + SecretAccessKey: aws.ToString(output.RoleCredentials.SecretAccessKey), + SessionToken: aws.ToString(output.RoleCredentials.SessionToken), + Expires: time.Unix(0, output.RoleCredentials.Expiration*int64(time.Millisecond)).UTC(), + CanExpire: true, + Source: ProviderName, + }, nil +} + +func getCacheFileName(url string) (string, error) { + hash := sha1.New() + _, err := hash.Write([]byte(url)) + if err != nil { + return "", err + } + return strings.ToLower(hex.EncodeToString(hash.Sum(nil))) + ".json", nil +} + +type rfc3339 time.Time + +func (r *rfc3339) UnmarshalJSON(bytes []byte) error { + var value string + + if err := json.Unmarshal(bytes, &value); err != nil { + return err + } + + parse, err := time.Parse(time.RFC3339, value) + if err != nil { + return fmt.Errorf("expected RFC3339 timestamp: %w", err) + } + + *r = rfc3339(parse) + + return nil +} + +type token struct { + AccessToken string `json:"accessToken"` + ExpiresAt rfc3339 `json:"expiresAt"` + Region string `json:"region,omitempty"` + StartURL string `json:"startUrl,omitempty"` +} + +func (t token) Expired() bool { + return sdk.NowTime().Round(0).After(time.Time(t.ExpiresAt)) +} + +// InvalidTokenError is the error type that is returned if loaded token has expired or is otherwise invalid. +// To refresh the SSO session run aws sso login with the corresponding profile. +type InvalidTokenError struct { + Err error +} + +func (i *InvalidTokenError) Unwrap() error { + return i.Err +} + +func (i *InvalidTokenError) Error() string { + const msg = "the SSO session has expired or is invalid" + if i.Err == nil { + return msg + } + return msg + ": " + i.Err.Error() +} + +func loadTokenFile(startURL string) (t token, err error) { + key, err := getCacheFileName(startURL) + if err != nil { + return token{}, &InvalidTokenError{Err: err} + } + + fileBytes, err := ioutil.ReadFile(filepath.Join(defaultCacheLocation(), key)) + if err != nil { + return token{}, &InvalidTokenError{Err: err} + } + + if err := json.Unmarshal(fileBytes, &t); err != nil { + return token{}, &InvalidTokenError{Err: err} + } + + if len(t.AccessToken) == 0 { + return token{}, &InvalidTokenError{} + } + + if t.Expired() { + return token{}, &InvalidTokenError{Err: fmt.Errorf("access token is expired")} + } + + return t, nil +} diff --git a/credentials/ssocreds/provider_test.go b/credentials/ssocreds/provider_test.go new file mode 100644 index 00000000000..cab42d98333 --- /dev/null +++ b/credentials/ssocreds/provider_test.go @@ -0,0 +1,154 @@ +package ssocreds + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/internal/sdk" + "github.com/aws/aws-sdk-go-v2/service/sso" + "github.com/aws/aws-sdk-go-v2/service/sso/types" + "github.com/google/go-cmp/cmp" +) + +type mockClient struct { + t *testing.T + + Output *sso.GetRoleCredentialsOutput + Err error + + ExpectedAccountID string + ExpectedAccessToken string + ExpectedRoleName string + + Response func(mockClient) (*sso.GetRoleCredentialsOutput, error) +} + +func (m mockClient) GetRoleCredentials(ctx context.Context, params *sso.GetRoleCredentialsInput, optFns ...func(options *sso.Options)) (out *sso.GetRoleCredentialsOutput, err error) { + m.t.Helper() + + if len(m.ExpectedAccountID) > 0 { + if diff := cmp.Diff(m.ExpectedAccountID, aws.ToString(params.AccountId)); len(diff) > 0 { + m.t.Error(diff) + } + } + + if len(m.ExpectedAccessToken) > 0 { + if diff := cmp.Diff(m.ExpectedAccessToken, aws.ToString(params.AccessToken)); len(diff) > 0 { + m.t.Error(diff) + } + } + + if len(m.ExpectedRoleName) > 0 { + if diff := cmp.Diff(m.ExpectedRoleName, aws.ToString(params.RoleName)); len(diff) > 0 { + m.t.Error(diff) + } + } + + if m.Response == nil { + return out, err + } + return m.Response(m) +} + +func swapCacheLocation(dir string) func() { + original := defaultCacheLocation + defaultCacheLocation = func() string { + return dir + } + return func() { + defaultCacheLocation = original + } +} + +func TestProvider(t *testing.T) { + restoreCache := swapCacheLocation("testdata") + defer restoreCache() + + restoreTime := sdk.TestingUseReferenceTime(time.Date(2021, 01, 19, 19, 50, 0, 0, time.UTC)) + defer restoreTime() + + cases := map[string]struct { + Client mockClient + AccountID string + Region string + RoleName string + StartURL string + Options []func(*Options) + + ExpectedErr bool + ExpectedCredentials aws.Credentials + }{ + "missing required parameter values": { + StartURL: "https://invalid-required", + ExpectedErr: true, + }, + "valid required parameter values": { + Client: mockClient{ + ExpectedAccountID: "012345678901", + ExpectedRoleName: "TestRole", + ExpectedAccessToken: "dGhpcyBpcyBub3QgYSByZWFsIHZhbHVl", + Response: func(mock mockClient) (*sso.GetRoleCredentialsOutput, error) { + return &sso.GetRoleCredentialsOutput{ + RoleCredentials: &types.RoleCredentials{ + AccessKeyId: aws.String("AccessKey"), + SecretAccessKey: aws.String("SecretKey"), + SessionToken: aws.String("SessionToken"), + Expiration: 1611177743123, + }, + }, nil + }, + }, + AccountID: "012345678901", + Region: "us-west-2", + RoleName: "TestRole", + StartURL: "https://valid-required-only", + ExpectedCredentials: aws.Credentials{ + AccessKeyID: "AccessKey", + SecretAccessKey: "SecretKey", + SessionToken: "SessionToken", + CanExpire: true, + Expires: time.Date(2021, 01, 20, 21, 22, 23, 0.123e9, time.UTC), + Source: ProviderName, + }, + }, + "expired access token": { + StartURL: "https://expired", + ExpectedErr: true, + }, + "api error": { + Client: mockClient{ + ExpectedAccountID: "012345678901", + ExpectedRoleName: "TestRole", + ExpectedAccessToken: "dGhpcyBpcyBub3QgYSByZWFsIHZhbHVl", + Response: func(mock mockClient) (*sso.GetRoleCredentialsOutput, error) { + return nil, fmt.Errorf("api error") + }, + }, + AccountID: "012345678901", + Region: "us-west-2", + RoleName: "TestRole", + StartURL: "https://valid-required-only", + ExpectedErr: true, + }, + } + + for name, tt := range cases { + t.Run(name, func(t *testing.T) { + tt.Client.t = t + + provider := New(tt.Client, tt.AccountID, tt.RoleName, tt.StartURL, tt.Options...) + + credentials, err := provider.Retrieve(context.Background()) + if (err != nil) != tt.ExpectedErr { + t.Errorf("expect error: %v", tt.ExpectedErr) + } + + if diff := cmp.Diff(tt.ExpectedCredentials, credentials); len(diff) > 0 { + t.Errorf(diff) + } + }) + } +} diff --git a/credentials/ssocreds/testdata/00126f0eb29dc1310529dcc8fc178693e1493135.json b/credentials/ssocreds/testdata/00126f0eb29dc1310529dcc8fc178693e1493135.json new file mode 100644 index 00000000000..42bf135ff78 --- /dev/null +++ b/credentials/ssocreds/testdata/00126f0eb29dc1310529dcc8fc178693e1493135.json @@ -0,0 +1,4 @@ +{ + "accessToken": "dGhpcyBpcyBub3QgYSByZWFsIHZhbHVl", + "expiresAt": "2021-01-19T23:00:00Z" +} diff --git a/credentials/ssocreds/testdata/b5f90cb535abf87a12eb4c57db2b1e837e229ea0.json b/credentials/ssocreds/testdata/b5f90cb535abf87a12eb4c57db2b1e837e229ea0.json new file mode 100644 index 00000000000..7d5bd2d53be --- /dev/null +++ b/credentials/ssocreds/testdata/b5f90cb535abf87a12eb4c57db2b1e837e229ea0.json @@ -0,0 +1,4 @@ +{ + "accessToken": "dGhpcyBpcyBub3QgYSByZWFsIHZhbHVl", + "expiresAt": "2021-01-19T18:00:00Z" +} diff --git a/credentials/ssocreds/testdata/f7f7ff326478d8c33d47eeb3408cf1c783cb611e.json b/credentials/ssocreds/testdata/f7f7ff326478d8c33d47eeb3408cf1c783cb611e.json new file mode 100644 index 00000000000..a103267e449 --- /dev/null +++ b/credentials/ssocreds/testdata/f7f7ff326478d8c33d47eeb3408cf1c783cb611e.json @@ -0,0 +1,6 @@ +{ + "accessToken": "", + "expiresAt": "", + "region": "", + "startUrl": "" +} diff --git a/internal/sdk/time.go b/internal/sdk/time.go index 7b1e5d92752..8e8dabad548 100644 --- a/internal/sdk/time.go +++ b/internal/sdk/time.go @@ -61,3 +61,14 @@ func TestingUseNopSleep() func() { Sleep = time.Sleep } } + +// TestingUseReferenceTime is a utility for swapping the time function across the SDK to return a specific reference time +// for testing purposes. +func TestingUseReferenceTime(referenceTime time.Time) func() { + NowTime = func() time.Time { + return referenceTime + } + return func() { + NowTime = time.Now + } +} diff --git a/local-mod-replace.sh b/local-mod-replace.sh new file mode 100755 index 00000000000..8a2aea99e2f --- /dev/null +++ b/local-mod-replace.sh @@ -0,0 +1,39 @@ +#1/usr/bin/env bash + +PROJECT_DIR="" +SDK_SOURCE_DIR=$(cd `dirname $0` && pwd) + +usage() { + echo "Usage: $0 [-s SDK_SOURCE_DIR] [-d PROJECT_DIR]" 1>&2 + exit 1 +} + +while getopts "hs:d:" options; do + case "${options}" in + s) + SDK_SOURCE_DIR=${OPTARG} + if [ "$SDK_SOURCE_DIR" == "" ]; then + echo "path to SDK source directory is required" || exit + usage + fi + ;; + d) + PROJECT_DIR=${OPTARG} + ;; + h) + usage + ;; + *) + usage + ;; + esac +done + +if [ "$PROJECT_DIR" != "" ]; then + cd $PROJECT_DIR || exit +fi + +go mod graph | awk '{print $1}' | cut -d '@' -f 1 | sort | uniq | grep "github.com/aws/aws-sdk-go-v2" | while read x; do + repPath=${x/github.com\/aws\/aws-sdk-go-v2/${SDK_SOURCE_DIR}} + echo -replace $x=$repPath +done | xargs go mod edit