From a59ebadd348ba3d154a063a498b5ea5ef4ab7452 Mon Sep 17 00:00:00 2001 From: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com> Date: Wed, 20 Aug 2025 18:56:15 +0530 Subject: [PATCH] [WEB-4712] chore: work item attachment patch endpoint (#7595) --- apps/api/plane/api/urls/issue.py | 4 +- apps/api/plane/api/views/issue.py | 82 +++++++++++++++++++++++++++++-- 2 files changed, 82 insertions(+), 4 deletions(-) diff --git a/apps/api/plane/api/urls/issue.py b/apps/api/plane/api/urls/issue.py index c8d1ea4af..df64684de 100644 --- a/apps/api/plane/api/urls/issue.py +++ b/apps/api/plane/api/urls/issue.py @@ -89,7 +89,9 @@ urlpatterns = [ ), 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 5a75503d7..489fa4a08 100644 --- a/apps/api/plane/api/views/issue.py +++ b/apps/api/plane/api/views/issue.py @@ -75,7 +75,7 @@ from plane.bgtasks.storage_metadata_task import get_asset_object_metadata 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.utils.openapi import ( ) 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): """ @@ -1787,7 +1803,6 @@ class IssueAttachmentListCreateAPIEndpoint(BaseAPIView): serializer_class = IssueAttachmentSerializer model = FileAsset - permission_classes = [ProjectEntityPermission] use_read_replica = True @issue_attachment_docs( @@ -1870,6 +1885,22 @@ class IssueAttachmentListCreateAPIEndpoint(BaseAPIView): 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") @@ -1994,7 +2025,6 @@ class IssueAttachmentDetailAPIEndpoint(BaseAPIView): """Issue Attachment Detail Endpoint""" serializer_class = IssueAttachmentSerializer - permission_classes = [ProjectEntityPermission] model = FileAsset use_read_replica = True @@ -2017,6 +2047,22 @@ class IssueAttachmentDetailAPIEndpoint(BaseAPIView): 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 ) @@ -2079,6 +2125,19 @@ class IssueAttachmentDetailAPIEndpoint(BaseAPIView): 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 @@ -2133,6 +2192,23 @@ class IssueAttachmentDetailAPIEndpoint(BaseAPIView): 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 )