From 9f8572c83a56267241f327c87e0ee70b1b3df78a Mon Sep 17 00:00:00 2001 From: KanteshMurade Date: Sat, 23 May 2026 16:29:21 +0530 Subject: [PATCH 1/2] fix: correct workspace admin permission validation in project member updates --- apps/api/plane/app/views/project/member.py | 33 ++++++++++- .../tests/contract/app/test_project_app.py | 58 +++++++++++++++++++ 2 files changed, 90 insertions(+), 1 deletion(-) diff --git a/apps/api/plane/app/views/project/member.py b/apps/api/plane/app/views/project/member.py index e747f573e74..6517983fd3c 100644 --- a/apps/api/plane/app/views/project/member.py +++ b/apps/api/plane/app/views/project/member.py @@ -206,6 +206,7 @@ def retrieve(self, request, slug, project_id, pk): def partial_update(self, request, slug, project_id, pk): project_member = ProjectMember.objects.get(pk=pk, workspace__slug=slug, project_id=project_id, is_active=True) + # 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 @@ -216,8 +217,20 @@ def partial_update(self, request, slug, project_id, pk): ).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 + + # Check if the user is not editing their own role if they are not an admin - if request.user.id == project_member.member_id and not is_workspace_admin: + if request.user.id == project_member.member_id and not is_requesting_workspace_admin: return Response( {"error": "You cannot update your own role"}, status=status.HTTP_400_BAD_REQUEST, @@ -230,6 +243,7 @@ def partial_update(self, request, slug, project_id, pk): is_active=True, ) + if "role" in request.data: # Only Admins can modify roles if requested_project_member.role < ROLE.ADMIN.value and not is_workspace_admin: @@ -261,6 +275,23 @@ def partial_update(self, request, slug, project_id, pk): status=status.HTTP_400_BAD_REQUEST, ) + if target_workspace_role in [5] and int(request.data.get("role", project_member.role)) in [15, 20]: + return Response( + {"error": "You cannot add a user with role higher than the workspace role"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + if ( + "role" in request.data + and int(request.data.get("role", project_member.role)) > requested_project_member.role + and not is_requesting_workspace_admin + ): + return Response( + {"error": "You cannot update a role that is higher than your own role"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + serializer = ProjectMemberSerializer(project_member, data=request.data, partial=True) if serializer.is_valid(): diff --git a/apps/api/plane/tests/contract/app/test_project_app.py b/apps/api/plane/tests/contract/app/test_project_app.py index 979c5e805c4..8530e3aee11 100644 --- a/apps/api/plane/tests/contract/app/test_project_app.py +++ b/apps/api/plane/tests/contract/app/test_project_app.py @@ -229,6 +229,64 @@ def test_create_project_with_all_optional_fields(self, session_client, workspace assert response_data["network"] == project_data["network"] +@pytest.mark.contract +class TestProjectMemberAPI: + """Test project member role operations""" + + def get_project_member_url(self, workspace_slug: str, project_id: uuid.UUID, pk: uuid.UUID) -> str: + return f"/api/workspaces/{workspace_slug}/projects/{project_id}/members/{pk}/" + + @pytest.mark.django_db + def test_workspace_admin_can_promote_member_above_project_role(self, session_client, workspace, create_user): + """Workspace admins can assign project roles above their own project role.""" + project = Project.objects.create(name="Role Project", identifier="RP", workspace=workspace) + requesting_project_member = ProjectMember.objects.create( + project=project, member=create_user, role=5, is_active=True + ) + + target_user = User.objects.create_user(email="target@example.com", username="target") + WorkspaceMember.objects.create(workspace=workspace, member=target_user, role=15, is_active=True) + target_project_member = ProjectMember.objects.create( + project=project, member=target_user, role=15, is_active=True + ) + + url = self.get_project_member_url(workspace.slug, project.id, target_project_member.id) + response = session_client.patch(url, {"role": 20}, format="json") + + assert response.status_code == status.HTTP_200_OK + target_project_member.refresh_from_db() + assert target_project_member.role == 20 + + requesting_project_member.refresh_from_db() + assert requesting_project_member.role == 5 + + @pytest.mark.django_db + def test_project_member_cannot_promote_member_above_own_project_role(self, api_client, workspace): + """Non-workspace-admin project members cannot assign roles above their own project role.""" + project = Project.objects.create(name="Protected Role Project", identifier="PRP", workspace=workspace) + + requesting_user = User.objects.create_user(email="requester@example.com", username="requester") + WorkspaceMember.objects.create(workspace=workspace, member=requesting_user, role=15, is_active=True) + ProjectMember.objects.create(project=project, member=requesting_user, role=15, is_active=True) + + target_user = User.objects.create_user(email="member-target@example.com", username="member-target") + WorkspaceMember.objects.create(workspace=workspace, member=target_user, role=15, is_active=True) + target_project_member = ProjectMember.objects.create( + project=project, member=target_user, role=15, is_active=True + ) + + api_client.force_authenticate(user=requesting_user) + + url = self.get_project_member_url(workspace.slug, project.id, target_project_member.id) + response = api_client.patch(url, {"role": 20}, format="json") + + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.data["error"] == "You cannot update a role that is higher than your own role" + + target_project_member.refresh_from_db() + assert target_project_member.role == 15 + + @pytest.mark.contract class TestProjectAPIGet(TestProjectBase): """Test project GET operations""" From 08043ffec0758bf160c99d02b31b30550c511e29 Mon Sep 17 00:00:00 2001 From: KanteshMurade Date: Sun, 24 May 2026 00:53:03 +0530 Subject: [PATCH 2/2] fix: refine role update validation --- apps/api/plane/app/views/project/member.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/apps/api/plane/app/views/project/member.py b/apps/api/plane/app/views/project/member.py index 6517983fd3c..37da30d4934 100644 --- a/apps/api/plane/app/views/project/member.py +++ b/apps/api/plane/app/views/project/member.py @@ -245,13 +245,6 @@ def partial_update(self, request, slug, project_id, pk): 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(