Skip to content

Make syncing secrets an opt in resource type. - #203

Open
ggreer wants to merge 1 commit into
mainfrom
ggreer/sync-secrets
Open

Make syncing secrets an opt in resource type.#203
ggreer wants to merge 1 commit into
mainfrom
ggreer/sync-secrets

Conversation

@ggreer

@ggreer ggreer commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

And deprecate the sync secrets config option.

This change is backwards compatible. Existing connectors with sync-secrets enabled will still sync API tokens.

This fixes a bug in incremental sync when sync secrets is disabled, but changes to api tokens still show up in the event feed.

…ets config option.

This change is backwards compatible. Existing connectors with sync-secrets enabled will still sync API tokens.

This fixes a bug in incremental sync when sync secrets is disabled, but changes to api tokens still show up in the event feed.
Comment thread pkg/connector/connector.go
Comment on lines +190 to 198
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)
}

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 +108 to +110
if !o.connector.shouldFetchApiTokens() {
return nil, nil, nil
}

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.

Comment on lines +54 to 57
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)
}

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.

@ggreer
ggreer force-pushed the ggreer/sync-secrets branch from 6370230 to 12e3b69 Compare August 26, 2026 22:18
Comment on lines +190 to 198
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)
}

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

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.

Comment thread docs/connector.mdx
- `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.

@github-actions

Copy link
Copy Markdown
Contributor

Connector PR Review: Make syncing secrets an opt in resource type.

Blocking Issues: 0 | Suggestions: 3 | Threads Resolved: 0
Criteria: Criteria status: loaded .claude/skills/ci-review.md from trusted base 12539c850f01.
Review mode: full
View review run

Review Summary

Scanned the full PR diff for security and correctness: the api-token syncer is now registered unconditionally and gated at List/Get by shouldFetchApiTokens(), the resource type carries the OptInRequired annotation, and --sync-secrets is kept as a deprecated escape hatch. Removing Okta.ListResourceTypes is safe: ConnectorBuilderV2 does not declare it, and the SDK method builder.ListResourceTypes (pkg/connectorbuilder/resource_syncer.go:90) already derives the list from ResourceSyncers(), so the removed method was dead code. Get returning a nil resource with a nil error when not opted in is mapped by the SDK to codes.NotFound rather than panicking, and no dependency manifests changed. No blocking issues found; three suggestions below, the main one about which channel actually carries the C1 resource-type opt-in.

Security Issues

None found.

Correctness Issues

None found.

Suggestions

  • pkg/connector/connector.go:190-198 — the opt-in gate reads ConnectorOpts.SyncResourceTypeIDs, which the SDK populates only from the sync-resource-types config value, while a C1 sync task can carry the selection in SyncFull.SyncResourceTypeIds (which tasks/c1api/full_sync.go treats as authoritative when set); if C1 sends the opt-in only on the task, api-token syncs return zero resources and okta.apiTokens.read is never requested. (Confidence: medium.)
  • pkg/connector/connector.go:442 — the scope predicate duplicates shouldFetchApiTokens() minus its nil-opts branch; extract one shared helper so the fetch decision and the scope decision cannot drift.
  • docs/connector.mdx:15-29 — the Capabilities table still lists Secrets - API tokens without the opt-in callout that Devices has, so that section is stale relative to this change.
Prompt for AI agents
Verify each finding against the current code and only fix it if needed.

Suggestions:

In pkg/connector/connector.go:
- Around line 190-198: shouldFetchApiTokens() decides whether to fetch API tokens from
  o.opts.SyncFilterIsExplicit() and o.opts.WillSyncResourceType("api-token"). The SDK
  populates ConnectorOpts.SyncResourceTypeIDs only from the sync-resource-types config
  value, but a C1 sync task can also carry the selection in SyncFull.SyncResourceTypeIds,
  which tasks/c1api/full_sync.go prefers over local config ("the UI is the authoritative
  source when set"). Confirm that C1 writes the resource-type opt-in into the connector
  config; if it only sends it on the task, an opted-in customer gets an advertised
  api-token resource type with zero resources and no okta.apiTokens.read scope, with no
  error surfaced. If the task channel is in play, the gate needs a signal that reflects
  the per-sync filter, or the guard should be dropped so the SDK resource-type filter
  alone decides.
- Around line 442: the okta.apiTokens.read scope condition
  (cc.SyncSecrets, or opts non-nil and SyncFilterIsExplicit and WillSyncResourceType)
  restates shouldFetchApiTokens() except for the nil-opts case, where the two disagree
  (fetch yes, scope no). Extract a single helper, for example
  func apiTokenSyncRequested(syncSecrets bool, opts *cli.ConnectorOpts) bool, and call it
  from both New() and shouldFetchApiTokens() so the fetch decision and the scope decision
  stay in sync. Consider extending pkg/connector/api_token_test.go to cover the scope path.

In docs/connector.mdx:
- Around line 24-29: the Capabilities table row "Secrets - API tokens" is presented as an
  unconditional sync capability, and only Devices carries the "Devices is opt-in." Note.
  Add an equivalent Note stating that the API Token (secrets) resource type is off by
  default and is enabled by selecting the API Token resource type in the connector sync
  configuration, and update the sentence at line 95 ("This connector can sync secrets and
  display them on the Inventory page") to mention the opt-in.

@github-actions github-actions Bot left a comment

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.

No blocking issues found.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant