Skip to content

feat(nan-5088): scopes remap — accept legacy and new scopes, expand existing keys#6088

Merged
pfreixes merged 18 commits into
masterfrom
pfreixes/nan-5088-scopes-remap
May 19, 2026
Merged

feat(nan-5088): scopes remap — accept legacy and new scopes, expand existing keys#6088
pfreixes merged 18 commits into
masterfrom
pfreixes/nan-5088-scopes-remap

Conversation

@pfreixes
Copy link
Copy Markdown
Contributor

@pfreixes pfreixes commented May 8, 2026

Summary

Add new fine-grained API key scopes alongside the legacy ones. Routes accept either via withAnyScope, so existing keys with legacy scopes keep working. A migration adds the new scopes alongside the legacy ones on existing customer keys (idempotent).

Legacy scopes are preserved on keys for rollback safety. Follow-up PRs will:

  • Remove route mappings for legacy scopes
  • Strip legacy scope strings from the scopes arrays via a final migration

Scope mapping

Legacy New
environment:integrations:write environment:integrations:create / update / delete
environment:connections:write environment:connections:create / update / delete
environment:syncs:manage environment:syncs:update / variant:create / variant:delete
environment:config:read (for /environment-variables) environment:variables:read
environment:config:read (for /scripts/config) environment:integrations:list_functions

What's included

  • New scope strings in @nangohq/types and @nangohq/utils
  • Route mappings in routes.public.ts accept legacy + new via withAnyScope
  • Frontend scope picker shows only the new scopes — legacy scopes are hidden from the picker and excluded from the count column
  • stripLegacyScopes runs in the dashboard save handler so any explicit save on a migrated key drops the legacy entries from the payload (closes the transient window where the API would still authorize via the legacy scope after the user thought they revoked it)
  • Migration 20260428110800_expand_legacy_api_key_scopes.cjs — thin wrapper around expandLegacyApiKeyScopes(knex) helper
  • 12 integration tests on the helper directly (no migration runner), break-verified
  • 38 unit tests on frontend scope-logic

Rollout plan

Three-step rollout designed so each step is independently rollback-safe:

  1. Backup the customer_keys table before deploying this PR. Used as a safety net for the unlikely case that a save during step 1 strips legacy scopes from a key that needs to be rolled back.
  2. Deploy this PR. Existing keys are expanded in-place (legacy + new scopes side by side). Any newly created key uses only the new scopes. Any save on a migrated key strips the legacy entries from the payload.
  3. Deploy follow-up PR that removes legacy scope acceptance from public route mappings. After this, only the new scopes authorize requests.
  4. Deploy final PR that strips legacy scope strings from the scopes arrays in the database (cleanup migration).

Rollback

Watch for an increase in 403 Forbidden responses on the affected endpoints after each step. If one shows up:

  • After step 2: rollback is safe — legacy scopes are still in the DB on migrated keys, and the previous version of the app accepts them.
  • After step 3: rollback is safe — legacy scopes are still in the DB and step 3's previous version still accepts them.
  • After step 4: rollback requires restoring from the backup taken in step 1, since the legacy scope strings are no longer in the DB.

Time the deploys with enough spacing (at least one business day) to give the 403 metric room to surface a regression before moving to the next step.

Test plan

  • Build passes
  • 12 helper tests pass (packages/database/lib/expandLegacyApiKeyScopes.integration.test.ts)
  • 38 frontend scope-logic unit tests pass
  • 49 server scope enforcement integration tests pass (legacy keys still work)
  • End-to-end manually verified locally: create key with legacy scopes on master → switch to PR branch → migration expands → save in dashboard strips legacy from DB

🤖 Generated with Claude Code

…xisting keys

Add new fine-grained scopes alongside the legacy ones. Routes accept
either via withAnyScope, so existing keys with legacy scopes keep
working. A migration adds the new scopes alongside the legacy ones
on existing customer keys (idempotent — no-op if both already present).
Legacy scopes are preserved on keys for rollback safety; a follow-up
PR will remove route mappings and a final migration will strip the
legacy strings from the scopes arrays.

New scopes:
- environment:variables:read (was: environment:config:read for /environment-variables)
- environment:integrations:list_functions (was: environment:config:read for /scripts/config)
- environment:integrations:create / update / delete (was: environment:integrations:write)
- environment:connections:create / update / delete (was: environment:connections:write)
- environment:syncs:update (was: environment:syncs:manage for update-connection-frequency)
- environment:syncs:variant:create / variant:delete (was: environment:syncs:manage)

Frontend scope picker shows new scopes; isScopeSelected expands legacy
scopes so existing keys still display correctly in the UI.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@linear
Copy link
Copy Markdown

linear Bot commented May 8, 2026

NAN-5088

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 8 files

Confidence score: 3/5

  • There is a medium-severity, high-confidence test-isolation issue in packages/database/lib/expandLegacyApiKeyScopes.integration.test.ts: destroying the shared db singleton can leak state across tests in the same worker.
  • Because this bypasses normal db.destroy() handling for the read-only pool, it increases regression risk through flaky or cascading test failures even if runtime code is unchanged.
  • Given the concrete impact and severity (6/10), this is some merge risk rather than a blocker; fixing the singleton teardown approach should make this safer.
  • Pay close attention to packages/database/lib/expandLegacyApiKeyScopes.integration.test.ts - avoid destroying shared DB state and preserve proper read-only pool cleanup.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/database/lib/expandLegacyApiKeyScopes.integration.test.ts">

<violation number="1" location="packages/database/lib/expandLegacyApiKeyScopes.integration.test.ts:47">
P2: Do not destroy the shared `db` singleton in this test file; it can break later tests in the same worker and bypasses `db.destroy()` cleanup for the read-only pool.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review, or fix all with cubic.

Comment thread packages/database/lib/expandLegacyApiKeyScopes.integration.test.ts Outdated
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
* Legacy scopes are kept on the keys for rollback safety; a follow-up
* migration will strip them.
*/
const LEGACY_SCOPE_EXPANSIONS = [
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

we will delete the "legacy" ones in another PR

pfreixes and others added 2 commits May 11, 2026 10:46
Match the pattern used in backfillConnectionTags.integration.test.ts:
let Postgres assign account ids, use UUID for unique names. Removes
the manual sequence counter and beforeEach cleanup.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
'environment:integrations:create',
'environment:integrations:update',
'environment:integrations:delete',
'environment:integrations:write', // legacy: covers create + update + delete
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

this and other legacy ones will be removed in a another PR

pfreixes and others added 3 commits May 11, 2026 10:55
Soft-deleted keys can be recovered. If we leave them with only the
legacy scopes, they would have stale scopes once the legacy ones
are removed in a follow-up PR.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Update the API keys reference and each endpoint's scope attribution
to use the new fine-grained scopes:

- environment:config:read (/environment-variables) -> environment:variables:read
- environment:config:read (/scripts/config)        -> environment:integrations:list_functions
- environment:integrations:write                   -> create / update / delete
- environment:connections:write                    -> create / update / delete
- environment:syncs:manage                         -> update / variant:create / variant:delete

Add Variables section, drop Config section. Remove legacy scope
entries from the listing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…copes-remap

# Conflicts:
#	docs/guides/auth/auth-guide.mdx
#	docs/reference/backend/http-api/api-keys.mdx
#	docs/reference/backend/http-api/connection/delete.mdx
#	docs/reference/backend/http-api/connection/post.mdx
#	docs/reference/backend/http-api/connection/set-metadata-legacy.mdx
#	docs/reference/backend/http-api/connection/set-metadata.mdx
#	docs/reference/backend/http-api/connection/update-metadata-legacy.mdx
#	docs/reference/backend/http-api/connection/update-metadata.mdx
#	docs/reference/backend/http-api/connections/delete.mdx
#	docs/reference/backend/http-api/connections/patch.mdx
#	docs/reference/backend/http-api/connections/post.mdx
#	docs/reference/backend/http-api/connections/set-metadata.mdx
#	docs/reference/backend/http-api/connections/update-metadata.mdx
#	docs/reference/backend/http-api/integration/create.mdx
#	docs/reference/backend/http-api/integration/delete.mdx
#	docs/reference/backend/http-api/integration/update.mdx
#	docs/reference/backend/http-api/scripts/config.mdx
#	docs/reference/backend/http-api/sync/create-variant.mdx
#	docs/reference/backend/http-api/sync/delete-variant.mdx
#	docs/reference/backend/http-api/sync/environment-variables.mdx
#	docs/reference/backend/http-api/sync/update-connection-frequency.mdx
@pfreixes pfreixes marked this pull request as ready for review May 11, 2026 09:29
@superagent-security superagent-security Bot added contributor:verified Contributor passed trust analysis. pr:verified PR passed security analysis. labels May 11, 2026
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ba5ca5dbe9

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

return selectedScopes.some((s) => s.endsWith(':*') && scope.startsWith(s.slice(0, -1)));
if (selectedScopes.some((s) => s.endsWith(':*') && scope.startsWith(s.slice(0, -1)))) return true;
// Legacy scope expansion — a legacy scope grants its mapped new scopes
return selectedScopes.some((s) => LEGACY_SCOPES.includes(s) && expandLegacyScope(s).includes(scope));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Remove legacy grants when unchecking remapped scopes

For keys that still contain a legacy scope (including migrated keys, since the migration intentionally preserves environment:integrations:write, environment:connections:write, etc.), this makes the new child scopes appear checked, but the toggle paths below only remove the concrete new scope from selectedScopes. For example, unchecking environment:integrations:create on a key that still has environment:integrations:write leaves the legacy grant in the saved payload, so the checkbox stays checked and the API continues to authorize create/update/delete via withAnyScope(..., 'environment:integrations:write'). The edit UI needs to drop or replace the corresponding legacy scope when a remapped permission is removed.

Useful? React with 👍 / 👎.

When a key carries a legacy scope (e.g. environment:integrations:write),
its remapped children (create/update/delete) appear checked in the UI
via isScopeSelected. Unchecking one of them used to only filter the
new scope from selectedScopes, leaving the legacy grant intact — so
the checkbox would stay checked and the API kept authorizing.

Mirror the existing wildcard handling: detect the legacy alias,
replace it with the expanded set minus the unchecked scope.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d1ceff399d

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +127 to +131
export function isScopeSelected(scope: string, selectedScopes: string[]): boolean {
if (selectedScopes.includes(scope)) return true;
return selectedScopes.some((s) => s.endsWith(':*') && scope.startsWith(s.slice(0, -1)));
if (selectedScopes.some((s) => s.endsWith(':*') && scope.startsWith(s.slice(0, -1)))) return true;
// Legacy scope expansion — a legacy scope grants its mapped new scopes
return selectedScopes.some((s) => LEGACY_SCOPES.includes(s) && expandLegacyScope(s).includes(scope));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Clear legacy aliases when toggling a whole group

When an edited key still contains a legacy alias such as environment:integrations:write, this legacy expansion makes the new child scopes appear selected, but toggleGroup only removes the wildcard/raw child scopes and never removes the matching legacy alias. In the migrated-key flow where a user checks a group and then unchecks it, the wildcard is dropped while the legacy alias remains, so create/update/delete permissions stay authorized and the boxes remain checked; group toggles need the same legacy-alias cleanup as individual toggles.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 2 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/webapp/src/pages/Environment/Settings/scope-logic.ts">

<violation number="1" location="packages/webapp/src/pages/Environment/Settings/scope-logic.ts:148">
P1: Legacy toggle rebuild keeps mapped scopes in `rest`, which can leave the deselected scope selected and introduce duplicates when keys contain both legacy and expanded scopes.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.
Fix all with cubic

Comment thread packages/webapp/src/pages/Environment/Settings/scope-logic.ts Outdated
…KeyScopes test

The test was inserting customer_keys with iv='' tag='' and secret='secret'.
encryptDatabaseIfNeeded() (run by encryption.manager test later in the
suite) iterates all customer_keys with empty iv/tag and tries to
re-encrypt them. With all rows sharing the same secret, the new hashed
value collides on the (hashed, key_type) unique constraint after the
first update.

Set iv/tag to non-empty placeholders so the encryption pass skips
these rows, and use a unique secret per row as defence-in-depth.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d80db9fff7

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

if (matchingLegacy) {
const expanded = expandLegacyScope(matchingLegacy);
const without = expanded.filter((s) => s !== scope && s !== credentialChild);
const rest = selectedScopes.filter((s) => s !== matchingLegacy);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Drop explicit migrated scopes when clearing legacy grants

For migrated keys, selectedScopes contains both the legacy alias and the expanded scopes added by the migration, e.g. environment:integrations:write plus environment:integrations:create/update/delete. This branch removes only matchingLegacy, so rest still contains the unchecked concrete scope and the returned array keeps it selected/saved; unchecking create (or variables:read from legacy config) therefore has no effect. Fresh evidence: the current code now removes the legacy alias, but this line still preserves the explicit scope added by the new migration.

Useful? React with 👍 / 👎.

pfreixes and others added 3 commits May 11, 2026 12:11
After the migration runs, every migrated key carries both the legacy
scope and the new fine-grained scopes literally in its scopes array.
The UI displays the new scopes as checked just from the literal scopes
— no expandLegacyScope needed.

Between this PR landing and the follow-up that strips legacy from
route mappings, unchecking a new scope on a migrated key leaves the
legacy grant in place, so the API keeps authorizing via withAnyScope.
That's a transient inconsistency that self-heals once the follow-up
ships. Not worth the complexity in the picker.

Remove LEGACY_SCOPES, expandLegacyScope, the isScopeSelected legacy
branch, the toggleScope legacy branch, and the associated tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When the user saves a change on a migrated key, drop legacy scopes
from the payload. Closes the transient window where the API still
authorizes via the legacy scope after the user thought they revoked.

Trade-off: a rollback after a save would lose the legacy entries,
making it harder to trace which keys were migrated. Accepted —
takes a backup before deploy and the number of impacted keys is low.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Migrated keys still carry their legacy scope alongside the expanded
new ones, which inflated the scopes count shown on the key list.
Strip legacy entries before counting so the column reflects what's
actually granted.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 3 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/webapp/src/pages/Environment/Settings/ApiKeys.tsx">

<violation number="1" location="packages/webapp/src/pages/Environment/Settings/ApiKeys.tsx:345">
P2: Validate the stripped scope list before assigning `updates.scopes`; otherwise a key with only legacy scopes can save as an empty scope set.

(Based on your team's feedback about explicit scope validation when remapping legacy scopes.) [FEEDBACK_USED]</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.
Fix all with cubic

Comment thread packages/webapp/src/pages/Environment/Settings/ApiKeys.tsx
Migrated keys keep their legacy scope hidden in editedScopes alongside
the expanded new ones. The raw length check let a user uncheck every
visible scope, save anyway, and end up with an empty scope array after
stripLegacyScopes ran on the payload.

Derive hasNoScopes from the stripped count so the inline warning, the
disabled Save button, and the toast all fire as soon as the visible
selection drops to zero.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 34d995d6cd

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

'environment:integrations:create',
'environment:integrations:update',
'environment:integrations:delete',
'environment:integrations:write', // legacy: covers create + update + delete
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Normalize legacy scopes on API key writes

Because the legacy values remain in apiKeyScopes, the public create/patch API still accepts them, and createApiKey/patchApiKey persist the parsed scopes array unchanged. Any client that creates or updates a key with environment:integrations:write (or the other legacy scopes) after this one-time migration has already run will store only the legacy grant; that key works during this dual-read release, but it will lose access when the planned follow-up removes legacy acceptance because it was never expanded to the new scopes. Please expand or reject legacy scopes in the API write path as well as in the migration.

Useful? React with 👍 / 👎.

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.

Gotta make sure the client (webapp) lands before the migration I guess? @pfreixes

@pfreixes pfreixes requested a review from a team May 11, 2026 15:12
Copy link
Copy Markdown
Collaborator

@TBonnin TBonnin left a comment

Choose a reason for hiding this comment

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

one comment. lgtm otherwise

@@ -338,7 +344,9 @@ const KeyConfig: React.FC<KeyConfigProps> = ({ apiKey, env, onBack, canReadSecre
try {
const updates: { keyId: number; scopes?: string[]; display_name?: string } = { keyId: apiKey.id };
if (scopesChanged) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

what if changing only the name? should it also trigger stripLegacyScopes?

The default API key created on signup has full access and is the simplest option.

If the user creates a scoped API key instead, this guide needs `environment:integrations:write` (step 2) and `environment:connect_sessions:write` (steps 5 and 8). See [API keys](/reference/backend/http-api/api-keys).
If the user creates a scoped API key instead, this guide needs `environment:integrations:create` (step 2) and `environment:connect_sessions:write` (steps 5 and 8). See [API keys](/reference/backend/http-api/api-keys).
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.

Should environment:connect_sessions:write become environment:connect_sessions:create in this migration as well?

'environment:integrations:create',
'environment:integrations:update',
'environment:integrations:delete',
'environment:integrations:write', // legacy: covers create + update + delete
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.

Gotta make sure the client (webapp) lands before the migration I guess? @pfreixes

@superagent-security superagent-security Bot removed the pr:verified PR passed security analysis. label May 14, 2026
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5179fa8b12

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

// Connections
publicAPI.use('/connections', jsonContentTypeMiddleware);
publicAPI.route('/connections').post(apiAuth, withScope('environment:connections:write'), postPublicConnection);
publicAPI.route('/connections').post(apiAuth, withAnyScope('environment:connections:create', 'environment:connections:write'), postPublicConnection);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Require update scope when POST upserts connections

When callers pass an existing connection_id, postPublicConnection goes through the import/upsert paths and explicitly handles updatedConnection.operation === 'override', so this endpoint can replace an existing connection's credentials/metadata. With only environment:connections:create required here, a create-only API key can update any connection ID it knows; the route needs to require environment:connections:update for the override case or keep this endpoint under a scope that covers both create and update.

Useful? React with 👍 / 👎.

.delete(apiAuth, withAnyScope('environment:integrations:delete', 'environment:integrations:write'), deletePublicIntegration);
publicAPI
.route('/integrations/:uniqueKey/functions/:name/code')
.get(apiAuth, withAnyScope('environment:integrations:read', 'environment:integrations:read_credentials'), getFunctionCode);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

why does the endpoint requires read_credentials permission?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

this has to be moved to a different scope, from what I discussed with @kaposke this is not yet public, and we should make - will you @kaposke ? - this change before we go public and users start using this scopes for reading function code, we agreed on creating a read function code scope.

Comment thread managed-manifest.json
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

looks like some formatting that doesn't belong into this PR

@superagent-security superagent-security Bot added the pr:verified PR passed security analysis. label May 18, 2026
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 56a88cdf61

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

.route('/connection')
.post(
apiAuth,
withAnyScope('environment:connections:create', 'environment:connections:write'),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Require update scope when legacy POST overrides connections

For callers that pass an existing connection_id to the deprecated POST /connection endpoint, createConnection still goes through the same import/upsert branches and treats updatedConnection.operation === 'override' as a successful credential/metadata replacement. With this route now allowing only environment:connections:create, a create-only key can update any existing connection ID it knows via the legacy endpoint; this endpoint needs to require environment:connections:update for the override case or remain under a scope that covers both create and update.

Useful? React with 👍 / 👎.

The original 20260428110800 timestamp now sits behind migrations that
have already been applied on master (e.g. 20260429212200_fix_disabled_syncs).
Rename the file so it sorts after the latest applied migration, otherwise
knex's "all applied migrations come before any pending ones" check trips
when this PR deploys.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…copes-remap

# Conflicts:
#	managed-manifest.json
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c5febcd3df

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

// until the user saves. Count only the scopes that would actually be persisted, so unchecking
// every visible scope flips the "Select at least one scope" warning even if a legacy entry
// is still lurking in the array.
const hasNoScopes = stripLegacyScopes(editedScopes).length === 0;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Allow renaming legacy-only scoped keys

When an API client creates or updates a key with one of the still-accepted legacy scopes after this release, the edit page can load with only hidden legacy scopes. In that case stripLegacyScopes(editedScopes).length is 0, so handleSave returns before sending even a display-name-only update; users cannot rename the key unless they also pick new visible scopes. The no-scope guard should only block scope updates that would persist an empty scope set, not name-only saves.

Useful? React with 👍 / 👎.

@pfreixes pfreixes added this pull request to the merge queue May 19, 2026
Merged via the queue into master with commit 1100b1e May 19, 2026
29 checks passed
@pfreixes pfreixes deleted the pfreixes/nan-5088-scopes-remap branch May 19, 2026 09:33
pfreixes added a commit that referenced this pull request May 19, 2026
- Remove duplicate 20260428110800_expand_legacy_api_key_scopes.cjs.
  PR #6088 renamed this file to 20260518120000_… before merge; our
  branch still carried the old filename alongside the new one. Both
  pointed at the same blob, so deleting the old name is a no-op for
  history.
- Restore master's .husky/prepare-commit-msg. A prior merge dropped
  the "skip during rebase/cherry-pick" guard that master added later.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
TBonnin pushed a commit to ettanbazil1/nango that referenced this pull request May 19, 2026
…Q#6099)

## Summary

Step 3 of the rollout described in NangoHQ#6088. Removes every
`withAnyScope('new', 'legacy')` mapping in `routes.public.ts` and
replaces it with `withScope('new')`. After this, only the new
fine-grained scopes authorize requests.

| Legacy scope (no longer accepted) | New scopes that replace it |
|---|---|
| `environment:integrations:write` | `environment:integrations:create` /
`update` / `delete` |
| `environment:connections:write` | `environment:connections:create` /
`update` / `delete` |
| `environment:syncs:manage` | `environment:syncs:update` /
`variant:create` / `variant:delete` |
| `environment:config:read` (`/environment-variables`) |
`environment:variables:read` |
| `environment:config:read` (`/scripts/config`) |
`environment:integrations:list_functions` |

## What's NOT changed

- The legacy scope strings remain valid in the `ApiKeyScope` union
(`packages/types`, `packages/utils`) so existing rows in `customer_keys`
still validate.
- The legacy scope strings are still present in `customer_keys.scopes`
arrays for any key that was created or migrated before this PR is
deployed.
- The legacy scope strings will be removed from the database by a
follow-up cleanup migration (step 4 of the rollout).

## Stack / merge order

This PR is stacked on top of NangoHQ#6088 (`pfreixes/nan-5088-scopes-remap`).
It must be merged **after** NangoHQ#6088 is deployed and the expansion
migration has run, otherwise keys that rely on a legacy scope today
would lose access. See the rollout plan in NangoHQ#6088 for the full sequence.

Once NangoHQ#6088 is merged into master, the base of this PR can be retargeted
to master via the GitHub UI.

## Rollback

If 403s spike after deploy, roll back to the previous server image.
Legacy scopes are still in the DB on migrated keys, so the previous
version's `withAnyScope` matches them again — no DB rollback needed.

## Test plan
- [x] `routes.public.ts` mappings replaced (20 occurrences across
integrations / connections / syncs / config endpoints)
- [x] `scopeEnforcement.integration.test.ts` updated to assert
allow/deny against the new scopes
- [ ] CI integration tests pass

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
pull Bot pushed a commit to nagyist/nango that referenced this pull request May 21, 2026
…HQ#6179)

## Summary

Step 4 (final) of the legacy → fine-grained scope rollout described in
NangoHQ#6088. With NangoHQ#6099 in prod, no route accepts the legacy scopes any more,
so the strings sitting in \`customer_keys\` are inert. This PR removes
them from the DB and rips out every dead code path that existed to deal
with them.

## DB

- \`20260519120000_strip_legacy_api_key_scopes.cjs\` — single UPDATE
that removes the 5 legacy strings from every \`customer_keys.scopes\`
array.
- Idempotent: \`&&\` overlap filter in the WHERE only touches rows that
still carry at least one legacy.
- Per the EXPLAIN runs on prod for the expansion migration, this is
~10ms work on ~72K rows.
- \`stripLegacyApiKeyScopes\` helper + 10 integration tests covering
each legacy scope, idempotency, soft-deleted keys, empty arrays, and
unrelated-scope preservation.

## Code

- **Types / utils**: removed the 5 legacy entries from the
\`ApiKeyScope\` union in \`packages/types\` and \`packages/utils\`.
- **Webapp scope picker**: deleted \`LEGACY_SCOPES\` and
\`stripLegacyScopes\` from \`scope-logic.ts\`. Removed the 3 call sites
in \`ApiKeys.tsx\` (handleSave, countSelectedScopes, hasNoScopes).
\`hasNoScopes\` reverts to the plain \`editedScopes.length === 0\`
check.
- **Tests**: dropped the 3 \`stripLegacyScopes\` unit tests; updated 2
\`scope.middleware.unit.test.ts\` assertions that were probing the
wildcard match using legacy strings (the union no longer accepts them).

## Kept

- \`expandLegacyApiKeyScopes\` helper + its migration + integration
test. They reference legacy strings as plain literals in \`.cjs\` (no
type), so they keep working after the union shrinks, and they document
what happened on the way through.

## Stack / merge order

This PR is stacked on top of NangoHQ#6099
(\`pfreixes/nan-5088-remove-legacy-scopes\`). It must merge after NangoHQ#6099
is deployed and has been live long enough to be confident no rollback is
needed (~3–5 days). If a rollback to before NangoHQ#6099 is ever needed after
this PR lands, you'd need to restore \`customer_keys\` from the backup
taken at step 1 of the rollout — the legacy scope strings will no longer
exist on any key.

Once NangoHQ#6099 lands on master, retarget the base of this PR to master via
the GitHub UI.

## Test plan
- [x] 10 helper integration tests pass
(\`stripLegacyApiKeyScopes.integration.test.ts\`)
- [x] 35 webapp scope-logic unit tests pass (down from 38 after dropping
the 3 strip-legacy tests)
- [x] 10 server scope.middleware unit tests pass (updated 2 assertions)
- [x] \`npm run ts-build\` clean across the workspace
- [ ] CI scope enforcement integration tests pass

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

contributor:verified Contributor passed trust analysis. pr:verified PR passed security analysis.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants