diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 8a1927a39ca..621f7210248 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -1,5 +1,26 @@ ### SDK Features ### SDK Enhancements +* `aws/endpoints`: Adds support for STS Regional Flags ([#2779](https://github.com/aws/aws-sdk-go/pull/2779)) + * Implements STS regional flag, with support for `legacy` and `regional` options. Defaults to `legacy`. Legacy, will force all regions specified in aws/endpoints/sts_legacy_regions.go to resolve to the STS global endpoint, sts.amazonaws.com. This is the SDK's current behavior. + * When the flag's value is `regional` the SDK will resolve the endpoint based on the endpoints.json model. This allows STS to update their service's modeled endpoints to be regionalized for all regions. When `regional` turned on use `aws-global` as the region to use the global endpoint. + * `AWS_STS_REGIONAL_ENDPOINTS=regional` for environment, or `sts_regional_endpoints=regional` in shared config file. + * The regions the SDK defaults to the STS global endpoint in `legacy` mode are: + * ap-northeast-1 + * ap-south-1 + * ap-southeast-1 + * ap-southeast-2 + * aws-global + * ca-central-1 + * eu-central-1 + * eu-north-1 + * eu-west-1 + * eu-west-2 + * eu-west-3 + * sa-east-1 + * us-east-1 + * us-east-2 + * us-west-1 + * us-west-2 ### SDK Bugs diff --git a/aws/client/default_retryer.go b/aws/client/default_retryer.go index 0fda42510f0..9f6af19dd45 100644 --- a/aws/client/default_retryer.go +++ b/aws/client/default_retryer.go @@ -16,11 +16,11 @@ import ( type DefaultRetryer struct { // Num max Retries is the number of max retries that will be performed. // By default, this is zero. - NumMaxRetries int + NumMaxRetries int // MinRetryDelay is the minimum retry delay after which retry will be performed. // If not set, the value is 0ns. - MinRetryDelay time.Duration + MinRetryDelay time.Duration // MinThrottleRetryDelay is the minimum retry delay when throttled. // If not set, the value is 0ns. @@ -28,7 +28,7 @@ type DefaultRetryer struct { // MaxRetryDelay is the maximum retry delay before which retry must be performed. // If not set, the value is 0ns. - MaxRetryDelay time.Duration + MaxRetryDelay time.Duration // MaxThrottleDelay is the maximum retry delay when throttled. // If not set, the value is 0ns. diff --git a/aws/client/default_retryer_test.go b/aws/client/default_retryer_test.go index 08d9ee7cdd5..b8794f3ccc8 100644 --- a/aws/client/default_retryer_test.go +++ b/aws/client/default_retryer_test.go @@ -166,7 +166,7 @@ func TestGetRetryDelay(t *testing.T) { } func TestRetryDelay(t *testing.T) { - d := DefaultRetryer{NumMaxRetries:100} + d := DefaultRetryer{NumMaxRetries: 100} r := request.Request{} for i := 0; i < 100; i++ { rTemp := r diff --git a/aws/config.go b/aws/config.go index fd1e240f6eb..8a7699b9619 100644 --- a/aws/config.go +++ b/aws/config.go @@ -246,6 +246,9 @@ type Config struct { // Disabling this feature is useful when you want to use local endpoints // for testing that do not support the modeled host prefix pattern. DisableEndpointHostPrefix *bool + + // STSRegionalEndpoint will enable regional or legacy endpoint resolving + STSRegionalEndpoint endpoints.STSRegionalEndpoint } // NewConfig returns a new Config pointer that can be chained with builder @@ -420,6 +423,13 @@ func (c *Config) MergeIn(cfgs ...*Config) { } } +// WithSTSRegionalEndpoint will set whether or not to use regional endpoint flag +// when resolving the endpoint for a service +func (c *Config) WithSTSRegionalEndpoint(sre endpoints.STSRegionalEndpoint) *Config { + c.STSRegionalEndpoint = sre + return c +} + func mergeInConfig(dst *Config, other *Config) { if other == nil { return @@ -520,6 +530,10 @@ func mergeInConfig(dst *Config, other *Config) { if other.DisableEndpointHostPrefix != nil { dst.DisableEndpointHostPrefix = other.DisableEndpointHostPrefix } + + if other.STSRegionalEndpoint != endpoints.UnsetSTSEndpoint { + dst.STSRegionalEndpoint = other.STSRegionalEndpoint + } } // Copy will return a shallow copy of the Config object. If any additional diff --git a/aws/endpoints/endpoints.go b/aws/endpoints/endpoints.go index 9c936be6cf9..c06901efa43 100644 --- a/aws/endpoints/endpoints.go +++ b/aws/endpoints/endpoints.go @@ -3,6 +3,7 @@ package endpoints import ( "fmt" "regexp" + "strings" "github.com/aws/aws-sdk-go/aws/awserr" ) @@ -46,6 +47,43 @@ type Options struct { // // This option is ignored if StrictMatching is enabled. ResolveUnknownService bool + + // STS Regional Endpoint flag helps with resolving the STS endpoint + STSRegionalEndpoint STSRegionalEndpoint +} + +// STSRegionalEndpoint is an enum type alias for int +// It is used internally by the core sdk as STS Regional Endpoint flag value +type STSRegionalEndpoint int + +const ( + + // UnsetSTSEndpoint represents that STS Regional Endpoint flag is not specified. + UnsetSTSEndpoint STSRegionalEndpoint = iota + + // LegacySTSEndpoint represents when STS Regional Endpoint flag is specified + // to use legacy endpoints. + LegacySTSEndpoint + + // RegionalSTSEndpoint represents when STS Regional Endpoint flag is specified + // to use regional endpoints. + RegionalSTSEndpoint +) + +// GetSTSRegionalEndpoint function returns the STSRegionalEndpointFlag based +// on the input string provided in env config or shared config by the user. +// +// `legacy`, `regional` are the only case-insensitive valid strings for +// resolving the STS regional Endpoint flag. +func GetSTSRegionalEndpoint(s string) (STSRegionalEndpoint, error) { + switch { + case strings.EqualFold(s, "legacy"): + return LegacySTSEndpoint, nil + case strings.EqualFold(s, "regional"): + return RegionalSTSEndpoint, nil + default: + return UnsetSTSEndpoint, fmt.Errorf("unable to resolve the value of STSRegionalEndpoint for %v", s) + } } // Set combines all of the option functions together. @@ -79,6 +117,12 @@ func ResolveUnknownServiceOption(o *Options) { o.ResolveUnknownService = true } +// STSRegionalEndpointOption enables the STS endpoint resolver behavior to resolve +// STS endpoint to their regional endpoint, instead of the global endpoint. +func STSRegionalEndpointOption(o *Options) { + o.STSRegionalEndpoint = RegionalSTSEndpoint +} + // A Resolver provides the interface for functionality to resolve endpoints. // The build in Partition and DefaultResolver return value satisfy this interface. type Resolver interface { @@ -194,7 +238,7 @@ func (p Partition) ID() string { return p.id } // require the provided service and region to be known by the partition. // If the endpoint cannot be strictly resolved an error will be returned. This // mode is useful to ensure the endpoint resolved is valid. Without -// StrictMatching enabled the endpoint returned my look valid but may not work. +// StrictMatching enabled the endpoint returned may look valid but may not work. // StrictMatching requires the SDK to be updated if you want to take advantage // of new regions and services expansions. // diff --git a/aws/endpoints/sts_legacy_regions.go b/aws/endpoints/sts_legacy_regions.go new file mode 100644 index 00000000000..68ed2d9e25f --- /dev/null +++ b/aws/endpoints/sts_legacy_regions.go @@ -0,0 +1,20 @@ +package endpoints + +var stsLegacyGlobalRegions = map[string]struct{}{ + "ap-northeast-1": {}, + "ap-south-1": {}, + "ap-southeast-1": {}, + "ap-southeast-2": {}, + "aws-global": {}, + "ca-central-1": {}, + "eu-central-1": {}, + "eu-north-1": {}, + "eu-west-1": {}, + "eu-west-2": {}, + "eu-west-3": {}, + "sa-east-1": {}, + "us-east-1": {}, + "us-east-2": {}, + "us-west-1": {}, + "us-west-2": {}, +} diff --git a/aws/endpoints/v3model.go b/aws/endpoints/v3model.go index 523ad79ac0a..49a3a398847 100644 --- a/aws/endpoints/v3model.go +++ b/aws/endpoints/v3model.go @@ -79,6 +79,12 @@ func (p partition) EndpointFor(service, region string, opts ...func(*Options)) ( var opt Options opt.Set(opts...) + if service == "sts" && opt.STSRegionalEndpoint != RegionalSTSEndpoint { + if _, ok := stsLegacyGlobalRegions[region]; ok { + region = "aws-global" + } + } + s, hasService := p.Services[service] if !(hasService || opt.ResolveUnknownService) { // Only return error if the resolver will not fallback to creating @@ -92,6 +98,7 @@ func (p partition) EndpointFor(service, region string, opts ...func(*Options)) ( } defs := []endpoint{p.Defaults, s.Defaults} + return e.resolve(service, region, p.DNSSuffix, defs, opt), nil } diff --git a/aws/endpoints/v3model_shared_test.go b/aws/endpoints/v3model_shared_test.go new file mode 100644 index 00000000000..b0b00b14ea3 --- /dev/null +++ b/aws/endpoints/v3model_shared_test.go @@ -0,0 +1,69 @@ +package endpoints + +import "regexp" + +var testPartitions = partitions{ + partition{ + ID: "part-id", + Name: "partitionName", + DNSSuffix: "amazonaws.com", + RegionRegex: regionRegex{ + Regexp: func() *regexp.Regexp { + reg, _ := regexp.Compile("^(us|eu|ap|sa|ca)\\-\\w+\\-\\d+$") + return reg + }(), + }, + Defaults: endpoint{ + Hostname: "{service}.{region}.{dnsSuffix}", + Protocols: []string{"https"}, + SignatureVersions: []string{"v4"}, + }, + Regions: regions{ + "us-east-1": region{ + Description: "region description", + }, + "us-west-2": region{}, + }, + Services: services{ + "s3": service{}, + "service1": service{ + Defaults: endpoint{ + CredentialScope: credentialScope{ + Service: "service1", + }, + }, + Endpoints: endpoints{ + "us-east-1": {}, + "us-west-2": { + HasDualStack: boxedTrue, + DualStackHostname: "{service}.dualstack.{region}.{dnsSuffix}", + }, + }, + }, + "service2": service{ + Defaults: endpoint{ + CredentialScope: credentialScope{ + Service: "service2", + }, + }, + }, + "httpService": service{ + Defaults: endpoint{ + Protocols: []string{"http"}, + }, + }, + "globalService": service{ + IsRegionalized: boxedFalse, + PartitionEndpoint: "aws-global", + Endpoints: endpoints{ + "aws-global": endpoint{ + CredentialScope: credentialScope{ + Region: "us-east-1", + }, + Hostname: "globalService.amazonaws.com", + }, + }, + }, + }, + }, +} diff --git a/aws/endpoints/v3model_sts_regional_test.go b/aws/endpoints/v3model_sts_regional_test.go new file mode 100644 index 00000000000..aa6784a897a --- /dev/null +++ b/aws/endpoints/v3model_sts_regional_test.go @@ -0,0 +1,582 @@ +// +build go1.7 + +package endpoints + +import ( + "regexp" + "testing" +) + +func TestEndpointFor_STSRegionalFlag(t *testing.T) { + + // mock STS regional endpoints model + mockSTSModelPartition := partition{ + ID: "aws", + Name: "AWS Standard", + DNSSuffix: "amazonaws.com", + RegionRegex: regionRegex{ + Regexp: func() *regexp.Regexp { + reg, _ := regexp.Compile("^(us|eu|ap|sa|ca|me)\\-\\w+\\-\\d+$") + return reg + }(), + }, + Defaults: endpoint{ + Hostname: "{service}.{region}.{dnsSuffix}", + Protocols: []string{"https"}, + SignatureVersions: []string{"v4"}, + }, + Regions: regions{ + "ap-east-1": region{ + Description: "Asia Pacific (Hong Kong)", + }, + "ap-northeast-1": region{ + Description: "Asia Pacific (Tokyo)", + }, + "ap-northeast-2": region{ + Description: "Asia Pacific (Seoul)", + }, + "ap-south-1": region{ + Description: "Asia Pacific (Mumbai)", + }, + "ap-southeast-1": region{ + Description: "Asia Pacific (Singapore)", + }, + "ap-southeast-2": region{ + Description: "Asia Pacific (Sydney)", + }, + "ca-central-1": region{ + Description: "Canada (Central)", + }, + "eu-central-1": region{ + Description: "EU (Frankfurt)", + }, + "eu-north-1": region{ + Description: "EU (Stockholm)", + }, + "eu-west-1": region{ + Description: "EU (Ireland)", + }, + "eu-west-2": region{ + Description: "EU (London)", + }, + "eu-west-3": region{ + Description: "EU (Paris)", + }, + "me-south-1": region{ + Description: "Middle East (Bahrain)", + }, + "sa-east-1": region{ + Description: "South America (Sao Paulo)", + }, + "us-east-1": region{ + Description: "US East (N. Virginia)", + }, + "us-east-2": region{ + Description: "US East (Ohio)", + }, + "us-west-1": region{ + Description: "US West (N. California)", + }, + "us-west-2": region{ + Description: "US West (Oregon)", + }, + }, + Services: services{ + "sts": service{ + PartitionEndpoint: "aws-global", + Defaults: endpoint{}, + Endpoints: endpoints{ + "ap-east-1": endpoint{}, + "ap-northeast-1": endpoint{}, + "ap-northeast-2": endpoint{}, + "ap-south-1": endpoint{}, + "ap-southeast-1": endpoint{}, + "ap-southeast-2": endpoint{}, + "aws-global": endpoint{ + Hostname: "sts.amazonaws.com", + CredentialScope: credentialScope{ + Region: "us-east-1", + }, + }, + "ca-central-1": endpoint{}, + "eu-central-1": endpoint{}, + "eu-north-1": endpoint{}, + "eu-west-1": endpoint{}, + "eu-west-2": endpoint{}, + "eu-west-3": endpoint{}, + "me-south-1": endpoint{}, + "sa-east-1": endpoint{}, + "us-east-1": endpoint{}, + "us-east-1-fips": endpoint{ + Hostname: "sts-fips.us-east-1.amazonaws.com", + CredentialScope: credentialScope{ + Region: "us-east-1", + }, + }, + "us-east-2": endpoint{}, + "us-east-2-fips": endpoint{ + Hostname: "sts-fips.us-east-2.amazonaws.com", + CredentialScope: credentialScope{ + Region: "us-east-2", + }, + }, + "us-west-1": endpoint{}, + "us-west-1-fips": endpoint{ + Hostname: "sts-fips.us-west-1.amazonaws.com", + CredentialScope: credentialScope{ + Region: "us-west-1", + }, + }, + "us-west-2": endpoint{}, + "us-west-2-fips": endpoint{ + Hostname: "sts-fips.us-west-2.amazonaws.com", + CredentialScope: credentialScope{ + Region: "us-west-2", + }, + }, + }, + }, + }, + } + + // resolver for mock STS regional endpoints model + resolver := mockSTSModelPartition + + cases := map[string]struct { + service, region string + regional bool + ExpectURL, ExpectSigningMethod, ExpectSigningRegion string + ExpectSigningNameDerived bool + }{ + // STS Endpoints resolver tests : + "sts/us-west-2/regional": { + service: "sts", + region: "us-west-2", + regional: true, + ExpectURL: "https://sts.us-west-2.amazonaws.com", + ExpectSigningMethod: "v4", + ExpectSigningNameDerived: true, + ExpectSigningRegion: "us-west-2", + }, + "sts/us-west-2/legacy": { + service: "sts", + region: "us-west-2", + regional: false, + ExpectURL: "https://sts.amazonaws.com", + ExpectSigningMethod: "v4", + ExpectSigningNameDerived: true, + ExpectSigningRegion: "us-east-1", + }, + "sts/ap-east-1/regional": { + service: "sts", + region: "ap-east-1", + regional: true, + ExpectURL: "https://sts.ap-east-1.amazonaws.com", + ExpectSigningMethod: "v4", + ExpectSigningNameDerived: true, + ExpectSigningRegion: "ap-east-1", + }, + "sts/ap-east-1/legacy": { + service: "sts", + region: "ap-east-1", + regional: false, + ExpectURL: "https://sts.ap-east-1.amazonaws.com", + ExpectSigningMethod: "v4", + ExpectSigningNameDerived: true, + ExpectSigningRegion: "ap-east-1", + }, + "sts/us-west-2-fips/regional": { + service: "sts", + region: "us-west-2-fips", + regional: true, + ExpectURL: "https://sts-fips.us-west-2.amazonaws.com", + ExpectSigningMethod: "v4", + ExpectSigningNameDerived: true, + ExpectSigningRegion: "us-west-2", + }, + "sts/us-west-2-fips/legacy": { + service: "sts", + region: "us-west-2-fips", + regional: false, + ExpectURL: "https://sts-fips.us-west-2.amazonaws.com", + ExpectSigningMethod: "v4", + ExpectSigningNameDerived: true, + ExpectSigningRegion: "us-west-2", + }, + "sts/aws-global/regional": { + service: "sts", + region: "aws-global", + regional: true, + ExpectURL: "https://sts.amazonaws.com", + ExpectSigningMethod: "v4", + ExpectSigningNameDerived: true, + ExpectSigningRegion: "us-east-1", + }, + "sts/aws-global/legacy": { + service: "sts", + region: "aws-global", + regional: false, + ExpectURL: "https://sts.amazonaws.com", + ExpectSigningMethod: "v4", + ExpectSigningNameDerived: true, + ExpectSigningRegion: "us-east-1", + }, + "sts/ap-south-1/regional": { + service: "sts", + region: "ap-south-1", + regional: true, + ExpectURL: "https://sts.ap-south-1.amazonaws.com", + ExpectSigningMethod: "v4", + ExpectSigningNameDerived: true, + ExpectSigningRegion: "ap-south-1", + }, + "sts/ap-south-1/legacy": { + service: "sts", + region: "ap-south-1", + regional: false, + ExpectURL: "https://sts.amazonaws.com", + ExpectSigningMethod: "v4", + ExpectSigningNameDerived: true, + ExpectSigningRegion: "us-east-1", + }, + "sts/ap-northeast-1/regional": { + service: "sts", + region: "ap-northeast-1", + regional: true, + ExpectURL: "https://sts.ap-northeast-1.amazonaws.com", + ExpectSigningMethod: "v4", + ExpectSigningNameDerived: true, + ExpectSigningRegion: "ap-northeast-1", + }, + "sts/ap-northeast-1/legacy": { + service: "sts", + region: "ap-northeast-1", + regional: false, + ExpectURL: "https://sts.amazonaws.com", + ExpectSigningMethod: "v4", + ExpectSigningNameDerived: true, + ExpectSigningRegion: "us-east-1", + }, + "sts/ap-southeast-1/regional": { + service: "sts", + region: "ap-southeast-1", + regional: true, + ExpectURL: "https://sts.ap-southeast-1.amazonaws.com", + ExpectSigningMethod: "v4", + ExpectSigningNameDerived: true, + ExpectSigningRegion: "ap-southeast-1", + }, + "sts/ap-southeast-1/legacy": { + service: "sts", + region: "ap-southeast-1", + regional: false, + ExpectURL: "https://sts.amazonaws.com", + ExpectSigningMethod: "v4", + ExpectSigningNameDerived: true, + ExpectSigningRegion: "us-east-1", + }, + "sts/ca-central-1/regional": { + service: "sts", + region: "ca-central-1", + regional: true, + ExpectURL: "https://sts.ca-central-1.amazonaws.com", + ExpectSigningMethod: "v4", + ExpectSigningNameDerived: true, + ExpectSigningRegion: "ca-central-1", + }, + "sts/ca-central-1/legacy": { + service: "sts", + region: "ca-central-1", + regional: false, + ExpectURL: "https://sts.amazonaws.com", + ExpectSigningMethod: "v4", + ExpectSigningNameDerived: true, + ExpectSigningRegion: "us-east-1", + }, + "sts/eu-central-1/regional": { + service: "sts", + region: "eu-central-1", + regional: true, + ExpectURL: "https://sts.eu-central-1.amazonaws.com", + ExpectSigningMethod: "v4", + ExpectSigningNameDerived: true, + ExpectSigningRegion: "eu-central-1", + }, + "sts/eu-central-1/legacy": { + service: "sts", + region: "eu-central-1", + regional: false, + ExpectURL: "https://sts.amazonaws.com", + ExpectSigningMethod: "v4", + ExpectSigningNameDerived: true, + ExpectSigningRegion: "us-east-1", + }, + "sts/eu-north-1/regional": { + service: "sts", + region: "eu-north-1", + regional: true, + ExpectURL: "https://sts.eu-north-1.amazonaws.com", + ExpectSigningMethod: "v4", + ExpectSigningNameDerived: true, + ExpectSigningRegion: "eu-north-1", + }, + "sts/eu-north-1/legacy": { + service: "sts", + region: "eu-north-1", + regional: false, + ExpectURL: "https://sts.amazonaws.com", + ExpectSigningMethod: "v4", + ExpectSigningNameDerived: true, + ExpectSigningRegion: "us-east-1", + }, + "sts/eu-west-1/regional": { + service: "sts", + region: "eu-west-1", + regional: true, + ExpectURL: "https://sts.eu-west-1.amazonaws.com", + ExpectSigningMethod: "v4", + ExpectSigningNameDerived: true, + ExpectSigningRegion: "eu-west-1", + }, + "sts/eu-west-1/legacy": { + service: "sts", + region: "eu-west-1", + regional: false, + ExpectURL: "https://sts.amazonaws.com", + ExpectSigningMethod: "v4", + ExpectSigningNameDerived: true, + ExpectSigningRegion: "us-east-1", + }, + "sts/eu-west-2/regional": { + service: "sts", + region: "eu-west-2", + regional: true, + ExpectURL: "https://sts.eu-west-2.amazonaws.com", + ExpectSigningMethod: "v4", + ExpectSigningNameDerived: true, + ExpectSigningRegion: "eu-west-2", + }, + "sts/eu-west-2/legacy": { + service: "sts", + region: "eu-west-2", + regional: false, + ExpectURL: "https://sts.amazonaws.com", + ExpectSigningMethod: "v4", + ExpectSigningNameDerived: true, + ExpectSigningRegion: "us-east-1", + }, + "sts/eu-west-3/regional": { + service: "sts", + region: "eu-west-3", + regional: true, + ExpectURL: "https://sts.eu-west-3.amazonaws.com", + ExpectSigningMethod: "v4", + ExpectSigningNameDerived: true, + ExpectSigningRegion: "eu-west-3", + }, + "sts/eu-west-3/legacy": { + service: "sts", + region: "eu-west-3", + regional: false, + ExpectURL: "https://sts.amazonaws.com", + ExpectSigningMethod: "v4", + ExpectSigningNameDerived: true, + ExpectSigningRegion: "us-east-1", + }, + "sts/sa-east-1/regional": { + service: "sts", + region: "sa-east-1", + regional: true, + ExpectURL: "https://sts.sa-east-1.amazonaws.com", + ExpectSigningMethod: "v4", + ExpectSigningNameDerived: true, + ExpectSigningRegion: "sa-east-1", + }, + "sts/sa-east-1/legacy": { + service: "sts", + region: "sa-east-1", + regional: false, + ExpectURL: "https://sts.amazonaws.com", + ExpectSigningMethod: "v4", + ExpectSigningNameDerived: true, + ExpectSigningRegion: "us-east-1", + }, + "sts/us-east-1/regional": { + service: "sts", + region: "us-east-1", + regional: true, + ExpectURL: "https://sts.us-east-1.amazonaws.com", + ExpectSigningMethod: "v4", + ExpectSigningNameDerived: true, + ExpectSigningRegion: "us-east-1", + }, + "sts/us-east-1/legacy": { + service: "sts", + region: "us-east-1", + regional: false, + ExpectURL: "https://sts.amazonaws.com", + ExpectSigningMethod: "v4", + ExpectSigningNameDerived: true, + ExpectSigningRegion: "us-east-1", + }, + "sts/us-east-2/regional": { + service: "sts", + region: "us-east-2", + regional: true, + ExpectURL: "https://sts.us-east-2.amazonaws.com", + ExpectSigningMethod: "v4", + ExpectSigningNameDerived: true, + ExpectSigningRegion: "us-east-2", + }, + "sts/us-east-2/legacy": { + service: "sts", + region: "us-east-2", + regional: false, + ExpectURL: "https://sts.amazonaws.com", + ExpectSigningMethod: "v4", + ExpectSigningNameDerived: true, + ExpectSigningRegion: "us-east-1", + }, + "sts/us-west-1/regional": { + service: "sts", + region: "us-west-1", + regional: true, + ExpectURL: "https://sts.us-west-1.amazonaws.com", + ExpectSigningMethod: "v4", + ExpectSigningNameDerived: true, + ExpectSigningRegion: "us-west-1", + }, + "sts/us-west-1/legacy": { + service: "sts", + region: "us-west-1", + regional: false, + ExpectURL: "https://sts.amazonaws.com", + ExpectSigningMethod: "v4", + ExpectSigningNameDerived: true, + ExpectSigningRegion: "us-east-1", + }, + } + + for name, c := range cases { + t.Run(name, func(t *testing.T) { + var optionSlice []func(o *Options) + optionSlice = append(optionSlice, func(o *Options) { + if c.regional { + o.STSRegionalEndpoint = RegionalSTSEndpoint + } + }) + + actual, err := resolver.EndpointFor(c.service, c.region, optionSlice...) + if err != nil { + t.Fatalf("failed to resolve endpoint, %v", err) + } + + if e, a := c.ExpectURL, actual.URL; e != a { + t.Errorf("expect %v, got %v", e, a) + } + + if e, a := c.ExpectSigningMethod, actual.SigningMethod; e != a { + t.Errorf("expect %v, got %v", e, a) + } + + if e, a := c.ExpectSigningNameDerived, actual.SigningNameDerived; e != a { + t.Errorf("expect %v, got %v", e, a) + } + + if e, a := c.ExpectSigningRegion, actual.SigningRegion; e != a { + t.Errorf("expect %v, got %v", e, a) + } + + }) + } +} + +func TestSTSRegionalEndpoint_CNPartition(t *testing.T) { + mockSTSCNPartition := partition{ + ID: "aws-cn", + Name: "AWS China", + DNSSuffix: "amazonaws.com.cn", + RegionRegex: regionRegex{ + Regexp: func() *regexp.Regexp { + reg, _ := regexp.Compile("^cn\\-\\w+\\-\\d+$") + return reg + }(), + }, + Defaults: endpoint{ + Hostname: "{service}.{region}.{dnsSuffix}", + Protocols: []string{"https"}, + SignatureVersions: []string{"v4"}, + }, + Regions: regions{ + "cn-north-1": region{ + Description: "China (Beijing)", + }, + "cn-northwest-1": region{ + Description: "China (Ningxia)", + }, + }, + Services: services{ + "sts": service{ + Endpoints: endpoints{ + "cn-north-1": endpoint{}, + "cn-northwest-1": endpoint{}, + }, + }, + }, + } + + resolver := mockSTSCNPartition + + cases := map[string]struct { + service, region string + regional bool + ExpectURL, ExpectSigningMethod, ExpectSigningRegion string + ExpectSigningNameDerived bool + }{ + "sts/cn-north-1/regional": { + service: "sts", + region: "cn-north-1", + regional: true, + ExpectURL: "https://sts.cn-north-1.amazonaws.com.cn", + ExpectSigningMethod: "v4", + ExpectSigningNameDerived: true, + ExpectSigningRegion: "cn-north-1", + }, + "sts/cn-north-1/legacy": { + service: "sts", + region: "cn-north-1", + regional: false, + ExpectURL: "https://sts.cn-north-1.amazonaws.com.cn", + ExpectSigningMethod: "v4", + ExpectSigningNameDerived: true, + ExpectSigningRegion: "cn-north-1", + }, + } + + for name, c := range cases { + var optionSlice []func(o *Options) + t.Run(name, func(t *testing.T) { + if c.regional { + optionSlice = append(optionSlice, STSRegionalEndpointOption) + } + actual, err := resolver.EndpointFor(c.service, c.region, optionSlice...) + if err != nil { + t.Fatalf("failed to resolve endpoint, %v", err) + } + + if e, a := c.ExpectURL, actual.URL; e != a { + t.Errorf("expect %v, got %v", e, a) + } + if e, a := c.ExpectSigningMethod, actual.SigningMethod; e != a { + t.Errorf("expect %v, got %v", e, a) + } + if e, a := c.ExpectSigningNameDerived, actual.SigningNameDerived; e != a { + t.Errorf("expect %v, got %v", e, a) + } + if e, a := c.ExpectSigningRegion, actual.SigningRegion; e != a { + t.Errorf("expect %v, got %v", e, a) + } + }) + } + +} diff --git a/aws/endpoints/v3model_test.go b/aws/endpoints/v3model_test.go index 0b8b00673d7..fc782a26717 100644 --- a/aws/endpoints/v3model_test.go +++ b/aws/endpoints/v3model_test.go @@ -1,3 +1,5 @@ +// +build go1.7 + package endpoints import ( @@ -253,72 +255,6 @@ func TestEndpointMergeIn(t *testing.T) { } } -var testPartitions = partitions{ - partition{ - ID: "part-id", - Name: "partitionName", - DNSSuffix: "amazonaws.com", - RegionRegex: regionRegex{ - Regexp: func() *regexp.Regexp { - reg, _ := regexp.Compile("^(us|eu|ap|sa|ca)\\-\\w+\\-\\d+$") - return reg - }(), - }, - Defaults: endpoint{ - Hostname: "{service}.{region}.{dnsSuffix}", - Protocols: []string{"https"}, - SignatureVersions: []string{"v4"}, - }, - Regions: regions{ - "us-east-1": region{ - Description: "region description", - }, - "us-west-2": region{}, - }, - Services: services{ - "s3": service{}, - "service1": service{ - Defaults: endpoint{ - CredentialScope: credentialScope{ - Service: "service1", - }, - }, - Endpoints: endpoints{ - "us-east-1": {}, - "us-west-2": { - HasDualStack: boxedTrue, - DualStackHostname: "{service}.dualstack.{region}.{dnsSuffix}", - }, - }, - }, - "service2": service{ - Defaults: endpoint{ - CredentialScope: credentialScope{ - Service: "service2", - }, - }, - }, - "httpService": service{ - Defaults: endpoint{ - Protocols: []string{"http"}, - }, - }, - "globalService": service{ - IsRegionalized: boxedFalse, - PartitionEndpoint: "aws-global", - Endpoints: endpoints{ - "aws-global": endpoint{ - CredentialScope: credentialScope{ - Region: "us-east-1", - }, - Hostname: "globalService.amazonaws.com", - }, - }, - }, - }, - }, -} - func TestResolveEndpoint(t *testing.T) { resolved, err := testPartitions.EndpointFor("service2", "us-west-2") @@ -539,3 +475,67 @@ func TestResolveEndpoint_AwsGlobal(t *testing.T) { t.Errorf("expect the signing name to be derived") } } + +func TestEndpointFor_RegionalFlag(t *testing.T) { + // AwsPartition resolver for STS regional endpoints in AWS Partition + resolver := AwsPartition() + + cases := map[string]struct { + service, region string + regional bool + ExpectURL, ExpectSigningMethod, ExpectSigningRegion string + ExpectSigningNameDerived bool + }{ + "acm/ap-northeast-1/regional": { + service: "acm", + region: "ap-northeast-1", + regional: true, + ExpectURL: "https://acm.ap-northeast-1.amazonaws.com", + ExpectSigningMethod: "v4", + ExpectSigningNameDerived: true, + ExpectSigningRegion: "ap-northeast-1", + }, + "acm/ap-northeast-1/legacy": { + service: "acm", + region: "ap-northeast-1", + regional: false, + ExpectURL: "https://acm.ap-northeast-1.amazonaws.com", + ExpectSigningMethod: "v4", + ExpectSigningNameDerived: true, + ExpectSigningRegion: "ap-northeast-1", + }, + } + + for name, c := range cases { + t.Run(name, func(t *testing.T) { + var optionSlice []func(o *Options) + optionSlice = append(optionSlice, func(o *Options) { + if c.regional { + o.STSRegionalEndpoint = RegionalSTSEndpoint + } + }) + + actual, err := resolver.EndpointFor(c.service, c.region, optionSlice...) + if err != nil { + t.Fatalf("failed to resolve endpoint, %v", err) + } + + if e, a := c.ExpectURL, actual.URL; e != a { + t.Errorf("expect %v, got %v", e, a) + } + + if e, a := c.ExpectSigningMethod, actual.SigningMethod; e != a { + t.Errorf("expect %v, got %v", e, a) + } + + if e, a := c.ExpectSigningNameDerived, actual.SigningNameDerived; e != a { + t.Errorf("expect %v, got %v", e, a) + } + + if e, a := c.ExpectSigningRegion, actual.SigningRegion; e != a { + t.Errorf("expect %v, got %v", e, a) + } + + }) + } +} diff --git a/aws/session/csm_test.go b/aws/session/csm_test.go index 0ed1c03a37b..90da22bc700 100644 --- a/aws/session/csm_test.go +++ b/aws/session/csm_test.go @@ -101,7 +101,10 @@ func TestSession_loadCSMConfig(t *testing.T) { os.Setenv(name, v) } - envCfg := loadEnvConfig() + envCfg, err := loadEnvConfig() + if err != nil { + t.Fatalf("failed to load the envcfg, %v", err) + } csmCfg, err := loadCSMConfig(envCfg, c.ConfigFiles) if len(c.Err) != 0 { if err == nil { diff --git a/aws/session/env_config.go b/aws/session/env_config.go index 60a6f9ce2a4..530cc3a9c06 100644 --- a/aws/session/env_config.go +++ b/aws/session/env_config.go @@ -1,12 +1,14 @@ package session import ( + "fmt" "os" "strconv" "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" ) // EnvProviderName provides a name of the provider when config is loaded from environment. @@ -125,6 +127,12 @@ type envConfig struct { // // AWS_ROLE_SESSION_NAME=session_name RoleSessionName string + + // Specifies the Regional Endpoint flag for the sdk to resolve the endpoint for a service + // + // AWS_STS_REGIONAL_ENDPOINTS =sts_regional_endpoint + // This can take value as `regional` or `legacy` + STSRegionalEndpoint endpoints.STSRegionalEndpoint } var ( @@ -179,6 +187,9 @@ var ( roleSessionNameEnvKey = []string{ "AWS_ROLE_SESSION_NAME", } + stsRegionalEndpointKey = []string{ + "AWS_STS_REGIONAL_ENDPOINTS", + } ) // loadEnvConfig retrieves the SDK's environment configuration. @@ -187,7 +198,7 @@ var ( // If the environment variable `AWS_SDK_LOAD_CONFIG` is set to a truthy value // the shared SDK config will be loaded in addition to the SDK's specific // configuration values. -func loadEnvConfig() envConfig { +func loadEnvConfig() (envConfig, error) { enableSharedConfig, _ := strconv.ParseBool(os.Getenv("AWS_SDK_LOAD_CONFIG")) return envConfigLoad(enableSharedConfig) } @@ -198,11 +209,11 @@ func loadEnvConfig() envConfig { // Loads the shared configuration in addition to the SDK's specific configuration. // This will load the same values as `loadEnvConfig` if the `AWS_SDK_LOAD_CONFIG` // environment variable is set. -func loadSharedEnvConfig() envConfig { +func loadSharedEnvConfig() (envConfig, error) { return envConfigLoad(true) } -func envConfigLoad(enableSharedConfig bool) envConfig { +func envConfigLoad(enableSharedConfig bool) (envConfig, error) { cfg := envConfig{} cfg.EnableSharedConfig = enableSharedConfig @@ -264,12 +275,23 @@ func envConfigLoad(enableSharedConfig bool) envConfig { cfg.CustomCABundle = os.Getenv("AWS_CA_BUNDLE") - return cfg + // STS Regional Endpoint variable + for _, k := range stsRegionalEndpointKey { + if v := os.Getenv(k); len(v) != 0 { + STSRegionalEndpoint, err := endpoints.GetSTSRegionalEndpoint(v) + if err != nil { + return cfg, fmt.Errorf("failed to load, %v from env config, %v", k, err) + } + cfg.STSRegionalEndpoint = STSRegionalEndpoint + } + } + + return cfg, nil } func setFromEnvVal(dst *string, keys []string) { for _, k := range keys { - if v := os.Getenv(k); len(v) > 0 { + if v := os.Getenv(k); len(v) != 0 { *dst = v break } diff --git a/aws/session/env_config_test.go b/aws/session/env_config_test.go index 03e4b4949e5..5d5e37ae49d 100644 --- a/aws/session/env_config_test.go +++ b/aws/session/env_config_test.go @@ -84,7 +84,10 @@ func TestLoadEnvConfig_Creds(t *testing.T) { os.Setenv(k, v) } - cfg := loadEnvConfig() + cfg, err := loadEnvConfig() + if err != nil { + t.Fatalf("failed to load env config, %v", err) + } if !reflect.DeepEqual(c.Val, cfg.Creds) { t.Errorf("expect credentials to match.\n%s", awstesting.SprintExpectActual(c.Val, cfg.Creds)) @@ -279,10 +282,17 @@ func TestLoadEnvConfig(t *testing.T) { } var cfg envConfig + var err error if c.UseSharedConfigCall { - cfg = loadSharedEnvConfig() + cfg, err = loadSharedEnvConfig() + if err != nil { + t.Errorf("failed to load shared env config, %v", err) + } } else { - cfg = loadEnvConfig() + cfg, err = loadEnvConfig() + if err != nil { + t.Errorf("failed to load env config, %v", err) + } } if !reflect.DeepEqual(c.Config, cfg) { diff --git a/aws/session/session.go b/aws/session/session.go index 7b0a942e223..245a5fb1bac 100644 --- a/aws/session/session.go +++ b/aws/session/session.go @@ -73,7 +73,7 @@ type Session struct { // func is called instead of waiting to receive an error until a request is made. func New(cfgs ...*aws.Config) *Session { // load initial config from environment - envCfg := loadEnvConfig() + envCfg, envErr := loadEnvConfig() if envCfg.EnableSharedConfig { var cfg aws.Config @@ -93,17 +93,17 @@ func New(cfgs ...*aws.Config) *Session { // Session creation failed, need to report the error and prevent // any requests from succeeding. s = &Session{Config: defaults.Config()} - s.Config.MergeIn(cfgs...) - s.Config.Logger.Log("ERROR:", msg, "Error:", err) - s.Handlers.Validate.PushBack(func(r *request.Request) { - r.Error = err - }) + s.logDeprecatedNewSessionError(msg, err, cfgs) } return s } s := deprecatedNewSession(cfgs...) + if envErr != nil { + msg := "failed to load env config" + s.logDeprecatedNewSessionError(msg, envErr, cfgs) + } if csmCfg, err := loadCSMConfig(envCfg, []string{}); err != nil { if l := s.Config.Logger; l != nil { @@ -112,11 +112,8 @@ func New(cfgs ...*aws.Config) *Session { } else if csmCfg.Enabled { err := enableCSM(&s.Handlers, csmCfg, s.Config.Logger) if err != nil { - err = fmt.Errorf("failed to enable CSM, %v", err) - s.Config.Logger.Log("ERROR:", err.Error()) - s.Handlers.Validate.PushBack(func(r *request.Request) { - r.Error = err - }) + msg := "failed to enable CSM" + s.logDeprecatedNewSessionError(msg, err, cfgs) } } @@ -279,10 +276,17 @@ type Options struct { // })) func NewSessionWithOptions(opts Options) (*Session, error) { var envCfg envConfig + var err error if opts.SharedConfigState == SharedConfigEnable { - envCfg = loadSharedEnvConfig() + envCfg, err = loadSharedEnvConfig() + if err != nil { + return nil, fmt.Errorf("failed to load shared config, %v", err) + } } else { - envCfg = loadEnvConfig() + envCfg, err = loadEnvConfig() + if err != nil { + return nil, fmt.Errorf("failed to load environment config, %v", err) + } } if len(opts.Profile) != 0 { @@ -550,6 +554,9 @@ func mergeConfigSrcs(cfg, userCfg *aws.Config, } } + // Regional Endpoint flag for STS endpoint resolving + mergeSTSRegionalEndpointConfig(cfg, envCfg, sharedCfg) + // Configure credentials if not already set by the user when creating the // Session. if cfg.Credentials == credentials.AnonymousCredentials && userCfg.Credentials == nil { @@ -563,6 +570,22 @@ func mergeConfigSrcs(cfg, userCfg *aws.Config, return nil } +// mergeSTSRegionalEndpointConfig function merges the STSRegionalEndpoint into cfg from +// envConfig and SharedConfig with envConfig being given precedence over SharedConfig +func mergeSTSRegionalEndpointConfig(cfg *aws.Config, envCfg envConfig, sharedCfg sharedConfig) error { + + cfg.STSRegionalEndpoint = envCfg.STSRegionalEndpoint + + if cfg.STSRegionalEndpoint == endpoints.UnsetSTSEndpoint { + cfg.STSRegionalEndpoint = sharedCfg.STSRegionalEndpoint + } + + if cfg.STSRegionalEndpoint == endpoints.UnsetSTSEndpoint { + cfg.STSRegionalEndpoint = endpoints.LegacySTSEndpoint + } + return nil +} + func initHandlers(s *Session) { // Add the Validate parameter handler if it is not disabled. s.Handlers.Validate.Remove(corehandlers.ValidateParametersHandler) @@ -616,6 +639,9 @@ func (s *Session) clientConfigWithErr(serviceName string, cfgs ...*aws.Config) ( func(opt *endpoints.Options) { opt.DisableSSL = aws.BoolValue(s.Config.DisableSSL) opt.UseDualStack = aws.BoolValue(s.Config.UseDualStack) + // Support for STSRegionalEndpoint where the STSRegionalEndpoint is + // provided in envconfig or sharedconfig with envconfig getting precedence. + opt.STSRegionalEndpoint = s.Config.STSRegionalEndpoint // Support the condition where the service is modeled but its // endpoint metadata is not available. @@ -658,3 +684,14 @@ func (s *Session) ClientConfigNoResolveEndpoint(cfgs ...*aws.Config) client.Conf SigningName: resolved.SigningName, } } + +// logDeprecatedNewSessionError function enables error handling for session +func (s *Session) logDeprecatedNewSessionError(msg string, err error, cfgs []*aws.Config) { + // Session creation failed, need to report the error and prevent + // any requests from succeeding. + s.Config.MergeIn(cfgs...) + s.Config.Logger.Log("ERROR:", msg, "Error:", err) + s.Handlers.Validate.PushBack(func(r *request.Request) { + r.Error = err + }) +} diff --git a/aws/session/shared_config.go b/aws/session/shared_config.go index d91ac93a544..8574668960b 100644 --- a/aws/session/shared_config.go +++ b/aws/session/shared_config.go @@ -5,6 +5,7 @@ import ( "github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/aws/endpoints" "github.com/aws/aws-sdk-go/internal/ini" ) @@ -40,6 +41,9 @@ const ( // Web Identity Token File webIdentityTokenFileKey = `web_identity_token_file` // optional + // Additional config fields for regional or legacy endpoints + stsRegionalEndpointSharedKey = `sts_regional_endpoints` + // DefaultSharedConfigProfile is the default profile to be used when // loading configuration from the config files if another profile name // is not provided. @@ -82,12 +86,17 @@ type sharedConfig struct { // // endpoint_discovery_enabled = true EnableEndpointDiscovery *bool - // CSM Options CSMEnabled *bool CSMHost string CSMPort string CSMClientID string + + // Specifies the Regional Endpoint flag for the sdk to resolve the endpoint for a service + // + // sts_regional_endpoints = sts_regional_endpoint + // This can take value as `LegacySTSEndpoint` or `RegionalSTSEndpoint` + STSRegionalEndpoint endpoints.STSRegionalEndpoint } type sharedConfigFile struct { @@ -244,8 +253,16 @@ func (cfg *sharedConfig) setFromIniFile(profile string, file sharedConfigFile, e updateString(&cfg.RoleSessionName, section, roleSessionNameKey) updateString(&cfg.SourceProfileName, section, sourceProfileKey) updateString(&cfg.CredentialSource, section, credentialSourceKey) - updateString(&cfg.Region, section, regionKey) + + if v := section.String(stsRegionalEndpointSharedKey); len(v) != 0 { + sre, err := endpoints.GetSTSRegionalEndpoint(v) + if err != nil { + return fmt.Errorf("failed to load %s from shared config, %s, %v", + stsRegionalEndpointKey, file.Filename, err) + } + cfg.STSRegionalEndpoint = sre + } } updateString(&cfg.CredentialProcess, section, credentialProcessKey) diff --git a/private/model/api/example_test.go b/private/model/api/example_test.go index 2c7149c3752..4083ebf8abd 100644 --- a/private/model/api/example_test.go +++ b/private/model/api/example_test.go @@ -296,8 +296,8 @@ func ExampleFooService_Foo_shared00() { } ` if expected != a.ExamplesGoCode() { - t.Errorf("Expected:\n%s\nReceived:\n%s\n", expected, a.ExamplesGoCode()) - } + t.Errorf("Expected:\n%s\nReceived:\n%s\n", expected, a.ExamplesGoCode()) + } } func TestBuildShape(t *testing.T) {