-
-
Notifications
You must be signed in to change notification settings - Fork 134
fix(rbac): clean channel overrides when org role binding is removed #3198
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
Merged
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
e52db58
fix(rbac): clean channel overrides when org binding is removed
cursoragent 164cd5f
ci: retrigger backend tests after flaky shard failure
cursoragent b57f978
fix(rbac): address review feedback on org binding override cleanup
cursoragent ef258c6
fix(test): hoist channel IDs for concurrent test cleanup scope
cursoragent 8bca113
docs(rbac): document rbac_principal_has_org_binding execution profile
cursoragent 40403de
ci: retrigger tests after cancelled workflow
cursoragent d8eab73
ci: retrigger pull_request test workflow
cursoragent 92be052
ci: trigger pull_request tests (single run)
cursoragent a6d4c6b
ci: retry tests after flaky Cloudflare shard failure
cursoragent 5e23930
fix(db): bump migration timestamp after main merge
cursoragent 834c84c
merge: sync main before CI retry
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
216 changes: 216 additions & 0 deletions
216
supabase/migrations/20260825113706_channel_overrides_cleanup_on_org_unbind.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,216 @@ | ||
| -- When a principal loses all role bindings in an org, stale channel_permission_overrides | ||
| -- must not keep granting channel-scoped permissions. Gate override application on an | ||
| -- active org binding (any scope) or group membership. | ||
| -- | ||
| -- Execution profile for rbac_principal_has_org_binding (channel override gate): | ||
| -- - Called at most once per rbac_check_permission_direct when p_channel_id IS NOT NULL | ||
| -- (apikey and user branches are mutually exclusive). | ||
| -- - Roles: service_role only; invoked from SECURITY DEFINER rbac_check_permission_direct. | ||
| -- - Frequency: console/RLS channel-scoped checks; not plugin /updates|/stats hot path. | ||
| -- - Cardinality: role_bindings rows per (principal, org) are typically single-digit; | ||
| -- group_members per user is bounded by org group membership. | ||
| -- - Indexes: role_bindings_principal_org_idx (principal_type, principal_id, org_id, | ||
| -- expires_at); idx_group_members_user_id_group_id for group-derived user path; | ||
| -- groups PK for group-principal branch. | ||
| -- - Worst case (user + apikey call sites): Index Scan on role_bindings_principal_org_idx | ||
| -- with expires_at filter; Nested Loop from group_members (user_id) to role_bindings | ||
| -- (group principal). No sequential scan over role_bindings in EXPLAIN (ANALYZE, | ||
| -- BUFFERS) on local seed data. | ||
|
|
||
| CREATE OR REPLACE FUNCTION public.rbac_principal_has_org_binding( | ||
| p_principal_type text, | ||
| p_principal_id uuid, | ||
| p_org_id uuid | ||
| ) | ||
| RETURNS boolean | ||
| LANGUAGE sql | ||
| STABLE | ||
| SECURITY DEFINER | ||
| SET search_path = '' | ||
| AS $$ | ||
| SELECT CASE | ||
| WHEN p_org_id IS NULL OR p_principal_id IS NULL OR p_principal_type IS NULL THEN false | ||
| WHEN p_principal_type = public.rbac_principal_group() THEN EXISTS ( | ||
| SELECT 1 | ||
| FROM public.groups | ||
| WHERE groups.id = p_principal_id | ||
| AND groups.org_id = p_org_id | ||
| ) | ||
| WHEN p_principal_type = public.rbac_principal_user() THEN ( | ||
| EXISTS ( | ||
| SELECT 1 | ||
| FROM public.role_bindings | ||
| WHERE role_bindings.principal_type = p_principal_type | ||
| AND role_bindings.principal_id = p_principal_id | ||
| AND role_bindings.org_id = p_org_id | ||
| AND (role_bindings.expires_at IS NULL OR role_bindings.expires_at > pg_catalog.now()) | ||
| ) | ||
| OR EXISTS ( | ||
| SELECT 1 | ||
| FROM public.group_members AS group_member | ||
| INNER JOIN public.role_bindings AS group_binding | ||
| ON group_binding.principal_type = public.rbac_principal_group() | ||
| AND group_binding.principal_id = group_member.group_id | ||
| WHERE group_member.user_id = p_principal_id | ||
| AND group_binding.org_id = p_org_id | ||
| AND (group_binding.expires_at IS NULL OR group_binding.expires_at > pg_catalog.now()) | ||
| ) | ||
| ) | ||
| ELSE EXISTS ( | ||
| SELECT 1 | ||
| FROM public.role_bindings | ||
| WHERE role_bindings.principal_type = p_principal_type | ||
| AND role_bindings.principal_id = p_principal_id | ||
| AND role_bindings.org_id = p_org_id | ||
| AND (role_bindings.expires_at IS NULL OR role_bindings.expires_at > pg_catalog.now()) | ||
| ) | ||
| END; | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| $$; | ||
|
|
||
| ALTER FUNCTION public.rbac_principal_has_org_binding(text, uuid, uuid) OWNER TO postgres; | ||
| REVOKE ALL ON FUNCTION public.rbac_principal_has_org_binding(text, uuid, uuid) FROM PUBLIC; | ||
| GRANT EXECUTE ON FUNCTION public.rbac_principal_has_org_binding(text, uuid, uuid) TO service_role; | ||
|
|
||
| COMMENT ON FUNCTION public.rbac_principal_has_org_binding(text, uuid, uuid) IS | ||
| 'True when the principal still has a non-expired role binding in the org (direct or via group membership for users) or the group belongs to the org. Called once per channel-scoped rbac_check_permission_direct (console/RLS only). Indexed lookups on role_bindings_principal_org_idx and idx_group_members_user_id_group_id; no table scan on role_bindings at seed scale.'; | ||
|
|
||
| CREATE INDEX IF NOT EXISTS role_bindings_principal_org_idx | ||
| ON public.role_bindings (principal_type, principal_id, org_id, expires_at); | ||
|
|
||
| CREATE OR REPLACE FUNCTION public.rbac_check_permission_direct( | ||
| p_permission_key text, | ||
| p_user_id uuid, | ||
| p_org_id uuid, | ||
| p_app_id character varying, | ||
| p_channel_id bigint, | ||
| p_apikey text DEFAULT NULL::text | ||
| ) | ||
| RETURNS boolean | ||
| LANGUAGE plpgsql | ||
| SECURITY DEFINER | ||
| SET search_path = '' | ||
| AS $$ | ||
| DECLARE | ||
| v_allowed boolean := false; | ||
| v_effective_org_id uuid; | ||
| v_effective_user_id uuid := p_user_id; | ||
| v_effective_app_id character varying; | ||
| v_api_key public.apikeys%ROWTYPE; | ||
| v_channel_scope boolean := p_channel_id IS NOT NULL; | ||
| v_override boolean; | ||
| v_scope_ok boolean; | ||
| v_use_apikey boolean; | ||
| v_request_apikey text := NULLIF(btrim(p_apikey), ''); | ||
| BEGIN | ||
| IF p_permission_key IS NULL OR p_permission_key = '' THEN | ||
| RETURN false; | ||
| END IF; | ||
|
|
||
| SELECT s.ok, s.effective_org_id, s.effective_app_id | ||
| INTO v_scope_ok, v_effective_org_id, v_effective_app_id | ||
| FROM public.rbac_resolve_permission_scope(p_org_id, p_app_id, p_channel_id) AS s; | ||
|
|
||
| IF NOT COALESCE(v_scope_ok, false) THEN | ||
| RETURN false; | ||
| END IF; | ||
|
|
||
| v_use_apikey := public.rbac_should_use_apikey_principal(p_user_id, p_apikey); | ||
|
|
||
| IF v_use_apikey THEN | ||
| SELECT * INTO v_api_key | ||
| FROM public.find_apikey_by_value(v_request_apikey) | ||
| LIMIT 1; | ||
|
|
||
| IF v_api_key.id IS NULL | ||
| OR (p_user_id IS NOT NULL AND p_user_id IS DISTINCT FROM v_api_key.user_id) | ||
| OR v_effective_org_id IS NULL | ||
| THEN | ||
| RETURN false; | ||
| END IF; | ||
|
|
||
| IF public.is_apikey_expired(v_api_key.expires_at) THEN | ||
| RETURN false; | ||
| END IF; | ||
|
|
||
| v_effective_user_id := v_api_key.user_id; | ||
|
|
||
| v_allowed := public.rbac_has_permission( | ||
| public.rbac_principal_apikey(), | ||
| v_api_key.rbac_id, | ||
| p_permission_key, | ||
| v_effective_org_id, | ||
| v_effective_app_id, | ||
| p_channel_id | ||
| ); | ||
|
|
||
| IF v_channel_scope | ||
| AND public.rbac_principal_has_org_binding( | ||
| public.rbac_principal_apikey(), | ||
| v_api_key.rbac_id, | ||
| v_effective_org_id | ||
| ) | ||
|
cursor[bot] marked this conversation as resolved.
|
||
| THEN | ||
| SELECT o.is_allowed INTO v_override | ||
| FROM public.channel_permission_overrides o | ||
| WHERE o.principal_type = public.rbac_principal_apikey() | ||
| AND o.principal_id = v_api_key.rbac_id | ||
| AND o.channel_id = p_channel_id | ||
| AND o.permission_key = p_permission_key | ||
| LIMIT 1; | ||
|
|
||
| IF v_override IS NOT NULL THEN | ||
| v_allowed := v_override; | ||
| END IF; | ||
| END IF; | ||
|
|
||
| RETURN v_allowed; | ||
| END IF; | ||
|
|
||
| IF v_effective_org_id IS NOT NULL THEN | ||
| IF (SELECT enforcing_2fa FROM public.orgs WHERE id = v_effective_org_id) | ||
| AND (v_effective_user_id IS NULL OR NOT public.has_2fa_enabled(v_effective_user_id)) | ||
| THEN | ||
| RETURN false; | ||
| END IF; | ||
|
|
||
| IF public.user_meets_password_policy(v_effective_user_id, v_effective_org_id) = false THEN | ||
| RETURN false; | ||
| END IF; | ||
| END IF; | ||
|
|
||
| IF v_effective_user_id IS NULL THEN | ||
| RETURN false; | ||
| END IF; | ||
|
|
||
| v_allowed := public.rbac_has_permission( | ||
| public.rbac_principal_user(), | ||
| v_effective_user_id, | ||
| p_permission_key, | ||
| v_effective_org_id, | ||
| v_effective_app_id, | ||
| p_channel_id | ||
| ); | ||
|
|
||
| IF v_channel_scope | ||
| AND public.rbac_principal_has_org_binding( | ||
| public.rbac_principal_user(), | ||
| v_effective_user_id, | ||
| v_effective_org_id | ||
| ) | ||
| THEN | ||
| SELECT o.is_allowed INTO v_override | ||
| FROM public.channel_permission_overrides o | ||
| WHERE o.principal_type = public.rbac_principal_user() | ||
| AND o.principal_id = v_effective_user_id | ||
| AND o.channel_id = p_channel_id | ||
| AND o.permission_key = p_permission_key | ||
| LIMIT 1; | ||
|
|
||
| IF v_override IS NOT NULL THEN | ||
| v_allowed := v_override; | ||
| END IF; | ||
| END IF; | ||
|
|
||
| RETURN v_allowed; | ||
| END; | ||
| $$; | ||
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.