Skip to content

fix(api): harden API key management auth#2403

Open
riderx wants to merge 44 commits into
mainfrom
codex/rbac-apikey-management-hardening
Open

fix(api): harden API key management auth#2403
riderx wants to merge 44 commits into
mainfrom
codex/rbac-apikey-management-hardening

Conversation

@riderx

@riderx riderx commented Jun 3, 2026

Copy link
Copy Markdown
Member

Summary (AI generated)

  • Removed stale route-level key-mode authorization such as middlewareV2(['all', 'write']); handlers now authenticate first and enforce authorization through RBAC checks, API route guards, and RLS.
  • Removed app-side use_new_rbac runtime gating while keeping the compatibility output field and old SQL compatibility helpers.
  • Hardened /apikey management: broad non-limited all API keys retain sibling key update/delete compatibility, while limited scoped keys are filtered out and all API keys are blocked from key creation, self-mutation, and role-binding replacement.
  • Hardened org settings writes: direct anonymous/API-key RLS updates to orgs are closed, use_new_rbac is forced true, rollback cannot disable RBAC, and cli organization set now calls PUT /organization instead of writing orgs directly.
  • Added explicit backend validation for password_policy_config so moving CLI org settings to the API route does not drop the password-policy feature.
  • Fixed the events notify-console path so it relies on the verified org/app resolution instead of an org-only preflight that broke app-scoped authorization.

Motivation (AI generated)

RBAC is now the authoritative authorization model. Leaving old key-mode write labels, app-side RBAC feature flags, and CLI direct org writes made the active security boundary ambiguous and could preserve upgrade/downgrade paths that no longer match the product model. This PR removes those stale surfaces while preserving intentional compatibility for old API-key read paths and old org payload shape.

Fix Justification (AI generated)

  • Auth-only middleware defaults: old ['all', 'write'] route declarations are no longer the security boundary and were misleading.
  • API-key mutation guard: a leaked API key, even a broad one, must not be able to update/regenerate/delete API keys or upgrade its own API-key RBAC bindings.
  • /apikey read/list compatibility: keeping API-key reads avoids breaking old compatible callers; scoped keys still use the existing limited-scope filter.
  • Org RLS hardening: orgs UPDATE is now authenticated-only and named-RBAC guarded; API-key org settings writes must go through the backend route where middlewareV2, checkPermission, and API-key org policy checks run before the service-role write.
  • CLI org settings route migration: cli organization set no longer relies on direct Supabase orgs.update(...), so it does not need the old write RLS policy to stay open.
  • Password-policy route validation: the backend route now accepts only a typed policy object with min_length bounded to 6..72, matching the DB constraint.
  • Compatibility helpers: rbac_enable_for_org remains service-role callable for old automation, but always returns RBAC enabled; rbac_rollback_org is retained but cannot disable RBAC or delete bindings.

Business Impact (AI generated)

This reduces the chance of API-key self-escalation, leaked-key damage, or accidental reactivation of legacy write behavior, while avoiding a breaking change for compatible API-key read/list callers and current CLI org-settings workflows.

Test Plan (AI generated)

  • bun lint:backend
  • bun typecheck:backend
  • commit hook: bun run cli:typecheck && bun run typecheck:backend && bun run typecheck:frontend
  • bun run cli:check
  • bun run supabase:db:reset
  • PGSSLMODE=disable bunx supabase test db --db-url postgresql://postgres:postgres@127.0.0.1:60642/postgres supabase/tests/00-supabase_test_helpers.sql supabase/tests/26_test_rls_policies.sql supabase/tests/48_test_rbac_admin_rpc_execute_grants.sql
  • bun test --timeout 60000 tests/events.test.ts tests/apikeys.test.ts tests/apikeys-expiration.test.ts tests/organization-api.test.ts tests/audit-logs.test.ts tests/password-policy.test.ts
  • bunx vitest run tests/organization-put-stripe-sync.unit.test.ts

Generated with AI

Summary by CodeRabbit

  • New Features

    • API key management now enforces centralized authorization with improved permission controls.
    • Organization settings can be updated via the API, enabling better programmatic management.
  • Improvements

    • Authorization system simplified to use role-based permissions exclusively, removing legacy compatibility modes.
    • Organization membership and invitations now use a cleaner role model for improved consistency.
    • API key creation and validation streamlined with stronger permission checks.

@coderabbitai

coderabbitai Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Replaces legacy key-mode/min-rights with RBAC-only checks. Updates middleware, handlers, SQL policies/migrations, generated types, seeds, CLI permission flow, frontend role handling, and tests/docs. Centralizes API-key management with JWT-only operations and ownership scoping. Removes obsolete functions/enums and aligns RLS assertions.

Changes

End-to-end RBAC unification across services

Layer / File(s) Summary
RBAC refactor, policy/type updates, and test/CLI/frontend alignment
supabase/functions/_backend/..., supabase/schemas/*.sql, supabase/migrations/*, supabase/seed.sql, cli/src/*, src/*, tests/*, supabase/tests/*, docs/*
Middleware switched to middlewareAuth/middlewareKey(options). Handlers enforce RBAC via checkPermission; API key management centralized. Schema/policies/types/seeds updated to RBAC (remove key_mode/user_min_right). CLI uses string permission keys; frontend roles/ranks updated. Tests/docs realigned to RBAC and removed legacy helpers.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • Cap-go/capgo#2391 — Also updates API key management in public/apikey/put.ts with RBAC-focused changes.
  • Cap-go/capgo#1997 — Touches private/create_device.ts to validate org scope against app owner; overlaps with this PR’s RBAC checks.
  • Cap-go/capgo#2330 — Modifies /private/events handler; intersects with this PR’s middleware/auth and parsing refactor.

@codspeed-hq

codspeed-hq Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 43 untouched benchmarks
⏩ 2 skipped benchmarks1


Comparing codex/rbac-apikey-management-hardening (7420610) with main (59df3ec)

Open in CodSpeed

Footnotes

  1. 2 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@riderx riderx force-pushed the codex/rbac-apikey-management-hardening branch from 2d98712 to 0ceea8a Compare June 3, 2026 12:52
@riderx riderx force-pushed the codex/rbac-apikey-management-hardening branch from 0ceea8a to 7489bf9 Compare June 3, 2026 13:05
@riderx riderx force-pushed the codex/rbac-apikey-management-hardening branch from 7489bf9 to 15a2742 Compare June 3, 2026 13:18
@riderx riderx force-pushed the codex/rbac-apikey-management-hardening branch from 15a2742 to 0f07372 Compare June 3, 2026 13:26
@riderx riderx force-pushed the codex/rbac-apikey-management-hardening branch from 0f07372 to 6d7e2b2 Compare June 3, 2026 13:30
@riderx riderx marked this pull request as ready for review June 3, 2026 13:48
@coderabbitai coderabbitai Bot added the codex label Jun 3, 2026

@coderabbitai coderabbitai 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.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
supabase/tests/26_test_rls_policies.sql (1)

377-393: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Assert the new deny policies are actually restrictive.

This only proves the three policy names exist. If a future migration recreates any of them as PERMISSIVE, the test still passes while the deny becomes ineffective against the existing owner allow-policies. Please add pg_policies assertions for permissive = 'RESTRICTIVE' and the expected roles/cmd on these three new policies.

As per coding guidelines: Add explicit deny policies for operations that must be impossible for user-facing roles, using RESTRICTIVE policies instead of relying on implicit deny.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@supabase/tests/26_test_rls_policies.sql` around lines 377 - 393, The test
currently only checks that policy names exist for table apikeys via policies_are
but does not verify they are restrictive; add assertions that query pg_policies
for the three deny policies (e.g., 'Deny anon delete on apikeys', 'Deny anon
select on apikeys', 'Deny anon update on apikeys') and assert permissive =
'RESTRICTIVE' and that the role(s) and cmd columns match the expected values for
each policy; locate this near the existing policies_are call for apikeys and add
one assertion per deny policy checking pg_policies.permissive, pg_policies.roles
and pg_policies.cmd to ensure the denies are actually restrictive.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@supabase/functions/_backend/public/apikey/auth.ts`:
- Around line 18-21: The code reads auth.authType without guarding for missing
auth; change the declaration to allow undefined (e.g., const auth =
c.get('auth') as AuthInfo | undefined) and update the guard to check for missing
auth first (if (!auth || auth.authType !== 'jwt' || !auth.userId) { ... }), and
when building the quickError payload use optional chaining for the authType
field (auth?.authType) so a missing auth produces the intended 401 instead of a
500; keep the same quickError call, errorCode, action and moreInfo variables.

In `@tests/apikeys-expiration.test.ts`:
- Around line 807-812: The test block removed an endpoint-level check, leaving
only direct Supabase/RLS reads (expectApiKeyCannotReadBaseOrg and
expectApiKeyCanReadBaseOrg); restore one HTTP-path assertion against the /apikey
endpoint in this block so middleware/header-parsing is exercised—add a single
assertion that the expiredKeyValue is rejected when calling the /apikey HTTP
route (alongside the RLS helper) and keep the validKeyValue HTTP-path check in
the other test block, referencing the existing helpers for RLS reads and the
/apikey route to locate where to add it.

In `@tests/apikeys.test.ts`:
- Around line 1030-1077: The test "plain key cannot update apikeys table
directly through RLS" leaves the created API key if an assertion fails; wrap the
cleanup DELETE call in a finally block so the seeded key is always removed.
Specifically, after creating the key (createResponse / createData), move the
fetch DELETE for `/apikey/${createData.id}` into a finally section that runs
regardless of test success, keeping the existing authHeaders and preserving the
rest of the assertions in the try block; ensure createData.id is available in
the finally (declare it in the outer scope if needed) so cleanup always
executes.

---

Outside diff comments:
In `@supabase/tests/26_test_rls_policies.sql`:
- Around line 377-393: The test currently only checks that policy names exist
for table apikeys via policies_are but does not verify they are restrictive; add
assertions that query pg_policies for the three deny policies (e.g., 'Deny anon
delete on apikeys', 'Deny anon select on apikeys', 'Deny anon update on
apikeys') and assert permissive = 'RESTRICTIVE' and that the role(s) and cmd
columns match the expected values for each policy; locate this near the existing
policies_are call for apikeys and add one assertion per deny policy checking
pg_policies.permissive, pg_policies.roles and pg_policies.cmd to ensure the
denies are actually restrictive.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9d385eca-b762-452d-a898-93a462ad60c4

📥 Commits

Reviewing files that changed from the base of the PR and between e64b645 and 6d7e2b2.

📒 Files selected for processing (10)
  • cli/src/types/supabase.types.ts
  • supabase/functions/_backend/public/apikey/auth.ts
  • supabase/functions/_backend/public/apikey/delete.ts
  • supabase/functions/_backend/public/apikey/get.ts
  • supabase/functions/_backend/public/apikey/put.ts
  • supabase/functions/_backend/public/apikey/scope.ts
  • supabase/migrations/20260603120805_deny_apikey_management_from_api_key_auth.sql
  • supabase/tests/26_test_rls_policies.sql
  • tests/apikeys-expiration.test.ts
  • tests/apikeys.test.ts
💤 Files with no reviewable changes (1)
  • supabase/functions/_backend/public/apikey/scope.ts

Comment thread supabase/functions/_backend/public/apikey/auth.ts Outdated
Comment thread tests/apikeys-expiration.test.ts Outdated
Comment thread tests/apikeys.test.ts Outdated
@riderx riderx force-pushed the codex/rbac-apikey-management-hardening branch from 6d7e2b2 to ed6303b Compare June 3, 2026 14:06
@riderx riderx force-pushed the codex/rbac-apikey-management-hardening branch from ed6303b to a8fc38e Compare June 3, 2026 14:14
@riderx riderx force-pushed the codex/rbac-apikey-management-hardening branch from a8fc38e to 3991392 Compare June 3, 2026 14:22

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cli/src/types/supabase.types.ts`:
- Around line 3037-3045: The RPC type for app_versions_has_app_permission
incorrectly requires both p_apikey and p_user_id as non-null strings; update the
SQL RPC signature so the inactive auth parameter is nullable (make p_apikey OR
p_user_id NULLABLE/optional in the function definition for
app_versions_has_app_permission), deploy the migration, then regenerate the
TypeScript types (run the project type generation command, e.g. `bun types`) so
the generated supabase.types.ts reflects p_apikey and p_user_id as
nullable/optional and callers no longer need unsafe casts or dummy values.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 81ad7474-945a-4192-9133-1c57dfd028ca

📥 Commits

Reviewing files that changed from the base of the PR and between 6d7e2b2 and a8fc38e.

📒 Files selected for processing (10)
  • cli/src/types/supabase.types.ts
  • supabase/functions/_backend/public/apikey/auth.ts
  • supabase/functions/_backend/public/apikey/delete.ts
  • supabase/functions/_backend/public/apikey/get.ts
  • supabase/functions/_backend/public/apikey/put.ts
  • supabase/functions/_backend/public/apikey/scope.ts
  • supabase/migrations/20260603120805_deny_apikey_management_from_api_key_auth.sql
  • supabase/tests/26_test_rls_policies.sql
  • tests/apikeys-expiration.test.ts
  • tests/apikeys.test.ts
💤 Files with no reviewable changes (1)
  • supabase/functions/_backend/public/apikey/scope.ts

Comment thread cli/src/types/supabase.types.ts Outdated
@riderx riderx force-pushed the codex/rbac-apikey-management-hardening branch from 3991392 to d1b7c51 Compare June 3, 2026 15:34
@riderx riderx force-pushed the codex/rbac-apikey-management-hardening branch from d1b7c51 to fea72cb Compare June 3, 2026 15:41

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

Copy link
Copy Markdown

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: fea72cb875

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread supabase/functions/_backend/public/organization/audit.ts Outdated
@riderx riderx force-pushed the codex/rbac-apikey-management-hardening branch from fea72cb to 507af66 Compare June 3, 2026 15:52

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

Copy link
Copy Markdown

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: 539dedc391

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread cli/src/bundle/upload.ts Outdated

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

Copy link
Copy Markdown

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: 45e3f74dec

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread supabase/migrations/20260608171354_harden_rbac_compat_cleanup.sql Outdated
@chatgpt-codex-connector

Copy link
Copy Markdown

💡 Codex Review

CREATE POLICY "Allow RBAC app_versions update" ON "public"."app_versions" FOR UPDATE TO "authenticated", "anon" USING (("public"."rbac_check_permission_request"("public"."rbac_perm_app_upload_bundle"(), "owner_org", "app_id", NULL::bigint) OR "public"."rbac_check_permission_request"("public"."rbac_perm_bundle_update"(), "owner_org", "app_id", NULL::bigint))) WITH CHECK (("public"."rbac_check_permission_request"("public"."rbac_perm_app_upload_bundle"(), "owner_org", "app_id", NULL::bigint) OR "public"."rbac_check_permission_request"("public"."rbac_perm_bundle_update"(), "owner_org", "app_id", NULL::bigint)));

P2 Badge Align app_versions update policy in prod schema

The cleanup migration now guards app_versions updates so upload-only principals can only update non-deleted rows and need bundle.delete to set deletion, but the added prod.sql dump still recreates the old policy where app.upload_bundle alone satisfies both USING and WITH CHECK. Because supabase/schemas/prod.sql is the current-schema reference, anyone applying or auditing the dump will still see upload-only roles as able to mark bundles deleted, contradicting the migration fix in 20260608171354_harden_rbac_compat_cleanup.sql.

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@riderx

riderx commented Jun 8, 2026

Copy link
Copy Markdown
Member Author

Followed up on the remaining standalone findings:

  • Fixed the stale app_versions policy in supabase/schemas/prod.sql in 2914aaf, matching the hardened migration so upload-only principals cannot mark bundles deleted.
  • Fixed the organization settings API-key write path in 42bd7b9. API-key requests now write through the request-scoped Supabase client instead of supabaseAdmin, and the orgs UPDATE RLS policy explicitly supports anon API-key traffic through RBAC.

Validation passed locally for the focused org/API-key tests and pgTAP policy test, and the latest CI run is green.

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

Copy link
Copy Markdown

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: 18cad6d46b

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread cli/src/channel/set.ts Outdated
@chatgpt-codex-connector

Copy link
Copy Markdown

💡 Codex Review

capgo/supabase/schemas/prod.sql

Lines 19659 to 19660 in 6932d77

GRANT ALL ON FUNCTION "public"."get_user_id"("apikey" "text") TO "authenticated";
GRANT ALL ON FUNCTION "public"."get_user_id"("apikey" "text") TO "service_role";

P2 Badge Preserve get_user_id anon grants in the schema dump

The new migration explicitly grants get_user_id(text) and get_user_id(text,text) to anon, but the updated schema dump removes those anon grants here. A database recreated from supabase/schemas/prod.sql will therefore deny CLI clients that authenticate with the anon key plus capgkey, so commands that still call resolveUserIdFromApiKey() can fail before the newer RBAC permission checks run; please keep the prod schema aligned with the migration grants.

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@github-actions

github-actions Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

🧪 Builder onboarding TUI preview — ✅ passed

▶ Open the interactive HTML report (zoomable journey tree + cast playback)

Commit: 7420610 · Job summary with the result table

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

Copy link
Copy Markdown

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: 77480e5841

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread supabase/migrations/20260611190328_harden_rbac_compat_cleanup.sql
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@sonarqubecloud

Copy link
Copy Markdown

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants