diff --git a/tool/tctl/common/plugin/awsic.go b/tool/tctl/common/plugin/awsic.go index 68788349ed1f8..31b86085739dd 100644 --- a/tool/tctl/common/plugin/awsic.go +++ b/tool/tctl/common/plugin/awsic.go @@ -55,6 +55,7 @@ type awsICInstallArgs struct { region string arn string useSystemCredentials bool + oidcIntegration string assumeRoleARN string userOrigins []string userLabels []string @@ -67,7 +68,7 @@ type awsICInstallArgs struct { excludeAccountIDFilters []string } -func (a *awsICInstallArgs) validate(ctx context.Context, log *slog.Logger) error { +func (a *awsICInstallArgs) validate(ctx context.Context, auth authClient, log *slog.Logger) error { if !awsregion.IsKnownRegion(a.region) { return trace.BadParameter("unknown AWS region: %s", a.region) } @@ -76,7 +77,7 @@ func (a *awsICInstallArgs) validate(ctx context.Context, log *slog.Logger) error return trace.BadParameter("SCIM token must not be empty") } - if err := a.validateSystemCredentialInput(); err != nil { + if err := a.validateCredentialInput(); err != nil { return trace.Wrap(err) } @@ -87,17 +88,28 @@ func (a *awsICInstallArgs) validate(ctx context.Context, log *slog.Logger) error return nil } -func (a *awsICInstallArgs) validateSystemCredentialInput() error { - if !a.useSystemCredentials { - return trace.BadParameter("--use-system-credentials must be set. The tctl-based AWS IAM Identity Center plugin installation only supports AWS local system credentials") +func (a *awsICInstallArgs) validateCredentialInput() error { + if a.useSystemCredentials { + if a.assumeRoleARN == "" { + return trace.BadParameter("--assume-role-arn must be set when --use-system-credentials is configured") + } + + if a.oidcIntegration != "" { + return trace.BadParameter("--oidc-integration must not be set when --use-system-credentials is configured") + } + + if _, err := awsutils.ParseRoleARN(a.assumeRoleARN); err != nil { + return trace.Wrap(err) + } + return nil } - if a.assumeRoleARN == "" { - return trace.BadParameter("--assume-role-arn must be set when --use-system-credentials is configured") + if a.oidcIntegration == "" { + return trace.BadParameter("--oidc-integration must be set when --no-use-system-credentials is configured") } - if _, err := awsutils.ParseRoleARN(a.assumeRoleARN); err != nil { - return trace.Wrap(err) + if a.assumeRoleARN != "" { + return trace.BadParameter("--assume-role-arn must not be set when --no-use-system-credentials is configured") } return nil @@ -108,7 +120,6 @@ func (a *awsICInstallArgs) validateSCIMBaseURL(ctx context.Context, log *slog.Lo if err == nil { a.scimURL = validatedBaseUrl return nil - } if a.forceSCIMURL { @@ -216,6 +227,8 @@ func (p *PluginsCommand) initInstallAWSIC(parent *kingpin.CmdClause) { BoolVar(&p.install.awsIC.useSystemCredentials) cmd.Flag("assume-role-arn", "ARN of a role that the system credential should assume."). StringVar(&p.install.awsIC.assumeRoleARN) + cmd.Flag("oidc-integration", "Name of the Teleport OIDC integration to use when authenticating with AWS. Must be supplied when --no-use-system-credentials is set."). + StringVar(&p.install.awsIC.oidcIntegration) cmd.Flag("user-origin", fmt.Sprintf(`Shorthand for "--user-label %s=ORIGIN"`, types.OriginLabel)). PlaceHolder("ORIGIN"). @@ -246,7 +259,7 @@ func (p *PluginsCommand) initInstallAWSIC(parent *kingpin.CmdClause) { // InstallAWSIC installs AWS Identity Center plugin. func (p *PluginsCommand) InstallAWSIC(ctx context.Context, args pluginServices) error { awsICArgs := p.install.awsIC - if err := awsICArgs.validate(ctx, p.config.Logger); err != nil { + if err := awsICArgs.validate(ctx, args.authClient, p.config.Logger); err != nil { return trace.Wrap(err) } @@ -299,6 +312,14 @@ func (p *PluginsCommand) InstallAWSIC(ctx context.Context, args pluginServices) // Set the deprecated CredentialsSource to the legacy value to allow old // versions of Teleport to handle the record. DELETE in Teleport 19 settings.CredentialsSource = types.AWSICCredentialsSource_AWSIC_CREDENTIALS_SOURCE_SYSTEM + } else { + settings.Credentials = &types.AWSICCredentials{ + Source: &types.AWSICCredentials_Oidc{ + Oidc: &types.AWSICCredentialSourceOIDC{ + IntegrationName: awsICArgs.oidcIntegration, + }, + }, + } } req := &pluginspb.CreatePluginRequest{ diff --git a/tool/tctl/common/plugin/awsic_test.go b/tool/tctl/common/plugin/awsic_test.go index 6fd702893ce43..58b891cb67ecd 100644 --- a/tool/tctl/common/plugin/awsic_test.go +++ b/tool/tctl/common/plugin/awsic_test.go @@ -277,35 +277,79 @@ func TestSCIMBaseURLValidation(t *testing.T) { } } -func TestUseSystemCredentialsInput(t *testing.T) { +type mockIntegrationGetter struct { + mock.Mock +} + +func maybeGet[T any](args mock.Arguments, index int) T { + value := args.Get(index) + if value == nil { + var zero T + return zero + } + return value.(T) +} + +func (m *mockIntegrationGetter) GetIntegration(ctx context.Context, name string) (types.Integration, error) { + result := m.Called(ctx, name) + return maybeGet[types.Integration](result, 0), result.Error(1) +} + +func TestCredentialsInput(t *testing.T) { testCases := []struct { name string useSystemCredential bool assumeRoleARN string + integrationName string + integrationExists bool expectError require.ErrorAssertionFunc }{ { - name: "valid system credential config", + name: "use system credentials", useSystemCredential: true, assumeRoleARN: "arn:aws:iam::026000000023:role/assume1", expectError: require.NoError, }, { - name: "no useSystemCredential", - useSystemCredential: false, + name: "use system credentials without assumeRoleARN is an error", + useSystemCredential: true, assumeRoleARN: "", expectError: require.Error, }, { - name: "useSystemCredential without assumeRoleARN", + name: "use system credentials with a malformed assumeRoleARN is an error", useSystemCredential: true, - assumeRoleARN: "", + assumeRoleARN: "i am not an arn", expectError: require.Error, }, { - name: "useSystemCredential with invalid assumeRoleARN", + name: "use system credentials with integration is an error", useSystemCredential: true, - assumeRoleARN: "example-credential", + assumeRoleARN: "arn:aws:iam::026000000023:role/assume1", + integrationName: "some-integration", + expectError: require.Error, + }, + { + name: "use oidc credentials", + useSystemCredential: false, + assumeRoleARN: "", + integrationName: "some-integration", + integrationExists: true, + expectError: require.NoError, + }, + { + name: "use oidc credentials with no integration set", + useSystemCredential: false, + assumeRoleARN: "", + integrationName: "", + expectError: require.Error, + }, + { + name: "use oidc credentials and setting assumeRoleARN is an error", + useSystemCredential: false, + assumeRoleARN: "arn:aws:iam::026000000023:role/assume1", + integrationName: "some-integration", + integrationExists: true, expectError: require.Error, }, } @@ -315,9 +359,22 @@ func TestUseSystemCredentialsInput(t *testing.T) { cliArgs := awsICInstallArgs{ useSystemCredentials: tc.useSystemCredential, assumeRoleARN: tc.assumeRoleARN, + oidcIntegration: tc.integrationName, + } + + integrations := &mockIntegrationGetter{} + if !tc.useSystemCredential { + var integrationErr error + if !tc.integrationExists { + integrationErr = trace.NotFound("yes, we have no bananas") + } + + integrations. + On("GetIntegration", anyContext, mock.AnythingOfType("string")). + Return(nil, integrationErr) } - err := cliArgs.validateSystemCredentialInput() + err := cliArgs.validateCredentialInput() tc.expectError(t, err) }) } @@ -330,11 +387,7 @@ type mockRoundTripper struct { // RoundTrip implements the [http.RoundTripper] interface for the mockRoundTripper func (m *mockRoundTripper) RoundTrip(request *http.Request) (*http.Response, error) { args := m.Called(request) - maybeResponse := args.Get(0) - if maybeResponse == nil { - return nil, args.Error(1) - } - return args.Get(0).(*http.Response), args.Error(1) + return maybeGet[*http.Response](args, 0), args.Error(1) } func TestRotateAWSICSCIMToken(t *testing.T) {