diff --git a/cmd/command.go b/cmd/command.go index 55a3f28..24d8ddc 100644 --- a/cmd/command.go +++ b/cmd/command.go @@ -116,7 +116,14 @@ func Command() *cobra.Command { //nolint:cyclop,funlen region = "us-east-1" } - federatePolicy := resolvePolicyAlias(flags.federatePolicy) + partition, consoleDomain, federationURL, ok := credentials.ResolveRegionPartition(region) //nolint:varnamelen + if !ok { + return fmt.Errorf("could not determine partition for region %s", region) + } + + // Resolve the IAM policy ARN that will be included along with the + // GetFederationToken request, if a request is made. + federatePolicy := resolvePolicyAlias(flags.federatePolicy, partition) // If the named profile was configured with user credentials // (opposed to a role), then the user must be federated before an @@ -128,13 +135,13 @@ func Command() *cobra.Command { //nolint:cyclop,funlen // Resolve the given location alias into a redirect url to a // service in the AWS Console. - location, ok := resolveLocationAlias(flags.location, region) + location, ok := resolveLocationAlias(flags.location, consoleDomain, region) if !ok { return fmt.Errorf("could not resolve location %q", flags.location) } // Generate a login URL for the AWS Console. - url, err := console.GenerateLoginURL(creds, flags.duration, location, flags.userAgent) + url, err := console.GenerateLoginURL(creds, federationURL, flags.duration, location, flags.userAgent) if err != nil { // There is a very specific failure case where if you attempt // to generate a Console login URL for an IAM Role, which @@ -152,7 +159,7 @@ func Command() *cobra.Command { //nolint:cyclop,funlen // | temporary credentials through role chaining. The operation // | will fail. // See https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_enable-console-custom-url.html - url, err = console.GenerateLoginURL(creds, 0, location, flags.userAgent) + url, err = console.GenerateLoginURL(creds, federationURL, 0, location, flags.userAgent) if err != nil { return err } diff --git a/cmd/template.go b/cmd/template.go index 18eb765..b4965ff 100644 --- a/cmd/template.go +++ b/cmd/template.go @@ -11,28 +11,34 @@ import "strings" // Console. Used for quickly redirecting the user to the desired service after // logging in. var locations = map[string]string{ //nolint:gochecknoglobals - "account": "https://console.aws.amazon.com/billing/home#/account", - "billing": "https://console.aws.amazon.com/billing/home", - "console": "https://{{.region}}.console.aws.amazon.com/console/home?region={{.region}}", - "ec2": "https://{{.region}}.console.aws.amazon.com/ec2/home?region={{.region}}#Home:", - "ecr": "https://{{.region}}.console.aws.amazon.com/ecr/repositories?region={{.region}}", - "eks": "https://{{.region}}.console.aws.amazon.com/eks/home?region={{.region}}#/clusters", - "groups": "https://console.aws.amazon.com/iamv2/home#/groups", - "home": "https://{{.region}}.console.aws.amazon.com/console/home?region={{.region}}", - "iam": "https://console.aws.amazon.com/iamv2/home#/home", - "kms": "https://{{.region}}.console.aws.amazon.com/kms/home?region={{.region}}#/kms/keys", - "org": "https://console.aws.amazon.com/organizations/v2/home/dashboard", - "policies": "https://console.aws.amazon.com/iamv2/home#/policies", - "r53": "https://console.aws.amazon.com/route53/v2/hostedzones#", - "rds": "https://{{.region}}.console.aws.amazon.com/rds/home?region={{.region}}#databases:", - "roles": "https://console.aws.amazon.com/iamv2/home#/roles", - "s3": "https://s3.console.aws.amazon.com/s3/buckets?region={{.region}}", - "support": "https://support.console.aws.amazon.com/support/home", - "users": "https://console.aws.amazon.com/iamv2/home#/users", - "vpn": "https://{{.region}}.console.aws.amazon.com/vpc/home?region={{.region}}#ClientVPNEndpoints:", + "account": "https://{region}.{console}/billing/home?region={region}#/account", + "billing": "https://{region}.{console}/costmanagement/home?region={region}#/home", + "cloudfront": "https://{region}.{console}/cloudfront/v4/home?region={region}#/distributions", + "cloudtrail": "https://{region}.{console}/cloudtrailv2/home?region={region}#/dashboard", + "cloudwatch": "https://{region}.{console}/cloudwatch/home?region={region}#home:", + "console": "https://{region}.{console}/console/home?region={region}", + "ec2": "https://{region}.{console}/ec2/home?region={region}#Instances:", + "ecr": "https://{region}.{console}/ecr/private-registry/repositories?region={region}", + "ecs": "https://{region}.{console}/ecs/v2/clusters?region={region}", + "eip": "https://{region}.{console}/vpcconsole/home?region={region}#Addresses:", + "eks": "https://{region}.{console}/eks/clusters?region={region}", + "groups": "https://{region}.{console}/iam/home?region={region}#/groups", + "home": "https://{region}.{console}/console/home?region={region}", + "iam": "https://{region}.{console}/iam/home?region={region}#/home", + "kms": "https://{region}.{console}/kms/home?region={region}#/kms/home", + "org": "https://{region}.{console}/organizations/v2/home?region={region}", + "policies": "https://{region}.{console}/iam/home?region={region}#/policies", + "r53": "https://{region}.{console}/route53/v2/hostedzones?region={region}", + "rds": "https://{region}.{console}/rds/home?region={region}#databases:", + "roles": "https://{region}.{console}/iam/home?region={region}#/roles", + "s3": "https://{region}.{console}/s3/buckets?region={region}", + "support": "https://support.{console}/support/home?region={region}#/case/history", + "users": "https://{region}.{console}/iam/home?region={region}#/users", + "vpc": "https://{region}.{console}/vpcconsole/home?region={region}#vpcs:", + "vpn": "https://{region}.{console}/vpcconsole/home?region={region}#ClientVPNEndpoints:", } -func resolveLocationAlias(alias, region string) (string, bool) { +func resolveLocationAlias(alias, consoleDomain, region string) (string, bool) { var template string if strings.HasPrefix(alias, "https://") { @@ -46,24 +52,30 @@ func resolveLocationAlias(alias, region string) (string, bool) { return "", false } - // Replace all region placeholders. - return strings.ReplaceAll(template, "{{.region}}", region), true + // Replace all the placeholders. + return strings.NewReplacer( + "{console}", consoleDomain, + "{region}", region, + ).Replace(template), true } // policies is a list of aliases that can be resolved to IAM policy ARNs. Used // for attaching a policy to a federated user session. var policies = map[string]string{ //nolint:gochecknoglobals - "admin": "arn:aws:iam::aws:policy/AdministratorAccess", - "all": "arn:aws:iam::aws:policy/AdministratorAccess", - "billing": "arn:aws:iam::aws:policy/job-function/Billing", - "readonly": "arn:aws:iam::aws:policy/ReadOnlyAccess", - "ro": "arn:aws:iam::aws:policy/ReadOnlyAccess", + "admin": "arn:{partition}:iam::aws:policy/AdministratorAccess", + "all": "arn:{partition}:iam::aws:policy/AdministratorAccess", + "billing": "arn:{partition}:iam::aws:policy/job-function/Billing", + "readonly": "arn:{partition}:iam::aws:policy/ReadOnlyAccess", + "ro": "arn:{partition}:iam::aws:policy/ReadOnlyAccess", } -func resolvePolicyAlias(alias string) string { +func resolvePolicyAlias(alias, partition string) string { + template := alias + if result, found := policies[alias]; found { - return result + // Resolve the alias into a URL. + template = result } - return alias + return strings.ReplaceAll(template, "{partition}", partition) } diff --git a/console/console.go b/console/console.go index ea86a29..7ebd36d 100644 --- a/console/console.go +++ b/console/console.go @@ -22,10 +22,7 @@ import ( // GenerateLoginURL takes the given sts.Credentials and generates a url.URL // that can be used to login to the AWS Console. // See https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_enable-console-custom-url.html. -func GenerateLoginURL(creds *aws.Credentials, duration time.Duration, location, userAgent string) (*url.URL, error) { - // federationURL is the url used for AWS federation actions. - const federationURL = "https://signin.aws.amazon.com/federation" - +func GenerateLoginURL(creds *aws.Credentials, federationURL string, duration time.Duration, location, userAgent string) (*url.URL, error) { // timeout is a hardcoded 15 second window for HTTP requests to complete. const timeout = 15 * time.Second diff --git a/credentials/partition.go b/credentials/partition.go new file mode 100644 index 000000000..ea9da91 --- /dev/null +++ b/credentials/partition.go @@ -0,0 +1,43 @@ +// Copyright Josh Komoroske. All rights reserved. +// Use of this source code is governed by the MIT license, +// a copy of which can be found in the LICENSE.txt file. +// SPDX-License-Identifier: MIT + +package credentials + +import ( + "github.com/aws/aws-sdk-go-v2/service/sts" +) + +var partitionURLs = map[string]struct { + consoleDomain string + federationURL string +}{ + "aws": { + consoleDomain: "console.aws.amazon.com", + federationURL: "https://signin.aws.amazon.com/federation", + }, + "aws-cn": { + // This partition has not been tested. + consoleDomain: "console.amazonaws.cn", + federationURL: "https://signin.amazonaws.cn/federation", + }, + "aws-us-gov": { + consoleDomain: "console.amazonaws-us-gov.com", + federationURL: "https://signin.amazonaws-us-gov.com/federation", + }, +} + +// ResolveRegionPartition uses the given AWS region to determine the corresponding AWS partition, Console URL, and federation URL. +func ResolveRegionPartition(region string) (string, string, string, bool) { + partition := "aws" + if endpoint, err := sts.NewDefaultEndpointResolver().ResolveEndpoint(region, sts.EndpointResolverOptions{}); err == nil { + partition = endpoint.PartitionID + } + + if urls, ok := partitionURLs[partition]; ok { + return partition, urls.consoleDomain, urls.federationURL, true + } + + return "", "", "", false +}