Restrict invite_user_to_org RPC to authenticated callers#1710
Restrict invite_user_to_org RPC to authenticated callers#1710
Conversation
📝 WalkthroughWalkthroughAdds a new SECURITY DEFINER SQL function Changes
Sequence Diagram(s)sequenceDiagram
participant Caller
participant DB_Function as invite_user_to_org
participant OrgsTable as Orgs
participant UsersTable as Users
participant TmpUsers as tmp_users
Caller->>DB_Function: CALL invite_user_to_org(email, org_id, invite_type)
DB_Function->>OrgsTable: validate org exists
DB_Function->>DB_Function: resolve caller identity & rights (RBAC / super-admin)
alt super-admin and 2FA required
DB_Function->>Caller: require 2FA (verify)
end
DB_Function->>UsersTable: check existing user by email
alt user exists
DB_Function->>DB_Function: create org membership/invite -> return OK or ALREADY_INVITED
else user does not exist
DB_Function->>TmpUsers: check/create pending invite, enforce recent-cancel rules
DB_Function->>DB_Function: return OK / ALREADY_INVITED / TOO_RECENT_INVITATION_CANCELATION / NO_EMAIL
end
DB_Function->>Caller: return status
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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/migrations/20260227150000_fix_invite_user_to_org_security.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: Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6c7b781740
ℹ️ 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".
| REVOKE EXECUTE ON FUNCTION public.invite_user_to_org( | ||
| character varying, | ||
| uuid, | ||
| public.user_min_right | ||
| ) FROM "anon"; |
There was a problem hiding this comment.
Reinstate anon execute on invite_user_to_org
/organization/members still calls invite_user_to_org for non-RBAC orgs (supabase/functions/_backend/public/organization/members/post.ts, legacy branch) through supabaseApikey, which authenticates with the anon key plus capgkey header (supabase/functions/_backend/utils/supabase.ts). Revoking anon execute here causes those valid API-key requests to fail with function-permission errors, so member invites break for legacy orgs; keep anon execute and rely on the function’s internal get_identity_org_allowed/check_min_rights checks to reject unauthenticated callers.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 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/20260227150000_fix_invite_user_to_org_security.sql`:
- Around line 62-63: The 2FA check and audit log are using auth.uid() but
authorization earlier uses calling_user_id from get_identity_org_allowed(), so
update the IF and pg_log call in invite_user_to_org to use calling_user_id
instead of auth.uid(): call public.has_2fa_enabled(calling_user_id) in the IF
condition when org.enforcing_2fa is true, and include 'uid': calling_user_id in
the jsonb_build_object passed to public.pg_log so the correct resolved caller is
checked and recorded; keep all other logic and names (invite_user_to_org,
org.enforcing_2fa, public.has_2fa_enabled, public.pg_log) unchanged.
- Around line 119-135: The function public.invite_user_to_org currently revokes
EXECUTE only from "anon" but inherits EXECUTE from PUBLIC; before the existing
REVOKE on "anon" add an explicit REVOKE EXECUTE ON FUNCTION
public.invite_user_to_org(character varying, uuid, public.user_min_right) FROM
PUBLIC so the PUBLIC grant is removed first, then keep the existing REVOKE FROM
"anon" and the subsequent GRANTs to "authenticated" and "service_role"; adjust
the ACL order around the invite_user_to_org function to ensure PUBLIC cannot
execute it after CREATE OR REPLACE.
supabase/migrations/20260227150000_fix_invite_user_to_org_security.sql
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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/20260227150000_fix_invite_user_to_org_security.sql`:
- Around line 125-129: The GRANT that gives EXECUTE on the SECURITY DEFINER RPC
invite_user_to_org to the "anon" role must be removed and replaced with a grant
to the authenticated role; locate the GRANT EXECUTE ON FUNCTION
public.invite_user_to_org(...) TO "anon" statement and delete it (or change the
grantee) so that only the authenticated role (not "anon") is granted EXECUTE,
ensuring the RPC can only be called by JWT-authenticated clients.
| GRANT EXECUTE ON FUNCTION public.invite_user_to_org( | ||
| character varying, | ||
| uuid, | ||
| public.user_min_right | ||
| ) TO "anon"; |
There was a problem hiding this comment.
Remove anon EXECUTE grant to match the security objective.
Line 125 re-grants EXECUTE to "anon", which reopens unauthenticated access to this SECURITY DEFINER RPC and conflicts with the stated goal of authenticated-only invocation.
Proposed fix
-GRANT EXECUTE ON FUNCTION public.invite_user_to_org(
+REVOKE EXECUTE ON FUNCTION public.invite_user_to_org(
character varying,
uuid,
public.user_min_right
-) TO "anon";
+) FROM "anon";Based on learnings: API keys authenticate through the authenticated role via JWT, not the anon role.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@supabase/migrations/20260227150000_fix_invite_user_to_org_security.sql`
around lines 125 - 129, The GRANT that gives EXECUTE on the SECURITY DEFINER RPC
invite_user_to_org to the "anon" role must be removed and replaced with a grant
to the authenticated role; locate the GRANT EXECUTE ON FUNCTION
public.invite_user_to_org(...) TO "anon" statement and delete it (or change the
grantee) so that only the authenticated role (not "anon") is granted EXECUTE,
ensuring the RPC can only be called by JWT-authenticated clients.
|



Summary (AI generated)
public.invite_user_to_orgto remove org-id enumeration and keep invitation status handling intact for existing callers.calling_user_id.PUBLICaccess while keeping legacy API-key flows compatible through explicitanonexecution rights.Motivation (AI generated)
anonexecution while still exposing the function toPUBLIC, and usedauth.uid()in 2FA checks where authenticated API-key flows resolve identity differently./organization/membersand related legacy flows use API-key-authenticatedsupabaseApikeyclients on theanonrole, so fully revokinganonwould break legitimate invitations.Business Impact (AI generated)
Test Plan (AI generated)
bun lint:backend./organization/memberslegacy org path (rbac=false) with a valid API key./organization/memberswith a non-existent org ID and verifyNO_RIGHTSresponse.Summary by CodeRabbit