diff --git a/api/types/constants.go b/api/types/constants.go index 90dbae176c11f..e74b00d4919dc 100644 --- a/api/types/constants.go +++ b/api/types/constants.go @@ -1252,6 +1252,13 @@ const ( // SCIM requests from the upstream organization. The content of the credential // is a bcrypt hash of actual token. OktaCredPurposeSCIMToken = "scim-bearer-token" + + // CredPurposeOKTAAPITokenWithSCIMOnlyIntegration is used when okta integration was enabled without + // app groups sync. Due to backward compatibility when teleport was downgraded to version where the + // AppGroupSyncDisabled flag is not supported we need to prevent plugin from starting. + // This is done by distinguishing between OktaCredPurposeAuth and CredPurposeOKTAAPITokenWithSCIMOnlyIntegration + // that are only set when AppGroupSyncDisabled is set to true. + CredPurposeOKTAAPITokenWithSCIMOnlyIntegration = "okta-auth-scim-only" ) const ( diff --git a/lib/authz/permissions.go b/lib/authz/permissions.go index 5ac55ed292ea7..7b12de5e09101 100644 --- a/lib/authz/permissions.go +++ b/lib/authz/permissions.go @@ -1173,6 +1173,7 @@ func definitionForBuiltinRole(clusterName string, recConfig types.SessionRecordi types.NewRule(types.KindClusterAuthPreference, services.RO()), types.NewRule(types.KindRole, services.RO()), types.NewRule(types.KindLock, services.RW()), + types.NewRule(types.KindSAML, services.ReadNoSecrets()), // Okta can manage access lists and roles it creates. { Resources: []string{types.KindRole}, diff --git a/tool/tctl/common/cmds.go b/tool/tctl/common/cmds.go index 9915fa591b745..30c5a21d76bec 100644 --- a/tool/tctl/common/cmds.go +++ b/tool/tctl/common/cmds.go @@ -20,6 +20,7 @@ package common import ( "github.com/gravitational/teleport/tool/tctl/common/loginrule" + "github.com/gravitational/teleport/tool/tctl/common/plugin" "github.com/gravitational/teleport/tool/tctl/sso/configure" "github.com/gravitational/teleport/tool/tctl/sso/tester" ) @@ -54,7 +55,7 @@ func Commands() []CLICommand { &ACLCommand{}, &loginrule.Command{}, &IdPCommand{}, - &PluginsCommand{}, + &plugin.PluginsCommand{}, } } diff --git a/tool/tctl/common/plugin/okta.go b/tool/tctl/common/plugin/okta.go new file mode 100644 index 0000000000000..ee9c967934da9 --- /dev/null +++ b/tool/tctl/common/plugin/okta.go @@ -0,0 +1,275 @@ +/* + * Teleport + * Copyright (C) 2024 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" + "net/url" + "strings" + + "github.com/alecthomas/kingpin/v2" + "github.com/gravitational/trace" + "golang.org/x/crypto/bcrypt" + + pluginsv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/plugins/v1" + "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/lib/utils" +) + +func (p *PluginsCommand) initInstallOkta(parent *kingpin.CmdClause) { + p.install.okta.cmd = parent.Command("okta", "Install an okta integration") + p.install.okta.cmd. + Flag("name", "Name of the plugin resource to create"). + Default("okta"). + StringVar(&p.install.name) + p.install.okta.cmd. + Flag("org", "URL of Okta organization"). + Required(). + URLVar(&p.install.okta.org) + p.install.okta.cmd. + Flag("api-token", "Okta API token for the plugin to use"). + StringVar(&p.install.okta.apiToken) + p.install.okta.cmd. + Flag("saml-connector", "SAML connector used for Okta SSO login."). + Required(). + StringVar(&p.install.okta.samlConnector) + p.install.okta.cmd. + Flag("app-id", "Okta ID of the APP used for SSO via SAML"). + StringVar(&p.install.okta.appID) + p.install.okta.cmd. + Flag("scim", "Enable SCIM OKTA integration"). + BoolVar(&p.install.okta.scimEnabled) + p.install.okta.cmd. + Flag("scim-token", "Okta SCIM auth token for the plugin to use"). + StringVar(&p.install.okta.scimToken) + p.install.okta.cmd. + Flag("users-sync", "Enable user synchronization"). + Default("true"). + BoolVar(&p.install.okta.userSync) + p.install.okta.cmd. + Flag("owner", "Add default owners for synced Access Lists"). + Short('o'). + StringsVar(&p.install.okta.defaultOwners) + p.install.okta.cmd. + Flag("accesslist-sync", "Enable group to Access List synchronization"). + Default("true"). + BoolVar(&p.install.okta.accessListSync) + p.install.okta.cmd. + Flag("appgroup-sync", "Enable Okta Applications and Groups sync"). + Default("true"). + BoolVar(&p.install.okta.appGroupSync) + p.install.okta.cmd. + Flag("group-filter", "Add a group filter. Supports globbing by default. Enclose in `^pattern$` for full regex support."). + Short('g'). + StringsVar(&p.install.okta.groupFilters) + p.install.okta.cmd. + Flag("app-filter", "Add an app filter. Supports globbing by default. Enclose in `^pattern$` for full regex support."). + Short('a'). + StringsVar(&p.install.okta.appFilters) +} + +type oktaArgs struct { + cmd *kingpin.CmdClause + org *url.URL + appID string + samlConnector string + apiToken string + scimEnabled bool + scimToken string + userSync bool + accessListSync bool + defaultOwners []string + appFilters []string + groupFilters []string + appGroupSync bool + + autoGeneratedSCIMToken bool +} + +func (s *oktaArgs) validateAndCheckDefaults(ctx context.Context, args *installPluginArgs) error { + if s.apiToken == "" { + if !s.scimEnabled { + return trace.BadParameter("API token is required") + } + if s.userSync { + return trace.BadParameter("User sync requires API token to be set") + } + if s.accessListSync { + return trace.BadParameter("AccessList sync requires API token to be set") + } + if s.appGroupSync { + return trace.BadParameter("AppGroup sync requires API token to be set") + } + } + if s.accessListSync { + if len(s.defaultOwners) == 0 { + return trace.BadParameter("AccessList sync requires at least one default owner to be set") + } + if !s.appGroupSync { + return trace.BadParameter("AppGroup sync is required for AccessList sync") + } + if !s.userSync { + return trace.BadParameter("User sync is required for AccessList sync") + } + } + if s.scimEnabled { + if s.scimToken == "" { + var err error + s.scimToken, err = utils.CryptoRandomHex(32) + if err != nil { + return trace.Wrap(err) + } + s.autoGeneratedSCIMToken = true + } + } + if s.scimToken != "" { + s.scimEnabled = true + } + connector, err := args.authClient.GetSAMLConnector(ctx, s.samlConnector, false) + if err != nil { + return trace.Wrap(err) + } + if s.appID == "" { + appID, ok := connector.GetMetadata().Labels[types.OktaAppIDLabel] + if ok { + s.appID = appID + } + } + if s.scimToken != "" && s.appID == "" && s.userSync { + msg := []string{ + "SCIM support requires App ID, which was not supplied and couldn't be deduced from the SAML connector", + "Specify the App ID explicitly with --app-id", + "SCIM support requires app-id to be set", + } + return trace.BadParameter(strings.Join(msg, "\n")) + } + return nil +} + +func (p *PluginsCommand) InstallOkta(ctx context.Context, args installPluginArgs) error { + oktaSettings := p.install.okta + if err := oktaSettings.validateAndCheckDefaults(ctx, &args); err != nil { + return trace.Wrap(err) + } + creds, err := generateCredentials(p.install.name, oktaSettings) + if err != nil { + return trace.Wrap(err) + } + settings := &types.PluginOktaSettings{ + OrgUrl: oktaSettings.org.String(), + SyncSettings: &types.PluginOktaSyncSettings{ + SsoConnectorId: oktaSettings.samlConnector, + AppId: oktaSettings.appID, + SyncUsers: oktaSettings.userSync, + SyncAccessLists: oktaSettings.accessListSync, + DefaultOwners: oktaSettings.defaultOwners, + GroupFilters: oktaSettings.groupFilters, + AppFilters: oktaSettings.appFilters, + }, + } + req := &pluginsv1.CreatePluginRequest{ + Plugin: &types.PluginV1{ + SubKind: types.PluginSubkindAccess, + Metadata: types.Metadata{ + Labels: map[string]string{ + types.HostedPluginLabel: "true", + }, + Name: p.install.name, + }, + Spec: types.PluginSpecV1{Settings: &types.PluginSpecV1_Okta{Okta: settings}}, + }, + StaticCredentialsList: creds, + CredentialLabels: map[string]string{ + types.OktaOrgURLLabel: oktaSettings.org.String(), + }, + } + + if _, err := args.plugins.CreatePlugin(ctx, req); err != nil { + return trace.Wrap(err) + } + + fmt.Printf("Successfully created OKTA plugin %q\n\n", p.install.name) + if oktaSettings.scimEnabled { + pingResp, err := args.authClient.Ping(ctx) + if err != nil { + return trace.Wrap(err, "failed fetching cluster info") + } + scimBaseURL := fmt.Sprintf("https://%s/v1/webapi/scim/%s", pingResp.GetProxyPublicAddr(), p.install.name) + fmt.Printf("SCIM Base URL: %s\n", scimBaseURL) + fmt.Printf("SCIM Identifier field for users: %s\n", "userName") + if oktaSettings.autoGeneratedSCIMToken { + fmt.Printf("SCIM Bearer Token: %s\n", oktaSettings.scimToken) + } + } + + fmt.Println("\nSee https://goteleport.com/docs/application-access/okta/hosted-guide for help configuring provisioning in Okta") + return nil +} + +func generateCredentials(pluginName string, oktaSettings oktaArgs) ([]*types.PluginStaticCredentialsV1, error) { + var creds []*types.PluginStaticCredentialsV1 + if oktaSettings.apiToken != "" { + label := types.OktaCredPurposeAuth + if !oktaSettings.appGroupSync { + label = types.CredPurposeOKTAAPITokenWithSCIMOnlyIntegration + } + + oktaAPICreds := &types.PluginStaticCredentialsV1{ + ResourceHeader: types.ResourceHeader{ + Metadata: types.Metadata{ + Name: pluginName, + Labels: map[string]string{ + types.OktaCredPurposeLabel: label, + }, + }, + }, + Spec: &types.PluginStaticCredentialsSpecV1{ + Credentials: &types.PluginStaticCredentialsSpecV1_APIToken{ + APIToken: oktaSettings.apiToken, + }, + }, + } + creds = append(creds, oktaAPICreds) + } + + if oktaSettings.scimToken != "" { + scimTokenHash, err := bcrypt.GenerateFromPassword([]byte(oktaSettings.scimToken), bcrypt.DefaultCost) + if err != nil { + return nil, trace.Wrap(err) + } + oktaSCIMCreds := &types.PluginStaticCredentialsV1{ + ResourceHeader: types.ResourceHeader{ + Metadata: types.Metadata{ + Name: pluginName + "-scim-token", + Labels: map[string]string{ + types.OktaCredPurposeLabel: types.OktaCredPurposeSCIMToken, + }, + }, + }, + Spec: &types.PluginStaticCredentialsSpecV1{ + Credentials: &types.PluginStaticCredentialsSpecV1_APIToken{ + APIToken: string(scimTokenHash), + }, + }, + } + creds = append(creds, oktaSCIMCreds) + } + return creds, nil +} diff --git a/tool/tctl/common/plugins_command.go b/tool/tctl/common/plugin/plugins_command.go similarity index 61% rename from tool/tctl/common/plugins_command.go rename to tool/tctl/common/plugin/plugins_command.go index 753a70809020d..56bebe1a655f9 100644 --- a/tool/tctl/common/plugins_command.go +++ b/tool/tctl/common/plugin/plugins_command.go @@ -16,13 +16,12 @@ * along with this program. If not, see . */ -package common +package plugin import ( "context" "fmt" "log/slog" - "net/url" "github.com/alecthomas/kingpin/v2" "github.com/gravitational/trace" @@ -31,6 +30,7 @@ import ( "google.golang.org/protobuf/types/known/emptypb" "github.com/gravitational/teleport" + "github.com/gravitational/teleport/api/client/proto" pluginsv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/plugins/v1" "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/lib/auth/authclient" @@ -55,20 +55,6 @@ type pluginInstallArgs struct { scim scimArgs } -type oktaArgs struct { - cmd *kingpin.CmdClause - org *url.URL - appID string - samlConnector string - apiToken string - scimToken string - userSync bool - accessListSync bool - defaultOwners []string - appFilters []string - groupFilters []string -} - type scimArgs struct { cmd *kingpin.CmdClause samlConnector string @@ -114,52 +100,6 @@ func (p *PluginsCommand) initInstall(parent *kingpin.CmdClause, config *servicec p.initInstallSCIM(p.install.cmd) } -func (p *PluginsCommand) initInstallOkta(parent *kingpin.CmdClause) { - p.install.okta.cmd = parent.Command("okta", "Install an okta integration") - p.install.okta.cmd. - Flag("name", "Name of the plugin resource to create"). - Default("okta"). - StringVar(&p.install.name) - p.install.okta.cmd. - Flag("org", "URL of Okta organization"). - Required(). - URLVar(&p.install.okta.org) - p.install.okta.cmd. - Flag("api-token", "Okta API token for the plugin to use"). - Required(). - StringVar(&p.install.okta.apiToken) - p.install.okta.cmd. - Flag("saml-connector", "SAML connector used for Okta SSO login."). - Required(). - StringVar(&p.install.okta.samlConnector) - p.install.okta.cmd. - Flag("app-id", "Okta ID of the APP used for SSO via SAML"). - StringVar(&p.install.okta.appID) - p.install.okta.cmd. - Flag("scim-token", "Okta SCIM auth token for the plugin to use"). - StringVar(&p.install.okta.scimToken) - p.install.okta.cmd. - Flag("sync-users", "Enable user synchronization"). - Default("true"). - BoolVar(&p.install.okta.userSync) - p.install.okta.cmd. - Flag("owner", "Add default owners for synced Access Lists"). - Short('o'). - StringsVar(&p.install.okta.defaultOwners) - p.install.okta.cmd. - Flag("sync-groups", "Enable group to Access List synchronization"). - Default("true"). - BoolVar(&p.install.okta.accessListSync) - p.install.okta.cmd. - Flag("group", "Add a group filter. Supports globbing by default. Enclose in `^pattern$` for full regex support."). - Short('g'). - StringsVar(&p.install.okta.groupFilters) - p.install.okta.cmd. - Flag("app", "Add an app filter. Supports globbing by default. Enclose in `^pattern$` for full regex support."). - Short('a'). - StringsVar(&p.install.okta.appFilters) -} - func (p *PluginsCommand) initInstallSCIM(parent *kingpin.CmdClause) { p.install.scim.cmd = p.install.cmd.Command("scim", "Install a new SCIM integration") p.install.scim.cmd. @@ -258,8 +198,9 @@ func (p *PluginsCommand) Cleanup(ctx context.Context, clusterAPI *authclient.Cli return nil } -type samlConnectorsClient interface { +type authClient interface { GetSAMLConnector(ctx context.Context, id string, withSecrets bool) (types.SAMLConnector, error) + Ping(ctx context.Context) (proto.PingResponse, error) } type pluginsClient interface { @@ -267,131 +208,8 @@ type pluginsClient interface { } type installPluginArgs struct { - samlConnectors samlConnectorsClient - plugins pluginsClient -} - -func (p *PluginsCommand) InstallOkta(ctx context.Context, args installPluginArgs) error { - log := p.config.Logger.With(logFieldPlugin, p.install.name) - oktaSettings := p.install.okta - - if oktaSettings.accessListSync { - if len(oktaSettings.defaultOwners) == 0 { - return trace.BadParameter("AccessList sync requires at least one default owner to be set") - } - } - - if oktaSettings.scimToken != "" { - if len(oktaSettings.defaultOwners) == 0 { - return trace.BadParameter("SCIM support requires at least one default owner to be set") - } - } - - log.DebugContext(ctx, "Validating SAML Connector...", - logFieldSAMLConnector, oktaSettings.samlConnector) - connector, err := args.samlConnectors.GetSAMLConnector(ctx, oktaSettings.samlConnector, false) - if err != nil { - log.ErrorContext(ctx, "Failed validating SAML connector", - slog.String(logFieldSAMLConnector, oktaSettings.samlConnector), - logErrorMessage(err)) - return trace.Wrap(err) - } - - if p.install.okta.appID == "" { - log.DebugContext(ctx, "Deducing Okta App ID from SAML Connector...", - logFieldSAMLConnector, oktaSettings.samlConnector) - appID, ok := connector.GetMetadata().Labels[types.OktaAppIDLabel] - if ok { - p.install.okta.appID = appID - } - } - - if oktaSettings.scimToken != "" && oktaSettings.appID == "" { - log.ErrorContext(ctx, "SCIM support requires App ID, which was not supplied and couldn't be deduced from the SAML connector") - log.ErrorContext(ctx, "Specify the App ID explicitly with --app-id") - return trace.BadParameter("SCIM support requires app-id to be set") - } - - creds := []*types.PluginStaticCredentialsV1{ - { - ResourceHeader: types.ResourceHeader{ - Metadata: types.Metadata{ - Name: p.install.name, - Labels: map[string]string{ - types.OktaCredPurposeLabel: types.OktaCredPurposeAuth, - }, - }, - }, - Spec: &types.PluginStaticCredentialsSpecV1{ - Credentials: &types.PluginStaticCredentialsSpecV1_APIToken{ - APIToken: oktaSettings.apiToken, - }, - }, - }, - } - - if oktaSettings.scimToken != "" { - scimTokenHash, err := bcrypt.GenerateFromPassword([]byte(oktaSettings.scimToken), bcrypt.DefaultCost) - if err != nil { - return trace.Wrap(err) - } - - creds = append(creds, &types.PluginStaticCredentialsV1{ - ResourceHeader: types.ResourceHeader{ - Metadata: types.Metadata{ - Name: p.install.name + "-scim-token", - Labels: map[string]string{ - types.OktaCredPurposeLabel: types.OktaCredPurposeSCIMToken, - }, - }, - }, - Spec: &types.PluginStaticCredentialsSpecV1{ - Credentials: &types.PluginStaticCredentialsSpecV1_APIToken{ - APIToken: string(scimTokenHash), - }, - }, - }) - } - - req := &pluginsv1.CreatePluginRequest{ - Plugin: &types.PluginV1{ - SubKind: types.PluginSubkindAccess, - Metadata: types.Metadata{ - Labels: map[string]string{ - types.HostedPluginLabel: "true", - }, - Name: p.install.name, - }, - Spec: types.PluginSpecV1{ - Settings: &types.PluginSpecV1_Okta{ - Okta: &types.PluginOktaSettings{ - OrgUrl: oktaSettings.org.String(), - SyncSettings: &types.PluginOktaSyncSettings{ - SsoConnectorId: oktaSettings.samlConnector, - AppId: oktaSettings.appID, - SyncUsers: oktaSettings.userSync, - SyncAccessLists: oktaSettings.accessListSync, - DefaultOwners: oktaSettings.defaultOwners, - GroupFilters: oktaSettings.groupFilters, - AppFilters: oktaSettings.appFilters, - }, - }, - }, - }, - }, - StaticCredentialsList: creds, - CredentialLabels: map[string]string{ - types.OktaOrgURLLabel: oktaSettings.org.String(), - }, - } - - if _, err := args.plugins.CreatePlugin(ctx, req); err != nil { - log.ErrorContext(ctx, "Plugin creation failed", logErrorMessage(err)) - return trace.Wrap(err) - } - - fmt.Println("See https://goteleport.com/docs/application-access/okta/hosted-guide for help configuring provisioning in Okta") - return nil + authClient authClient + plugins pluginsClient } // InstallSCIM implements `tctl plugins install scim`, installing a SCIM integration @@ -488,7 +306,7 @@ func (p *PluginsCommand) TryRun(ctx context.Context, cmd string, client *authcli case p.cleanupCmd.FullCommand(): err = p.Cleanup(ctx, client) case p.install.okta.cmd.FullCommand(): - args := installPluginArgs{samlConnectors: client, plugins: client.PluginsClient()} + args := installPluginArgs{authClient: client, plugins: client.PluginsClient()} err = p.InstallOkta(ctx, args) case p.install.scim.cmd.FullCommand(): err = p.InstallSCIM(ctx, client) diff --git a/tool/tctl/common/plugins_command_test.go b/tool/tctl/common/plugin/plugins_command_test.go similarity index 81% rename from tool/tctl/common/plugins_command_test.go rename to tool/tctl/common/plugin/plugins_command_test.go index e76a367e000d3..0c31736d97327 100644 --- a/tool/tctl/common/plugins_command_test.go +++ b/tool/tctl/common/plugin/plugins_command_test.go @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -package common +package plugin import ( "context" @@ -30,6 +30,7 @@ import ( "google.golang.org/grpc" "google.golang.org/protobuf/types/known/emptypb" + "github.com/gravitational/teleport/api/client/proto" pluginsv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/plugins/v1" "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/lib/service/servicecfg" @@ -40,6 +41,7 @@ func TestPluginsInstallOkta(t *testing.T) { name string cmd PluginsCommand expectSAMLConnectorQuery string + expectPing bool expectRequest *pluginsv1.CreatePluginRequest expectError require.ErrorAssertionFunc }{ @@ -78,6 +80,9 @@ func TestPluginsInstallOkta(t *testing.T) { samlConnector: "fake-saml-connector", scimToken: "i am a scim token", defaultOwners: []string{"admin"}, + scimEnabled: true, + userSync: true, + apiToken: "api-token-goes-here", }, }, }, @@ -93,6 +98,7 @@ func TestPluginsInstallOkta(t *testing.T) { org: mustParseURL("https://example.okta.com"), samlConnector: "okta-integration", apiToken: "api-token-goes-here", + appGroupSync: true, }, }, }, @@ -151,6 +157,7 @@ func TestPluginsInstallOkta(t *testing.T) { samlConnector: "saml-connector-name", userSync: true, accessListSync: true, + appGroupSync: true, defaultOwners: []string{"admin"}, groupFilters: []string{"group-alpha", "group-beta"}, appFilters: []string{"app-gamma", "app-delta", "app-epsilon"}, @@ -219,6 +226,7 @@ func TestPluginsInstallOkta(t *testing.T) { scimToken: "i am a scim token", userSync: true, accessListSync: true, + appGroupSync: true, defaultOwners: []string{"admin"}, groupFilters: []string{"group-alpha", "group-beta"}, appFilters: []string{"app-gamma", "app-delta", "app-epsilon"}, @@ -290,6 +298,65 @@ func TestPluginsInstallOkta(t *testing.T) { }, expectError: require.NoError, }, + { + name: "app group sync sync disabled should send okta-auth-scim-only creds", + cmd: PluginsCommand{ + install: pluginInstallArgs{ + name: "okta-barebones-test", + okta: oktaArgs{ + org: mustParseURL("https://example.okta.com"), + samlConnector: "okta-integration", + apiToken: "api-token-goes-here", + appGroupSync: false, + scimToken: "OktaCredPurposeSCIMToken", + }, + }, + }, + expectSAMLConnectorQuery: "okta-integration", + expectPing: true, + expectRequest: &pluginsv1.CreatePluginRequest{ + Plugin: &types.PluginV1{ + SubKind: types.PluginSubkindAccess, + Metadata: types.Metadata{ + Labels: map[string]string{ + types.HostedPluginLabel: "true", + }, + Name: "okta-barebones-test", + }, + Spec: types.PluginSpecV1{ + Settings: &types.PluginSpecV1_Okta{ + Okta: &types.PluginOktaSettings{ + OrgUrl: "https://example.okta.com", + SyncSettings: &types.PluginOktaSyncSettings{ + SsoConnectorId: "okta-integration", + }, + }, + }, + }, + }, + StaticCredentialsList: []*types.PluginStaticCredentialsV1{ + { + ResourceHeader: types.ResourceHeader{ + Metadata: types.Metadata{ + Name: "okta-barebones-test", + Labels: map[string]string{ + types.OktaCredPurposeLabel: types.CredPurposeOKTAAPITokenWithSCIMOnlyIntegration, + }, + }, + }, + Spec: &types.PluginStaticCredentialsSpecV1{ + Credentials: &types.PluginStaticCredentialsSpecV1_APIToken{ + APIToken: "api-token-goes-here", + }, + }, + }, + }, + CredentialLabels: map[string]string{ + types.OktaOrgURLLabel: "https://example.okta.com", + }, + }, + expectError: require.NoError, + }, } cmpOptions := []cmp.Option{ @@ -326,17 +393,24 @@ func TestPluginsInstallOkta(t *testing.T) { args.plugins = pluginsClient } + authClient := &mockAuthClient{} if testCase.expectSAMLConnectorQuery != "" { - samlConnectorsClient := &mockSAMLConnectorsClient{} - t.Cleanup(func() { samlConnectorsClient.AssertExpectations(t) }) + t.Cleanup(func() { authClient.AssertExpectations(t) }) - samlConnectorsClient. + authClient. On("GetSAMLConnector", anyContext, testCase.expectSAMLConnectorQuery, false). Return(&types.SAMLConnectorV2{}, nil) - args.samlConnectors = samlConnectorsClient + args.authClient = authClient } + if testCase.expectPing { + authClient. + On("Ping", anyContext). + Return(proto.PingResponse{ + ProxyPublicAddr: "example.com", + }, nil) + } ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) @@ -372,14 +446,19 @@ func (m *mockPluginsClient) CreatePlugin(ctx context.Context, in *pluginsv1.Crea return result.Get(0).(*emptypb.Empty), result.Error(1) } -type mockSAMLConnectorsClient struct { +type mockAuthClient struct { mock.Mock } -func (m *mockSAMLConnectorsClient) GetSAMLConnector(ctx context.Context, id string, withSecrets bool) (types.SAMLConnector, error) { +func (m *mockAuthClient) GetSAMLConnector(ctx context.Context, id string, withSecrets bool) (types.SAMLConnector, error) { result := m.Called(ctx, id, withSecrets) return result.Get(0).(types.SAMLConnector), result.Error(1) } +func (m *mockAuthClient) Ping(ctx context.Context) (proto.PingResponse, error) { + result := m.Called(ctx) + return result.Get(0).(proto.PingResponse), result.Error(1) +} + // anyContext is an argument matcher for testify mocks that matches any context. var anyContext any = mock.MatchedBy(func(context.Context) bool { return true })