Skip to content
Open
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
25 changes: 22 additions & 3 deletions apps/api/plane/app/views/asset/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

# Module imports
from ..base import BaseAPIView, BaseViewSet
from plane.db.models import FileAsset, Workspace
from plane.db.models import FileAsset, Workspace, WorkspaceMember
from plane.app.serializers import FileAssetSerializer


Expand All @@ -21,6 +21,12 @@ class FileAssetEndpoint(BaseAPIView):
"""

def get(self, request, workspace_id, asset_key):
# Verify the requesting user is a member of this workspace
if not WorkspaceMember.objects.filter(workspace_id=workspace_id, member=request.user, is_active=True).exists():
return Response(
{"error": "Requested resource could not be found.", "status": False},
status=status.HTTP_404_NOT_FOUND,
)
asset_key = str(workspace_id) + "/" + asset_key
files = FileAsset.objects.filter(asset=asset_key)
if files.exists():
Expand All @@ -33,15 +39,25 @@ def get(self, request, workspace_id, asset_key):
)

def post(self, request, slug):
# Verify the requesting user is a member of this workspace
workspace = Workspace.objects.filter(slug=slug).first()
if not workspace:
return Response({"error": "Workspace not found.", "status": False}, status=status.HTTP_404_NOT_FOUND)
if not WorkspaceMember.objects.filter(workspace=workspace, member=request.user, is_active=True).exists():
return Response(
{"error": "Requested resource could not be found.", "status": False},
status=status.HTTP_404_NOT_FOUND,
)
serializer = FileAssetSerializer(data=request.data)
if serializer.is_valid():
# Get the workspace
workspace = Workspace.objects.get(slug=slug)
serializer.save(workspace_id=workspace.id)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

def delete(self, request, workspace_id, asset_key):
# Verify the requesting user is a member of this workspace
if not WorkspaceMember.objects.filter(workspace_id=workspace_id, member=request.user, is_active=True).exists():
return Response({"error": "Requested resource could not be found."}, status=status.HTTP_404_NOT_FOUND)
asset_key = str(workspace_id) + "/" + asset_key
file_asset = FileAsset.objects.get(asset=asset_key)
file_asset.is_deleted = True
Expand All @@ -51,6 +67,9 @@ def delete(self, request, workspace_id, asset_key):

class FileAssetViewSet(BaseViewSet):
def restore(self, request, workspace_id, asset_key):
# Verify the requesting user is a member of this workspace
if not WorkspaceMember.objects.filter(workspace_id=workspace_id, member=request.user, is_active=True).exists():
return Response({"error": "Requested resource could not be found."}, status=status.HTTP_404_NOT_FOUND)
asset_key = str(workspace_id) + "/" + asset_key
file_asset = FileAsset.objects.get(asset=asset_key)
file_asset.is_deleted = False
Expand Down
23 changes: 15 additions & 8 deletions apps/api/plane/app/views/asset/v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,17 @@ def post(self, request, slug):
status=status.HTTP_400_BAD_REQUEST,
)

# WORKSPACE_LOGO may only be uploaded by workspace admins
if entity_type == FileAsset.EntityTypeContext.WORKSPACE_LOGO:
workspace_member = WorkspaceMember.objects.filter(
workspace__slug=slug, member=request.user, is_active=True
).first()
if not workspace_member or workspace_member.role != ROLE.ADMIN.value:
return Response(
{"error": "Only workspace admins can upload a workspace logo."},
status=status.HTTP_403_FORBIDDEN,
)

# Check if the file type is allowed
allowed_types = [
"image/jpeg",
Expand Down Expand Up @@ -646,8 +657,8 @@ def post(self, request, slug, project_id, entity_id):
if not asset_ids:
return Response({"error": "No asset ids provided."}, status=status.HTTP_400_BAD_REQUEST)

# get the asset id
assets = FileAsset.objects.filter(id__in=asset_ids, workspace__slug=slug)
# get the asset id — scope to the project to prevent cross-project IDOR
assets = FileAsset.objects.filter(id__in=asset_ids, workspace__slug=slug, project_id=project_id)

# Get the first asset
asset = assets.first()
Expand Down Expand Up @@ -757,15 +768,11 @@ def post(self, request, slug, asset_id):
return Response({"error": "Project not found"}, status=status.HTTP_404_NOT_FOUND)

storage = S3Storage(request=request)
# Scope the source asset lookup to workspaces the caller is a member of
user_workspace_ids = WorkspaceMember.objects.filter(
member=request.user,
is_active=True,
).values_list("workspace_id", flat=True)
# Restrict the source asset to the same destination workspace to prevent cross-workspace asset copying
original_asset = FileAsset.objects.filter(
id=asset_id,
is_uploaded=True,
workspace_id__in=user_workspace_ids,
workspace=workspace,
).first()

if not original_asset:
Expand Down
11 changes: 6 additions & 5 deletions apps/api/plane/space/views/asset.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,10 @@ def get(self, request, anchor, pk):
status=status.HTTP_404_NOT_FOUND,
)

# get the asset id
# get the asset id — scope to project to prevent cross-project IDOR
asset = FileAsset.objects.get(
workspace_id=deploy_board.workspace_id,
project_id=deploy_board.project_id,
pk=pk,
entity_type__in=[
FileAsset.EntityTypeContext.ISSUE_DESCRIPTION,
Expand Down Expand Up @@ -140,8 +141,8 @@ def patch(self, request, anchor, pk):
if not deploy_board:
return Response({"error": "Project is not published"}, status=status.HTTP_404_NOT_FOUND)

# get the asset id
asset = FileAsset.objects.get(id=pk, workspace=deploy_board.workspace)
# get the asset id — scope to project to prevent cross-project IDOR
asset = FileAsset.objects.get(id=pk, workspace=deploy_board.workspace, project_id=deploy_board.project_id)
# get the storage metadata
asset.is_uploaded = True
# get the storage metadata
Expand Down Expand Up @@ -180,8 +181,8 @@ def post(self, request, anchor, pk):
if not deploy_board:
return Response({"error": "Project is not published"}, status=status.HTTP_404_NOT_FOUND)

# Get the asset
asset = FileAsset.all_objects.get(id=pk, workspace=deploy_board.workspace)
# Get the asset — scope to project to prevent cross-project IDOR
asset = FileAsset.all_objects.get(id=pk, workspace=deploy_board.workspace, project_id=deploy_board.project_id)
asset.is_deleted = False
asset.deleted_at = None
asset.save(update_fields=["is_deleted", "deleted_at"])
Expand Down
Loading