feat(apikeys): service-principal model — phases 1–3#1815
feat(apikeys): service-principal model — phases 1–3#1815ToriChanIntegration wants to merge 3 commits into
Conversation
Adds the schema and helper functions needed to support the service-principal auth model for API keys (GH#1190). In this model each API key can have a corresponding auth.users entry (id = rbac_id), so standard auth.uid()-based RLS works identically for human users and API key "users". Changes: - apikeys.service_principal_provisioned column (default false) tracks whether an auth.users entry has been created for the key's rbac_id - get_service_principal_info(apikey_value) returns metadata needed by the edge-function middleware to sign a service-principal JWT - mark_service_principal_provisioned(apikey_id, rbac_id) is called by edge functions (admin client) after creating the auth.users entry - pgTAP tests covering the new column defaults and function behaviour Phase 2 (next PR): update hono_middleware to provision service principals lazily and sign a JWT (sub=rbac_id) for provisioned keys. Phase 3: simplify RLS policies to use auth.uid() directly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…se 2) When an API key request is authenticated, the middleware now lazily provisions an auth.users entry (id = rbac_id) for the key's service principal on first use via supabaseAdmin.auth.admin.createUser(), then marks the key as provisioned via mark_service_principal_provisioned(). A short-lived HS256 JWT (sub = rbac_id, role = authenticated, 1 h) is signed on every API key request and stored in context as `servicePrincipalJwt`. A new `supabaseServicePrincipal()` helper in supabase.ts returns a Supabase client authenticated with this JWT when available, falling back to the legacy capgkey path. The existing capgkey-based auth path is **unchanged** — all current handlers continue to work. Provisioning is non-fatal: any error is logged and the request proceeds normally. Phase 3 will migrate handlers to use `supabaseServicePrincipal()` and simplify RLS accordingly. Supporting changes: - postgres_schema.ts: `service_principal_provisioned` column added to the apikeys Drizzle schema - hono.ts: `servicePrincipalJwt?` added to MiddlewareKeyVariables so the new context key is type-safe - hono_middleware.ts: ServicePrincipalInfoRow type, helper functions fetchServicePrincipalInfo / provisionServicePrincipal / signServicePrincipalJwt / applyServicePrincipal Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add is_service_principal(uuid) helper to identify provisioned SP API keys - Exempt service principals from 2FA and password-policy enforcement in check_min_rights, check_min_rights_legacy, and check_min_rights_legacy_no_password_policy - Add SP JWT fallback path in all three permission functions: when auth.uid() returns an SP's rbac_id (no capgkey header), look up the apikey by rbac_id and evaluate access via key_mode (read/upload/write/all inheriting owner right) - Add 13 pgTAP tests covering is_service_principal, legacy SP fallback with mode mapping, org/app scoping, and no-password-policy variant - Fix plan(14)→plan(13) off-by-one in Phase 1 test file Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
📝 WalkthroughWalkthroughImplements service-principal support for API keys across multiple layers: middleware authentication flow adds provisioning logic with JWT signing; database schema and migrations introduce provisioning state tracking, SQL functions for metadata retrieval and provisioning, and RBAC enhancements with service-principal fallback paths; new utilities enable service-principal client selection; comprehensive test suites validate provisioning and authorization. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant Middleware
participant Database as Database<br/>(SQL Functions)
participant Crypto as Crypto<br/>(JWT)
participant Context as Request<br/>Context
Client->>Middleware: API Key Request
Middleware->>Middleware: Authenticate via API Key<br/>(capgkey)
activate Middleware
Middleware->>Database: fetchServicePrincipalInfo<br/>(keyString)
Database-->>Middleware: SP metadata<br/>(apikey_id, rbac_id, etc.)
alt SP Metadata Found & Not Provisioned
Middleware->>Database: provisionServicePrincipal<br/>(apikey_id, rbac_id)
Database->>Database: Create auth.users entry<br/>mark provisioned
Database-->>Middleware: Provisioning complete
end
Middleware->>Crypto: signServicePrincipalJwt<br/>(rbac_id)
Crypto-->>Middleware: JWT token
Middleware->>Context: Store servicePrincipalJwt
deactivate Middleware
Middleware-->>Client: Request + SP Context
Context->>Database: supabaseServicePrincipal()<br/>(uses stored JWT)
Database-->>Context: SP-authorized client
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
📝 Coding Plan
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 SQLFluff (4.0.4)supabase/tests/47_test_service_principal_apikeys.sqlUser Error: No dialect was specified. You must configure a dialect or specify one on the command line using --dialect after the command. Available dialects: supabase/migrations/20260317100000_service_principal_phase3_check_min_rights.sqlUser Error: No dialect was specified. You must configure a dialect or specify one on the command line using --dialect after the command. Available dialects: supabase/tests/48_test_service_principal_phase3.sqlUser Error: No dialect was specified. You must configure a dialect or specify one on the command line using --dialect after the command. Available dialects:
Comment |
|
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
supabase/tests/47_test_service_principal_apikeys.sql (1)
11-30: Consider adding cleanup before test key insertion.Unlike
48_test_service_principal_phase3.sqlwhich DELETEs existing keys (line 12), this test file doesn't clean up potentially leftover keys from interrupted runs. This could cause failures if keys 99950/99951 already exist.🧹 Proposed fix to add cleanup
SELECT tests.authenticate_as_service_role(); -- ============================================================================= -- Setup: create test API keys for service-principal tests -- ============================================================================= +DELETE FROM apikeys WHERE id IN (99950, 99951); + INSERT INTO apikeys (id, user_id, key, mode, name, expires_at) VALUES🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@supabase/tests/47_test_service_principal_apikeys.sql` around lines 11 - 30, Test insertion can fail if rows with ids 99950/99951 already exist; add a cleanup DELETE before the INSERT to remove any leftover test keys. In the same test block that inserts into the apikeys table (IDs 99950 and 99951 for user_id '6aa76066-55ef-4238-ade6-0b32334a4097'), run a DELETE FROM apikeys WHERE id IN (99950, 99951) (or delete by key strings 'sp-test-key-valid','sp-test-key-expired') immediately before the INSERT so the INSERT of those rows will not conflict with leftovers from previous runs.supabase/migrations/20260317100000_service_principal_phase3_check_min_rights.sql (1)
291-333: Consider extracting duplicated SP fallback logic.The SP fallback logic (org/app scope checks + key_mode → user_min_right mapping) is duplicated between
check_min_rights_legacyandcheck_min_rights_legacy_no_password_policy. This could be extracted into a shared helper function for maintainability.Also applies to: 400-437
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@supabase/migrations/20260317100000_service_principal_phase3_check_min_rights.sql` around lines 291 - 333, Extract the duplicated service-principal fallback block into a single helper function (e.g. public.check_min_rights_sp_fallback) that accepts the apikey row (v_sp_apikey or user_id), org_id, app_id and min_right and returns either a boolean allow or the computed user_min_right; move the org/app scope checks (limited_to_orgs/apps) and the key_mode → user_min_right mapping (including the special 'all' lookup against org_users to derive v_sp_right) into that helper, preserve and emit the same pg_log messages when denying, and replace the duplicated code in check_min_rights_legacy and check_min_rights_legacy_no_password_policy with a single call to the new helper (using v_sp_apikey and the same inputs) to decide the RETURN true or continue flow.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@supabase/migrations/20260317024912_service_principal_apikeys.sql`:
- Around line 111-114: Update the COMMENT ON FUNCTION for
"mark_service_principal_provisioned" to replace the hyphenated word
"mis-marking" with the correct single-word form "mismarking"; edit the comment
string passed to COMMENT ON FUNCTION (the block referencing function name
mark_service_principal_provisioned and parameters p_apikey_id and p_rbac_id) so
the sentence reads "...acts as a guard to prevent accidental mismarking."
without changing other text.
---
Nitpick comments:
In
`@supabase/migrations/20260317100000_service_principal_phase3_check_min_rights.sql`:
- Around line 291-333: Extract the duplicated service-principal fallback block
into a single helper function (e.g. public.check_min_rights_sp_fallback) that
accepts the apikey row (v_sp_apikey or user_id), org_id, app_id and min_right
and returns either a boolean allow or the computed user_min_right; move the
org/app scope checks (limited_to_orgs/apps) and the key_mode → user_min_right
mapping (including the special 'all' lookup against org_users to derive
v_sp_right) into that helper, preserve and emit the same pg_log messages when
denying, and replace the duplicated code in check_min_rights_legacy and
check_min_rights_legacy_no_password_policy with a single call to the new helper
(using v_sp_apikey and the same inputs) to decide the RETURN true or continue
flow.
In `@supabase/tests/47_test_service_principal_apikeys.sql`:
- Around line 11-30: Test insertion can fail if rows with ids 99950/99951
already exist; add a cleanup DELETE before the INSERT to remove any leftover
test keys. In the same test block that inserts into the apikeys table (IDs 99950
and 99951 for user_id '6aa76066-55ef-4238-ade6-0b32334a4097'), run a DELETE FROM
apikeys WHERE id IN (99950, 99951) (or delete by key strings
'sp-test-key-valid','sp-test-key-expired') immediately before the INSERT so the
INSERT of those rows will not conflict with leftovers from previous runs.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: b10e6f0d-bd54-4e25-b457-dc0cf318473d
📒 Files selected for processing (8)
supabase/functions/_backend/utils/hono.tssupabase/functions/_backend/utils/hono_middleware.tssupabase/functions/_backend/utils/postgres_schema.tssupabase/functions/_backend/utils/supabase.tssupabase/migrations/20260317024912_service_principal_apikeys.sqlsupabase/migrations/20260317100000_service_principal_phase3_check_min_rights.sqlsupabase/tests/47_test_service_principal_apikeys.sqlsupabase/tests/48_test_service_principal_phase3.sql
| COMMENT ON FUNCTION "public"."mark_service_principal_provisioned"("p_apikey_id" bigint, "p_rbac_id" "uuid") IS | ||
| 'Marks an API key as having a provisioned service-principal auth.users entry. ' | ||
| 'The rbac_id parameter acts as a guard to prevent accidental mis-marking. ' | ||
| 'Only callable by service_role (edge functions with admin client).'; |
There was a problem hiding this comment.
Fix typo flagged by pipeline: "mis-marking" → "mismarking".
The pipeline flagged "mis" as a typo. The correct form is "mismarking" (one word, no hyphen).
📝 Proposed fix
COMMENT ON FUNCTION "public"."mark_service_principal_provisioned"("p_apikey_id" bigint, "p_rbac_id" "uuid") IS
'Marks an API key as having a provisioned service-principal auth.users entry. '
- 'The rbac_id parameter acts as a guard to prevent accidental mis-marking. '
+ 'The rbac_id parameter acts as a guard to prevent accidental mismarking. '
'Only callable by service_role (edge functions with admin client).';📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| COMMENT ON FUNCTION "public"."mark_service_principal_provisioned"("p_apikey_id" bigint, "p_rbac_id" "uuid") IS | |
| 'Marks an API key as having a provisioned service-principal auth.users entry. ' | |
| 'The rbac_id parameter acts as a guard to prevent accidental mis-marking. ' | |
| 'Only callable by service_role (edge functions with admin client).'; | |
| COMMENT ON FUNCTION "public"."mark_service_principal_provisioned"("p_apikey_id" bigint, "p_rbac_id" "uuid") IS | |
| 'Marks an API key as having a provisioned service-principal auth.users entry. ' | |
| 'The rbac_id parameter acts as a guard to prevent accidental mismarking. ' | |
| 'Only callable by service_role (edge functions with admin client).'; |
🧰 Tools
🪛 GitHub Actions: Run tests
[warning] 113-113: typos: 'mis' should be 'miss' or 'mist'.
🪛 GitHub Check: Run tests
[warning] 113-113:
"mis" should be "miss" or "mist".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@supabase/migrations/20260317024912_service_principal_apikeys.sql` around
lines 111 - 114, Update the COMMENT ON FUNCTION for
"mark_service_principal_provisioned" to replace the hyphenated word
"mis-marking" with the correct single-word form "mismarking"; edit the comment
string passed to COMMENT ON FUNCTION (the block referencing function name
mark_service_principal_provisioned and parameters p_apikey_id and p_rbac_id) so
the sentence reads "...acts as a guard to prevent accidental mismarking."
without changing other text.
|
Thanks for the contribution. We’re closing this PR because we can’t accept repository changes in this form from an integration-style / automated contributor account. For Capgo repos, we expect contributions to be tightly scoped, manually validated, and submitted by an identifiable contributor who can clearly explain the problem, why this approach fits the project, and what was verified locally. Large or bulk-generated change sets without that ownership context create too much review and maintenance risk for us. If this was opened in error or if you are a real contributor behind this account, you’re welcome to resubmit a smaller, clearly-scoped PR with manual validation notes and repository-specific reasoning. |



Summary
Implements the service-principal API key model in three backward-compatible phases. Closes #1190.
service_principal_provisionedflag toapikeys, plusget_service_principal_info()andmark_service_principal_provisioned()helpers. 14 pgTAP tests.auth.usersentries (id = rbac_id) on first API key use; signs a 1 h SP JWT (sub = rbac_id, role = authenticated) and stores it in Hono context asservicePrincipalJwt. NewsupabaseServicePrincipal(c)helper insupabase.ts.check_min_rights: 2FA and password-policy blocks skip service principals; new SP JWT fallback path evaluates access viarbac_has_permission(rbac_principal_apikey(), …)whenauth.uid()returns the SP'srbac_id. Coverscheck_min_rights,check_min_rights_legacy, andcheck_min_rights_legacy_no_password_policy. 13 pgTAP tests (909 total suite-wide).All changes are fully backward-compatible — existing capgkey auth path is untouched until handlers are migrated to
supabaseServicePrincipal(c).Test Plan
bun test:backend)check_min_rightsSP pathsbun lint:backend+bun typecheckcleanSummary by CodeRabbit
Release Notes
New Features
Tests