From 76f02dc259d5b914d0bb6379e31404f147a37d30 Mon Sep 17 00:00:00 2001 From: Amirhossein Khalili Date: Tue, 28 Apr 2026 10:46:15 +0330 Subject: [PATCH] feat(workspaces): expose role-aware membership details --- apps/workspaces/api/serializers.py | 39 +++++++++++++++------- apps/workspaces/api/views.py | 7 ++-- apps/workspaces/tests/test_capabilities.py | 25 ++++++++++++++ 3 files changed, 57 insertions(+), 14 deletions(-) diff --git a/apps/workspaces/api/serializers.py b/apps/workspaces/api/serializers.py index 057f7da..0cb4a37 100644 --- a/apps/workspaces/api/serializers.py +++ b/apps/workspaces/api/serializers.py @@ -4,6 +4,7 @@ from rest_framework import serializers from apps.notifications.services import notify_workspace_membership_added from apps.users.models import User +from apps.workspaces.services import WORKSPACE_MEMBERS_VIEW, has_workspace_capability from core.serializers.base import BaseModelSerializer from apps.workspaces.models import PriceUnit, Workspace, WorkspaceMembership, WorkspaceUserRate from core.serializers.mini import UserMiniSerializer @@ -76,22 +77,36 @@ class WorkspaceSerializer(BaseModelSerializer): class WorkspaceMembershipSerializer(BaseModelSerializer): - class Meta: - model = WorkspaceMembership - fields = BaseModelSerializer.Meta.fields + ( - "workspace", - "user", + user = serializers.SerializerMethodField() + + class Meta: + model = WorkspaceMembership + fields = BaseModelSerializer.Meta.fields + ( + "workspace", + "user", "role", "is_active", ) - def to_representation(self, instance): - data = super().to_representation(instance) - data["user"] = UserMiniSerializer( - instance.user, - context=self.context - ).data - return data + def get_user(self, instance): + request = self.context.get("request") + viewer = getattr(request, "user", None) + can_view_sensitive_details = bool( + viewer + and viewer.is_authenticated + and has_workspace_capability(viewer, instance.workspace, WORKSPACE_MEMBERS_VIEW) + ) + + user_data = UserMiniSerializer(instance.user, context=self.context).data + if can_view_sensitive_details: + return user_data + + return { + "id": user_data["id"], + "first_name": user_data.get("first_name"), + "last_name": user_data.get("last_name"), + "profile_picture": user_data.get("profile_picture"), + } class PriceUnitSerializer(BaseModelSerializer): diff --git a/apps/workspaces/api/views.py b/apps/workspaces/api/views.py index 7c8d871..cb72d4e 100644 --- a/apps/workspaces/api/views.py +++ b/apps/workspaces/api/views.py @@ -33,6 +33,7 @@ from apps.workspaces.models import PriceUnit, Workspace, WorkspaceMembership, Wo from apps.workspaces.services import ( WORKSPACE_MEMBERS_VIEW, WORKSPACE_EDIT, + WORKSPACE_VIEW, can_assign_workspace_role, can_change_workspace_membership, has_workspace_capability, @@ -102,7 +103,9 @@ class WorkspaceMembershipViewSet(ModelViewSet): ).distinct() def get_permissions(self): - if self.action in ["list", "retrieve", "create", "update", "partial_update"]: + if self.action in ["list", "retrieve"]: + return [IsAuthenticated()] + if self.action in ["create", "update", "partial_update"]: return [IsAuthenticated(), CanWorkspaceManageMembers()] if self.action in ["destroy"]: return [IsAuthenticated(), CanWorkspaceManageMembers()] @@ -118,7 +121,7 @@ class WorkspaceMembershipViewSet(ModelViewSet): ) workspace = get_object_or_404(Workspace, id=workspace_id, is_deleted=False) - if not has_workspace_capability(request.user, workspace, WORKSPACE_MEMBERS_VIEW): + if not has_workspace_capability(request.user, workspace, WORKSPACE_VIEW): return Response( {"detail": "You do not have permission to view workspace members."}, status=status.HTTP_403_FORBIDDEN, diff --git a/apps/workspaces/tests/test_capabilities.py b/apps/workspaces/tests/test_capabilities.py index ef7c4c8..bb6db83 100644 --- a/apps/workspaces/tests/test_capabilities.py +++ b/apps/workspaces/tests/test_capabilities.py @@ -230,6 +230,31 @@ def test_member_project_manager_cannot_edit_project(api_client, member, project) assert response.status_code == 403 +def test_member_can_list_workspace_members_with_restricted_user_fields(api_client, member, workspace): + api_client.force_authenticate(user=member) + + response = api_client.get(f"/api/workspace-memberships/?workspace={workspace.id}") + + assert response.status_code == 200 + payload = response.data.get("items", response.data) if isinstance(response.data, dict) else response.data + assert len(payload) >= 1 + first_user = payload[0]["user"] + assert "mobile" not in first_user + assert "email" not in first_user + + +def test_owner_can_list_workspace_members_with_full_user_fields(api_client, owner, workspace): + api_client.force_authenticate(user=owner) + + response = api_client.get(f"/api/workspace-memberships/?workspace={workspace.id}") + + assert response.status_code == 200 + payload = response.data.get("items", response.data) if isinstance(response.data, dict) else response.data + assert len(payload) >= 1 + first_user = payload[0]["user"] + assert "mobile" in first_user + + def test_admin_cannot_change_owner_membership_but_canonical_owner_can( api_client, owner, admin, extra_owner, workspace ):