Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ For syncing custom roles `--sync-custom-roles` must be provided. Its default val

Devices are an opt-in resource type: enable the **Device** resource type in the connector's sync configuration to include it. Device sync is read-only; the connector does not manage device-to-user assignments.

Secrets (Okta API tokens) sync only when `--sync-secrets` is provided. Its default value is `false`. They are read-only, and C1 displays them on the Inventory page.
Secrets (Okta API tokens) are an opt-in resource type: enable the **API Token** resource type in the connector's sync configuration to include them. `--sync-secrets` is deprecated but still honored. They are read-only, and C1 displays them on the Inventory page.

The connector also supports an event feed and targeted sync. The event feed streams a filtered set of Okta System Log events: user lifecycle and profile changes, application creation and update, group creation, group memberships added and removed, application assignment and admin-role grants, and SSO sign-ins. Targeted sync narrows a run to specific resource types or resource IDs via `--sync-resource-types` and `--sync-resources`.

Expand Down Expand Up @@ -231,7 +231,7 @@ Flags:
--sync-inactive-apps Whether to sync inactive apps or not ($BATON_SYNC_INACTIVE_APPS) (default true)
--sync-resource-types strings The resource type IDs to sync ($BATON_SYNC_RESOURCE_TYPES)
--sync-resources strings The resource IDs to sync ($BATON_SYNC_RESOURCES)
--sync-secrets Whether to sync secrets or not ($BATON_SYNC_SECRETS)
--sync-secrets Deprecated: enable the API Token resource type in the connector sync configuration instead. When true, API tokens are still synced. ($BATON_SYNC_SECRETS)
--task-concurrency int The number of Baton tasks to run concurrently in service mode. Tasks may include sync, grant, revoke, and more. Minimum value is 1, maximum value is 100. ($BATON_TASK_CONCURRENCY) (default 3)
--ticketing This must be set to enable ticketing support ($BATON_TICKETING)
-v, --version version for baton-okta
Expand Down
6 changes: 5 additions & 1 deletion baton_capabilities.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
"permission": "okta.apiTokens.read"
}
]
},
{
"@type": "type.googleapis.com/c1.connector.v2.OptInRequired"
}
]
},
Expand All @@ -36,7 +39,8 @@
"permission": "okta.apiTokens.read"
}
]
}
},
"optInRequired": true
},
{
"resourceType": {
Expand Down
2 changes: 1 addition & 1 deletion config_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@
{
"name": "sync-secrets",
"displayName": "Sync secrets",
"description": "Whether to sync secrets or not",
"description": "Deprecated: enable the API Token resource type in the connector sync configuration instead. When true, API tokens are still synced.",
"boolField": {}
},
{
Expand Down
4 changes: 2 additions & 2 deletions docs/connector.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,7 @@ Navigate to the **Okta API Scopes** tab and grant the scopes required for your u
- `okta.users.read` and `okta.groups.read` (required for sync)
- `okta.roles.read` and `okta.apps.read` (required for sync)
- `okta.users.manage`, `okta.groups.manage`, `okta.roles.manage`, `okta.apps.manage` (required for provisioning)
- `okta.apiTokens.read` (required when **Sync secrets** is enabled)
- `okta.apiTokens.read` (required when the **API Token** resource type is enabled)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: the Capabilities section (lines 15-29) still presents Secrets - API tokens as an unconditional sync row, and only Devices carries the **Devices is opt-in.** callout. Since API tokens are now opt-in too, add a matching note next to the table (and consider line 95, "This connector can sync secrets…") so the capabilities section isn't stale relative to this change.

- `okta.devices.read` (required when the **Device** resource type is enabled)

<Note>
Expand Down Expand Up @@ -442,7 +442,7 @@ Enter your credentials:
**Optional.** Inactive (disabled) Okta applications are synced by default. Clear the **Sync inactive apps** checkbox to exclude them.
</Step>
<Step>
**Optional.** Enable **Sync secrets** to display them on the [Inventory page](/product/admin/inventory).
**Optional.** Enable the **API Token** resource type in the connector's sync configuration to display Okta API tokens on the [Inventory page](/product/admin/inventory).
</Step>
<Step>
**Optional.** Enter a list of user email domains that will be included in the connector's sync. If you do not specify any domains here, the connector will sync all available accounts regardless of email domain.
Expand Down
4 changes: 2 additions & 2 deletions docs/docs-info.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Internal technical notes for maintainers. Customer-facing setup lives in [`docs/
- **Applications** — Okta apps; optional inactive apps via `--sync-inactive-apps`
- **Roles** — Standard admin roles
- **Custom roles / resource sets / bindings** — When `--sync-custom-roles` is enabled
- **Secrets (API tokens)** — When `--sync-secrets` is enabled
- **Secrets (API tokens)** — **Opt-in** resource type (`api-token`); `--sync-secrets` is deprecated but still honored
- **Devices** — Okta-managed devices; **opt-in** resource type, off unless explicitly selected (see [Devices](#devices))

2. Can the connector provision any resources? If so, which ones?
Expand Down Expand Up @@ -83,7 +83,7 @@ Consequences for setup docs:

The users case is the dangerous one: the sync exits 0 and writes a bundle containing zero users, so a missing admin role presents as a successful empty sync rather than an error. Assigning Super Administrator makes both return data.

**Requested scopes.** On this path the connector requests the four default read scopes plus all four `*.manage` provisioning scopes unconditionally, then adds `okta.apiTokens.read` when `--sync-secrets` is set and `okta.devices.read` when device sync is enabled. Per the note in `connector.go`, a scope the app has not been granted drops from the issued token and only surfaces as a 403 on first use, so a read-only app still authenticates.
**Requested scopes.** On this path the connector requests the four default read scopes plus all four `*.manage` provisioning scopes unconditionally, then adds `okta.apiTokens.read` when API token sync is enabled (`--sync-secrets` or an explicit `api-token` resource-type filter) and `okta.devices.read` when device sync is enabled. Per the note in `connector.go`, a scope the app has not been granted drops from the issued token and only surfaces as a 403 on first use, so a read-only app still authenticates.

**Console caveat for whoever writes customer docs.** During CXH-2092 / CXH-2124 validation, the Okta Admin Console's **Okta API Scopes** tab twice failed to persist scope grants with no error shown — the grants did not appear in `GET /api/v1/apps/{id}/grants`. `POST /api/v1/apps/{id}/grants` worked reliably. The customer-facing walkthrough directs readers to that tab, so it carries a note telling them to refresh and visually confirm the granted scopes.

Expand Down
2 changes: 1 addition & 1 deletion pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ var (
)
syncSecrets = field.BoolField("sync-secrets",
field.WithDisplayName("Sync secrets"),
field.WithDescription("Whether to sync secrets or not"),
field.WithDescription("Deprecated: enable the API Token resource type in the connector sync configuration instead. When true, API tokens are still synced."),
field.WithDefaultValue(false),
)
filterEmailDomains = field.StringSliceField("filter-email-domains",
Expand Down
20 changes: 14 additions & 6 deletions pkg/connector/api_token.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import (

type apiTokenResourceType struct {
resourceType *v2.ResourceType
clientV5 *oktav5.APIClient
connector *Okta
}

func (o *apiTokenResourceType) Entitlements(ctx context.Context, resource *v2.Resource, attrs resource.SyncOpAttrs) ([]*v2.Entitlement, *resource.SyncOpResults, error) {
Expand All @@ -37,6 +37,10 @@ func (o *apiTokenResourceType) List(
resourceID *v2.ResourceId,
attrs resource.SyncOpAttrs,
) ([]*v2.Resource, *resource.SyncOpResults, error) {
if !o.connector.shouldFetchApiTokens() {
return nil, &resource.SyncOpResults{}, nil
}

token := &attrs.PageToken
bag, prevSerializedResp, err := parsePageToken(token.Token, &v2.ResourceId{ResourceType: resourceTypeApiToken.Id})
if err != nil {
Expand All @@ -47,7 +51,7 @@ func (o *apiTokenResourceType) List(
var resp *oktav5.APIResponse

if prevSerializedResp == "" {
apiTokens, resp, err = o.clientV5.ApiTokenAPI.ListApiTokens(ctx).Execute()
apiTokens, resp, err = o.connector.clientV5.ApiTokenAPI.ListApiTokens(ctx).Execute()
if err != nil {
return nil, nil, fmt.Errorf("okta-connector-v5: failed to list api tokens: %w", err)
}
Comment on lines +54 to 57

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: Now that api-token is advertised to every install as an opt-in type, any customer can enable it from the C1 UI without having granted okta.apiTokens.read. Per the note in docs/docs-info.md, an ungranted scope silently drops from the issued token and only surfaces as a 403 here — which this hard error turns into a full sync failure rather than a missing resource type. Consider degrading gracefully on 403/404 (log at Warn, return an empty page) as R7 suggests.

Expand All @@ -57,7 +61,7 @@ func (o *apiTokenResourceType) List(
return nil, nil, fmt.Errorf("okta-connector-v5: failed to deserialize page token: %w", err)
}

localOktaAPIResponse := oktav5.NewAPIResponse(prevResp.Response, o.clientV5, nil)
localOktaAPIResponse := oktav5.NewAPIResponse(prevResp.Response, o.connector.clientV5, nil)
if localOktaAPIResponse.HasNextPage() {
resp, err = localOktaAPIResponse.Next(&apiTokens)
if err != nil {
Expand Down Expand Up @@ -93,20 +97,24 @@ func (o *apiTokenResourceType) List(
return ret, &resource.SyncOpResults{NextPageToken: nextPageToken, Annotations: annos}, nil
}

func apiTokenBuilder(clientV5 *oktav5.APIClient) *apiTokenResourceType {
func apiTokenBuilder(connector *Okta) *apiTokenResourceType {
return &apiTokenResourceType{
resourceType: resourceTypeApiToken,
clientV5: clientV5,
connector: connector,
}
}

func (o *apiTokenResourceType) Get(ctx context.Context, resourceId *v2.ResourceId, parentResourceId *v2.ResourceId) (*v2.Resource, annotations.Annotations, error) {
if !o.connector.shouldFetchApiTokens() {
return nil, nil, nil
}
Comment on lines +108 to +110

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: Returning (nil, nil, nil) from Get gives the caller a nil resource with a nil error, which is ambiguous — the api-token type advertises CAPABILITY_TARGETED_SYNC, so this path is reachable from a targeted sync/expand and the SDK has no way to distinguish "not opted in" from "found nothing." Prefer an explicit status.Error(codes.NotFound, "okta-connector-v5: api token sync is not enabled") (or PermissionDenied) so the failure is legible rather than a silent nil.


l := ctxzap.Extract(ctx)
l.Debug("getting api token", zap.String("api_token_id", resourceId.Resource))

var annos annotations.Annotations

apiToken, resp, err := o.clientV5.ApiTokenAPI.GetApiToken(ctx, resourceId.Resource).Execute()
apiToken, resp, err := o.connector.clientV5.ApiTokenAPI.GetApiToken(ctx, resourceId.Resource).Execute()
if err != nil {
return nil, nil, fmt.Errorf("okta-connector-v5: failed to get api token: %w", err)
}
Expand Down
65 changes: 65 additions & 0 deletions pkg/connector/api_token_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package connector

import (
"testing"

"github.com/conductorone/baton-sdk/pkg/cli"
)

func TestShouldFetchApiTokens(t *testing.T) {
t.Parallel()

tests := []struct {
name string
syncSecrets bool
opts *cli.ConnectorOpts
want bool
}{
{
name: "deprecated flag still fetches",
syncSecrets: true,
opts: &cli.ConnectorOpts{},
want: true,
},
{
name: "unfiltered sync does not fetch",
syncSecrets: false,
opts: &cli.ConnectorOpts{},
want: false,
},
{
name: "explicit opt-in fetches",
syncSecrets: false,
opts: &cli.ConnectorOpts{SyncResourceTypeIDs: []string{resourceTypeApiToken.Id}},
want: true,
},
{
name: "explicit filter without api-token does not fetch",
syncSecrets: false,
opts: &cli.ConnectorOpts{SyncResourceTypeIDs: []string{resourceTypeUser.Id}},
want: false,
},
{
name: "flag wins over filter that omits api-token",
syncSecrets: true,
opts: &cli.ConnectorOpts{SyncResourceTypeIDs: []string{resourceTypeUser.Id}},
want: true,
},
{
name: "nil opts (capabilities) reports fetch",
syncSecrets: false,
opts: nil,
want: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
o := &Okta{SyncSecrets: tt.syncSecrets, opts: tt.opts}
if got := o.shouldFetchApiTokens(); got != tt.want {
t.Fatalf("shouldFetchApiTokens() = %v, want %v", got, tt.want)
}
})
}
}
41 changes: 15 additions & 26 deletions pkg/connector/connector.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ var (
Id: "api-token",
DisplayName: "API Token",
Traits: []v2.ResourceType_Trait{v2.ResourceType_TRAIT_SECRET},
Annotations: v1AnnotationsForResourceType("api-token", true, capabilityPermissions("okta.apiTokens.read")),
Annotations: v1AnnotationsForResourceType("api-token", true, capabilityPermissions("okta.apiTokens.read"), &v2.OptInRequired{}),
Comment thread
ggreer marked this conversation as resolved.
}
resourceTypeDevice = &v2.ResourceType{
Id: "device",
Expand Down Expand Up @@ -152,6 +152,8 @@ func shouldSyncResourceType(opts *cli.ConnectorOpts, resourceTypeID string) bool
return opts.WillSyncResourceType(resourceTypeID)
}

var _ connectorbuilder.ConnectorBuilderV2 = (*Okta)(nil)

func (o *Okta) ResourceSyncers(ctx context.Context) []connectorbuilder.ResourceSyncerV2 {
resourceSyncer := []connectorbuilder.ResourceSyncerV2{
roleBuilder(o.client, o),
Expand All @@ -176,36 +178,23 @@ func (o *Okta) ResourceSyncers(ctx context.Context) []connectorbuilder.ResourceS
)
}

if o.SyncSecrets {
resourceSyncer = append(resourceSyncer, apiTokenBuilder(o.clientV5))
}
resourceSyncer = append(resourceSyncer, apiTokenBuilder(o))

return resourceSyncer
}

func (c *Okta) ListResourceTypes(ctx context.Context, request *v2.ResourceTypesServiceListResourceTypesRequest) (*v2.ResourceTypesServiceListResourceTypesResponse, error) {
resourceTypes := []*v2.ResourceType{
resourceTypeUser,
resourceTypeGroup,
resourceTypeRole,
resourceTypeApp,
}

if c.SyncCustomRoles {
resourceTypes = append(resourceTypes, resourceTypeCustomRole, resourceTypeResourceSets, resourceTypeResourceSetsBindings)
}

if c.SyncSecrets {
resourceTypes = append(resourceTypes, resourceTypeApiToken)
// shouldFetchApiTokens reports whether API token resources should be fetched from Okta.
// The api-token type is always advertised (OptInRequired). Fetching happens when the
// deprecated --sync-secrets flag is set, or when the sync filter explicitly includes
// api-token (C1 resource-type opt-in). A full unfiltered CLI sync does not fetch tokens.
func (o *Okta) shouldFetchApiTokens() bool {
if o.SyncSecrets {
return true
}

if shouldSyncResourceType(c.opts, resourceTypeDevice.Id) {
resourceTypes = append(resourceTypes, resourceTypeDevice)
if o.opts == nil {
return true
}

return &v2.ResourceTypesServiceListResourceTypesResponse{
List: resourceTypes,
}, nil
return o.opts.SyncFilterIsExplicit() && o.opts.WillSyncResourceType(resourceTypeApiToken.Id)
}
Comment on lines +190 to 198

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: This predicate and the scope decision in New() (line 442) encode the same policy but disagree on opts == nil: here nil opts returns true (fetch tokens), while line 442 requires opts != nil before adding okta.apiTokens.read. On the private-key auth path, a nil-opts sync with SyncSecrets=false would therefore call ListApiTokens without ever having requested the scope, and the 403 fails the whole sync. Consider extracting one shared helper (e.g. shouldFetchApiTokens(syncSecrets bool, opts *cli.ConnectorOpts) bool) and calling it from both places so the two can't drift.

Comment on lines +190 to 198

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: this gate reads ConnectorOpts.SyncResourceTypeIDs, which the SDK populates only from the sync-resource-types config value (pkg/config/config.go:28, pkg/cli/lambda_server__added.go:503). The C1 UI's resource-type selection is also delivered per sync task via SyncFull.SyncResourceTypeIds, and tasks/c1api/full_sync.go:243-248 calls that "the authoritative source when set", falling back to local config. If C1 ships an api-token opt-in only on the task, shouldFetchApiTokens() stays false and the opted-in customer gets an advertised api-token type with zero resources (and, on private-key auth, no okta.apiTokens.read scope) — silently, with no error. Worth confirming C1 also writes sync-resource-types into the connector config for opt-in types before relying on this alone. (Confidence: medium.)


func (c *Okta) Metadata(ctx context.Context) (*v2.ConnectorMetadata, error) {
Expand Down Expand Up @@ -450,7 +439,7 @@ func New(ctx context.Context, cc *cfg.Okta, opts *cli.ConnectorOpts) (connectorb
case cfg.PrivateKeyGroup:
scopes = append(scopes, provisioningScopes...)

if cc.SyncSecrets {
if cc.SyncSecrets || (opts != nil && opts.SyncFilterIsExplicit() && opts.WillSyncResourceType(resourceTypeApiToken.Id)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: this predicate duplicates shouldFetchApiTokens() minus its opts == nil branch, so the two can drift — with nil opts the connector would fetch tokens but this line would not request okta.apiTokens.read (a silent 403 on private-key auth). Not reachable today since New() is always called with non-nil opts, but extracting one shared helper (e.g. apiTokenSyncRequested(syncSecrets bool, opts *cli.ConnectorOpts) bool) used by both New() and shouldFetchApiTokens() would keep the fetch decision and the scope decision from diverging. The new unit test covers only the method, not this line.

scopes = append(scopes, "okta.apiTokens.read")
}

Expand Down
Loading