Skip to content

fix: correct workspace admin permission validation in project member updates#9119

Open
KanteshMurade wants to merge 2 commits into
makeplane:previewfrom
KanteshMurade:clean-role-fix
Open

fix: correct workspace admin permission validation in project member updates#9119
KanteshMurade wants to merge 2 commits into
makeplane:previewfrom
KanteshMurade:clean-role-fix

Conversation

@KanteshMurade
Copy link
Copy Markdown
Contributor

@KanteshMurade KanteshMurade commented May 23, 2026

Description

Fixes issue #9109 where workspace admins could not promote project members to Admin if their own project role was lower.

The backend permission validation in ProjectMemberViewSet.partial_update was incorrectly checking the workspace role of the target member being edited instead of the requesting user.

This change updates the validation logic to use the requesting user's workspace role for permission checks.

Type of Change

  • Bug fix (non-breaking change which fixes an issue)

Screenshots and Media (if applicable)

N/A

Test Scenarios

  • Verified workspace admins can assign higher project roles even if their own project role is lower.
  • Verified non-admin project members still cannot assign roles higher than their own.
  • Reviewed backend permission flow for requester vs target role validation.

References

Fixes #9109

Summary by CodeRabbit

  • Bug Fixes

    • Strengthened project member role update validation: corrected workspace-admin detection and stricter rules to prevent unauthorized elevation of project roles. Role changes now block assigning higher privileges when constrained by the target’s workspace role and prevent promotions that would exceed the requester’s permission level unless the requester is a workspace admin.
  • Tests

    • Added contract tests validating role update permission rules, including admin promotions, blocking of unauthorized promotions, and asserting error responses and unchanged target roles.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 23, 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: bcf578ee-2ade-4572-beaf-964126c82abb

📥 Commits

Reviewing files that changed from the base of the PR and between 9f8572c and 08043ff.

📒 Files selected for processing (1)
  • apps/api/plane/app/views/project/member.py
💤 Files with no reviewable changes (1)
  • apps/api/plane/app/views/project/member.py

📝 Walkthrough

Walkthrough

Refactors ProjectMemberViewSet.partial_update role-update validation to use the requester's workspace role, add a guard blocking certain upgrades when the target workspace role is 5, and deny non-admins from assigning roles above their own; adds contract tests for admin-allowed and non-admin-denied scenarios.

Changes

Role-Assignment Permission Fix

Layer / File(s) Summary
Permission checks in partial_update
apps/api/plane/app/views/project/member.py
Refactors partial_update to read the target and requester workspace roles separately, blocks updates when the target workspace role is 5 for certain promoted roles, and prevents non-workspace-admins from setting a role higher than their own ProjectMember.role.
Contract tests for role updates
apps/api/plane/tests/contract/app/test_project_app.py
Adds TestProjectMemberAPI with a URL helper and tests asserting workspace admins can promote a member to a higher role and non-workspace-admin members are blocked from promoting others above their own role (checks HTTP 200/400, error payload, and DB persistence).

Sequence Diagram(s)

sequenceDiagram
  participant Client as Client
  participant API as ProjectMemberViewSet.partial_update
  participant DB as Database
  Client->>API: PATCH /projects/{id}/members/{pk} (role change)
  API->>DB: fetch target ProjectMember (workspace role)
  API->>DB: fetch requesting user's ProjectMember (workspace role)
  API->>API: validate requested role vs target.workspace_role and requester.role/admin status
  alt allowed (workspace admin or within requester role)
    API->>DB: update ProjectMember.role
    API-->>Client: 200 OK
  else denied
    API-->>Client: 400 with error message
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • dheeru0198
  • pablohashescobar

Poem

🐰 I hopped through code, with whiskers twitching bright,
Checked roles and slots beneath the moonlit byte.
Now patches hit true, denied or granted fair,
Permissions hum tidy — tidy as my lair.
Hooray — role rules balanced in the night! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title accurately describes the main change: correcting workspace admin permission validation in project member role updates.
Description check ✅ Passed The PR description follows the template, includes all required sections with clear explanation of the bug fix, type of change, test scenarios, and references to the linked issue.
Linked Issues check ✅ Passed The changes directly address issue #9109 by fixing the permission validation to use the requester's workspace role instead of the target member's role, allowing workspace admins to assign higher project roles.
Out of Scope Changes check ✅ Passed All changes are scoped to fixing the workspace admin permission validation in project member role updates and adding corresponding contract tests, with no unrelated modifications.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
apps/api/plane/app/views/project/member.py (2)

209-230: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Duplicate workspace role queries with inconsistent variable names.

The code fetches target_workspace_role and the requester's workspace role twice:

  1. Lines 211-218: requester_workspace_role / is_workspace_admin
  2. Lines 220-229: requesting_workspace_role / is_requesting_workspace_admin

This causes redundant database queries and creates confusion about which variables to use downstream. The HEAD conflict block uses is_workspace_admin while the other block uses is_requesting_workspace_admin.

After resolving the merge conflict, keep only one set of these queries with consistent naming.

Proposed fix (after resolving merge conflict)
-
 # Fetch the target's workspace role (used to cap the new project role)
 target_workspace_role = WorkspaceMember.objects.get(
     workspace__slug=slug, member=project_member.member, is_active=True
 ).role
 # Fetch the requester's workspace role to decide if they may bypass project-role checks
 requester_workspace_role = WorkspaceMember.objects.get(
     workspace__slug=slug, member=request.user, is_active=True
 ).role
 is_workspace_admin = requester_workspace_role == ROLE.ADMIN.value
-
-        # Fetch the workspace role of the project member
-        target_workspace_role = WorkspaceMember.objects.get(
-            workspace__slug=slug, member=project_member.member, is_active=True
-        ).role
-
-        # Fetch the workspace role of the requesting user for permission checks
-        requesting_workspace_role = WorkspaceMember.objects.get(
-            workspace__slug=slug, member=request.user, is_active=True
-        ).role
-        is_requesting_workspace_admin = requesting_workspace_role == ROLE.ADMIN.value
-         
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/plane/app/views/project/member.py` around lines 209 - 230, Remove
the duplicated WorkspaceMember queries and normalize variable names: keep a
single fetch for the target's workspace role into target_workspace_role (from
WorkspaceMember with workspace__slug=slug and member=project_member.member) and
a single fetch for the requesting user's workspace role into
requester_workspace_role (from WorkspaceMember with workspace__slug=slug and
member=request.user), then compute is_workspace_admin = requester_workspace_role
== ROLE.ADMIN.value and use those variables throughout (replace any uses of
requesting_workspace_role or is_requesting_workspace_admin with
requester_workspace_role and is_workspace_admin) to eliminate redundant DB calls
and make naming consistent.

246-293: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Critical: Unresolved merge conflict markers will cause syntax errors.

The file contains literal merge conflict markers (<<<<<<< HEAD, =======, >>>>>>> c31299f8dd) that must be resolved before this code can run. Python will raise a SyntaxError when attempting to parse this file.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/plane/app/views/project/member.py` around lines 246 - 293, Resolve
the literal merge markers by removing the conflict block and keeping a single
coherent role-validation flow: delete the lines from <<<<<<< HEAD through
======= and the >>>>>>> marker, and retain the consolidated checks that first
validate workspace limits using target_workspace_role and
int(request.data.get("role", project_member.role)) against [15, 20], then
validate the requester’s authority by comparing int(request.data.get("role",
project_member.role)) to requested_project_member.role and using the consistent
boolean name is_requesting_workspace_admin (or rename uses of is_workspace_admin
to match). Ensure the code uses the same variable names (project_member vs
requested_project_member) consistently, converts request.data role to int once,
and returns the appropriate Response/status as in the consolidated version.
🧹 Nitpick comments (1)
apps/api/plane/tests/contract/app/test_project_app.py (1)

239-261: 💤 Low value

Test relies on implicit workspace admin setup from fixtures.

The test assumes create_user is a workspace admin (role 20) through the workspace fixture, but this isn't explicit in the test. If the fixture behavior changes, this test could fail unexpectedly.

Consider adding an explicit assertion or comment documenting this fixture dependency:

# create_user is a workspace admin (role=20) via the workspace fixture

Alternatively, verify the workspace membership explicitly at the start of the test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/plane/tests/contract/app/test_project_app.py` around lines 239 -
261, The test test_workspace_admin_can_promote_member_above_project_role relies
implicitly on the create_user being a workspace admin via the workspace fixture;
make that dependency explicit by asserting or ensuring the workspace
membership/role at the start of the test (e.g., verify WorkspaceMember for
create_user has role 20) or add a one-line comment documenting that create_user
is a workspace admin; reference the test name
test_workspace_admin_can_promote_member_above_project_role, the create_user
fixture, and the WorkspaceMember/ProjectMember checks so maintainers can locate
and make the verification or comment change.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/api/plane/tests/contract/app/test_project_app.py`:
- Around line 283-284: The test in
apps/api/plane/tests/contract/app/test_project_app.py asserts
response.data["error"] equals "You cannot update a role that is higher than your
own role", which is one side of an unresolved merge in member.py (the
alternative message is "You cannot assign a role equal to or higher than your
own"); update the test to match the final error string chosen in member.py (or
make the assertion resilient by checking for the canonical substring used in the
resolved message) so the assertion aligns with the actual message thrown by the
role-update validation.

---

Outside diff comments:
In `@apps/api/plane/app/views/project/member.py`:
- Around line 209-230: Remove the duplicated WorkspaceMember queries and
normalize variable names: keep a single fetch for the target's workspace role
into target_workspace_role (from WorkspaceMember with workspace__slug=slug and
member=project_member.member) and a single fetch for the requesting user's
workspace role into requester_workspace_role (from WorkspaceMember with
workspace__slug=slug and member=request.user), then compute is_workspace_admin =
requester_workspace_role == ROLE.ADMIN.value and use those variables throughout
(replace any uses of requesting_workspace_role or is_requesting_workspace_admin
with requester_workspace_role and is_workspace_admin) to eliminate redundant DB
calls and make naming consistent.
- Around line 246-293: Resolve the literal merge markers by removing the
conflict block and keeping a single coherent role-validation flow: delete the
lines from <<<<<<< HEAD through ======= and the >>>>>>> marker, and retain the
consolidated checks that first validate workspace limits using
target_workspace_role and int(request.data.get("role", project_member.role))
against [15, 20], then validate the requester’s authority by comparing
int(request.data.get("role", project_member.role)) to
requested_project_member.role and using the consistent boolean name
is_requesting_workspace_admin (or rename uses of is_workspace_admin to match).
Ensure the code uses the same variable names (project_member vs
requested_project_member) consistently, converts request.data role to int once,
and returns the appropriate Response/status as in the consolidated version.

---

Nitpick comments:
In `@apps/api/plane/tests/contract/app/test_project_app.py`:
- Around line 239-261: The test
test_workspace_admin_can_promote_member_above_project_role relies implicitly on
the create_user being a workspace admin via the workspace fixture; make that
dependency explicit by asserting or ensuring the workspace membership/role at
the start of the test (e.g., verify WorkspaceMember for create_user has role 20)
or add a one-line comment documenting that create_user is a workspace admin;
reference the test name
test_workspace_admin_can_promote_member_above_project_role, the create_user
fixture, and the WorkspaceMember/ProjectMember checks so maintainers can locate
and make the verification or comment change.
🪄 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: 4476ba48-3f71-4f22-aac5-1839bc709b6e

📥 Commits

Reviewing files that changed from the base of the PR and between 039d582 and 91e807d.

📒 Files selected for processing (2)
  • apps/api/plane/app/views/project/member.py
  • apps/api/plane/tests/contract/app/test_project_app.py

Comment thread apps/api/plane/tests/contract/app/test_project_app.py
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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/api/plane/app/views/project/member.py (1)

247-253: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't block all non-admin project members from role edits.

Lines 249-253 still short-circuit every requester whose project role is below Admin, so the comparison-based checks below never run for members/guests. That means a project member cannot update someone to a lower role, and the new contract test in apps/api/plane/tests/contract/app/test_project_app.py will currently get 403 {"error": "You do not have permission to update roles"} instead of the intended “above your own role” denial.

Suggested minimal fix
         if "role" in request.data:
-            # Only Admins can modify roles
-            if requested_project_member.role < ROLE.ADMIN.value and not is_workspace_admin:
-                return Response(
-                    {"error": "You do not have permission to update roles"},
-                    status=status.HTTP_403_FORBIDDEN,
-                )
-
             # Cannot modify a member whose role is equal to or higher than your own
             if project_member.role >= requested_project_member.role and not is_workspace_admin:
                 return Response(
                     {"error": "You cannot update the role of a member with a role equal to or higher than your own"},
                     status=status.HTTP_403_FORBIDDEN,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/plane/app/views/project/member.py` around lines 247 - 253, The
current check unconditionally blocks anyone below Admin from changing roles;
change it to only block unauthorized promotions or edits of admins: when "role"
in request.data, read the intended new role (new_role = request.data["role"])
and the requester's project role (e.g., requester_role), then if not
is_workspace_admin enforce: if requester_role < ROLE.ADMIN.value and new_role >
requester_role (attempting to promote above your role) OR if
requested_project_member.role >= ROLE.ADMIN.value and requester_role <
ROLE.ADMIN.value (non-admin trying to change an admin), return the 403; use ROLE
enum values for comparisons and keep requested_project_member.role and
is_workspace_admin checks in place.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@apps/api/plane/app/views/project/member.py`:
- Around line 247-253: The current check unconditionally blocks anyone below
Admin from changing roles; change it to only block unauthorized promotions or
edits of admins: when "role" in request.data, read the intended new role
(new_role = request.data["role"]) and the requester's project role (e.g.,
requester_role), then if not is_workspace_admin enforce: if requester_role <
ROLE.ADMIN.value and new_role > requester_role (attempting to promote above your
role) OR if requested_project_member.role >= ROLE.ADMIN.value and requester_role
< ROLE.ADMIN.value (non-admin trying to change an admin), return the 403; use
ROLE enum values for comparisons and keep requested_project_member.role and
is_workspace_admin checks in place.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e16afcc6-902b-46d4-8a40-d6d9b9c086e5

📥 Commits

Reviewing files that changed from the base of the PR and between 91e807d and 9f8572c.

📒 Files selected for processing (2)
  • apps/api/plane/app/views/project/member.py
  • apps/api/plane/tests/contract/app/test_project_app.py

@KanteshMurade
Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 23, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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.

[bug]: You cannot assign a role equal to or higher than your own

1 participant