Skip to content

fix(organizations): allow global admin to bypass org-level guards (#3505)#3509

Merged
PierreBrisorgueil merged 1 commit into
masterfrom
fix/org-admin-bypass-3505
Apr 24, 2026
Merged

fix(organizations): allow global admin to bypass org-level guards (#3505)#3509
PierreBrisorgueil merged 1 commit into
masterfrom
fix/org-admin-bypass-3505

Conversation

@PierreBrisorgueil

Copy link
Copy Markdown
Contributor

Summary

Two org-management controller guards currently treat global platform admins as plain users and block them from moderating organizations they don't belong to:

  • organizations.membership.controller.js#remove — 403 when req.membership is undefined (admin not in the org), regardless of req.user.roles.
  • organizations.controller.js#remove — 422 "cannot delete your last organization" whenever listByUser(req.user).length <= 1, even for admins touching someone else's org.

Both now short-circuit to allow when req.user.roles includes admin. The UX protection on org delete is preserved for regular users (only skipped when the actor is not a member of the target org, or is a global admin).

Closes #3505.

Scope / intentionally excluded

The issue mentions a concern about cascade-delete integrity (dangling user.currentOrganization refs when an org is deleted by someone who isn't a member). Per the ticket, that is intentionally not bundled here — this PR stays narrowly the admin-bypass fix. A follow-up issue should be filed if the audit of the delete handler shows a real gap.

Test plan

  • npm run test:unit — 550 passed
  • Organizations integration + e2e + unit in isolation — 24 passed
  • npm run lint — clean
  • New unit tests cover:
    • admin with membership=undefined can remove any member, including owners
    • admin with zero memberships can delete any org (UX check skipped)
    • admin who is a member of the target org with memberships.length <= 1 can still delete (UX check skipped)
    • non-admin member with one membership still gets 422 (regression guard)
    • non-admin member with several memberships can delete their current org (regression guard)

Two controller guards were blocking platform admins from moderating
organizations they don't belong to:

- remove-member refused because req.membership was undefined, so the
  actor-role check fell through to a 403 regardless of the global role.
- delete-org always called listByUser(req.user) and bailed with 422
  "cannot delete your last organization" for any admin with zero or one
  own memberships.

Short-circuit both to "allow" when req.user.roles includes "admin".
The delete-org UX protection is preserved for regular members (only
skipped when the actor is not in the target org, or is a global admin),
so a normal user deleting their own last org still sees the 422.

Tests: extend organizations.membership.controller.unit to cover admin
bypass with membership=undefined and admin removing an owner. Add
organizations.controller.unit for the delete-org paths (regular last
org rejected, multi-org member allowed, admin with zero/one memberships
allowed).

Closes #3505
Copilot AI review requested due to automatic review settings April 24, 2026 07:08
@coderabbitai

coderabbitai Bot commented Apr 24, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@PierreBrisorgueil has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 42 minutes and 55 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 42 minutes and 55 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9eb19432-0719-4e79-9302-d70ec8776299

📥 Commits

Reviewing files that changed from the base of the PR and between c93a577 and 266b1b2.

📒 Files selected for processing (4)
  • modules/organizations/controllers/organizations.controller.js
  • modules/organizations/controllers/organizations.membership.controller.js
  • modules/organizations/tests/organizations.controller.unit.tests.js
  • modules/organizations/tests/organizations.membership.controller.unit.tests.js
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/org-admin-bypass-3505

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.

@codacy-production

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 12 complexity · 16 duplication

Metric Results
Complexity 12
Duplication 16

View in Codacy

AI Reviewer: first review requested successfully. AI can make mistakes. Always validate suggestions.

Run reviewer

TIP This summary will be updated as you push new changes.

@codecov

codecov Bot commented Apr 24, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 86.67%. Comparing base (c93a577) to head (266b1b2).
⚠️ Report is 1 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #3509      +/-   ##
==========================================
+ Coverage   86.45%   86.67%   +0.22%     
==========================================
  Files         116      116              
  Lines        2989     2994       +5     
  Branches      840      844       +4     
==========================================
+ Hits         2584     2595      +11     
+ Misses        322      318       -4     
+ Partials       83       81       -2     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Fixes org-management delete/remove controller guards so global platform admins (roles includes admin) aren’t incorrectly blocked when they’re not members of the target organization, while preserving the “don’t delete your last org” UX guard for regular org members.

Changes:

  • Allow global admins to remove organization members even when req.membership is missing (admin is not a member of that org).
  • Scope the “cannot delete your last organization” guard to non-admin actors who are members of the target org.
  • Add unit tests covering admin bypass and non-admin last-org protection behavior.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.

File Description
modules/organizations/controllers/organizations.membership.controller.js Adds isGlobalAdmin bypass to the member-removal RBAC guard.
modules/organizations/controllers/organizations.controller.js Updates org deletion UX guard to skip for global admins and non-members of the target org.
modules/organizations/tests/organizations.membership.controller.unit.tests.js Adds unit tests ensuring global admins can remove members without org membership context.
modules/organizations/tests/organizations.controller.unit.tests.js Adds unit tests for org deletion behavior across admin/non-admin and membership scenarios.

@codacy-production codacy-production Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull Request Overview

The PR successfully implements the bypass for global admins to delete organizations and remove members regardless of their membership status, addressing the core issue. Codacy results indicate the project remains up to standards; however, a functional gap exists: the updateRole function remains restricted, preventing platform admins from performing critical moderation tasks like transferring ownership. Additionally, there is a significant increase in code clones (16), primarily due to the repeated logic used to verify the 'admin' role across controllers. This duplication should be addressed to ensure future maintainability and consistency in how platform-level permissions are enforced.

About this PR

  • The pattern for checking global admin status (Array.isArray(req.user?.roles) && req.user.roles.includes('admin')) is now duplicated across several controllers (Organizations, Tasks, Users). This logic should be centralized into an authorization middleware or a helper on the request object to improve maintainability.

Test suggestions

  • A regular user with exactly one membership attempts to delete their organization and receives a 422 error.
  • A regular user with multiple memberships successfully deletes their current organization.
  • A global admin with no memberships in the target organization successfully deletes the organization.
  • A global admin who is a member of only the target organization successfully deletes it.
  • A global admin with no membership in an organization successfully removes an owner from that organization.

🗒️ Improve review quality by adding custom instructions

Comment thread modules/organizations/controllers/organizations.controller.js
@PierreBrisorgueil

Copy link
Copy Markdown
Contributor Author

Addressed threads from Codacy review — filed as follow-up #3510 to keep this PR narrowly the admin-bypass fix per CLAUDE.md small-PR rule.

@PierreBrisorgueil
PierreBrisorgueil merged commit 247ba7b into master Apr 24, 2026
10 checks passed
@PierreBrisorgueil
PierreBrisorgueil deleted the fix/org-admin-bypass-3505 branch April 24, 2026 07:13
PierreBrisorgueil added a commit that referenced this pull request Apr 24, 2026
…alAdmin helper (#3512)

* fix(organizations): extend global-admin bypass to updateRole + isGlobalAdmin helper

Completes the platform-admin bypass started in #3509 which missed updateRole.
A global admin with no membership on the target org used to get a 403 on
PUT /api/organizations/:orgId/memberships/:memberId, blocking moderation
use-cases like transferring ownership.

Also extracts the repeated `Array.isArray(req.user?.roles) && r.includes('admin')`
expression into `lib/helpers/isGlobalAdmin.js` — single source of truth for the
three controller call-sites (remove-member, remove-org, updateRole). Policies
still inline the check; a broader refactor is out of scope here.

Closes #3510

* refactor(organizations): rename admin→isPlatformAdmin + clarify 403 message + test.each

Address review feedback on #3512:
- Rename local `admin` → `isPlatformAdmin` in all 3 controller call-sites to
  avoid confusion with the org-level `MEMBERSHIP_ROLES.ADMIN` used nearby.
- Update the updateRole 403 message to reflect the actual rule ("Only owners
  or global admins can change member roles").
- Refactor the two updateRole rejection regression tests with `test.each`.

* refactor(helpers): use function declaration for isGlobalAdmin to silence Codacy
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.

fix(organizations): global admin cannot remove last member or delete org when not a member of it

2 participants