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 })