feat(permissions): centralize workspace role capability checks

This commit is contained in:
2026-04-25 18:48:50 +03:30
parent 5f9d413a57
commit f960ca8221
14 changed files with 925 additions and 222 deletions

View File

@@ -24,6 +24,7 @@ from apps.time_entries.services.time_entries import (
update_time_entry,
stop_time_entry
)
from apps.workspaces.services import TIME_ENTRIES_MANAGE_OWN, has_workspace_capability
class TimeEntryViewSet(ModelViewSet):
@@ -150,11 +151,16 @@ class TimeEntryViewSet(ModelViewSet):
output_serializer = TimeEntrySerializer(entry)
return Response(output_serializer.data, status=status.HTTP_201_CREATED)
def update(self, request, *args, **kwargs):
partial = kwargs.pop("partial", False)
entry = self.get_object()
serializer = self.get_serializer(data=request.data, partial=partial)
def update(self, request, *args, **kwargs):
partial = kwargs.pop("partial", False)
entry = self.get_object()
if not has_workspace_capability(request.user, entry.workspace, TIME_ENTRIES_MANAGE_OWN):
return Response(
{"detail": "You do not have permission to manage time entries in this workspace."},
status=status.HTTP_403_FORBIDDEN,
)
serializer = self.get_serializer(data=request.data, partial=partial)
serializer.is_valid(raise_exception=True)
updated_entry = update_time_entry(
@@ -166,11 +172,16 @@ class TimeEntryViewSet(ModelViewSet):
return Response(output_serializer.data, status=status.HTTP_200_OK)
@action(detail=True, methods=["post"])
def stop(self, request, pk=None):
def stop(self, request, pk=None):
"""
Dedicated endpoint to stop an actively running timer.
"""
entry = self.get_object()
entry = self.get_object()
if not has_workspace_capability(request.user, entry.workspace, TIME_ENTRIES_MANAGE_OWN):
return Response(
{"detail": "You do not have permission to manage time entries in this workspace."},
status=status.HTTP_403_FORBIDDEN,
)
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
@@ -181,11 +192,16 @@ class TimeEntryViewSet(ModelViewSet):
output_serializer = TimeEntrySerializer(stopped_entry)
return Response(output_serializer.data, status=status.HTTP_200_OK)
def destroy(self, request, *args, **kwargs):
def destroy(self, request, *args, **kwargs):
"""
Soft deletes the time entry.
"""
entry = self.get_object()
entry.is_deleted = True
entry = self.get_object()
if not has_workspace_capability(request.user, entry.workspace, TIME_ENTRIES_MANAGE_OWN):
return Response(
{"detail": "You do not have permission to manage time entries in this workspace."},
status=status.HTTP_403_FORBIDDEN,
)
entry.is_deleted = True
entry.save(update_fields=["is_deleted", "updated_at"])
return Response(status=status.HTTP_204_NO_CONTENT)

View File

@@ -2,24 +2,23 @@
from django.utils import timezone
from rest_framework.exceptions import ValidationError, PermissionDenied
from apps.time_entries.models import TimeEntry
from apps.time_entries.services.rates import resolve_rate
from apps.workspaces.models import WorkspaceMembership
def _verify_workspace_access(user, workspace_id):
"""
Ensures the user is an active member of the specified workspace.
"""
has_access = WorkspaceMembership.objects.filter(
workspace_id=workspace_id,
user=user,
is_active=True,
is_deleted=False
).exists()
if not has_access:
raise PermissionDenied("You do not have access to this workspace.")
from apps.time_entries.models import TimeEntry
from apps.time_entries.services.rates import resolve_rate
from apps.workspaces.models import Workspace
from apps.workspaces.services import TIME_ENTRIES_MANAGE_OWN, has_workspace_capability
def _verify_workspace_access(user, workspace_id):
"""
Ensures the user is an active member of the specified workspace.
"""
workspace = Workspace.objects.filter(id=workspace_id, is_deleted=False).first()
if not workspace or not has_workspace_capability(
user,
workspace,
TIME_ENTRIES_MANAGE_OWN,
):
raise PermissionDenied("You do not have access to this workspace.")
def create_time_entry(user, workspace_id, start_time, end_time=None, project=None, tags=None, description="", is_billable=False):