From 367ac0ae2f0e5680040ed241c3322bcecfdab476 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Smoli=C5=84ski?= Date: Mon, 20 Jan 2025 18:19:38 +0100 Subject: [PATCH 1/6] Adds `tctl plugins install awsic` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a comand-line installation tool for the AWS Identity Center integration. Co-authored-by: Marek SmoliƄski --- tool/tctl/common/plugin/awsic.go | 173 +++++++++++++++++++++ tool/tctl/common/plugin/awsic_test.go | 134 ++++++++++++++++ tool/tctl/common/plugin/plugins_command.go | 4 + 3 files changed, 311 insertions(+) create mode 100644 tool/tctl/common/plugin/awsic.go create mode 100644 tool/tctl/common/plugin/awsic_test.go diff --git a/tool/tctl/common/plugin/awsic.go b/tool/tctl/common/plugin/awsic.go new file mode 100644 index 0000000000000..9b5170a99a3f4 --- /dev/null +++ b/tool/tctl/common/plugin/awsic.go @@ -0,0 +1,173 @@ +// Teleport +// Copyright (C) 2025 Gravitational, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package plugin + +import ( + "context" + "fmt" + + "github.com/alecthomas/kingpin/v2" + "github.com/gravitational/trace" + + pluginspb "github.com/gravitational/teleport/api/gen/proto/go/teleport/plugins/v1" + "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/api/types/common" + "github.com/gravitational/teleport/lib/utils" +) + +type awsICArgs struct { + cmd *kingpin.CmdClause + defaultOwners []string + scimToken string + scimURL string + region string + arn string + useSystemCredentials bool + groupNameFilters []string + accountNameFilters []string + accountIDFilters []string +} + +func (a *awsICArgs) validate() error { + if !a.useSystemCredentials { + return trace.BadParameter("only AWS Local system credentials are supported") + } + return nil +} + +// parseAWSICNameFilters validates that all elements of the supplied [names] slice +// are valid regexes or globs and wraps them in [types.AWSICResourceFilter]s for +// inclusion in a [types.PluginAWSICSettings]. +// +// We are using a manual validator here rather than the canonical one defined +// in the AWS IC integration itself, because those filter tools are not +// available to OSS builds of tctl. +func parseAWSICNameFilters(names []string) ([]*types.AWSICResourceFilter, error) { + var filters []*types.AWSICResourceFilter + for _, n := range names { + if _, err := utils.CompileExpression(n); err != nil { + return nil, trace.Wrap(err) + } + filters = append(filters, &types.AWSICResourceFilter{ + Include: &types.AWSICResourceFilter_NameRegex{NameRegex: n}, + }) + } + return filters, nil +} + +func (a *awsICArgs) parseGroupFilters() ([]*types.AWSICResourceFilter, error) { + return parseAWSICNameFilters(a.groupNameFilters) +} + +func (a *awsICArgs) parseAccountFilters() ([]*types.AWSICResourceFilter, error) { + filters, err := parseAWSICNameFilters(a.accountNameFilters) + if err != nil { + return nil, trace.Wrap(err) + } + + for _, id := range a.accountIDFilters { + filters = append(filters, &types.AWSICResourceFilter{ + Include: &types.AWSICResourceFilter_Id{Id: id}, + }) + } + + return filters, nil +} + +func (p *PluginsCommand) initInstallAWSIC(parent *kingpin.CmdClause) { + p.install.awsIC.cmd = parent.Command("awsic", "Install an AWS Identity Center integration.") + cmd := p.install.awsIC.cmd + cmd.Flag("default-owner", "List of Teleport users that are default owners for the imported access lists. Multiple flags allowed.").Required().StringsVar(&p.install.awsIC.defaultOwners) + cmd.Flag("url", "AWS Identity Center SCIM provisioning endpoint").Required().StringVar(&p.install.awsIC.scimURL) + cmd.Flag("token", "AWS Identify Center SCIM provisioning token.").Required().StringVar(&p.install.awsIC.scimToken) + cmd.Flag("region", "AWS Identity center instance region").Required().StringVar(&p.install.awsIC.region) + cmd.Flag("arn", "AWS Identify center instance ARN").Required().StringVar(&p.install.awsIC.arn) + cmd.Flag("use-system-credentials", "Uses system credentials instead of OIDC.").Default("true").BoolVar(&p.install.awsIC.useSystemCredentials) + cmd.Flag("group-filter", "Add AWS group to group import list by name. Can be a glob, or enclosed in ^$ to specify a regular expression. If no filters are supplied then all AWS groups will be imported."). + StringsVar(&p.install.awsIC.groupNameFilters) + cmd.Flag("account-name", "Add AWS Account to account import list by name. Can be a glob, or enclosed in ^$ to specify a regular expression. All AWS accounts will be imported if no items are added to account import list."). + StringsVar(&p.install.awsIC.accountNameFilters) + cmd.Flag("account-id", "Add AWS Account to account import list by ID. All AWS accounts will be imported if no items are added to account import list."). + StringsVar(&p.install.awsIC.accountIDFilters) +} + +// InstallAWSIC installs AWS Identity Center plugin. +func (p *PluginsCommand) InstallAWSIC(ctx context.Context, args installPluginArgs) error { + awsICArgs := p.install.awsIC + if err := awsICArgs.validate(); err != nil { + return trace.Wrap(err) + } + + groupFilters, err := awsICArgs.parseGroupFilters() + if err != nil { + return trace.Wrap(err) + } + + accountFilters, err := awsICArgs.parseAccountFilters() + if err != nil { + return trace.Wrap(err) + } + + req := &pluginspb.CreatePluginRequest{ + Plugin: &types.PluginV1{ + Metadata: types.Metadata{ + Name: common.OriginAWSIdentityCenter, + Labels: map[string]string{ + "teleport.dev/hosted-plugin": "true", + }, + }, + Spec: types.PluginSpecV1{ + Settings: &types.PluginSpecV1_AwsIc{ + AwsIc: &types.PluginAWSICSettings{ + IntegrationName: common.OriginAWSIdentityCenter, + Region: awsICArgs.region, + Arn: awsICArgs.arn, + ProvisioningSpec: &types.AWSICProvisioningSpec{ + BaseUrl: awsICArgs.scimURL, + }, + AccessListDefaultOwners: awsICArgs.defaultOwners, + CredentialsSource: types.AWSICCredentialsSource_AWSIC_CREDENTIALS_SOURCE_SYSTEM, + GroupSyncFilters: groupFilters, + AwsAccountsFilters: accountFilters, + }, + }, + }, + }, + StaticCredentials: &types.PluginStaticCredentialsV1{ + ResourceHeader: types.ResourceHeader{ + Metadata: types.Metadata{ + Labels: map[string]string{ + "aws-ic/scim-api-endpoint": awsICArgs.scimURL, + }, + Name: types.PluginTypeAWSIdentityCenter, + }, + }, + Spec: &types.PluginStaticCredentialsSpecV1{ + Credentials: &types.PluginStaticCredentialsSpecV1_APIToken{ + APIToken: awsICArgs.scimToken, + }, + }, + }, + } + + _, err = args.plugins.CreatePlugin(ctx, req) + if err != nil { + return trace.Wrap(err) + } + fmt.Println("Successfully created AWS Identity Center plugin.") + return nil +} diff --git a/tool/tctl/common/plugin/awsic_test.go b/tool/tctl/common/plugin/awsic_test.go new file mode 100644 index 0000000000000..c3f68e609b207 --- /dev/null +++ b/tool/tctl/common/plugin/awsic_test.go @@ -0,0 +1,134 @@ +// Teleport +// Copyright (C) 2025 Gravitational, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package plugin + +import ( + "math/rand/v2" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/gravitational/teleport/api/types" +) + +func TestAWSICGroupFilters(t *testing.T) { + t.Run("parse", func(t *testing.T) { + // GIVEN some arbitrary set of account name filter CLI args + cliArgs := awsICArgs{ + groupNameFilters: []string{"alpha", "bravo", "charlie"}, + } + + // WHEN I attempt to convert the command-line args into filters + actualFilters, err := cliArgs.parseGroupFilters() + + // EXPECT the operation to succeed + require.NoError(t, err) + + // EXPECT that the returned filters are an accurate representation of + // the command line args, in arbitrary order + expectedFilters := []*types.AWSICResourceFilter{ + {Include: &types.AWSICResourceFilter_NameRegex{NameRegex: "alpha"}}, + {Include: &types.AWSICResourceFilter_NameRegex{NameRegex: "bravo"}}, + {Include: &types.AWSICResourceFilter_NameRegex{NameRegex: "charlie"}}, + } + rand.Shuffle(len(expectedFilters), func(i, j int) { + expectedFilters[i], expectedFilters[j] = expectedFilters[j], expectedFilters[i] + }) + require.ElementsMatch(t, expectedFilters, actualFilters) + }) + + t.Run("empty lists are valid", func(*testing.T) { + // GIVEN a cli arg set that doesn't specify any group filters + cliArgs := awsICArgs{} + + // WHEN I attempt to convert the command-line args into filters + filters, err := cliArgs.parseGroupFilters() + + // EXPECT that the operation succeeds and returns an empty filter list + require.NoError(t, err) + require.Empty(t, filters) + }) + + t.Run("regex errors are detected", func(*testing.T) { + // GIVEN a name filter with a malformed regex + cliArgs := awsICArgs{ + groupNameFilters: []string{"alpha", "^[)$", "charlie"}, + } + + // WHEN I attempt to convert the command-line args into filters + _, err := cliArgs.parseGroupFilters() + + // EXPECT the operation to fail + require.Error(t, err) + }) +} + +func TestAWSICAccountFilters(t *testing.T) { + t.Run("parse", func(t *testing.T) { + // GIVEN some arbitrary combination of Name- and ID-based account filters + // CLI args.. + cliArgs := awsICArgs{ + accountNameFilters: []string{"alpha", "bravo", "charlie"}, + accountIDFilters: []string{"0123456789", "9876543210"}, + } + + // WHEN I attempt to convert the command-line args into filters + actualFilters, err := cliArgs.parseAccountFilters() + + // EXPECT the operation to succeed + require.NoError(t, err) + + // EXPECT that the returned filters are an accurate representation of + // the command line args, in arbitrary order + expectedFilters := []*types.AWSICResourceFilter{ + {Include: &types.AWSICResourceFilter_NameRegex{NameRegex: "alpha"}}, + {Include: &types.AWSICResourceFilter_NameRegex{NameRegex: "bravo"}}, + {Include: &types.AWSICResourceFilter_NameRegex{NameRegex: "charlie"}}, + {Include: &types.AWSICResourceFilter_Id{Id: "0123456789"}}, + {Include: &types.AWSICResourceFilter_Id{Id: "9876543210"}}, + } + rand.Shuffle(len(expectedFilters), func(i, j int) { + expectedFilters[i], expectedFilters[j] = expectedFilters[j], expectedFilters[i] + }) + require.ElementsMatch(t, expectedFilters, actualFilters) + }) + + t.Run("empty lists are valid", func(*testing.T) { + // GIVEN a cli arg set that doesn't specify any account filters + cliArgs := awsICArgs{} + + // WHEN I attempt to convert the command-line args into filters + filters, err := cliArgs.parseAccountFilters() + + // EXPECT that the operation succeeds and returns an empty filter list + require.NoError(t, err) + require.Empty(t, filters) + }) + + t.Run("regex errors are detected", func(*testing.T) { + // GIVEN a name filter with a malformed regex + cliArgs := awsICArgs{ + accountNameFilters: []string{"alpha", "^[)$", "charlie"}, + } + + // WHEN I attempt to convert the command-line args into filters + _, err := cliArgs.parseAccountFilters() + + // EXPECT the operation to fail + require.Error(t, err) + }) +} diff --git a/tool/tctl/common/plugin/plugins_command.go b/tool/tctl/common/plugin/plugins_command.go index b5edc45405d23..75bfc94672beb 100644 --- a/tool/tctl/common/plugin/plugins_command.go +++ b/tool/tctl/common/plugin/plugins_command.go @@ -57,6 +57,7 @@ type pluginInstallArgs struct { scim scimArgs entraID entraArgs netIQ netIQArgs + awsIC awsICArgs } type scimArgs struct { @@ -104,6 +105,7 @@ func (p *PluginsCommand) initInstall(parent *kingpin.CmdClause, config *servicec p.initInstallSCIM(p.install.cmd) p.initInstallEntra(p.install.cmd) p.initInstallNetIQ(p.install.cmd) + p.initInstallAWSIC(p.install.cmd) } func (p *PluginsCommand) initInstallSCIM(parent *kingpin.CmdClause) { @@ -330,6 +332,8 @@ func (p *PluginsCommand) TryRun(ctx context.Context, cmd string, clientFunc comm commandFunc = p.InstallEntra case p.install.netIQ.cmd.FullCommand(): commandFunc = p.InstallNetIQ + case p.install.awsIC.cmd.FullCommand(): + commandFunc = p.InstallAWSIC case p.delete.cmd.FullCommand(): commandFunc = p.Delete default: From 14161927184bcd0f8d4be4438cdf6e0e33e174ac Mon Sep 17 00:00:00 2001 From: Trent Clarke Date: Mon, 3 Feb 2025 14:24:28 +1100 Subject: [PATCH 2/6] Add user label filter --- tool/common/labels.go | 66 +++++++++++++++++++++ tool/common/labels_test.go | 85 +++++++++++++++++++++++++++ tool/tctl/common/plugin/awsic.go | 43 ++++++++++++-- tool/tctl/common/plugin/awsic_test.go | 51 ++++++++++++++++ 4 files changed, 241 insertions(+), 4 deletions(-) create mode 100644 tool/common/labels.go create mode 100644 tool/common/labels_test.go diff --git a/tool/common/labels.go b/tool/common/labels.go new file mode 100644 index 0000000000000..3c16196857d07 --- /dev/null +++ b/tool/common/labels.go @@ -0,0 +1,66 @@ +// Teleport +// Copyright (C) 2025 Gravitational, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package common + +import ( + "fmt" + "maps" + "strings" + + "github.com/gravitational/trace" + + "github.com/gravitational/teleport/lib/client" +) + +// Labels is a common type for parsing Teleport labels from commandline args +// using kingpin +type Labels map[string]string + +// IsCumulative tells kingpin it is safe to invoke [Labels.Set(string)] multiple times +func (l *Labels) IsCumulative() bool { + return true +} + +// Set implements [flag.Value] for a collection of Teleport labels +func (l *Labels) Set(value string) error { + items, err := client.ParseLabelSpec(value) + if err != nil { + return trace.Wrap(err) + } + if *l == nil { + *l = items + return nil + } + maps.Copy(*l, items) + return nil +} + +// String implements [fmt.Stringer] for a collection of Teleport labels +func (l *Labels) String() string { + if len(*l) == 0 { + return "" + } + + var buf strings.Builder + for k, v := range *l { + fmt.Fprintf(&buf, "%s=%s,", k, v) + } + + // trim trailing comma and return + result := buf.String() + return result[:len(result)-1] +} diff --git a/tool/common/labels_test.go b/tool/common/labels_test.go new file mode 100644 index 0000000000000..557ed50eada23 --- /dev/null +++ b/tool/common/labels_test.go @@ -0,0 +1,85 @@ +// Teleport +// Copyright (C) 2025 Gravitational, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package common + +import ( + "testing" + + "github.com/alecthomas/kingpin/v2" + "github.com/stretchr/testify/require" +) + +func TestLabelCLI(t *testing.T) { + testCases := []struct { + name string + labelArgs []string + expectedError require.ErrorAssertionFunc + expectedLabels Labels + }{ + { + name: "empty", + expectedError: require.NoError, + }, + { + name: "simple", + labelArgs: []string{"key=value"}, + expectedError: require.NoError, + expectedLabels: Labels{"key": "value"}, + }, + { + name: "malformed", + labelArgs: []string{"key="}, + expectedError: require.Error, + }, + { + name: "multiple inline", + labelArgs: []string{"a=alpha,b=beta"}, + expectedError: require.NoError, + expectedLabels: Labels{"a": "alpha", "b": "beta"}, + }, + { + name: "multiple args", + labelArgs: []string{"a=alpha,b=beta", "g=gamma,d=delta"}, + expectedError: require.NoError, + expectedLabels: Labels{"a": "alpha", "b": "beta", "g": "gamma", "d": "delta"}, + }, + { + name: "last definition wins", + labelArgs: []string{"a=alpha,b=beta", "a=apple,d=delta"}, + expectedError: require.NoError, + expectedLabels: Labels{"a": "apple", "b": "beta", "d": "delta"}, + }, + } + + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + var actualLabels Labels + app := kingpin.New(t.Name(), "test label parsing") + app.Flag("label", "???"). + SetValue(&actualLabels) + + var args []string + for _, arg := range test.labelArgs { + args = append(args, "--label", arg) + } + + _, err := app.Parse(args) + test.expectedError(t, err) + require.Equal(t, test.expectedLabels, actualLabels) + }) + } +} diff --git a/tool/tctl/common/plugin/awsic.go b/tool/tctl/common/plugin/awsic.go index 9b5170a99a3f4..58d224e3c049c 100644 --- a/tool/tctl/common/plugin/awsic.go +++ b/tool/tctl/common/plugin/awsic.go @@ -19,14 +19,16 @@ package plugin import ( "context" "fmt" + "maps" "github.com/alecthomas/kingpin/v2" "github.com/gravitational/trace" pluginspb "github.com/gravitational/teleport/api/gen/proto/go/teleport/plugins/v1" "github.com/gravitational/teleport/api/types" - "github.com/gravitational/teleport/api/types/common" + apicommon "github.com/gravitational/teleport/api/types/common" "github.com/gravitational/teleport/lib/utils" + toolcommon "github.com/gravitational/teleport/tool/common" ) type awsICArgs struct { @@ -37,6 +39,8 @@ type awsICArgs struct { region string arn string useSystemCredentials bool + userOrigin string + userLabels toolcommon.Labels groupNameFilters []string accountNameFilters []string accountIDFilters []string @@ -88,6 +92,22 @@ func (a *awsICArgs) parseAccountFilters() ([]*types.AWSICResourceFilter, error) return filters, nil } +func (a *awsICArgs) parseUserFilters() ([]*types.AWSICUserSyncFilter, error) { + var result []*types.AWSICUserSyncFilter + + labels := make(toolcommon.Labels) + maps.Copy(labels, a.userLabels) + if a.userOrigin != "" { + labels[types.OriginLabel] = a.userOrigin + } + + if len(labels) > 0 { + result = append(result, &types.AWSICUserSyncFilter{Labels: labels}) + } + + return result, nil +} + func (p *PluginsCommand) initInstallAWSIC(parent *kingpin.CmdClause) { p.install.awsIC.cmd = parent.Command("awsic", "Install an AWS Identity Center integration.") cmd := p.install.awsIC.cmd @@ -97,7 +117,15 @@ func (p *PluginsCommand) initInstallAWSIC(parent *kingpin.CmdClause) { cmd.Flag("region", "AWS Identity center instance region").Required().StringVar(&p.install.awsIC.region) cmd.Flag("arn", "AWS Identify center instance ARN").Required().StringVar(&p.install.awsIC.arn) cmd.Flag("use-system-credentials", "Uses system credentials instead of OIDC.").Default("true").BoolVar(&p.install.awsIC.useSystemCredentials) - cmd.Flag("group-filter", "Add AWS group to group import list by name. Can be a glob, or enclosed in ^$ to specify a regular expression. If no filters are supplied then all AWS groups will be imported."). + + cmd.Flag("user-origin", fmt.Sprintf(`Shorthand for "--user-label %s=ORIGIN"`, types.OriginLabel)). + PlaceHolder("ORIGIN"). + EnumVar(&p.install.awsIC.userOrigin, types.OriginValues...) + + cmd.Flag("user-label", "Add item to user label filter, in the form \"name=value\". If no labels are supplied, all Teleport users will be provisioned to Identity Center"). + SetValue(&p.install.awsIC.userLabels) + + cmd.Flag("group-name", "Add AWS group to group import list by name. Can be a glob, or enclosed in ^$ to specify a regular expression. If no filters are supplied then all AWS groups will be imported."). StringsVar(&p.install.awsIC.groupNameFilters) cmd.Flag("account-name", "Add AWS Account to account import list by name. Can be a glob, or enclosed in ^$ to specify a regular expression. All AWS accounts will be imported if no items are added to account import list."). StringsVar(&p.install.awsIC.accountNameFilters) @@ -112,6 +140,11 @@ func (p *PluginsCommand) InstallAWSIC(ctx context.Context, args installPluginArg return trace.Wrap(err) } + userFilters, err := awsICArgs.parseUserFilters() + if err != nil { + return trace.Wrap(err) + } + groupFilters, err := awsICArgs.parseGroupFilters() if err != nil { return trace.Wrap(err) @@ -125,7 +158,7 @@ func (p *PluginsCommand) InstallAWSIC(ctx context.Context, args installPluginArg req := &pluginspb.CreatePluginRequest{ Plugin: &types.PluginV1{ Metadata: types.Metadata{ - Name: common.OriginAWSIdentityCenter, + Name: apicommon.OriginAWSIdentityCenter, Labels: map[string]string{ "teleport.dev/hosted-plugin": "true", }, @@ -133,7 +166,7 @@ func (p *PluginsCommand) InstallAWSIC(ctx context.Context, args installPluginArg Spec: types.PluginSpecV1{ Settings: &types.PluginSpecV1_AwsIc{ AwsIc: &types.PluginAWSICSettings{ - IntegrationName: common.OriginAWSIdentityCenter, + IntegrationName: apicommon.OriginAWSIdentityCenter, Region: awsICArgs.region, Arn: awsICArgs.arn, ProvisioningSpec: &types.AWSICProvisioningSpec{ @@ -141,6 +174,7 @@ func (p *PluginsCommand) InstallAWSIC(ctx context.Context, args installPluginArg }, AccessListDefaultOwners: awsICArgs.defaultOwners, CredentialsSource: types.AWSICCredentialsSource_AWSIC_CREDENTIALS_SOURCE_SYSTEM, + UserSyncFilters: userFilters, GroupSyncFilters: groupFilters, AwsAccountsFilters: accountFilters, }, @@ -169,5 +203,6 @@ func (p *PluginsCommand) InstallAWSIC(ctx context.Context, args installPluginArg return trace.Wrap(err) } fmt.Println("Successfully created AWS Identity Center plugin.") + return nil } diff --git a/tool/tctl/common/plugin/awsic_test.go b/tool/tctl/common/plugin/awsic_test.go index c3f68e609b207..56c0b795e65ec 100644 --- a/tool/tctl/common/plugin/awsic_test.go +++ b/tool/tctl/common/plugin/awsic_test.go @@ -25,6 +25,57 @@ import ( "github.com/gravitational/teleport/api/types" ) +func TestAWSICUserFilters(t *testing.T) { + t.Run("parse", func(t *testing.T) { + // GIVEN some arbitrary set of account name filter CLI args + cliArgs := awsICArgs{ + userLabels: map[string]string{"a": "alpha", "b": "bravo", "c": "charlie"}, + } + + // WHEN I attempt to convert the command-line args into filters + actualFilters, err := cliArgs.parseUserFilters() + + // EXPECT the operation to succeed + require.NoError(t, err) + + // EXPECT that the returned filters are an accurate representation of + // the command line args, in arbitrary order + expectedFilters := []*types.AWSICUserSyncFilter{ + {Labels: map[string]string{"a": "alpha", "b": "bravo", "c": "charlie"}}, + } + require.ElementsMatch(t, expectedFilters, actualFilters) + }) + + t.Run("empty lists are valid", func(*testing.T) { + // GIVEN a cli arg set that doesn't specify any group filters + cliArgs := awsICArgs{} + + // WHEN I attempt to convert the command-line args into filters + filters, err := cliArgs.parseUserFilters() + + // EXPECT that the operation succeeds and returns an empty filter list + require.NoError(t, err) + require.Empty(t, filters) + }) + + t.Run("origin is applied", func(*testing.T) { + // GIVEN a name filter with a malformed regex + cliArgs := awsICArgs{ + userLabels: map[string]string{"a": "alpha", "b": "bravo", "c": "charlie"}, + userOrigin: types.OriginOkta, + } + + // WHEN I attempt to convert the command-line args into filters + actualFilters, err := cliArgs.parseUserFilters() + require.NoError(t, err) + + expectedFilters := []*types.AWSICUserSyncFilter{ + {Labels: map[string]string{"a": "alpha", "b": "bravo", "c": "charlie", types.OriginLabel: types.OriginOkta}}, + } + require.ElementsMatch(t, expectedFilters, actualFilters) + }) +} + func TestAWSICGroupFilters(t *testing.T) { t.Run("parse", func(t *testing.T) { // GIVEN some arbitrary set of account name filter CLI args From 59b979c3b0f3ef80dbb9fc3b30c53bffaae15958 Mon Sep 17 00:00:00 2001 From: Trent Clarke Date: Wed, 5 Feb 2025 17:06:15 +1100 Subject: [PATCH 3/6] Add validation per review feedback --- tool/tctl/common/plugin/awsic.go | 39 ++++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/tool/tctl/common/plugin/awsic.go b/tool/tctl/common/plugin/awsic.go index 58d224e3c049c..81ff0dc1cdbac 100644 --- a/tool/tctl/common/plugin/awsic.go +++ b/tool/tctl/common/plugin/awsic.go @@ -20,6 +20,7 @@ import ( "context" "fmt" "maps" + "net/url" "github.com/alecthomas/kingpin/v2" "github.com/gravitational/trace" @@ -28,6 +29,7 @@ import ( "github.com/gravitational/teleport/api/types" apicommon "github.com/gravitational/teleport/api/types/common" "github.com/gravitational/teleport/lib/utils" + awsutils "github.com/gravitational/teleport/lib/utils/aws" toolcommon "github.com/gravitational/teleport/tool/common" ) @@ -35,7 +37,7 @@ type awsICArgs struct { cmd *kingpin.CmdClause defaultOwners []string scimToken string - scimURL string + scimURL *url.URL region string arn string useSystemCredentials bool @@ -47,6 +49,10 @@ type awsICArgs struct { } func (a *awsICArgs) validate() error { + if !awsutils.IsKnownRegion(a.region) { + return trace.BadParameter("unknown AWS region: %s", a.region) + } + if !a.useSystemCredentials { return trace.BadParameter("only AWS Local system credentials are supported") } @@ -60,6 +66,8 @@ func (a *awsICArgs) validate() error { // We are using a manual validator here rather than the canonical one defined // in the AWS IC integration itself, because those filter tools are not // available to OSS builds of tctl. +// +// TODO(tcsc): Move the filter validation somewhere that the that the OSS build can access it. func parseAWSICNameFilters(names []string) ([]*types.AWSICResourceFilter, error) { var filters []*types.AWSICResourceFilter for _, n := range names { @@ -111,13 +119,24 @@ func (a *awsICArgs) parseUserFilters() ([]*types.AWSICUserSyncFilter, error) { func (p *PluginsCommand) initInstallAWSIC(parent *kingpin.CmdClause) { p.install.awsIC.cmd = parent.Command("awsic", "Install an AWS Identity Center integration.") cmd := p.install.awsIC.cmd - cmd.Flag("default-owner", "List of Teleport users that are default owners for the imported access lists. Multiple flags allowed.").Required().StringsVar(&p.install.awsIC.defaultOwners) - cmd.Flag("url", "AWS Identity Center SCIM provisioning endpoint").Required().StringVar(&p.install.awsIC.scimURL) - cmd.Flag("token", "AWS Identify Center SCIM provisioning token.").Required().StringVar(&p.install.awsIC.scimToken) - cmd.Flag("region", "AWS Identity center instance region").Required().StringVar(&p.install.awsIC.region) - cmd.Flag("arn", "AWS Identify center instance ARN").Required().StringVar(&p.install.awsIC.arn) - cmd.Flag("use-system-credentials", "Uses system credentials instead of OIDC.").Default("true").BoolVar(&p.install.awsIC.useSystemCredentials) - + cmd.Flag("default-owner", "Teleport user to set as default owners for the imported access lists. Multiple flags allowed."). + Required(). + StringsVar(&p.install.awsIC.defaultOwners) + cmd.Flag("scim-url", "AWS Identity Center SCIM provisioning endpoint"). + Required(). + URLVar(&p.install.awsIC.scimURL) + cmd.Flag("scim-token", "AWS Identify Center SCIM provisioning token."). + Required(). + StringVar(&p.install.awsIC.scimToken) + cmd.Flag("instance-region", "AWS Identity center instance region"). + Required(). + StringVar(&p.install.awsIC.region) + cmd.Flag("instance-arn", "AWS Identify center instance ARN"). + Required(). + StringVar(&p.install.awsIC.arn) + cmd.Flag("use-system-credentials", "Uses system credentials instead of OIDC."). + Default("true"). + BoolVar(&p.install.awsIC.useSystemCredentials) cmd.Flag("user-origin", fmt.Sprintf(`Shorthand for "--user-label %s=ORIGIN"`, types.OriginLabel)). PlaceHolder("ORIGIN"). EnumVar(&p.install.awsIC.userOrigin, types.OriginValues...) @@ -170,7 +189,7 @@ func (p *PluginsCommand) InstallAWSIC(ctx context.Context, args installPluginArg Region: awsICArgs.region, Arn: awsICArgs.arn, ProvisioningSpec: &types.AWSICProvisioningSpec{ - BaseUrl: awsICArgs.scimURL, + BaseUrl: awsICArgs.scimURL.String(), }, AccessListDefaultOwners: awsICArgs.defaultOwners, CredentialsSource: types.AWSICCredentialsSource_AWSIC_CREDENTIALS_SOURCE_SYSTEM, @@ -185,7 +204,7 @@ func (p *PluginsCommand) InstallAWSIC(ctx context.Context, args installPluginArg ResourceHeader: types.ResourceHeader{ Metadata: types.Metadata{ Labels: map[string]string{ - "aws-ic/scim-api-endpoint": awsICArgs.scimURL, + "aws-ic/scim-api-endpoint": awsICArgs.scimURL.String(), }, Name: types.PluginTypeAWSIdentityCenter, }, From ee586b81db31bd14b0bcdfe64e3c7cc2dda9bac6 Mon Sep 17 00:00:00 2001 From: Trent Clarke Date: Thu, 6 Feb 2025 16:22:06 +1100 Subject: [PATCH 4/6] validation --- tool/tctl/common/plugin/awsic.go | 8 ++++++-- tool/tctl/common/plugin/awsic_test.go | 10 +++++----- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/tool/tctl/common/plugin/awsic.go b/tool/tctl/common/plugin/awsic.go index 81ff0dc1cdbac..e76c70b1801d3 100644 --- a/tool/tctl/common/plugin/awsic.go +++ b/tool/tctl/common/plugin/awsic.go @@ -53,6 +53,10 @@ func (a *awsICArgs) validate() error { return trace.BadParameter("unknown AWS region: %s", a.region) } + if a.scimToken == "" { + return trace.BadParameter("SCIM token must not be empty") + } + if !a.useSystemCredentials { return trace.BadParameter("only AWS Local system credentials are supported") } @@ -117,9 +121,9 @@ func (a *awsICArgs) parseUserFilters() ([]*types.AWSICUserSyncFilter, error) { } func (p *PluginsCommand) initInstallAWSIC(parent *kingpin.CmdClause) { - p.install.awsIC.cmd = parent.Command("awsic", "Install an AWS Identity Center integration.") + p.install.awsIC.cmd = parent.Command("awsic", "Install an AWS IAM Identity Center integration.") cmd := p.install.awsIC.cmd - cmd.Flag("default-owner", "Teleport user to set as default owners for the imported access lists. Multiple flags allowed."). + cmd.Flag("access-list-default-owner", "Teleport user to set as default owners for the imported access lists. Multiple flags allowed."). Required(). StringsVar(&p.install.awsIC.defaultOwners) cmd.Flag("scim-url", "AWS Identity Center SCIM provisioning endpoint"). diff --git a/tool/tctl/common/plugin/awsic_test.go b/tool/tctl/common/plugin/awsic_test.go index 56c0b795e65ec..a4ac50f49b110 100644 --- a/tool/tctl/common/plugin/awsic_test.go +++ b/tool/tctl/common/plugin/awsic_test.go @@ -35,11 +35,9 @@ func TestAWSICUserFilters(t *testing.T) { // WHEN I attempt to convert the command-line args into filters actualFilters, err := cliArgs.parseUserFilters() - // EXPECT the operation to succeed + // EXPECT the operation succeeds AND that the returned filters are an + // accurate representation of the command line args, in arbitrary order require.NoError(t, err) - - // EXPECT that the returned filters are an accurate representation of - // the command line args, in arbitrary order expectedFilters := []*types.AWSICUserSyncFilter{ {Labels: map[string]string{"a": "alpha", "b": "bravo", "c": "charlie"}}, } @@ -67,8 +65,10 @@ func TestAWSICUserFilters(t *testing.T) { // WHEN I attempt to convert the command-line args into filters actualFilters, err := cliArgs.parseUserFilters() - require.NoError(t, err) + // EXPECT that the operation succeeds and that the returned filters are + // an accurate representation of the command line args. + require.NoError(t, err) expectedFilters := []*types.AWSICUserSyncFilter{ {Labels: map[string]string{"a": "alpha", "b": "bravo", "c": "charlie", types.OriginLabel: types.OriginOkta}}, } From bed216452b5be91254784602f9a9cbb0d1dca667 Mon Sep 17 00:00:00 2001 From: Trent Clarke Date: Fri, 7 Feb 2025 16:26:57 +1100 Subject: [PATCH 5/6] Validate SCIM emdpoint, mate user filter work like other filters (each option is a separate filter) --- tool/common/labels.go | 50 +++++++++----- tool/common/labels_test.go | 95 +++++++++++++++++++++++---- tool/tctl/common/plugin/awsic.go | 70 ++++++++++++++++---- tool/tctl/common/plugin/awsic_test.go | 23 +++++-- 4 files changed, 193 insertions(+), 45 deletions(-) diff --git a/tool/common/labels.go b/tool/common/labels.go index 3c16196857d07..2a9a8d85baa67 100644 --- a/tool/common/labels.go +++ b/tool/common/labels.go @@ -18,7 +18,6 @@ package common import ( "fmt" - "maps" "strings" "github.com/gravitational/trace" @@ -26,31 +25,23 @@ import ( "github.com/gravitational/teleport/lib/client" ) -// Labels is a common type for parsing Teleport labels from commandline args +// LabelFilter is a common type for parsing Teleport labels from commandline args // using kingpin -type Labels map[string]string - -// IsCumulative tells kingpin it is safe to invoke [Labels.Set(string)] multiple times -func (l *Labels) IsCumulative() bool { - return true -} +type LabelFilter map[string]string // Set implements [flag.Value] for a collection of Teleport labels -func (l *Labels) Set(value string) error { +func (l *LabelFilter) Set(value string) error { items, err := client.ParseLabelSpec(value) if err != nil { return trace.Wrap(err) } - if *l == nil { - *l = items - return nil - } - maps.Copy(*l, items) + + *l = items return nil } // String implements [fmt.Stringer] for a collection of Teleport labels -func (l *Labels) String() string { +func (l *LabelFilter) String() string { if len(*l) == 0 { return "" } @@ -64,3 +55,32 @@ func (l *Labels) String() string { result := buf.String() return result[:len(result)-1] } + +// LabelFilter is a common type for parsing multiple Teleport label filters from +// commandline args using kingpin +type LabelFilters []LabelFilter + +// Set implements [flag.Value] for a collection of Teleport label filters +func (l *LabelFilters) Set(value string) error { + items, err := client.ParseLabelSpec(value) + if err != nil { + return trace.Wrap(err) + } + (*l) = append((*l), items) + return nil +} + +// IsCumulative tells [kingpin] that multiple args can be collected into this +// one value +func (l *LabelFilters) IsCumulative() bool { + return true +} + +// String implements [fmt.Stringer] for a collection of Teleport label filters +func (l *LabelFilters) String() string { + if len(*l) == 0 { + return "[]" + } + + return fmt.Sprintf("%v", []LabelFilter(*l)) +} diff --git a/tool/common/labels_test.go b/tool/common/labels_test.go index 557ed50eada23..da194e29f70cd 100644 --- a/tool/common/labels_test.go +++ b/tool/common/labels_test.go @@ -23,22 +23,33 @@ import ( "github.com/stretchr/testify/require" ) -func TestLabelCLI(t *testing.T) { +func TestLabelFilterCLI(t *testing.T) { testCases := []struct { name string labelArgs []string expectedError require.ErrorAssertionFunc - expectedLabels Labels + expectedLabels LabelFilter }{ { - name: "empty", + name: "not specified", expectedError: require.NoError, }, + { + name: "empty spec is legal", + expectedError: require.NoError, + labelArgs: []string{""}, + expectedLabels: LabelFilter{}, + }, + { + name: "malformed spec is an error", + expectedError: require.Error, + labelArgs: []string{"potato"}, + }, { name: "simple", labelArgs: []string{"key=value"}, expectedError: require.NoError, - expectedLabels: Labels{"key": "value"}, + expectedLabels: LabelFilter{"key": "value"}, }, { name: "malformed", @@ -49,25 +60,87 @@ func TestLabelCLI(t *testing.T) { name: "multiple inline", labelArgs: []string{"a=alpha,b=beta"}, expectedError: require.NoError, - expectedLabels: Labels{"a": "alpha", "b": "beta"}, + expectedLabels: LabelFilter{"a": "alpha", "b": "beta"}, }, { - name: "multiple args", + name: "repeated options are an error", labelArgs: []string{"a=alpha,b=beta", "g=gamma,d=delta"}, - expectedError: require.NoError, - expectedLabels: Labels{"a": "alpha", "b": "beta", "g": "gamma", "d": "delta"}, + expectedError: require.Error, + expectedLabels: LabelFilter{"a": "alpha", "b": "beta"}, }, { name: "last definition wins", - labelArgs: []string{"a=alpha,b=beta", "a=apple,d=delta"}, + labelArgs: []string{"a=alpha,b=beta,a=aardvark,d=delta"}, expectedError: require.NoError, - expectedLabels: Labels{"a": "apple", "b": "beta", "d": "delta"}, + expectedLabels: LabelFilter{"a": "aardvark", "b": "beta", "d": "delta"}, + }, + } + + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + var actualLabels LabelFilter + app := kingpin.New(t.Name(), "test label parsing") + app.Flag("label", "???"). + SetValue(&actualLabels) + + var args []string + for _, arg := range test.labelArgs { + args = append(args, "--label", arg) + } + + _, err := app.Parse(args) + test.expectedError(t, err) + require.Equal(t, test.expectedLabels, actualLabels) + }) + } +} + +func TestLabelFiltersCLI(t *testing.T) { + testCases := []struct { + name string + labelArgs []string + expectedError require.ErrorAssertionFunc + expectedLabels LabelFilters + }{ + { + name: "empty", + expectedError: require.NoError, + }, + { + name: "single", + labelArgs: []string{"key=value"}, + expectedError: require.NoError, + expectedLabels: LabelFilters{{"key": "value"}}, + }, + { + name: "multiple", + labelArgs: []string{"a=alpha,b=beta", "g=gamma,d=delta"}, + expectedError: require.NoError, + expectedLabels: LabelFilters{ + {"a": "alpha", "b": "beta"}, + {"g": "gamma", "d": "delta"}, + }, + }, + { + name: "multiple filters are independent", + labelArgs: []string{"a=alpha,b=beta", "a=aardvark,g=gamma,d=delta"}, + expectedError: require.NoError, + expectedLabels: LabelFilters{ + {"a": "alpha", "b": "beta"}, + {"a": "aardvark", "g": "gamma", "d": "delta"}, + }, + }, + { + name: "malformed spec is an error", + labelArgs: []string{"a=alpha,b=beta", "potato", "g=gamma,d=delta"}, + expectedError: require.Error, + expectedLabels: LabelFilters{{"a": "alpha", "b": "beta"}}, }, } for _, test := range testCases { t.Run(test.name, func(t *testing.T) { - var actualLabels Labels + var actualLabels LabelFilters app := kingpin.New(t.Name(), "test label parsing") app.Flag("label", "???"). SetValue(&actualLabels) diff --git a/tool/tctl/common/plugin/awsic.go b/tool/tctl/common/plugin/awsic.go index e76c70b1801d3..af78f6f5078a0 100644 --- a/tool/tctl/common/plugin/awsic.go +++ b/tool/tctl/common/plugin/awsic.go @@ -19,15 +19,17 @@ package plugin import ( "context" "fmt" - "maps" + "log/slog" "net/url" "github.com/alecthomas/kingpin/v2" "github.com/gravitational/trace" + "google.golang.org/protobuf/encoding/protojson" pluginspb "github.com/gravitational/teleport/api/gen/proto/go/teleport/plugins/v1" "github.com/gravitational/teleport/api/types" apicommon "github.com/gravitational/teleport/api/types/common" + scimsdk "github.com/gravitational/teleport/e/lib/scim/sdk" "github.com/gravitational/teleport/lib/utils" awsutils "github.com/gravitational/teleport/lib/utils/aws" toolcommon "github.com/gravitational/teleport/tool/common" @@ -41,11 +43,12 @@ type awsICArgs struct { region string arn string useSystemCredentials bool - userOrigin string - userLabels toolcommon.Labels + userOrigins []string + userLabels toolcommon.LabelFilters groupNameFilters []string accountNameFilters []string accountIDFilters []string + validateSCIM bool } func (a *awsICArgs) validate() error { @@ -60,6 +63,30 @@ func (a *awsICArgs) validate() error { if !a.useSystemCredentials { return trace.BadParameter("only AWS Local system credentials are supported") } + + return nil +} + +func (a *awsICArgs) validateSCIMArgs(ctx context.Context) error { + if !a.validateSCIM { + return nil + } + + client, err := scimsdk.New(&scimsdk.Config{ + Endpoint: a.scimURL.String(), + Token: a.scimToken, + IntegrationType: types.PluginTypeAWSIdentityCenter, + }) + if err != nil { + return trace.Wrap(err) + } + + slog.DebugContext(ctx, "Validating SCIM endpoint and Token") + + if err := client.Ping(ctx); err != nil { + return trace.Wrap(err) + } + return nil } @@ -107,14 +134,20 @@ func (a *awsICArgs) parseAccountFilters() ([]*types.AWSICResourceFilter, error) func (a *awsICArgs) parseUserFilters() ([]*types.AWSICUserSyncFilter, error) { var result []*types.AWSICUserSyncFilter - labels := make(toolcommon.Labels) - maps.Copy(labels, a.userLabels) - if a.userOrigin != "" { - labels[types.OriginLabel] = a.userOrigin + if len(a.userOrigins) > 0 { + result = make([]*types.AWSICUserSyncFilter, 0, len(a.userOrigins)) + for _, origin := range a.userOrigins { + result = append(result, &types.AWSICUserSyncFilter{ + Labels: map[string]string{types.OriginLabel: origin}, + }) + } } - if len(labels) > 0 { - result = append(result, &types.AWSICUserSyncFilter{Labels: labels}) + if len(a.userLabels) > 0 { + result = append(make([]*types.AWSICUserSyncFilter, 0, len(result)+len(a.userLabels)), result...) + for _, labels := range a.userLabels { + result = append(result, &types.AWSICUserSyncFilter{Labels: labels}) + } } return result, nil @@ -123,7 +156,7 @@ func (a *awsICArgs) parseUserFilters() ([]*types.AWSICUserSyncFilter, error) { func (p *PluginsCommand) initInstallAWSIC(parent *kingpin.CmdClause) { p.install.awsIC.cmd = parent.Command("awsic", "Install an AWS IAM Identity Center integration.") cmd := p.install.awsIC.cmd - cmd.Flag("access-list-default-owner", "Teleport user to set as default owners for the imported access lists. Multiple flags allowed."). + cmd.Flag("access-list-default-owner", "Teleport user to set as default owner for the imported access lists. Multiple flags allowed."). Required(). StringsVar(&p.install.awsIC.defaultOwners) cmd.Flag("scim-url", "AWS Identity Center SCIM provisioning endpoint"). @@ -132,6 +165,9 @@ func (p *PluginsCommand) initInstallAWSIC(parent *kingpin.CmdClause) { cmd.Flag("scim-token", "AWS Identify Center SCIM provisioning token."). Required(). StringVar(&p.install.awsIC.scimToken) + cmd.Flag("scim-validate", "Check that the AWS Identity Center SCIM provisioning endpoint and token are valid."). + Default("true"). + BoolVar(&p.install.awsIC.validateSCIM) cmd.Flag("instance-region", "AWS Identity center instance region"). Required(). StringVar(&p.install.awsIC.region) @@ -141,11 +177,12 @@ func (p *PluginsCommand) initInstallAWSIC(parent *kingpin.CmdClause) { cmd.Flag("use-system-credentials", "Uses system credentials instead of OIDC."). Default("true"). BoolVar(&p.install.awsIC.useSystemCredentials) + cmd.Flag("user-origin", fmt.Sprintf(`Shorthand for "--user-label %s=ORIGIN"`, types.OriginLabel)). PlaceHolder("ORIGIN"). - EnumVar(&p.install.awsIC.userOrigin, types.OriginValues...) - - cmd.Flag("user-label", "Add item to user label filter, in the form \"name=value\". If no labels are supplied, all Teleport users will be provisioned to Identity Center"). + EnumsVar(&p.install.awsIC.userOrigins, types.OriginValues...) + cmd.Flag("user-label", "Add user label filter, in the form of a comma-separated list of \"name=value\" pairs. If no label filters are supplied, all Teleport users will be provisioned to Identity Center"). + PlaceHolder("LABELSPEC"). SetValue(&p.install.awsIC.userLabels) cmd.Flag("group-name", "Add AWS group to group import list by name. Can be a glob, or enclosed in ^$ to specify a regular expression. If no filters are supplied then all AWS groups will be imported."). @@ -178,6 +215,10 @@ func (p *PluginsCommand) InstallAWSIC(ctx context.Context, args installPluginArg return trace.Wrap(err) } + if err := awsICArgs.validateSCIMArgs(ctx); err != nil { + return trace.Wrap(err) + } + req := &pluginspb.CreatePluginRequest{ Plugin: &types.PluginV1{ Metadata: types.Metadata{ @@ -221,6 +262,9 @@ func (p *PluginsCommand) InstallAWSIC(ctx context.Context, args installPluginArg }, } + text, _ := protojson.MarshalOptions{Multiline: true, Indent: " "}.Marshal(req) + fmt.Println(string(text)) + _, err = args.plugins.CreatePlugin(ctx, req) if err != nil { return trace.Wrap(err) diff --git a/tool/tctl/common/plugin/awsic_test.go b/tool/tctl/common/plugin/awsic_test.go index a4ac50f49b110..fc60a22503431 100644 --- a/tool/tctl/common/plugin/awsic_test.go +++ b/tool/tctl/common/plugin/awsic_test.go @@ -23,13 +23,16 @@ import ( "github.com/stretchr/testify/require" "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/tool/common" ) func TestAWSICUserFilters(t *testing.T) { t.Run("parse", func(t *testing.T) { // GIVEN some arbitrary set of account name filter CLI args cliArgs := awsICArgs{ - userLabels: map[string]string{"a": "alpha", "b": "bravo", "c": "charlie"}, + userLabels: common.LabelFilters{ + {"a": "alpha", "b": "bravo", "c": "charlie"}, + }, } // WHEN I attempt to convert the command-line args into filters @@ -57,20 +60,28 @@ func TestAWSICUserFilters(t *testing.T) { }) t.Run("origin is applied", func(*testing.T) { - // GIVEN a name filter with a malformed regex + // GIVEN a CLI arg set that supplies user origin values cliArgs := awsICArgs{ - userLabels: map[string]string{"a": "alpha", "b": "bravo", "c": "charlie"}, - userOrigin: types.OriginOkta, + userLabels: common.LabelFilters{ + {"a": "alpha", "b": "bravo", "c": "charlie"}, + }, + userOrigins: []string{ + types.OriginEntraID, + types.OriginOkta, + }, } // WHEN I attempt to convert the command-line args into filters actualFilters, err := cliArgs.parseUserFilters() // EXPECT that the operation succeeds and that the returned filters are - // an accurate representation of the command line args. + // an accurate representation of the command line args. Returned filters + // are not expected to have any particular order. require.NoError(t, err) expectedFilters := []*types.AWSICUserSyncFilter{ - {Labels: map[string]string{"a": "alpha", "b": "bravo", "c": "charlie", types.OriginLabel: types.OriginOkta}}, + {Labels: map[string]string{"a": "alpha", "b": "bravo", "c": "charlie"}}, + {Labels: map[string]string{types.OriginLabel: types.OriginOkta}}, + {Labels: map[string]string{types.OriginLabel: types.OriginEntraID}}, } require.ElementsMatch(t, expectedFilters, actualFilters) }) From a6a0f52dd6057beb974eed86ce2a289ab3a89825 Mon Sep 17 00:00:00 2001 From: Trent Clarke Date: Sat, 8 Feb 2025 00:01:46 +1100 Subject: [PATCH 6/6] Rollback SCIM validation --- tool/tctl/common/plugin/awsic.go | 33 -------------------------------- 1 file changed, 33 deletions(-) diff --git a/tool/tctl/common/plugin/awsic.go b/tool/tctl/common/plugin/awsic.go index af78f6f5078a0..86ede2d30afec 100644 --- a/tool/tctl/common/plugin/awsic.go +++ b/tool/tctl/common/plugin/awsic.go @@ -19,7 +19,6 @@ package plugin import ( "context" "fmt" - "log/slog" "net/url" "github.com/alecthomas/kingpin/v2" @@ -29,7 +28,6 @@ import ( pluginspb "github.com/gravitational/teleport/api/gen/proto/go/teleport/plugins/v1" "github.com/gravitational/teleport/api/types" apicommon "github.com/gravitational/teleport/api/types/common" - scimsdk "github.com/gravitational/teleport/e/lib/scim/sdk" "github.com/gravitational/teleport/lib/utils" awsutils "github.com/gravitational/teleport/lib/utils/aws" toolcommon "github.com/gravitational/teleport/tool/common" @@ -48,7 +46,6 @@ type awsICArgs struct { groupNameFilters []string accountNameFilters []string accountIDFilters []string - validateSCIM bool } func (a *awsICArgs) validate() error { @@ -67,29 +64,6 @@ func (a *awsICArgs) validate() error { return nil } -func (a *awsICArgs) validateSCIMArgs(ctx context.Context) error { - if !a.validateSCIM { - return nil - } - - client, err := scimsdk.New(&scimsdk.Config{ - Endpoint: a.scimURL.String(), - Token: a.scimToken, - IntegrationType: types.PluginTypeAWSIdentityCenter, - }) - if err != nil { - return trace.Wrap(err) - } - - slog.DebugContext(ctx, "Validating SCIM endpoint and Token") - - if err := client.Ping(ctx); err != nil { - return trace.Wrap(err) - } - - return nil -} - // parseAWSICNameFilters validates that all elements of the supplied [names] slice // are valid regexes or globs and wraps them in [types.AWSICResourceFilter]s for // inclusion in a [types.PluginAWSICSettings]. @@ -165,9 +139,6 @@ func (p *PluginsCommand) initInstallAWSIC(parent *kingpin.CmdClause) { cmd.Flag("scim-token", "AWS Identify Center SCIM provisioning token."). Required(). StringVar(&p.install.awsIC.scimToken) - cmd.Flag("scim-validate", "Check that the AWS Identity Center SCIM provisioning endpoint and token are valid."). - Default("true"). - BoolVar(&p.install.awsIC.validateSCIM) cmd.Flag("instance-region", "AWS Identity center instance region"). Required(). StringVar(&p.install.awsIC.region) @@ -215,10 +186,6 @@ func (p *PluginsCommand) InstallAWSIC(ctx context.Context, args installPluginArg return trace.Wrap(err) } - if err := awsICArgs.validateSCIMArgs(ctx); err != nil { - return trace.Wrap(err) - } - req := &pluginspb.CreatePluginRequest{ Plugin: &types.PluginV1{ Metadata: types.Metadata{