feat(workspaces): expose role-aware membership details
This commit is contained in:
@@ -4,6 +4,7 @@ from rest_framework import serializers
|
|||||||
|
|
||||||
from apps.notifications.services import notify_workspace_membership_added
|
from apps.notifications.services import notify_workspace_membership_added
|
||||||
from apps.users.models import User
|
from apps.users.models import User
|
||||||
|
from apps.workspaces.services import WORKSPACE_MEMBERS_VIEW, has_workspace_capability
|
||||||
from core.serializers.base import BaseModelSerializer
|
from core.serializers.base import BaseModelSerializer
|
||||||
from apps.workspaces.models import PriceUnit, Workspace, WorkspaceMembership, WorkspaceUserRate
|
from apps.workspaces.models import PriceUnit, Workspace, WorkspaceMembership, WorkspaceUserRate
|
||||||
from core.serializers.mini import UserMiniSerializer
|
from core.serializers.mini import UserMiniSerializer
|
||||||
@@ -76,6 +77,8 @@ class WorkspaceSerializer(BaseModelSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class WorkspaceMembershipSerializer(BaseModelSerializer):
|
class WorkspaceMembershipSerializer(BaseModelSerializer):
|
||||||
|
user = serializers.SerializerMethodField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = WorkspaceMembership
|
model = WorkspaceMembership
|
||||||
fields = BaseModelSerializer.Meta.fields + (
|
fields = BaseModelSerializer.Meta.fields + (
|
||||||
@@ -85,13 +88,25 @@ class WorkspaceMembershipSerializer(BaseModelSerializer):
|
|||||||
"is_active",
|
"is_active",
|
||||||
)
|
)
|
||||||
|
|
||||||
def to_representation(self, instance):
|
def get_user(self, instance):
|
||||||
data = super().to_representation(instance)
|
request = self.context.get("request")
|
||||||
data["user"] = UserMiniSerializer(
|
viewer = getattr(request, "user", None)
|
||||||
instance.user,
|
can_view_sensitive_details = bool(
|
||||||
context=self.context
|
viewer
|
||||||
).data
|
and viewer.is_authenticated
|
||||||
return data
|
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):
|
class PriceUnitSerializer(BaseModelSerializer):
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ from apps.workspaces.models import PriceUnit, Workspace, WorkspaceMembership, Wo
|
|||||||
from apps.workspaces.services import (
|
from apps.workspaces.services import (
|
||||||
WORKSPACE_MEMBERS_VIEW,
|
WORKSPACE_MEMBERS_VIEW,
|
||||||
WORKSPACE_EDIT,
|
WORKSPACE_EDIT,
|
||||||
|
WORKSPACE_VIEW,
|
||||||
can_assign_workspace_role,
|
can_assign_workspace_role,
|
||||||
can_change_workspace_membership,
|
can_change_workspace_membership,
|
||||||
has_workspace_capability,
|
has_workspace_capability,
|
||||||
@@ -102,7 +103,9 @@ class WorkspaceMembershipViewSet(ModelViewSet):
|
|||||||
).distinct()
|
).distinct()
|
||||||
|
|
||||||
def get_permissions(self):
|
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()]
|
return [IsAuthenticated(), CanWorkspaceManageMembers()]
|
||||||
if self.action in ["destroy"]:
|
if self.action in ["destroy"]:
|
||||||
return [IsAuthenticated(), CanWorkspaceManageMembers()]
|
return [IsAuthenticated(), CanWorkspaceManageMembers()]
|
||||||
@@ -118,7 +121,7 @@ class WorkspaceMembershipViewSet(ModelViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
workspace = get_object_or_404(Workspace, id=workspace_id, is_deleted=False)
|
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(
|
return Response(
|
||||||
{"detail": "You do not have permission to view workspace members."},
|
{"detail": "You do not have permission to view workspace members."},
|
||||||
status=status.HTTP_403_FORBIDDEN,
|
status=status.HTTP_403_FORBIDDEN,
|
||||||
|
|||||||
@@ -230,6 +230,31 @@ def test_member_project_manager_cannot_edit_project(api_client, member, project)
|
|||||||
assert response.status_code == 403
|
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(
|
def test_admin_cannot_change_owner_membership_but_canonical_owner_can(
|
||||||
api_client, owner, admin, extra_owner, workspace
|
api_client, owner, admin, extra_owner, workspace
|
||||||
):
|
):
|
||||||
|
|||||||
Reference in New Issue
Block a user