[buddy] Update URI creation for Roles Anywhere integration in govcloud (#64516)

* Update URI creation for Roles Anywhere integration in govcloud

Noticed that RA defaults to a commercial URI which will not successfully
launch a console in govcloud. This change updates the URI creation logic
to use the correct URI when the parsed ARNs indicate a govcloud partition.

* Use established constants for all use cases

* Add more comprehensive tests to profile syncer tests

---------

Co-authored-by: Benjamin McKenna <bemckenn@cisco.com>
This commit is contained in:
Marco Dinis
2026-03-25 09:14:51 +00:00
committed by GitHub
parent f353970dd9
commit 83f22e2b28
2 changed files with 200 additions and 1 deletions
+17 -1
View File
@@ -518,6 +518,22 @@ func processProfile(ctx context.Context, req processProfileRequest) error {
return nil
}
// awsConsoleURLForARN returns the AWS management console URL appropriate for
// the partition and region encoded in the given ARN. For GovCloud, it returns
// a region-scoped URL (e.g. https://us-gov-west-1.console.amazonaws-us-gov.com)
// so that downstream code can derive the correct region-scoped federation
// endpoint required by GovCloud.
func awsConsoleURLForARN(parsedARN arn.ARN) string {
switch parsedARN.Partition {
case "aws-us-gov":
return constants.AWSUSGovConsoleURL
case "aws-cn":
return constants.AWSCNConsoleURL
default:
return constants.AWSConsoleURL
}
}
func convertProfile(params AWSRolesAnywhereProfileSyncerParams, profile *integrationv1.RolesAnywhereProfile, integrationName string, proxyPublicAddr string) (types.AppServer, error) {
parsedProfileARN, err := arn.Parse(profile.Arn)
if err != nil {
@@ -560,7 +576,7 @@ func convertProfile(params AWSRolesAnywhereProfileSyncerParams, profile *integra
Labels: labels,
},
Spec: types.AppSpecV3{
URI: constants.AWSConsoleURL,
URI: awsConsoleURLForARN(parsedProfileARN),
Integration: integrationName,
PublicAddr: appURL,
AWS: &types.AppAWS{
@@ -25,11 +25,13 @@ import (
"time"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/aws/arn"
"github.com/aws/aws-sdk-go-v2/service/rolesanywhere"
ratypes "github.com/aws/aws-sdk-go-v2/service/rolesanywhere/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/gravitational/teleport/api/constants"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/auth/keystore"
"github.com/gravitational/teleport/lib/backend/memory"
@@ -366,6 +368,187 @@ func TestRunAWSRolesAnywherProfileSyncer(t *testing.T) {
require.NotEmpty(t, lastSyncSummary.ErrorMessage)
})
})
t.Run("app server console URL is partition-specific", func(t *testing.T) {
for _, tt := range []struct {
name string
integration string
appName string
syncProfile string
appProfile string
trustAnchor string
roleARN string
expectedURI string
expectedAppID string
}{
{
name: "govcloud",
integration: "govcloud-integration",
appName: "GovProfile",
syncProfile: "arn:aws-us-gov:rolesanywhere:us-gov-west-1:123456789012:profile/sync-profile",
appProfile: "arn:aws-us-gov:rolesanywhere:us-gov-west-1:123456789012:profile/uuid-gov",
trustAnchor: "arn:aws-us-gov:rolesanywhere:us-gov-west-1:123456789012:trust-anchor/ExampleTrustAnchor",
roleARN: "arn:aws-us-gov:iam::123456789012:role/SyncRole",
expectedURI: constants.AWSUSGovConsoleURL,
expectedAppID: "GovProfile-govcloud-integration",
},
{
name: "china",
integration: "china-integration",
appName: "ChinaProfile",
syncProfile: "arn:aws-cn:rolesanywhere:cn-north-1:123456789012:profile/sync-profile",
appProfile: "arn:aws-cn:rolesanywhere:cn-north-1:123456789012:profile/uuid-cn",
trustAnchor: "arn:aws-cn:rolesanywhere:cn-north-1:123456789012:trust-anchor/ExampleTrustAnchor",
roleARN: "arn:aws-cn:iam::123456789012:role/SyncRole",
expectedURI: constants.AWSCNConsoleURL,
expectedAppID: "ChinaProfile-china-integration",
},
} {
t.Run(tt.name, func(t *testing.T) {
integration, err := types.NewIntegrationAWSRA(types.Metadata{Name: tt.integration}, &types.AWSRAIntegrationSpecV1{
TrustAnchorARN: tt.trustAnchor,
ProfileSyncConfig: &types.AWSRolesAnywhereProfileSyncConfig{
Enabled: true,
ProfileARN: tt.syncProfile,
ProfileAcceptsRoleSessionName: true,
RoleARN: tt.roleARN,
},
})
require.NoError(t, err)
serverClient := &mockCache{
integrations: map[string]types.Integration{
integration.GetName(): integration,
},
ca: newCertAuthority(t, types.AWSRACA, "cluster-name"),
}
syncProfile := ratypes.ProfileDetail{
Name: aws.String("SyncProfile"),
ProfileArn: aws.String(tt.syncProfile),
Enabled: aws.Bool(true),
AcceptRoleSessionName: aws.Bool(true),
}
appProfile := ratypes.ProfileDetail{
Name: aws.String(tt.appName),
ProfileArn: aws.String(tt.appProfile),
Enabled: aws.Bool(true),
AcceptRoleSessionName: aws.Bool(true),
}
params := baseParams(serverClient)
params.rolesAnywhereClient = &mockRolesAnywhereClient{
profiles: []ratypes.ProfileDetail{
syncProfile,
appProfile,
},
tags: map[string][]ratypes.Tag{},
}
synctest.Test(t, func(t *testing.T) {
go func() {
err := RunAWSRolesAnywhereProfileSyncerWhileLocked(t.Context(), params)
assert.NoError(t, err)
}()
synctest.Wait()
require.Len(t, serverClient.appServers, 1)
appServer := serverClient.appServers[0]
require.Equal(t, tt.expectedAppID, appServer.GetName())
require.Equal(t, tt.expectedURI, appServer.GetApp().GetURI())
require.Equal(t, tt.appProfile, appServer.GetApp().GetAWSRolesAnywhereProfileARN())
status := serverClient.integrations[integration.GetName()].GetStatus()
require.NotNil(t, status)
lastSyncSummary := status.AWSRolesAnywhere.LastProfileSync
require.Equal(t, types.IntegrationAWSRolesAnywhereProfileSyncStatusSuccess, lastSyncSummary.Status)
require.NotEmpty(t, lastSyncSummary.StartTime)
require.NotEmpty(t, lastSyncSummary.EndTime)
require.Equal(t, int32(1), lastSyncSummary.SyncedProfiles)
require.Empty(t, lastSyncSummary.ErrorMessage)
})
})
}
})
t.Run("invalid profile ARN reports error status", func(t *testing.T) {
serverClient := baseServerClient(t)
invalidProfile := ratypes.ProfileDetail{
Name: aws.String("InvalidProfile"),
ProfileArn: aws.String("not-an-arn"),
Enabled: aws.Bool(true),
AcceptRoleSessionName: aws.Bool(true),
}
params := baseParams(serverClient)
params.rolesAnywhereClient = &mockRolesAnywhereClient{
profiles: []ratypes.ProfileDetail{
syncProfile,
invalidProfile,
},
tags: map[string][]ratypes.Tag{},
}
synctest.Test(t, func(t *testing.T) {
go func() {
err := RunAWSRolesAnywhereProfileSyncerWhileLocked(t.Context(), params)
assert.NoError(t, err)
}()
synctest.Wait()
require.Empty(t, serverClient.appServers)
status := serverClient.integrations[integrationWithProfileSync.GetName()].GetStatus()
require.NotNil(t, status)
lastSyncSummary := status.AWSRolesAnywhere.LastProfileSync
require.Equal(t, types.IntegrationAWSRolesAnywhereProfileSyncStatusError, lastSyncSummary.Status)
require.NotEmpty(t, lastSyncSummary.StartTime)
require.NotEmpty(t, lastSyncSummary.EndTime)
require.Equal(t, int32(0), lastSyncSummary.SyncedProfiles)
require.NotEmpty(t, lastSyncSummary.ErrorMessage)
})
})
}
func TestAWSConsoleURLForARN(t *testing.T) {
tests := []struct {
name string
inputARN string
expectedURL string
}{
{
name: "GovCloud us-gov-west-1",
inputARN: "arn:aws-us-gov:rolesanywhere:us-gov-west-1:123456789012:profile/uuid1",
expectedURL: constants.AWSUSGovConsoleURL,
},
{
name: "GovCloud us-gov-east-1",
inputARN: "arn:aws-us-gov:rolesanywhere:us-gov-east-1:123456789012:profile/uuid1",
expectedURL: constants.AWSUSGovConsoleURL,
},
{
name: "AWS China",
inputARN: "arn:aws-cn:rolesanywhere:cn-north-1:123456789012:profile/uuid1",
expectedURL: "https://console.amazonaws.cn",
},
{
name: "AWS Standard",
inputARN: "arn:aws:rolesanywhere:eu-west-2:123456789012:profile/uuid1",
expectedURL: "https://console.aws.amazon.com",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
parsed, err := arn.Parse(tt.inputARN)
require.NoError(t, err)
require.Equal(t, tt.expectedURL, awsConsoleURLForARN(parsed))
})
}
}
type mockRolesAnywhereClient struct {