-
-
Notifications
You must be signed in to change notification settings - Fork 134
fix(db): make org customer_id service-managed only #3205
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
Merged
+651
−30
Merged
Changes from all commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
e064981
fix(db): block org customer_id writes without org.update_billing
cursoragent 7f38013
test: fix org billing guard user id typing
cursoragent 8082e60
test: isolate org billing guard fixtures from creator bootstrap
cursoragent 8618d1a
test: strengthen org billing guard pgTAP assertions
cursoragent 3268af6
ci: retrigger workflow after Cloudflare shard flake
cursoragent b34457c
fix(db): address CodeRabbit review on org billing guard
cursoragent 20a1464
fix(db): wrap org billing guard migration comments for SQLFluff LT05
cursoragent 622fa84
fix(db): re-stamp org billing guard migration after main advance
cursoragent ed4a7b0
Merge branch 'main' into cursor/org-billing-column-guard-9734
TorichanCapgo cfee12d
fix(db): make org customer_id service-managed only
cursoragent 5433a86
test(db): fix org customer_id pgTAP bootstrap assertions
cursoragent d113950
merge: sync main into org customer_id guard branch
cursoragent faa4b42
fix(org): keep JWT org create on auth path for audit and MFA
cursoragent b1895bf
test(org): delete orgs before pending stripe cleanup
cursoragent 4c1635c
fix(db): re-stamp org customer_id guard migration after main
cursoragent bc8ae44
ci: retrigger after flaky stats plugin test
cursoragent 302f199
ci: rerun full test suite
cursoragent 7c8dda9
docs(org): note customer_id bootstrap on JWT org create
cursoragent dfe306e
fix(org): reload org row before Stripe bootstrap on create
cursoragent 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
166 changes: 166 additions & 0 deletions
166
supabase/migrations/20260826103000_org_billing_column_guard.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,166 @@ | ||
| -- Block user-context writes to orgs.customer_id. Only service_role/postgres | ||
| -- (is_internal_request_role) and the org-create bootstrap path may set it. | ||
| -- | ||
| -- Execution profile for guard_org_billing_columns (BEFORE INSERT OR UPDATE OF | ||
| -- customer_id): | ||
| -- - Frequency: once per org row when customer_id is supplied on INSERT or | ||
| -- changes on UPDATE. Console-scale, not plugin hot path. | ||
| -- - Roles: authenticated and anon (capgkey) via PostgREST are always denied | ||
| -- unless org-create bootstrap GUC matches the row during AFTER INSERT setup. | ||
| -- service_role/postgres bypass via is_internal_request_role(). | ||
| -- - Cardinality: single-row trigger; no table scans. | ||
|
|
||
| CREATE OR REPLACE FUNCTION "public"."guard_org_billing_columns"() RETURNS "trigger" | ||
| LANGUAGE "plpgsql" SECURITY DEFINER | ||
| SET "search_path" TO '' | ||
| AS $$ | ||
| DECLARE | ||
| v_request_role text := public.current_request_role(); | ||
| v_bootstrap_org_id text := pg_catalog.current_setting('capgo.org_creation_bootstrap_org_id', true); | ||
| BEGIN | ||
| IF public.is_internal_request_role(v_request_role) THEN | ||
| RETURN NEW; | ||
| END IF; | ||
|
|
||
| IF TG_OP = 'UPDATE' | ||
| AND NEW.customer_id IS DISTINCT FROM OLD.customer_id | ||
| AND v_bootstrap_org_id <> '' | ||
| AND v_bootstrap_org_id = NEW.id::text | ||
| THEN | ||
| RETURN NEW; | ||
| END IF; | ||
|
|
||
| IF TG_OP = 'INSERT' AND NEW.customer_id IS NOT NULL THEN | ||
| RAISE EXCEPTION 'PERMISSION_DENIED_ORG_CUSTOMER_ID' | ||
| USING ERRCODE = '42501'; | ||
| END IF; | ||
|
|
||
| IF TG_OP = 'UPDATE' AND NEW.customer_id IS DISTINCT FROM OLD.customer_id THEN | ||
| RAISE EXCEPTION 'PERMISSION_DENIED_ORG_CUSTOMER_ID' | ||
| USING ERRCODE = '42501'; | ||
| END IF; | ||
|
|
||
| RETURN NEW; | ||
| END; | ||
| $$; | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| ALTER FUNCTION "public"."guard_org_billing_columns"() OWNER TO "postgres"; | ||
|
|
||
| REVOKE ALL ON FUNCTION "public"."guard_org_billing_columns"() FROM PUBLIC; | ||
|
|
||
| GRANT ALL ON FUNCTION "public"."guard_org_billing_columns"() TO "service_role"; | ||
|
|
||
| COMMENT ON FUNCTION "public"."guard_org_billing_columns"() IS | ||
| 'BEFORE INSERT/UPDATE OF customer_id guard. User/capgkey roles cannot write customer_id; ' | ||
| 'service_role/postgres bypass via is_internal_request_role. Org-create bootstrap may set ' | ||
| 'pending customer_id while capgo.org_creation_bootstrap_org_id matches the row id.'; | ||
|
|
||
| DROP TRIGGER IF EXISTS "guard_org_billing_columns" ON "public"."orgs"; | ||
| DROP TRIGGER IF EXISTS "guard_org_billing_columns_insert" ON "public"."orgs"; | ||
|
|
||
| CREATE TRIGGER "guard_org_billing_columns_insert" | ||
| BEFORE INSERT ON "public"."orgs" | ||
| FOR EACH ROW | ||
| EXECUTE FUNCTION "public"."guard_org_billing_columns"(); | ||
|
|
||
| CREATE TRIGGER "guard_org_billing_columns" | ||
| BEFORE UPDATE OF "customer_id" ON "public"."orgs" | ||
| FOR EACH ROW | ||
| EXECUTE FUNCTION "public"."guard_org_billing_columns"(); | ||
|
|
||
| -- Keep bootstrap GUC active through pending customer_id assignment so the guard | ||
| -- allows generate_org_user_stripe_info_on_org_create to finish for user inserts | ||
| -- that omit customer_id (legacy/direct PostgREST path). | ||
| CREATE OR REPLACE FUNCTION "public"."generate_org_user_stripe_info_on_org_create"() RETURNS "trigger" | ||
| LANGUAGE "plpgsql" SECURITY DEFINER | ||
| SET "search_path" TO '' | ||
| AS $$ | ||
| DECLARE | ||
| solo_plan_stripe_id varchar; | ||
| pending_customer_id varchar; | ||
| trial_at_date timestamptz; | ||
| org_super_admin_role_id uuid; | ||
| BEGIN | ||
| PERFORM set_config('capgo.org_creation_bootstrap_org_id', NEW.id::text, true); | ||
|
|
||
| INSERT INTO public.org_users (user_id, org_id, rbac_role_name, is_invite) | ||
| VALUES (NEW.created_by, NEW.id, public.rbac_role_org_super_admin(), false); | ||
|
|
||
| SELECT id INTO org_super_admin_role_id | ||
| FROM public.roles | ||
| WHERE name = public.rbac_role_org_super_admin() | ||
| AND scope_type = public.rbac_scope_org() | ||
| LIMIT 1; | ||
|
|
||
| IF org_super_admin_role_id IS NOT NULL THEN | ||
| INSERT INTO public.role_bindings ( | ||
| principal_type, principal_id, role_id, scope_type, org_id, | ||
| granted_by, granted_at, reason, is_direct | ||
| ) VALUES ( | ||
| public.rbac_principal_user(), NEW.created_by, org_super_admin_role_id, public.rbac_scope_org(), NEW.id, | ||
| NEW.created_by, now(), 'Organization creator', true | ||
| ) ON CONFLICT DO NOTHING; | ||
| END IF; | ||
|
|
||
| IF NEW.customer_id IS NOT NULL THEN | ||
| PERFORM set_config('capgo.org_creation_bootstrap_org_id', '', true); | ||
| RETURN NEW; | ||
| END IF; | ||
|
|
||
| pending_customer_id := 'pending_' || NEW.id::text; | ||
|
|
||
| IF EXISTS ( | ||
| SELECT 1 | ||
| FROM public.stripe_info | ||
| WHERE customer_id = pending_customer_id | ||
| ) THEN | ||
| UPDATE public.orgs | ||
| SET customer_id = pending_customer_id | ||
| WHERE id = NEW.id; | ||
|
|
||
| PERFORM set_config('capgo.org_creation_bootstrap_org_id', '', true); | ||
| RETURN NEW; | ||
| END IF; | ||
|
|
||
| SELECT stripe_id INTO solo_plan_stripe_id | ||
| FROM public.plans | ||
| WHERE name = 'Solo' | ||
| LIMIT 1; | ||
|
|
||
| IF solo_plan_stripe_id IS NULL THEN | ||
| PERFORM set_config('capgo.org_creation_bootstrap_org_id', '', true); | ||
| RAISE WARNING 'Solo plan not found, skipping sync stripe_info creation for org %', NEW.id; | ||
| RETURN NEW; | ||
| END IF; | ||
|
|
||
| trial_at_date := NOW() + INTERVAL '15 days'; | ||
|
|
||
| INSERT INTO public.stripe_info ( | ||
| customer_id, | ||
| product_id, | ||
| trial_at, | ||
| status, | ||
| is_good_plan | ||
| ) VALUES ( | ||
| pending_customer_id, | ||
| solo_plan_stripe_id, | ||
| trial_at_date, | ||
| NULL, | ||
| true | ||
| ); | ||
|
|
||
| UPDATE public.orgs | ||
| SET customer_id = pending_customer_id | ||
| WHERE id = NEW.id; | ||
|
|
||
| PERFORM set_config('capgo.org_creation_bootstrap_org_id', '', true); | ||
|
|
||
| RETURN NEW; | ||
| END; | ||
| $$; | ||
|
|
||
| ALTER FUNCTION "public"."generate_org_user_stripe_info_on_org_create"() OWNER TO "postgres"; | ||
|
|
||
| REVOKE ALL ON FUNCTION "public"."generate_org_user_stripe_info_on_org_create"() FROM PUBLIC; | ||
|
|
||
| GRANT ALL ON FUNCTION "public"."generate_org_user_stripe_info_on_org_create"() TO "service_role"; | ||
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.
Uh oh!
There was an error while loading. Please reload this page.