Skip to content

Commit 91e807d

Browse files
committed
fix: correct workspace admin permission validation in project member updates
1 parent 039d582 commit 91e807d

2 files changed

Lines changed: 90 additions & 1 deletion

File tree

apps/api/plane/app/views/project/member.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@ def retrieve(self, request, slug, project_id, pk):
206206
def partial_update(self, request, slug, project_id, pk):
207207
project_member = ProjectMember.objects.get(pk=pk, workspace__slug=slug, project_id=project_id, is_active=True)
208208

209+
209210
# Fetch the target's workspace role (used to cap the new project role)
210211
target_workspace_role = WorkspaceMember.objects.get(
211212
workspace__slug=slug, member=project_member.member, is_active=True
@@ -216,8 +217,20 @@ def partial_update(self, request, slug, project_id, pk):
216217
).role
217218
is_workspace_admin = requester_workspace_role == ROLE.ADMIN.value
218219

220+
# Fetch the workspace role of the project member
221+
target_workspace_role = WorkspaceMember.objects.get(
222+
workspace__slug=slug, member=project_member.member, is_active=True
223+
).role
224+
225+
# Fetch the workspace role of the requesting user for permission checks
226+
requesting_workspace_role = WorkspaceMember.objects.get(
227+
workspace__slug=slug, member=request.user, is_active=True
228+
).role
229+
is_requesting_workspace_admin = requesting_workspace_role == ROLE.ADMIN.value
230+
231+
219232
# Check if the user is not editing their own role if they are not an admin
220-
if request.user.id == project_member.member_id and not is_workspace_admin:
233+
if request.user.id == project_member.member_id and not is_requesting_workspace_admin:
221234
return Response(
222235
{"error": "You cannot update your own role"},
223236
status=status.HTTP_400_BAD_REQUEST,
@@ -230,6 +243,7 @@ def partial_update(self, request, slug, project_id, pk):
230243
is_active=True,
231244
)
232245

246+
<<<<<<< HEAD
233247
if "role" in request.data:
234248
# Only Admins can modify roles
235249
if requested_project_member.role < ROLE.ADMIN.value and not is_workspace_admin:
@@ -260,6 +274,23 @@ def partial_update(self, request, slug, project_id, pk):
260274
{"error": "You cannot add a user with role higher than the workspace role"},
261275
status=status.HTTP_400_BAD_REQUEST,
262276
)
277+
=======
278+
if target_workspace_role in [5] and int(request.data.get("role", project_member.role)) in [15, 20]:
279+
return Response(
280+
{"error": "You cannot add a user with role higher than the workspace role"},
281+
status=status.HTTP_400_BAD_REQUEST,
282+
)
283+
284+
if (
285+
"role" in request.data
286+
and int(request.data.get("role", project_member.role)) > requested_project_member.role
287+
and not is_requesting_workspace_admin
288+
):
289+
return Response(
290+
{"error": "You cannot update a role that is higher than your own role"},
291+
status=status.HTTP_400_BAD_REQUEST,
292+
)
293+
>>>>>>> c31299f8dd (fix: correct workspace admin role validation)
263294

264295
serializer = ProjectMemberSerializer(project_member, data=request.data, partial=True)
265296

apps/api/plane/tests/contract/app/test_project_app.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,64 @@ def test_create_project_with_all_optional_fields(self, session_client, workspace
229229
assert response_data["network"] == project_data["network"]
230230

231231

232+
@pytest.mark.contract
233+
class TestProjectMemberAPI:
234+
"""Test project member role operations"""
235+
236+
def get_project_member_url(self, workspace_slug: str, project_id: uuid.UUID, pk: uuid.UUID) -> str:
237+
return f"/api/workspaces/{workspace_slug}/projects/{project_id}/members/{pk}/"
238+
239+
@pytest.mark.django_db
240+
def test_workspace_admin_can_promote_member_above_project_role(self, session_client, workspace, create_user):
241+
"""Workspace admins can assign project roles above their own project role."""
242+
project = Project.objects.create(name="Role Project", identifier="RP", workspace=workspace)
243+
requesting_project_member = ProjectMember.objects.create(
244+
project=project, member=create_user, role=5, is_active=True
245+
)
246+
247+
target_user = User.objects.create_user(email="target@example.com", username="target")
248+
WorkspaceMember.objects.create(workspace=workspace, member=target_user, role=15, is_active=True)
249+
target_project_member = ProjectMember.objects.create(
250+
project=project, member=target_user, role=15, is_active=True
251+
)
252+
253+
url = self.get_project_member_url(workspace.slug, project.id, target_project_member.id)
254+
response = session_client.patch(url, {"role": 20}, format="json")
255+
256+
assert response.status_code == status.HTTP_200_OK
257+
target_project_member.refresh_from_db()
258+
assert target_project_member.role == 20
259+
260+
requesting_project_member.refresh_from_db()
261+
assert requesting_project_member.role == 5
262+
263+
@pytest.mark.django_db
264+
def test_project_member_cannot_promote_member_above_own_project_role(self, api_client, workspace):
265+
"""Non-workspace-admin project members cannot assign roles above their own project role."""
266+
project = Project.objects.create(name="Protected Role Project", identifier="PRP", workspace=workspace)
267+
268+
requesting_user = User.objects.create_user(email="requester@example.com", username="requester")
269+
WorkspaceMember.objects.create(workspace=workspace, member=requesting_user, role=15, is_active=True)
270+
ProjectMember.objects.create(project=project, member=requesting_user, role=15, is_active=True)
271+
272+
target_user = User.objects.create_user(email="member-target@example.com", username="member-target")
273+
WorkspaceMember.objects.create(workspace=workspace, member=target_user, role=15, is_active=True)
274+
target_project_member = ProjectMember.objects.create(
275+
project=project, member=target_user, role=15, is_active=True
276+
)
277+
278+
api_client.force_authenticate(user=requesting_user)
279+
280+
url = self.get_project_member_url(workspace.slug, project.id, target_project_member.id)
281+
response = api_client.patch(url, {"role": 20}, format="json")
282+
283+
assert response.status_code == status.HTTP_400_BAD_REQUEST
284+
assert response.data["error"] == "You cannot update a role that is higher than your own role"
285+
286+
target_project_member.refresh_from_db()
287+
assert target_project_member.role == 15
288+
289+
232290
@pytest.mark.contract
233291
class TestProjectAPIGet(TestProjectBase):
234292
"""Test project GET operations"""

0 commit comments

Comments
 (0)