Skip to content

Fix webhook org-scope precedence#1753

Open
riderx wants to merge 3 commits intomainfrom
riderx/fix-webhook-org-scope
Open

Fix webhook org-scope precedence#1753
riderx wants to merge 3 commits intomainfrom
riderx/fix-webhook-org-scope

Conversation

@riderx
Copy link
Member

@riderx riderx commented Mar 8, 2026

Summary (AI generated)

  • Added migration 20260308235013_webhook-org-scope-api-key-org-check.sql to enforce API-key-first authorization on webhook and webhook_delivery RLS paths, including org-scoped checks and safe update validation.
  • Added regression coverage in tests/hashed-apikey-rls.test.ts for webhook read precedence and unauthorized webhook_deliveries.org_id updates when both user auth and an out-of-scope key are present.

Motivation (AI generated)

  • API-key-limited org scope should be authoritative so an authenticated user cannot bypass it when a capgkey header is supplied.

Business Impact (AI generated)

  • Prevents cross-organization webhook data access and unauthorized webhook delivery mutations with mismatched user/key identity, reducing the risk of webhook data leakage.

Test Plan (AI generated)

  • bun lint:backend
  • bun test tests/hashed-apikey-rls.test.ts (targeted regression)

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 8, 2026

📝 Walkthrough

Walkthrough

This migration hardens API-key-scoped authorization for webhooks and webhook_deliveries by dropping existing RLS policies and recreating them with conditional routing: API-key identity checks via get_apikey_header, get_identity_org_allowed, and check_min_rights when an API key is present, otherwise falling back to user ID authorization across all CRUD operations.

Changes

Cohort / File(s) Summary
Webhook Authorization Hardening
supabase/migrations/20260308180224_webhook-api-key-org-scope-hardening.sql
Drops and recreates RLS policies for public.webhooks and public.webhook_deliveries. Implements API-key-first authorization routing with conditional identity checks (SELECT, INSERT, UPDATE, DELETE operations), setting appropriate key_mode flags and falling back to user ID when no API key present.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Policy as RLS Policy Engine
    participant KeyFunc as get_apikey_header()
    participant OrgFunc as get_identity_org_allowed()
    participant RightsFunc as check_min_rights()
    participant Fallback as User ID Auth
    
    Client->>Policy: Request webhooks/deliveries (with/without API key)
    Policy->>KeyFunc: Check for API key header
    alt API Key Present
        KeyFunc-->>Policy: API key found
        Policy->>OrgFunc: Validate org scope
        OrgFunc-->>Policy: Scope valid?
        alt Scope Valid
            Policy->>RightsFunc: Check operation rights
            RightsFunc-->>Policy: Rights granted?
            alt Rights Sufficient
                Policy-->>Client: ✓ Access allowed
            else
                Policy-->>Client: ✗ Access denied
            end
        else
            Policy-->>Client: ✗ Org mismatch
        end
    else No API Key
        KeyFunc-->>Policy: No API key
        Policy->>Fallback: Route to user ID auth
        Fallback-->>Policy: User authorized?
        alt User Valid
            Policy-->>Client: ✓ Access allowed
        else
            Policy-->>Client: ✗ Access denied
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 A migration hopped in with keys so tight,
API scope checks guard the webhook night,
Org boundaries held, rights verified true,
Fallback to users when API keys are through!
Secrets stay safe as policies dance—
Authorization hardened at last, by a rabbit's advance! 🔐

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check ✅ Passed The title 'Fix webhook org-scope precedence' is concise and directly addresses the main change—enforcing API-key-scoped authorization with proper precedence in webhook access policies.
Description check ✅ Passed The description includes Summary, Motivation, Business Impact, and Test Plan sections covering the key aspects. However, the description is mostly AI-generated summaries rather than detailed author explanation, and some template sections (Checklist items) lack explicit checkmarks or completion status.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch riderx/fix-webhook-org-scope

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

@riderx riderx changed the title Fix webhook key org-scope and policy Fix webhook scope enforcement for API keys Mar 8, 2026
@riderx riderx force-pushed the riderx/fix-webhook-org-scope branch from 3c11624 to a1be230 Compare March 8, 2026 18:03
@riderx riderx changed the title Fix webhook scope enforcement for API keys fix: enforce API-key precedence for webhook RLS Mar 8, 2026
Copy link
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/20260308091000_restrict_webhook_key_modes.sql`:
- Around line 138-153: The UPDATE policy "Allow admin to update
webhook_deliveries" on public.webhook_deliveries is missing a WITH CHECK clause
so new values (e.g., org_id) aren't validated; add a WITH CHECK that mirrors the
USING condition but evaluates the proposed row (use NEW.org_id or equivalent)
and calls public.check_min_rights(...) with
public.get_identity_org_allowed('{all,write,upload}'::public.key_mode[],
NEW.org_id) so updates are authorized against the target org; reference the
existing webhooks UPDATE policy for the exact predicate structure and use the
same functions public.check_min_rights and public.get_identity_org_allowed.
- Around line 1-11: The migration 20260308091000_restrict_webhook_key_modes.sql
references the function public.get_identity_org_allowed() which doesn't exist
until migration 20260309091500_enforce_api_key_app_scoped_org_access.sql,
causing the migration to fail; fix by ensuring the function exists before this
policy migration — either rename 20260308091000 to a timestamp after
20260309091500 so it runs later, or merge the two migrations into one file with
the CREATE FUNCTION public.get_identity_org_allowed(...) placed before the ALTER
POLICY/GRANT statements that reference it.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 992b48c6-5fb1-44ad-9ea8-5c4b6c78ed88

📥 Commits

Reviewing files that changed from the base of the PR and between 2af3a08 and b28a0c5.

📒 Files selected for processing (2)
  • supabase/migrations/20260308091000_restrict_webhook_key_modes.sql
  • supabase/migrations/20260309091500_enforce_api_key_app_scoped_org_access.sql

Comment on lines +1 to +11
-- =============================================================================
-- Migration: Restrict webhook table access to write-capable API keys
--
-- Direct REST queries with read-only API keys can currently resolve identity via
-- get_identity_org_allowed() because its key mode allow-list includes `read`.
-- That enables webhook secret reads from public webhooks table.
--
-- This migration tightens webhook and webhook_deliveries policies so only
-- all/upload/write-capable keys can satisfy the identity checks for any
-- write/read operation and the same org-scoped admin checks apply.
-- =============================================================================
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: Migration order causes failure — function does not exist yet.

This migration (timestamp 20260308091000) calls public.get_identity_org_allowed(), but that function is created in 20260309091500_enforce_api_key_app_scoped_org_access.sql (timestamp 20260309091500). Since migrations run in timestamp order, this policy migration executes first and will fail with a "function does not exist" error.

🔧 Suggested fix

Rename this migration file to have a timestamp after the function migration:

- 20260308091000_restrict_webhook_key_modes.sql
+ 20260309092000_restrict_webhook_key_modes.sql

Or alternatively, merge both migrations into a single file with the function defined before the policies.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@supabase/migrations/20260308091000_restrict_webhook_key_modes.sql` around
lines 1 - 11, The migration 20260308091000_restrict_webhook_key_modes.sql
references the function public.get_identity_org_allowed() which doesn't exist
until migration 20260309091500_enforce_api_key_app_scoped_org_access.sql,
causing the migration to fail; fix by ensuring the function exists before this
policy migration — either rename 20260308091000 to a timestamp after
20260309091500 so it runs later, or merge the two migrations into one file with
the CREATE FUNCTION public.get_identity_org_allowed(...) placed before the ALTER
POLICY/GRANT statements that reference it.

Comment on lines +138 to +153
CREATE POLICY "Allow admin to update webhook_deliveries"
ON public.webhook_deliveries
FOR UPDATE
TO authenticated, anon
USING (
public.check_min_rights(
'admin'::public.user_min_right,
public.get_identity_org_allowed(
'{all,write,upload}'::public.key_mode[],
org_id
),
org_id,
null::character varying,
null::bigint
)
);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Missing WITH CHECK clause on UPDATE policy allows potential org_id manipulation.

Unlike the webhooks UPDATE policy (lines 53-80), this policy only has USING without WITH CHECK. A user with access to the current row could potentially update org_id to an unauthorized organization since the new values aren't validated.

🔧 Suggested fix
 CREATE POLICY "Allow admin to update webhook_deliveries"
 ON public.webhook_deliveries
 FOR UPDATE
 TO authenticated, anon
 USING (
     public.check_min_rights(
         'admin'::public.user_min_right,
         public.get_identity_org_allowed(
             '{all,write,upload}'::public.key_mode[],
             org_id
         ),
         org_id,
         null::character varying,
-            null::bigint
+        null::bigint
     )
+)
+WITH CHECK (
+    public.check_min_rights(
+        'admin'::public.user_min_right,
+        public.get_identity_org_allowed(
+            '{all,write,upload}'::public.key_mode[],
+            org_id
+        ),
+        org_id,
+        null::character varying,
+        null::bigint
+    )
 );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
CREATE POLICY "Allow admin to update webhook_deliveries"
ON public.webhook_deliveries
FOR UPDATE
TO authenticated, anon
USING (
public.check_min_rights(
'admin'::public.user_min_right,
public.get_identity_org_allowed(
'{all,write,upload}'::public.key_mode[],
org_id
),
org_id,
null::character varying,
null::bigint
)
);
CREATE POLICY "Allow admin to update webhook_deliveries"
ON public.webhook_deliveries
FOR UPDATE
TO authenticated, anon
USING (
public.check_min_rights(
'admin'::public.user_min_right,
public.get_identity_org_allowed(
'{all,write,upload}'::public.key_mode[],
org_id
),
org_id,
null::character varying,
null::bigint
)
)
WITH CHECK (
public.check_min_rights(
'admin'::public.user_min_right,
public.get_identity_org_allowed(
'{all,write,upload}'::public.key_mode[],
org_id
),
org_id,
null::character varying,
null::bigint
)
);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@supabase/migrations/20260308091000_restrict_webhook_key_modes.sql` around
lines 138 - 153, The UPDATE policy "Allow admin to update webhook_deliveries" on
public.webhook_deliveries is missing a WITH CHECK clause so new values (e.g.,
org_id) aren't validated; add a WITH CHECK that mirrors the USING condition but
evaluates the proposed row (use NEW.org_id or equivalent) and calls
public.check_min_rights(...) with
public.get_identity_org_allowed('{all,write,upload}'::public.key_mode[],
NEW.org_id) so updates are authorized against the target org; reference the
existing webhooks UPDATE policy for the exact predicate structure and use the
same functions public.check_min_rights and public.get_identity_org_allowed.

Copy link
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/20260308180224_webhook-api-key-org-scope-hardening.sql`:
- Around line 23-24: The SELECT allowlist for API keys mistakenly includes the
"read" mode; update the calls to public.get_identity_org_allowed that pass
'{read,upload,write,all}'::public.key_mode[] (seen around the
webhook/webhook_deliveries allowlist) to remove "read" so they pass
'{upload,write,all}'::public.key_mode[] instead; apply the same change to the
other occurrence referenced (lines near the second call to
public.get_identity_org_allowed).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: dba9de6b-5484-47ab-b7d8-79a06c9d080b

📥 Commits

Reviewing files that changed from the base of the PR and between b28a0c5 and a1be230.

📒 Files selected for processing (1)
  • supabase/migrations/20260308180224_webhook-api-key-org-scope-hardening.sql

@riderx riderx changed the title fix: enforce API-key precedence for webhook RLS Fix webhook org-scope precedence Mar 8, 2026
@sonarqubecloud
Copy link

sonarqubecloud bot commented Mar 8, 2026

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