Skip to content

fix: enforce org scope on organization members endpoint#1866

Open
riderx wants to merge 4 commits intomainfrom
fix/advisory-q7xv-org-members-scope
Open

fix: enforce org scope on organization members endpoint#1866
riderx wants to merge 4 commits intomainfrom
fix/advisory-q7xv-org-members-scope

Conversation

@riderx
Copy link
Copy Markdown
Member

@riderx riderx commented Mar 26, 2026

Summary (AI generated)

  • enforce limited_to_orgs + org API key policy checks on GET /organization/members
  • align the members endpoint with existing /organization scope enforcement behavior
  • add a regression test proving a scoped key cannot read a sibling organization

Motivation (AI generated)

A draft security advisory showed that an org-limited API key could still read another organization's member list when the key owner had broader access. The endpoint already used RBAC, but it skipped the explicit API-key org-scope check used by sibling handlers.

Business Impact (AI generated)

This closes an authorization bypass that exposed organization membership metadata across org boundaries, which improves safe API key delegation and reduces customer-facing security risk.

Test Plan (AI generated)

  • bun run lint:backend
  • bun run supabase:with-env -- bunx vitest run tests/organization-api.test.ts
  • bun run test:backend

Generated with AI

Summary by CodeRabbit

  • Bug Fixes

    • Strengthened authorization so API keys are validated against organization policies; access is denied more precisely when a key lacks required rights.
    • Returns a distinct error when an organization requires an expiring/renewable key.
    • Improved handling and logging of policy lookup failures to avoid inadvertently permitting access.
  • Tests

    • Added tests confirming read-limited API keys cannot access organization members outside their authorized scope and that valid keys succeed.

@riderx riderx added the codex label Mar 26, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 26, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5cfc5abd-e0fc-4d24-9695-2546ee3f174c

📥 Commits

Reviewing files that changed from the base of the PR and between e7e9042 and 1b2564e.

📒 Files selected for processing (1)
  • supabase/functions/_backend/public/organization/members/get.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • supabase/functions/_backend/public/organization/members/get.ts

📝 Walkthrough

Walkthrough

Added a policy-aware API-key authorization gate to the organization members GET handler: the handler now computes an effective API key (preferring request auth), checks org policy via a Supabase client before creating the RLS-backed client, and surfaces a specific expiring-key error path when applicable.

Changes

Cohort / File(s) Summary
Organization members handler
supabase/functions/_backend/public/organization/members/get.ts
Imported quickError alongside simpleError; read optional request auth and set effectiveApikey = auth?.apikey ?? apikey; perform apikeyHasOrgRightWithPolicy(c, effectiveApikey, body.orgId, supabase) using a supabase client created with effectiveApikey.key before initializing the RLS client and calling the members RPC; throw quickError(401, 'org_requires_expiring_key', ...) for that specific policy failure, otherwise simpleError('cannot_access_organization', ...); pass effectiveApikey.user_id to the RPC.
Supabase utils / policy check
supabase/functions/_backend/utils/supabase.ts
checkApikeyMeetsOrgPolicy now destructures { data: org, error }, logs lookup failures with cloudlogErr, and returns { valid: false, error: 'org_policy_lookup_failed' } when the org query errors or returns no row (previously could treat missing org as valid).
Tests — authorization coverage
tests/organization-api.test.ts
Added two concurrent tests for GET /organization/members: one positive case using default headers (expects 200 and member present), and one negative case using a read-only API key limited to limited_to_orgs (expects HTTP 400 with JSON error cannot_access_organization).

Sequence Diagram(s)

sequenceDiagram
  participant Client as Client
  participant Func as MembersHandler
  participant Policy as ApiKeyPolicyChecker
  participant SBPolicy as Supabase (policy lookup)
  participant SB as Supabase (RLS RPC)

  Client->>Func: GET /organization/members (apikey header / auth)
  Func->>Func: derive effectiveApikey = auth?.apikey ?? apikey
  Func->>SBPolicy: create client with effectiveApikey.key
  Func->>Policy: apikeyHasOrgRightWithPolicy(effectiveApikey, orgId)
  alt policy valid
    Func->>SB: create RLS client with effectiveApikey.key
    Func->>SB: call get_org_members RPC (user_id = effectiveApikey.user_id)
    SB-->>Func: members data
    Func-->>Client: 200 + members
  else requires expiring key
    Policy-->>Func: error = org_requires_expiring_key
    Func-->>Client: 401 org_requires_expiring_key
  else policy invalid/other
    Policy-->>Func: invalid
    Func-->>Client: 400 cannot_access_organization
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 I hopped through headers, sniffed the key,
I checked the rules before letting them see,
If keys must fade or policies bind,
I guard the list and leave no hole behind,
A carrot for safe members — hop, hop, hooray! 🥕

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description includes a summary of changes and motivation, but lacks the required 'Test plan' section and incomplete 'Checklist' section as specified in the template. Add a dedicated 'Test plan' section with step-by-step testing instructions and complete the 'Checklist' by checking applicable boxes to verify code quality and test coverage requirements.
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title directly and clearly describes the main security fix: enforcing organization scope on the members endpoint, which matches the primary objective of the changeset.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/advisory-q7xv-org-members-scope

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: 7485addb1c

ℹ️ 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".

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

🧹 Nitpick comments (1)
tests/organization-api.test.ts (1)

225-234: Pin the bypass precondition in this regression.

This still passes if USER_ID ever loses access to ORG_ID, which would turn it into a generic “non-member gets 400” test instead of the scoped-key bypass case. Add a control request that proves the same principal can read ORG_ID without the limited_to_orgs restriction before asserting the scoped key is blocked.

Suggested tweak
 it.concurrent('rejects GET /organization/members outside limited_to_orgs scope', async () => {
+  const ownerResponse = await fetch(`${BASE_URL}/organization/members?orgId=${ORG_ID}`, {
+    headers,
+    method: 'GET',
+  })
+  expect(ownerResponse.status).toBe(200)
+
   const response = await fetch(`${BASE_URL}/organization/members?orgId=${ORG_ID}`, {
     headers: { ...readOnlyHeaders, capgkey: readOnlyKey },
     method: 'GET',
   })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/organization-api.test.ts` around lines 225 - 234, Add a preceding
control request that proves the same principal can read ORG_ID without the
scoped key: before the existing scoped-key fetch, call
fetch(`${BASE_URL}/organization/members?orgId=${ORG_ID}`) using the principal's
normal auth headers (i.e., omit the capgkey/readOnlyKey or use the non-limited
headers used elsewhere in tests), assert a successful status (e.g., 200) and
that the payload contains expected members; then keep the current scoped-key
request (headers: { ...readOnlyHeaders, capgkey: readOnlyKey }) and assert it
returns 400 with error 'cannot_access_organization' so the test proves the
failure is due to limited_to_orgs scope rather than a general loss of access.
🤖 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/functions/_backend/public/organization/members/get.ts`:
- Around line 47-53: The org-policy check currently treats missing/errored
policy lookups as success; update the lookup logic in checkApikeyMeetsOrgPolicy
(and any helper used by apikeyHasOrgRightWithPolicy) so that if the DB query
returns no rows or an error occurs it returns { valid: false, error:
'org_policy_lookup_failed' } (or similar) instead of { valid: true }; ensure
apikeyHasOrgRightWithPolicy surfaces that failure so the caller (the
get_org_members flow) will throw the 401/deny branch rather than allowing access
when the policy lookup fails.

---

Nitpick comments:
In `@tests/organization-api.test.ts`:
- Around line 225-234: Add a preceding control request that proves the same
principal can read ORG_ID without the scoped key: before the existing scoped-key
fetch, call fetch(`${BASE_URL}/organization/members?orgId=${ORG_ID}`) using the
principal's normal auth headers (i.e., omit the capgkey/readOnlyKey or use the
non-limited headers used elsewhere in tests), assert a successful status (e.g.,
200) and that the payload contains expected members; then keep the current
scoped-key request (headers: { ...readOnlyHeaders, capgkey: readOnlyKey }) and
assert it returns 400 with error 'cannot_access_organization' so the test proves
the failure is due to limited_to_orgs scope rather than a general loss of
access.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9ef516b6-dad4-4ef3-8c6a-3aa677f62a2d

📥 Commits

Reviewing files that changed from the base of the PR and between 7f89965 and 7485add.

📒 Files selected for processing (2)
  • supabase/functions/_backend/public/organization/members/get.ts
  • tests/organization-api.test.ts

@riderx
Copy link
Copy Markdown
Member Author

riderx commented Mar 27, 2026

Pushed fixes for org members scope enforcement and policy lookup fail-closed, plus added a control test for unrestricted access. Commit: e7e9042.\n\nTest run: failed locally because Supabase CLI is not installed in this environment.\n\nPR: #1866

@riderx
Copy link
Copy Markdown
Member Author

riderx commented Mar 27, 2026

Pushed fixes for org members scope enforcement and policy lookup fail-closed, plus added a control test for unrestricted access. Commit: e7e9042.

Test run: bun run supabase:with-env -- bunx vitest run tests/organization-api.test.ts failed locally because Supabase CLI is not installed in this environment.

PR: #1866

@codspeed-hq
Copy link
Copy Markdown
Contributor

codspeed-hq bot commented Mar 27, 2026

Merging this PR will not alter performance

✅ 28 untouched benchmarks


Comparing fix/advisory-q7xv-org-members-scope (1b2564e) with main (a843519)

Open in CodSpeed

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.

🧹 Nitpick comments (1)
supabase/functions/_backend/public/organization/members/get.ts (1)

58-62: Inconsistent apikey reference in RPC call.

The policy check and Supabase client use effectiveApikey, but the RPC call uses apikey.user_id (the original parameter). While these should be identical when set by middleware, using different references is inconsistent and could cause subtle issues if the code evolves.

♻️ Suggested fix for consistency
   const { data, error } = await supabase
     .rpc('get_org_members', {
-      user_id: apikey.user_id,
+      user_id: effectiveApikey.user_id,
       guild_id: body.orgId,
     })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@supabase/functions/_backend/public/organization/members/get.ts` around lines
58 - 62, The RPC call to get_org_members is using apikey.user_id while the rest
of the function and policy checks use effectiveApikey; change the RPC parameter
to use effectiveApikey.user_id for consistency (update the call on
supabase.rpc('get_org_members', { user_id: ..., guild_id: body.orgId }) to pass
effectiveApikey.user_id) so all checks and the Supabase call reference the same
apikey object.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@supabase/functions/_backend/public/organization/members/get.ts`:
- Around line 58-62: The RPC call to get_org_members is using apikey.user_id
while the rest of the function and policy checks use effectiveApikey; change the
RPC parameter to use effectiveApikey.user_id for consistency (update the call on
supabase.rpc('get_org_members', { user_id: ..., guild_id: body.orgId }) to pass
effectiveApikey.user_id) so all checks and the Supabase call reference the same
apikey object.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7cf59f02-fb4e-4b34-b22b-ff9c9daef450

📥 Commits

Reviewing files that changed from the base of the PR and between 7485add and e7e9042.

📒 Files selected for processing (3)
  • supabase/functions/_backend/public/organization/members/get.ts
  • supabase/functions/_backend/utils/supabase.ts
  • tests/organization-api.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/organization-api.test.ts

@riderx
Copy link
Copy Markdown
Member Author

riderx commented Mar 29, 2026

PR #1866 I pushed a one-line fix in supabase/functions/_backend/public/organization/members/get.ts so get_org_members uses effectiveApikey.user_id. The remaining CI failure is in tests/audit-logs.test.ts (expected 200 vs 400 and changed_fields missing comment), which looks unrelated to the org-members diff and needs a separate audit-logs investigation.

@sonarqubecloud
Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant