From 1dadb34b29c1833aedde4de4fb45f3bb3c62ca0d Mon Sep 17 00:00:00 2001 From: "STeve (Xin) Huang" Date: Wed, 31 Aug 2022 12:29:40 -0400 Subject: [PATCH] Support AWS Console for US GovCloud Partition (#13442) --- api/constants/constants.go | 7 ++++++ api/types/app.go | 13 ++++++++++- api/types/app_test.go | 47 ++++++++++++++++++++++++++++++++++++++ lib/srv/app/cloud.go | 31 +++++++++++++++++++++---- lib/srv/app/cloud_test.go | 31 +++++++++++++++++++++++++ 5 files changed, 123 insertions(+), 6 deletions(-) diff --git a/api/constants/constants.go b/api/constants/constants.go index 9b7a8b17a55..812c4a2ad2a 100644 --- a/api/constants/constants.go +++ b/api/constants/constants.go @@ -126,6 +126,13 @@ const ( // AWSConsoleURL is the URL of AWS management console. AWSConsoleURL = "https://console.aws.amazon.com" + // AWSUSGovConsoleURL is the URL of AWS management console for AWS GovCloud + // (US) Partition. + AWSUSGovConsoleURL = "https://console.amazonaws-us-gov.com" + // AWSCNConsoleURL is the URL of AWS management console for AWS China + // Partition. + AWSCNConsoleURL = "https://console.amazonaws.cn" + // AWSAccountIDLabel is the key of the label containing AWS account ID. AWSAccountIDLabel = "aws_account_id" diff --git a/api/types/app.go b/api/types/app.go index 11c49dab67c..389feb935c3 100644 --- a/api/types/app.go +++ b/api/types/app.go @@ -237,7 +237,18 @@ func (a *AppV3) GetRewrite() *Rewrite { // IsAWSConsole returns true if this app is AWS management console. func (a *AppV3) IsAWSConsole() bool { - return strings.HasPrefix(a.Spec.URI, constants.AWSConsoleURL) + // TODO(greedy52) support region based console URL like: + // https://us-east-1.console.aws.amazon.com/ + for _, consoleURL := range []string{ + constants.AWSConsoleURL, + constants.AWSUSGovConsoleURL, + constants.AWSCNConsoleURL, + } { + if strings.HasPrefix(a.Spec.URI, consoleURL) { + return true + } + } + return false } // IsTCP returns true if this app represents a TCP endpoint. diff --git a/api/types/app_test.go b/api/types/app_test.go index 8565477fc88..dd14fe29cf5 100644 --- a/api/types/app_test.go +++ b/api/types/app_test.go @@ -167,6 +167,53 @@ func TestAppServerSorter(t *testing.T) { require.True(t, trace.IsNotImplemented(AppServers(servers).SortByCustom(sortBy))) } +func TestAppIsAWSConsole(t *testing.T) { + tests := []struct { + name string + uri string + assertIsAWSConsole require.BoolAssertionFunc + }{ + { + name: "AWS Standard", + uri: "https://console.aws.amazon.com/ec2/v2/home", + assertIsAWSConsole: require.True, + }, + { + name: "AWS China", + uri: "https://console.amazonaws.cn/console/home", + assertIsAWSConsole: require.True, + }, + { + name: "AWS GovCloud (US)", + uri: "https://console.amazonaws-us-gov.com/console/home", + assertIsAWSConsole: require.True, + }, + { + name: "Region based not supported yet", + uri: "https://us-west-1.console.aws.amazon.com", + assertIsAWSConsole: require.False, + }, + { + name: "Not an AWS Console URL", + uri: "https://hello.world", + assertIsAWSConsole: require.False, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + app, err := NewAppV3(Metadata{ + Name: "aws", + }, AppSpecV3{ + URI: test.uri, + }) + require.NoError(t, err) + + test.assertIsAWSConsole(t, app.IsAWSConsole()) + }) + } +} + func TestApplicationGetAWSExternalID(t *testing.T) { t.Parallel() diff --git a/lib/srv/app/cloud.go b/lib/srv/app/cloud.go index a8cf88bd7a4..d219962c6fc 100644 --- a/lib/srv/app/cloud.go +++ b/lib/srv/app/cloud.go @@ -22,8 +22,10 @@ import ( "net/http" "net/url" "strconv" + "strings" "time" + "github.com/gravitational/teleport/api/constants" "github.com/gravitational/teleport/lib/tlsca" "github.com/aws/aws-sdk-go/aws" @@ -67,7 +69,7 @@ func (r *AWSSigninRequest) CheckAndSetDefaults() error { return trace.Wrap(err) } if r.TargetURL == "" { - r.TargetURL = consoleURL + return trace.BadParameter("missing TargetURL") } if r.Issuer == "" { return trace.BadParameter("missing Issuer") @@ -131,6 +133,7 @@ func (c *cloud) GetAWSSigninURL(req AWSSigninRequest) (*AWSSigninResponse, error return nil, trace.Wrap(err) } + federationURL := getFederationURL(req.TargetURL) signinToken, err := c.getAWSSigninToken(&req, federationURL) if err != nil { return nil, trace.Wrap(err) @@ -333,11 +336,29 @@ type federationResponse struct { SigninToken string `json:"SigninToken"` } +// getFederationURL picks the AWS federation endpoint based on the AWS +// partition of the target URL. +// +// https://docs.aws.amazon.com/general/latest/gr/signin-service.html +// https://docs.amazonaws.cn/en_us/aws/latest/userguide/endpoints-Beijing.html +func getFederationURL(targetURL string) string { + // TODO(greedy52) support region based sign-in. + switch { + // AWS GovCloud (US) Partition. + case strings.HasPrefix(targetURL, constants.AWSUSGovConsoleURL): + return "https://signin.amazonaws-us-gov.com/federation" + + // AWS China Partition. + case strings.HasPrefix(targetURL, constants.AWSCNConsoleURL): + return "https://signin.amazonaws.cn/federation" + + // AWS Standard Partition. + default: + return "https://signin.aws.amazon.com/federation" + } +} + const ( - // federationURL is the AWS federation endpoint. - federationURL = "https://signin.aws.amazon.com/federation" - // consoleURL is the default AWS console destination. - consoleURL = "https://console.aws.amazon.com/ec2/v2/home" // maxSessionDuration is the max federation session duration, which is 12 // hours. The federation endpoint will error out if we request more. // diff --git a/lib/srv/app/cloud_test.go b/lib/srv/app/cloud_test.go index de6e46c48fb..caa1f4647eb 100644 --- a/lib/srv/app/cloud_test.go +++ b/lib/srv/app/cloud_test.go @@ -29,6 +29,7 @@ import ( "github.com/aws/aws-sdk-go/service/sts" "github.com/jonboulle/clockwork" + "github.com/gravitational/teleport/api/constants" "github.com/gravitational/teleport/lib/tlsca" "github.com/gravitational/trace" "github.com/stretchr/testify/require" @@ -287,3 +288,33 @@ func TestCloudGetAWSSigninToken(t *testing.T) { }) } } + +func TestCloudGetFederationURL(t *testing.T) { + tests := []struct { + name string + inputTargetURL string + expectedFederationURL string + }{ + { + name: "AWS GovCloud (US)", + inputTargetURL: constants.AWSUSGovConsoleURL, + expectedFederationURL: "https://signin.amazonaws-us-gov.com/federation", + }, + { + name: "AWS China", + inputTargetURL: constants.AWSCNConsoleURL, + expectedFederationURL: "https://signin.amazonaws.cn/federation", + }, + { + name: "AWS Standard", + inputTargetURL: constants.AWSConsoleURL, + expectedFederationURL: "https://signin.aws.amazon.com/federation", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require.Equal(t, test.expectedFederationURL, getFederationURL(test.inputTargetURL)) + }) + } +}