Skip to content

Restrict invite_user_to_org RPC to authenticated callers#1710

Merged
riderx merged 2 commits into
mainfrom
riderx/fix-invite-oracle
Mar 3, 2026
Merged

Restrict invite_user_to_org RPC to authenticated callers#1710
riderx merged 2 commits into
mainfrom
riderx/fix-invite-oracle

Conversation

@riderx
Copy link
Copy Markdown
Member

@riderx riderx commented Feb 27, 2026

Summary (AI generated)

  • Hardened public.invite_user_to_org to remove org-id enumeration and keep invitation status handling intact for existing callers.
  • Fixed caller-context usage in the super-admin 2FA gate and audit logging to rely on resolved calling_user_id.
  • Tightened execution privileges by removing blanket PUBLIC access while keeping legacy API-key flows compatible through explicit anon execution rights.

Motivation (AI generated)

  • The previous migration revoked only anon execution while still exposing the function to PUBLIC, and used auth.uid() in 2FA checks where authenticated API-key flows resolve identity differently.
  • /organization/members and related legacy flows use API-key-authenticated supabaseApikey clients on the anon role, so fully revoking anon would break legitimate invitations.
  • Reworking identity usage and ACL ordering keeps security fixes in place without regressing existing backend behavior.

Business Impact (AI generated)

  • Prevents org existence enumeration and unauthenticated privilege probing on invitation calls.
  • Preserves member-invite behavior for API-key-authenticated clients used by existing integrations.
  • Improves correctness of 2FA enforcement and audit logs for super-admin invitations.

Test Plan (AI generated)

  • Ran bun lint:backend.
  • Optional: Run a focused integration check for /organization/members legacy org path (rbac=false) with a valid API key.
  • Optional: Run a focused integration check for /organization/members with a non-existent org ID and verify NO_RIGHTS response.

Summary by CodeRabbit

  • Bug Fixes
    • Strengthened security and permission checks for organization invitations, preventing unauthorized sends and enforcing 2FA where required.
    • Fixed edge cases around existing users, pending invitations, and recently canceled invites to reduce duplicate or invalid invites.
  • Improvements
    • Invitation flow now provides clearer, consistent status outcomes so invite sender sees precise results (e.g., already invited, no rights, too-recent cancellation).

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 27, 2026

📝 Walkthrough

Walkthrough

Adds a new SECURITY DEFINER SQL function public.invite_user_to_org(email, org_id, invite_type) that authenticates callers, validates org existence and RBAC (including super-admin and 2FA checks), handles existing users and pending tmp_users invitations, returns specific status codes, and updates EXECUTE grants for access control.

Changes

Cohort / File(s) Summary
Database migration
supabase/migrations/20260227150000_fix_invite_user_to_org_security.sql
Creates/replaces public.invite_user_to_org(email varchar, org_id uuid, invite_type public.user_min_right) as SECURITY DEFINER with a fixed search_path. Implements caller authentication, org existence check, RBAC and super-admin gating (with optional 2FA), handling for existing users and pending tmp_users, returns status codes (NO_RIGHTS, OK, ALREADY_INVITED, NO_EMAIL, TOO_RECENT_INVITATION_CANCELATION), and updates REVOKE/GRANT EXECUTE statements to adjust public/anon/authenticated/service_role access.

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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

💰 Rewarded

Poem

🐰 I hopped into the SQL night,
Secured the invite with all my might,
RBAC checked, two-factor in view,
Invitations sent — both old and new,
A tiny rabbit, approving through and through.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description includes Summary, Motivation, and Business Impact sections with detailed explanations; however, it lacks a formal Test Plan section following the template structure and is missing the required Checklist. Add a formal Test Plan section with step-by-step testing instructions and complete the Checklist with appropriate checkboxes marked for applicable items (lint verification, test coverage, manual testing).
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: restricting the invite_user_to_org RPC to authenticated callers, which is the core security improvement in this migration.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch riderx/fix-invite-oracle

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.sql

User Error: No dialect was specified. You must configure a dialect or specify one on the command line using --dialect after the command. Available dialects:
ansi, athena, bigquery, clickhouse, databricks, db2, doris, duckdb, exasol, flink, greenplum, hive, impala, mariadb, materialize, mysql, oracle, postgres, redshift, snowflake, soql, sparksql, sqlite, starrocks, teradata, trino, tsql, vertica


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +119 to +123
REVOKE EXECUTE ON FUNCTION public.invite_user_to_org(
character varying,
uuid,
public.user_min_right
) FROM "anon";
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e719357 and 6c7b781.

📒 Files selected for processing (1)
  • supabase/migrations/20260227150000_fix_invite_user_to_org_security.sql

Comment thread supabase/migrations/20260227150000_fix_invite_user_to_org_security.sql Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6c7b781 and f04482b.

📒 Files selected for processing (1)
  • supabase/migrations/20260227150000_fix_invite_user_to_org_security.sql

Comment on lines +125 to +129
GRANT EXECUTE ON FUNCTION public.invite_user_to_org(
character varying,
uuid,
public.user_min_right
) TO "anon";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented Mar 2, 2026

@riderx riderx merged commit 43cd7a6 into main Mar 3, 2026
15 checks passed
@riderx riderx deleted the riderx/fix-invite-oracle branch March 3, 2026 12:03
riderx added a commit that referenced this pull request Mar 3, 2026
* fix(api): preserve channel owner on channel upsert

* fix(auth): block sensitive account actions for unverified users (#1690)

* fix(auth): block account deletion for unverified users

* fix(auth): refresh session fields for email verification gate

* fix(auth): make delete_user insert idempotent

* fix(auth): explain blocked delete/settings when email unverified

* fix(auth): block delete action when email is unverified

* fix(auth): localize resend email block and make delete_user idempotent

* Restrict invite_user_to_org RPC to authenticated callers (#1710)

* fix(db): restrict invite_user_to_org public rpc

* fix(db): use caller identity in invite 2FA check

* fix(security): restrict webhook select to admin users (#1705)

* Secure record_build_time RPC for authorized callers (#1711)

* fix(db): secure record_build_time rpc writes

* fix(db): preserve service-role record_build_time path

* fix(api): preserve channel owner on channel upsert
riderx added a commit that referenced this pull request Mar 3, 2026
* fix(security): restrict apikey oracle rpc access

* fix: webapp url

* fix: fix

* chore(release): 12.116.9

* fix: envs

* Revert "Merge pull request #1707 from Cap-go/fix_webapp_url"

This reverts commit ff20d1a.

* fix: typo

* chore(release): 12.116.10

* fix(security): restrict apikey oracle rpc access

* fix: return 503 instead of 400 for service_unavailable build errors

Builder availability errors (not configured, call failed, error response,
missing upload URL) are transient server-side failures, not client errors.
Returning 503 allows the CLI retry logic to automatically retry these
requests instead of treating them as terminal 400 errors.

* chore(release): 12.116.11

* fix: update PWD script

* fix: env vars

* fix: modal responsive

* feat: forward buildOptions + buildCredentials to builder (pass-through)

* fix: correct vue/html-indent in DemoOnboardingModal

* fix: use snake_case (build_options, build_credentials) in public API, map to camelCase for builder

* fix(security): sanitize SQL interpolation in Cloudflare Analytics Engine queries (#1702)

* chore(release): 12.116.12

* Add unit tests for builder payload shape contract

Extract buildBuilderPayload() from the inline fetch body so the
snake_case → camelCase mapping and exact key set can be tested.
6 vitest cases verify: camelCase output, no legacy credentials field,
correct metadata keys, and pass-through of contents.

* Reject deprecated `credentials` field with clear upgrade error

Old CLI clients sending the flat `credentials` field would have it
silently dropped, causing confusing builder failures. Now the proxy
explicitly rejects non-empty `credentials` with a migration message
pointing to `build_credentials`.

* fix(security): clean up role_bindings on member removal (#1722)

* chore(release): 12.116.13

* fix(security): use parameterized query in getStoreAppByIdCF to prevent SQL injection

The appId parameter was directly interpolated into the D1 SQL query string,
creating a SQL injection vulnerability. Switched to bound parameter via .bind().

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(security): prevent privilege escalation in role_bindings endpoint

Add priority_rank check so callers cannot assign or update roles with
higher privileges than their own. Without this, any user with
org.update_user_roles could escalate to org_super_admin.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(security): enforce is_assignable in role_bindings INSERT RLS policy

Direct PostgREST inserts could bypass the endpoint's is_assignable check
and assign non-assignable roles (e.g. platform_super_admin). The RLS
INSERT policy now requires the target role to have is_assignable = true.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(security): cascade all role bindings on member removal

delete_org_member_role previously only deleted the org-level binding,
leaving orphaned app/channel bindings. A removed member could retain
app-level access. Now deletes all bindings for the user in the org.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(security): add trigger to prevent deleting last super_admin binding

Direct PostgREST DELETEs on role_bindings could bypass the last
super_admin guards in delete_org_member_role. A BEFORE DELETE trigger
now rejects deletion of the last org_super_admin binding in any org.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(security): support hashed API keys in rbac_check_permission_direct

The RBAC path in rbac_check_permission_direct looked up API keys with
WHERE key = p_apikey, which silently failed for hashed keys. Switched
to find_apikey_by_value() which handles both plain-text and hashed keys.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: reword comment to pass typos CI check

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: remove unused desc import from role_bindings.ts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(security): add FOR UPDATE lock to prevent write-skew on last super_admin delete

Two concurrent DELETE transactions could both pass the count check and
both delete their rows, leaving zero super_admins. A SELECT ... FOR
UPDATE on the super_admin binding set now serializes concurrent deletes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: prevent API key privilege escalation and fix organization member deletion test

- Add validation to prevent limited API keys from creating unlimited keys
- Fix organization-api test to work with sync_org_user_to_role_binding trigger
- Change test user_right from 'invite_read' to 'read' (trigger-compatible)
- Verify trigger-created role_bindings instead of manually inserting them

* fix: allow CASCADE deletions in prevent_last_super_admin_binding_delete and fix RBAC test compatibility

- Add org existence check in trigger to allow CASCADE deletions when org is being deleted
- Add service_role bypass for administrative operations and tests
- Update tests to work with RBAC security constraints:
  - 34_test_rbac_rls.sql: Remove DELETE operation that violated super_admin protection
  - 35_test_is_admin_rbac.sql: Use service_role for test setup INSERT
- All SQL database tests now pass (860 tests)
- Backend tests remain passing (68 tests)

* fix(security): make getCallerMaxPriorityRank auth-type-aware and remove API key data leak

* chore(release): 12.116.14

* fix(security): correct API key RBAC principal mapping and remove service_role bypass

* fix(security): correct RBAC migration comments and add privilege check on delete

- Update migration comments to accurately reflect that service_role is NOT exempt

  from the last super_admin protection trigger

- Replace FOR UPDATE scan with pg_advisory_xact_lock to avoid cross-transaction deadlocks

- Add privilege-rank check in delete handler to prevent deleting higher-ranked role bindings

- Aligns with established advisory lock patterns in codebase

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* fix: add self-2fa-required message for 2FA enforcement in multiple languages

* chore(release): 12.116.15

* fix(frontend): validate 2fa before enabling org enforcement (#1729)

* chore(release): 12.116.16

* fix(deps): update vue monorepo to v3.5.29 (#1731)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(release): 12.116.17

* chore: remove unused cloudflare function getStoreAppByIdCF

* chore(release): 12.116.18

* chore: stop editing immutable base migration

* fix(frontend): disable auto demo onboarding modal (#1733)

* chore(release): 12.116.19

* fix(auth): block sensitive account actions for unverified users (#1690)

* fix(auth): block account deletion for unverified users

* fix(auth): refresh session fields for email verification gate

* fix(auth): make delete_user insert idempotent

* fix(auth): explain blocked delete/settings when email unverified

* fix(auth): block delete action when email is unverified

* fix(auth): localize resend email block and make delete_user idempotent

* Restrict invite_user_to_org RPC to authenticated callers (#1710)

* fix(db): restrict invite_user_to_org public rpc

* fix(db): use caller identity in invite 2FA check

* fix(security): restrict webhook select to admin users (#1705)

* Secure record_build_time RPC for authorized callers (#1711)

* fix(db): secure record_build_time rpc writes

* fix(db): preserve service-role record_build_time path

* fix(security): restrict apikey oracle rpc access

* chore: stop editing immutable base migration

* fix(security): restrict apikey oracle rpc access

* chore(release): 12.116.20

* fix(security): restrict apikey oracle rpc access

* chore: stop editing immutable base migration

* fix(security): restrict apikey oracle rpc access

* chore: stop editing immutable base migration

* fix(security): restrict apikey oracle rpc access

---------

Co-authored-by: WcaleNieWolny <isupermichael007@gmail.com>
Co-authored-by: WcaleNieWolny <50914789+WcaleNieWolny@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: LOLO <131777939+artylobos@users.noreply.github.com>
Co-authored-by: Jordan Lorho <jordan.lorho@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant