Skip to content

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

Merged
riderx merged 1 commit into
mainfrom
fix-member-removal-rbac
Feb 28, 2026
Merged

fix(security): clean up role_bindings on member removal#1722
riderx merged 1 commit into
mainfrom
fix-member-removal-rbac

Conversation

@Dalanir
Copy link
Copy Markdown
Contributor

@Dalanir Dalanir commented Feb 28, 2026

Summary

  • Fixes RBAC privilege persistence vulnerability where removed org members retained their role bindings (security issues retribution #1667)
  • deleteMember now deletes all role_bindings entries for the user within the org (all scopes: org, app, channel, bundle) after removing from org_users
  • Uses supabaseAdmin — permission already validated via checkPermission('org.update_user_roles') before any mutations

Test plan

  • 'delete organization member' test extended: seeds an org_member role_binding before deletion, asserts it is removed after
  • All other member deletion error-case tests unchanged
  • Run bun test tests/organization-api.test.ts to verify

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Fixed organization member removal to ensure all associated role bindings and access controls are properly cleaned up when a user is removed from an organization.
  • Tests

    • Added test coverage to verify that role bindings are completely removed along with the user when organization members are deleted.

When a member is removed from an org, delete all their role_bindings
entries for that org (all scopes) in addition to the org_users row.
Prevents RBAC privilege persistence after member removal (#1667).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 28, 2026

📝 Walkthrough

Walkthrough

Adds cleanup logic to remove RBAC role bindings when a user is deleted from an organization, with corresponding test coverage to verify the role_bindings entries are properly removed alongside org_users records.

Changes

Cohort / File(s) Summary
Role Binding Cleanup
supabase/functions/_backend/public/organization/members/delete.ts
Adds deletion of role_bindings table entries (where principal_type='user' and org_id matches) immediately after removing a user from organization, with dedicated error handling.
Test Coverage
tests/organization-api.test.ts
Extends member deletion test to seed a role_bindings entry and verify it is cleaned up alongside the org_users record when a member is deleted.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐰 When members hop away with haste,
We clean their bindings, not a trace to waste!
The roles they had must disappear,
Just as the user's org record, clear.
Hopping through databases with care and grace,
Leaving the organization a tidy place! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: cleaning up role_bindings on member removal, which is the core security fix in this changeset.
Description check ✅ Passed The description covers the summary (vulnerability fix and solution), test plan with checkbox items, and references the related issue. All major required sections are present and adequately filled out.

✏️ 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 fix-member-removal-rbac

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


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
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)
tests/organization-api.test.ts (1)

328-339: Consider adding error handling for the role lookup.

The query at line 329 doesn't destructure the error. If the 'org_member' role doesn't exist in seed data, roleData will be null and the ! assertion on line 334 will throw a runtime error with an unclear message.

🔧 Proposed improvement for clearer test failure messages
     // Seed a role_binding to verify it gets cleaned up on member removal
-    const { data: roleData } = await getSupabaseClient().from('roles').select('id').eq('name', 'org_member').single()
+    const { data: roleData, error: roleError } = await getSupabaseClient().from('roles').select('id').eq('name', 'org_member').single()
+    expect(roleError).toBeNull()
     expect(roleData).toBeTruthy()
🤖 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 328 - 339, The roles lookup
ignores possible query errors and may return null roleData, causing a confusing
runtime failure when later using roleData!.id; update the query that calls
getSupabaseClient().from('roles').select('id').eq('name', 'org_member').single()
to destructure both { data: roleData, error: roleError }, assert roleError is
null and add an explicit expect(roleData).toBeTruthy() (or fail with a clear
message) before inserting into role_bindings, so the test fails with a clear
error if the 'org_member' seed is missing.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@tests/organization-api.test.ts`:
- Around line 328-339: The roles lookup ignores possible query errors and may
return null roleData, causing a confusing runtime failure when later using
roleData!.id; update the query that calls
getSupabaseClient().from('roles').select('id').eq('name', 'org_member').single()
to destructure both { data: roleData, error: roleError }, assert roleError is
null and add an explicit expect(roleData).toBeTruthy() (or fail with a clear
message) before inserting into role_bindings, so the test fails with a clear
error if the 'org_member' seed is missing.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e8a3ced and 614e451.

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

@sonarqubecloud
Copy link
Copy Markdown

@riderx riderx merged commit 574dcca into main Feb 28, 2026
13 of 14 checks passed
@riderx riderx deleted the fix-member-removal-rbac branch February 28, 2026 15:41
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.

2 participants