diff --git a/apps/api/plane/api/urls/issue.py b/apps/api/plane/api/urls/issue.py index c8d1ea4afe1..df64684de23 100644 --- a/apps/api/plane/api/urls/issue.py +++ b/apps/api/plane/api/urls/issue.py @@ -89,7 +89,9 @@ ), path( "workspaces//projects//issues//issue-attachments//", - IssueAttachmentDetailAPIEndpoint.as_view(http_method_names=["get", "delete"]), + IssueAttachmentDetailAPIEndpoint.as_view( + http_method_names=["get", "patch", "delete"] + ), name="issue-attachment", ), ] diff --git a/apps/api/plane/api/views/issue.py b/apps/api/plane/api/views/issue.py index 94dc9e1d030..20db26dd6e0 100644 --- a/apps/api/plane/api/views/issue.py +++ b/apps/api/plane/api/views/issue.py @@ -75,7 +75,7 @@ from .base import BaseAPIView from plane.utils.host import base_host from plane.bgtasks.webhook_task import model_activity - +from plane.app.permissions import ROLE from plane.utils.openapi import ( work_item_docs, label_docs, @@ -145,6 +145,22 @@ ) from plane.bgtasks.work_item_link_task import crawl_work_item_link_title +def user_has_issue_permission( + user_id, project_id, issue=None, allowed_roles=None, allow_creator=True +): + if allow_creator and issue is not None and user_id == issue.created_by_id: + return True + + qs = ProjectMember.objects.filter( + project_id=project_id, + member_id=user_id, + is_active=True, + ) + if allowed_roles is not None: + qs = qs.filter(role__in=allowed_roles) + + return qs.exists() + class WorkspaceIssueAPIEndpoint(BaseAPIView): """ @@ -1782,7 +1798,6 @@ class IssueAttachmentListCreateAPIEndpoint(BaseAPIView): serializer_class = IssueAttachmentSerializer model = FileAsset - permission_classes = [ProjectEntityPermission] use_read_replica = True @issue_attachment_docs( @@ -1865,6 +1880,22 @@ def post(self, request, slug, project_id, issue_id): Generate presigned URL for uploading file attachments to a work item. Validates file type and size before creating the attachment record. """ + issue = Issue.objects.get( + pk=issue_id, workspace__slug=slug, project_id=project_id + ) + # if the user is creator or admin,member then allow the upload + if not user_has_issue_permission( + request.user.id, + project_id=project_id, + issue=issue, + allowed_roles=[ROLE.ADMIN.value, ROLE.MEMBER.value], + allow_creator=True, + ): + return Response( + {"error": "You are not allowed to upload this attachment"}, + status=status.HTTP_403_FORBIDDEN, + ) + name = request.data.get("name") type = request.data.get("type", False) size = request.data.get("size") @@ -1989,7 +2020,6 @@ class IssueAttachmentDetailAPIEndpoint(BaseAPIView): """Issue Attachment Detail Endpoint""" serializer_class = IssueAttachmentSerializer - permission_classes = [ProjectEntityPermission] model = FileAsset use_read_replica = True @@ -2012,6 +2042,22 @@ def delete(self, request, slug, project_id, issue_id, pk): Soft delete an attachment from a work item by marking it as deleted. Records deletion activity and triggers metadata cleanup. """ + issue = Issue.objects.get( + pk=issue_id, workspace__slug=slug, project_id=project_id + ) + # if the request user is creator or admin then delete the attachment + if not user_has_issue_permission( + request.user, + project_id=project_id, + issue=issue, + allowed_roles=[ROLE.ADMIN.value], + allow_creator=True, + ): + return Response( + {"error": "You are not allowed to delete this attachment"}, + status=status.HTTP_403_FORBIDDEN, + ) + issue_attachment = FileAsset.objects.get( pk=pk, workspace__slug=slug, project_id=project_id ) @@ -2074,6 +2120,19 @@ def get(self, request, slug, project_id, issue_id, pk): Retrieve details of a specific attachment. """ + # if the user is part of the project then allow the download + if not user_has_issue_permission( + request.user, + project_id=project_id, + issue=None, + allowed_roles=None, + allow_creator=False, + ): + return Response( + {"error": "You are not allowed to download this attachment"}, + status=status.HTTP_403_FORBIDDEN, + ) + # Get the asset asset = FileAsset.objects.get( id=pk, workspace__slug=slug, project_id=project_id @@ -2128,6 +2187,23 @@ def patch(self, request, slug, project_id, issue_id, pk): Mark an attachment as uploaded after successful file transfer to storage. Triggers activity logging and metadata extraction. """ + + issue = Issue.objects.get( + pk=issue_id, workspace__slug=slug, project_id=project_id + ) + # if the user is creator or admin then allow the upload + if not user_has_issue_permission( + request.user, + project_id=project_id, + issue=issue, + allowed_roles=[ROLE.ADMIN.value, ROLE.MEMBER.value], + allow_creator=True, + ): + return Response( + {"error": "You are not allowed to upload this attachment"}, + status=status.HTTP_403_FORBIDDEN, + ) + issue_attachment = FileAsset.objects.get( pk=pk, workspace__slug=slug, project_id=project_id )