Skip to content

fix(rbac): allow org_admin to manage user roles in legacy permission path#2007

Closed
Dalanir wants to merge 3 commits intomainfrom
fix/org-admin-update-user-roles-legacy-mapping
Closed

fix(rbac): allow org_admin to manage user roles in legacy permission path#2007
Dalanir wants to merge 3 commits intomainfrom
fix/org-admin-update-user-roles-legacy-mapping

Conversation

@Dalanir
Copy link
Copy Markdown
Contributor

@Dalanir Dalanir commented May 1, 2026

Summary (AI generated)

  • Fixed org.update_user_roles legacy permission mapping from super_admin to admin in both SQL (rbac_legacy_right_for_permission) and TypeScript (PERMISSION_TO_LEGACY_RIGHT)
  • Added test confirming org_admin can delete a read member from an organization

Motivation (AI generated)

org_admin has org.update_user_roles explicitly granted in the RBAC system, but the legacy fallback path mapped this permission to super_admin, creating an inconsistency. This prevented org_admin users from managing members when the legacy path was used. The priority_rank guard (org_admin=90 < org_super_admin=95) already prevents privilege escalation, so the super_admin gate was unnecessarily restrictive.

Business Impact (AI generated)

Org admins can now properly manage members (invite, delete) as intended by the RBAC design. Previously this was silently blocked in the legacy path, causing confusion for customers with admin-level roles.

Test Plan (AI generated)

  • New test: org_admin can delete a read member — creates a legacy org, adds USER_ID as admin, verifies they can delete another member
  • All 61 existing organization-api.test.ts tests pass (no regressions)
  • org.delete still requires super_admin (existing test at line 1298 unchanged)
  • priority_rank guard unchanged — org_admin still cannot delete org_super_admin bindings

Generated with AI

Summary by CodeRabbit

  • Bug Fixes

    • Org admins can now update member roles under legacy RBAC settings.
  • Chores

    • Enabled the new RBAC model for all organizations still on the legacy setting.
  • Tests

    • Added an integration test verifying member deletion and permission behavior in legacy RBAC mode.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 1, 2026

📝 Walkthrough

Walkthrough

Updates legacy RBAC handling so org.update_user_roles falls back to the legacy 'admin' minimum-right, adds a migration to enable new RBAC for all orgs, adds a DB function mapping permissions to legacy rights, and adds an integration test exercising legacy-mode member deletion. (47 words)

Changes

Cohort / File(s) Summary
Permission mapping
supabase/functions/_backend/utils/rbac.ts
Changed legacy compatibility mapping so org.update_user_roles maps to legacy 'admin' (was 'super_admin').
Migrations
supabase/migrations/20260429135552_enable_rbac_all_orgs.sql, supabase/migrations/20260501161128_fix_org_admin_update_user_roles_legacy_mapping.sql
Added migration to enable new RBAC for existing legacy orgs and created public.rbac_legacy_right_for_permission(p_permission_key text) mapping function (maps org.update_user_rolesrbac_right_admin).
Tests
tests/organization-api.test.ts
Added integration test verifying an org_admin with user_right: 'admin' can delete a member in legacy RBAC mode.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐇 I hopped through rows and altered a key,
Made admins able to prune with glee,
A migration dance and a test so neat,
Legacy rights now line up complete. ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main fix: allowing org_admin to manage user roles in the legacy permission path, which is the core objective of this PR.
Description check ✅ Passed The description covers the main changes with clear motivation, business impact, and test plan sections, but lacks explicit checklist completion markers as specified in the template.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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/org-admin-update-user-roles-legacy-mapping

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.1.0)
supabase/migrations/20260429135552_enable_rbac_all_orgs.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

supabase/migrations/20260501161128_fix_org_admin_update_user_roles_legacy_mapping.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


Review rate limit: 4/5 reviews remaining, refill in 12 minutes.

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

@codspeed-hq
Copy link
Copy Markdown
Contributor

codspeed-hq Bot commented May 1, 2026

Merging this PR will not alter performance

✅ 28 untouched benchmarks


Comparing fix/org-admin-update-user-roles-legacy-mapping (b55b325) with main (7dc7b27)

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.

Actionable comments posted: 1

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

988-1014: ⚡ Quick win

Consider marking this regression test it.concurrent.

The test already uses dedicated org/customer fixtures, so it should be safe to parallelize with the rest of the file. As per coding guidelines, tests/**/*.test.ts: Design all tests for parallel execution across files; use it.concurrent() instead of it() to maximize parallelism within test files.

Suggested change
-  it('org_admin can delete a read member', async () => {
+  it.concurrent('org_admin can delete a read member', async () => {
🤖 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 988 - 1014, The test named
"org_admin can delete a read member" should be marked concurrent to allow
parallel execution; update the test declaration (the it(...) call for that test)
to use it.concurrent(...) so the spec for the function handling organization
deletion (the DELETE request to /organization/members in this test) runs
concurrently with other tests while keeping the same assertions and fixture
usage.
🤖 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/20260501161128_fix_org_admin_update_user_roles_legacy_mapping.sql`:
- Around line 12-75: The rbac_legacy_right_for_permission SQL function is
created with default PUBLIC execute rights; fix by adding explicit permission
hardening: REVOKE ALL ON FUNCTION public.rbac_legacy_right_for_permission(text)
FROM PUBLIC; then GRANT EXECUTE ON FUNCTION
public.rbac_legacy_right_for_permission(text) TO the intended role(s) (e.g.,
rbac_role or service_role) and set the function owner explicitly with ALTER
FUNCTION ... OWNER TO <owner_role>; ensure the REVOKE runs before the GRANT and
include the same signature (text) when referencing the function.

---

Nitpick comments:
In `@tests/organization-api.test.ts`:
- Around line 988-1014: The test named "org_admin can delete a read member"
should be marked concurrent to allow parallel execution; update the test
declaration (the it(...) call for that test) to use it.concurrent(...) so the
spec for the function handling organization deletion (the DELETE request to
/organization/members in this test) runs concurrently with other tests while
keeping the same assertions and fixture usage.
🪄 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: 26f0e065-1e2a-4e90-b410-f6d43f5fe726

📥 Commits

Reviewing files that changed from the base of the PR and between 8c68276 and 8fbf5b0.

📒 Files selected for processing (4)
  • supabase/functions/_backend/utils/rbac.ts
  • supabase/migrations/20260429135552_enable_rbac_all_orgs.sql
  • supabase/migrations/20260501161128_fix_org_admin_update_user_roles_legacy_mapping.sql
  • tests/organization-api.test.ts

Dalanir added 3 commits May 1, 2026 18:51
Address CodeRabbit review:
- Replace raw UPDATE with rbac_enable_for_org() loop to migrate
  org_users to role_bindings before enabling the flag
- Add rollback documentation in migration header
…path

org.update_user_roles was mapped to super_admin in the legacy fallback
(rbac_legacy_right_for_permission), preventing org_admin from deleting
members. This was inconsistent with RBAC where org_admin explicitly has
this permission. The priority_rank guard still prevents org_admin from
deleting org_super_admin bindings.
@Dalanir Dalanir force-pushed the fix/org-admin-update-user-roles-legacy-mapping branch from 8fbf5b0 to b55b325 Compare May 1, 2026 16:51
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented May 1, 2026

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 `@tests/organization-api.test.ts`:
- Around line 998-1024: The test "org_admin can delete a read member" currently
verifies the org_users row removal but not the RBAC cleanup; after the existing
verification of org_users (using getSupabaseClient() and adminDeleteOrgId /
userData!.id), add a query against the role_bindings table (select where org_id
= adminDeleteOrgId and user_id = userData!.id) and assert that the query returns
no data and an error or empty result (i.e., role binding is removed); use the
same Supabase client pattern as the existing checks to fetch from
'role_bindings' and assert the binding is gone.
🪄 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: 899c639d-8796-48d9-ab28-c1e6410df6a6

📥 Commits

Reviewing files that changed from the base of the PR and between 8fbf5b0 and b55b325.

📒 Files selected for processing (4)
  • supabase/functions/_backend/utils/rbac.ts
  • supabase/migrations/20260429135552_enable_rbac_all_orgs.sql
  • supabase/migrations/20260501161128_fix_org_admin_update_user_roles_legacy_mapping.sql
  • tests/organization-api.test.ts
✅ Files skipped from review due to trivial changes (1)
  • supabase/migrations/20260429135552_enable_rbac_all_orgs.sql
🚧 Files skipped from review as they are similar to previous changes (1)
  • supabase/functions/_backend/utils/rbac.ts

Comment on lines +998 to +1024
it.concurrent('org_admin can delete a read member', async () => {
// Add USER_ADMIN as a read member to the org
const { data: userData, error: userError } = await getSupabaseClient().from('users').select().eq('email', USER_ADMIN_EMAIL).single()
expect(userError).toBeNull()
expect(userData).toBeTruthy()

const { error: addError } = await getSupabaseClient().from('org_users').insert({
org_id: adminDeleteOrgId,
user_id: userData!.id,
user_right: 'read',
})
expect(addError).toBeNull()

// USER_ID (admin) deletes USER_ADMIN (read) — should succeed
const response = await fetch(`${BASE_URL}/organization/members?orgId=${adminDeleteOrgId}&email=${USER_ADMIN_EMAIL}`, {
headers,
method: 'DELETE',
})
expect(response.status).toBe(200)
const responseData = await response.json() as { status: string }
expect(responseData.status).toBe('ok')

// Verify the member was actually removed
const { data, error: orgUserError } = await getSupabaseClient().from('org_users').select().eq('org_id', adminDeleteOrgId).eq('user_id', userData!.id).single()
expect(orgUserError).toBeTruthy()
expect(data).toBeNull()
})
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 | 🟡 Minor | ⚡ Quick win

Also assert the lingering RBAC binding is removed.

This regression test proves the member row is deleted, but it still misses the authorization state cleanup. If a role_bindings row survives, the legacy-path bug could still slip through unnoticed.

Suggested test addition
     const { data, error: orgUserError } = await getSupabaseClient().from('org_users').select().eq('org_id', adminDeleteOrgId).eq('user_id', userData!.id).single()
     expect(orgUserError).toBeTruthy()
     expect(data).toBeNull()
+
+    const { data: bindingsAfterDelete, error: bindingsAfterDeleteError } = await getSupabaseClient()
+      .from('role_bindings')
+      .select('id')
+      .eq('principal_type', 'user')
+      .eq('principal_id', userData!.id)
+      .eq('org_id', adminDeleteOrgId)
+    expect(bindingsAfterDeleteError).toBeNull()
+    expect(bindingsAfterDelete).toHaveLength(0)
   })
 })
📝 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
it.concurrent('org_admin can delete a read member', async () => {
// Add USER_ADMIN as a read member to the org
const { data: userData, error: userError } = await getSupabaseClient().from('users').select().eq('email', USER_ADMIN_EMAIL).single()
expect(userError).toBeNull()
expect(userData).toBeTruthy()
const { error: addError } = await getSupabaseClient().from('org_users').insert({
org_id: adminDeleteOrgId,
user_id: userData!.id,
user_right: 'read',
})
expect(addError).toBeNull()
// USER_ID (admin) deletes USER_ADMIN (read) — should succeed
const response = await fetch(`${BASE_URL}/organization/members?orgId=${adminDeleteOrgId}&email=${USER_ADMIN_EMAIL}`, {
headers,
method: 'DELETE',
})
expect(response.status).toBe(200)
const responseData = await response.json() as { status: string }
expect(responseData.status).toBe('ok')
// Verify the member was actually removed
const { data, error: orgUserError } = await getSupabaseClient().from('org_users').select().eq('org_id', adminDeleteOrgId).eq('user_id', userData!.id).single()
expect(orgUserError).toBeTruthy()
expect(data).toBeNull()
})
it.concurrent('org_admin can delete a read member', async () => {
// Add USER_ADMIN as a read member to the org
const { data: userData, error: userError } = await getSupabaseClient().from('users').select().eq('email', USER_ADMIN_EMAIL).single()
expect(userError).toBeNull()
expect(userData).toBeTruthy()
const { error: addError } = await getSupabaseClient().from('org_users').insert({
org_id: adminDeleteOrgId,
user_id: userData!.id,
user_right: 'read',
})
expect(addError).toBeNull()
// USER_ID (admin) deletes USER_ADMIN (read) — should succeed
const response = await fetch(`${BASE_URL}/organization/members?orgId=${adminDeleteOrgId}&email=${USER_ADMIN_EMAIL}`, {
headers,
method: 'DELETE',
})
expect(response.status).toBe(200)
const responseData = await response.json() as { status: string }
expect(responseData.status).toBe('ok')
// Verify the member was actually removed
const { data, error: orgUserError } = await getSupabaseClient().from('org_users').select().eq('org_id', adminDeleteOrgId).eq('user_id', userData!.id).single()
expect(orgUserError).toBeTruthy()
expect(data).toBeNull()
const { data: bindingsAfterDelete, error: bindingsAfterDeleteError } = await getSupabaseClient()
.from('role_bindings')
.select('id')
.eq('principal_type', 'user')
.eq('principal_id', userData!.id)
.eq('org_id', adminDeleteOrgId)
expect(bindingsAfterDeleteError).toBeNull()
expect(bindingsAfterDelete).toHaveLength(0)
})
🤖 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 998 - 1024, The test "org_admin
can delete a read member" currently verifies the org_users row removal but not
the RBAC cleanup; after the existing verification of org_users (using
getSupabaseClient() and adminDeleteOrgId / userData!.id), add a query against
the role_bindings table (select where org_id = adminDeleteOrgId and user_id =
userData!.id) and assert that the query returns no data and an error or empty
result (i.e., role binding is removed); use the same Supabase client pattern as
the existing checks to fetch from 'role_bindings' and assert the binding is
gone.

@Dalanir Dalanir closed this May 1, 2026
@Dalanir Dalanir deleted the fix/org-admin-update-user-roles-legacy-mapping branch May 1, 2026 17:21
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