Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion apps/api/plane/api/urls/issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,9 @@
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-attachments/<uuid:pk>/",
IssueAttachmentDetailAPIEndpoint.as_view(http_method_names=["get", "delete"]),
IssueAttachmentDetailAPIEndpoint.as_view(
http_method_names=["get", "patch", "delete"]
),
name="issue-attachment",
),
]
82 changes: 79 additions & 3 deletions apps/api/plane/api/views/issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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):
"""
Expand Down Expand Up @@ -1782,7 +1798,6 @@ class IssueAttachmentListCreateAPIEndpoint(BaseAPIView):

serializer_class = IssueAttachmentSerializer
model = FileAsset
permission_classes = [ProjectEntityPermission]
use_read_replica = True

@issue_attachment_docs(
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -1989,7 +2020,6 @@ class IssueAttachmentDetailAPIEndpoint(BaseAPIView):
"""Issue Attachment Detail Endpoint"""

serializer_class = IssueAttachmentSerializer
permission_classes = [ProjectEntityPermission]
model = FileAsset
use_read_replica = True

Expand All @@ -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
)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
)
Expand Down
Loading