-
-
Notifications
You must be signed in to change notification settings - Fork 127
feat(apikeys): service-principal model — phases 1–3 #1815
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
ToriChanIntegration
wants to merge
3
commits into
Cap-go:main
from
ToriChanIntegration:feat/apikey-service-principals
Closed
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
114 changes: 114 additions & 0 deletions
114
supabase/migrations/20260317024912_service_principal_apikeys.sql
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,114 @@ | ||
| -- Phase 1: Service-Principal Infrastructure for API Keys | ||
| -- | ||
| -- The service-principal model treats each API key as its own "user" in the | ||
| -- auth system. Each API key (via its rbac_id) can have a corresponding | ||
| -- auth.users entry, enabling standard auth.uid()-based RLS to work for | ||
| -- API key authentication — identical to how it works for regular users. | ||
| -- | ||
| -- Architecture (phased rollout): | ||
| -- Phase 1 (this migration): Schema + helper functions. No auth flow changes. | ||
| -- Phase 2 (middleware PR): Edge function signs a JWT with sub=rbac_id for | ||
| -- provisioned keys. auth.uid() returns rbac_id. | ||
| -- Phase 3 (RLS cleanup): Simplify RLS policies to use auth.uid() once all | ||
| -- orgs have service principals provisioned. | ||
| -- | ||
| -- Key concept: | ||
| -- - apikeys.rbac_id IS the service principal UUID (stable, already exists) | ||
| -- - When provisioned: auth.users row exists with id = rbac_id | ||
| -- - Middleware can sign JWTs with sub = rbac_id for provisioned keys | ||
| -- - Service principals appear in org_users / role_bindings for authorization | ||
|
|
||
| -- ============================================================================ | ||
| -- 1. Track provisioning state on each API key | ||
| -- ============================================================================ | ||
|
|
||
| ALTER TABLE "public"."apikeys" | ||
| ADD COLUMN IF NOT EXISTS "service_principal_provisioned" boolean DEFAULT false NOT NULL; | ||
|
|
||
| COMMENT ON COLUMN "public"."apikeys"."service_principal_provisioned" IS | ||
| 'When true, an auth.users entry exists with id=rbac_id for this API key. ' | ||
| 'The middleware can then sign a JWT with sub=rbac_id, making auth.uid() ' | ||
| 'return the service principal ID for standard RLS evaluation.'; | ||
|
|
||
| -- ============================================================================ | ||
| -- 2. get_service_principal_info() — retrieve key info needed for JWT signing | ||
| -- Called by edge function middleware when a capgkey header is detected. | ||
| -- ============================================================================ | ||
|
|
||
| CREATE OR REPLACE FUNCTION "public"."get_service_principal_info"("p_apikey_value" "text") | ||
| RETURNS TABLE ( | ||
| "apikey_id" bigint, | ||
| "service_principal_id" "uuid", -- rbac_id; used as auth.users id | ||
| "owner_user_id" "uuid", -- human who owns this key | ||
| "is_provisioned" boolean, -- auth.users entry exists | ||
| "key_mode" "public"."key_mode", | ||
| "is_expired" boolean, | ||
| "limited_to_orgs" "uuid"[], | ||
| "limited_to_apps" character varying[] | ||
| ) | ||
| LANGUAGE "plpgsql" | ||
| SECURITY DEFINER | ||
| SET "search_path" = '' | ||
| AS $$ | ||
| BEGIN | ||
| RETURN QUERY | ||
| SELECT | ||
| ak.id AS apikey_id, | ||
| ak.rbac_id AS service_principal_id, | ||
| ak.user_id AS owner_user_id, | ||
| ak.service_principal_provisioned AS is_provisioned, | ||
| ak.mode AS key_mode, | ||
| "public"."is_apikey_expired"(ak.expires_at) AS is_expired, | ||
| ak.limited_to_orgs, | ||
| ak.limited_to_apps | ||
| FROM "public"."find_apikey_by_value"(p_apikey_value) ak | ||
| WHERE ak.id IS NOT NULL; | ||
| END; | ||
| $$; | ||
|
|
||
| ALTER FUNCTION "public"."get_service_principal_info"("p_apikey_value" "text") OWNER TO "postgres"; | ||
| REVOKE ALL ON FUNCTION "public"."get_service_principal_info"("p_apikey_value" "text") FROM PUBLIC; | ||
| GRANT ALL ON FUNCTION "public"."get_service_principal_info"("p_apikey_value" "text") TO "service_role"; | ||
|
|
||
| COMMENT ON FUNCTION "public"."get_service_principal_info"("p_apikey_value" "text") IS | ||
| 'Returns service-principal metadata for a given API key value (plain or hashed). ' | ||
| 'Used by edge function middleware to decide whether to sign a service-principal JWT ' | ||
| 'and, if so, what UUID to use as the JWT subject (= rbac_id).'; | ||
|
|
||
| -- ============================================================================ | ||
| -- 3. mark_service_principal_provisioned() — called after auth.users is created | ||
| -- The edge function creates the auth.users entry using the admin client, | ||
| -- then calls this function to record the provisioned state. | ||
| -- ============================================================================ | ||
|
|
||
| CREATE OR REPLACE FUNCTION "public"."mark_service_principal_provisioned"( | ||
| "p_apikey_id" bigint, | ||
| "p_rbac_id" "uuid" | ||
| ) | ||
| RETURNS void | ||
| LANGUAGE "plpgsql" | ||
| SECURITY DEFINER | ||
| SET "search_path" = '' | ||
| AS $$ | ||
| BEGIN | ||
| UPDATE "public"."apikeys" | ||
| SET service_principal_provisioned = true | ||
| WHERE id = p_apikey_id | ||
| AND rbac_id = p_rbac_id; | ||
|
|
||
| IF NOT FOUND THEN | ||
| RAISE EXCEPTION | ||
| 'API key not found or rbac_id mismatch: apikey_id=%, rbac_id=%', | ||
| p_apikey_id, p_rbac_id; | ||
| END IF; | ||
| END; | ||
| $$; | ||
|
|
||
| ALTER FUNCTION "public"."mark_service_principal_provisioned"("p_apikey_id" bigint, "p_rbac_id" "uuid") OWNER TO "postgres"; | ||
| REVOKE ALL ON FUNCTION "public"."mark_service_principal_provisioned"("p_apikey_id" bigint, "p_rbac_id" "uuid") FROM PUBLIC; | ||
| GRANT ALL ON FUNCTION "public"."mark_service_principal_provisioned"("p_apikey_id" bigint, "p_rbac_id" "uuid") TO "service_role"; | ||
|
|
||
| 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).'; | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
🧰 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