mirror of
https://github.com/makeplane/plane.git
synced 2026-08-29 10:08:51 +02:00
fix(api): address PR review + resolve merge conflict in asset duplicate endpoint
- api/permissions/__init__.py: add missing AGPL license header (fixes failing addlicense CI check) - api/permissions/workspace.py: guard workspace_slug lookup to deny cleanly instead of raising AttributeError/500; use canonical ROLE enum instead of duplicated role literals; add method docstrings - app/views/asset/v2.py: resolve merge-conflict markers in DuplicateAssetEndpoint left by the preview merge — keep preview's source-workspace scoping + sanitize_filename and retain the owning-project membership guard
This commit is contained in:
@@ -1 +1,5 @@
|
||||
# Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# See the LICENSE file for details.
|
||||
|
||||
from .workspace import WorkspaceAdminOnlyPermission, WorkspaceAdminWriteMemberReadPermission
|
||||
|
||||
@@ -4,11 +4,17 @@
|
||||
|
||||
from rest_framework.permissions import BasePermission, SAFE_METHODS
|
||||
|
||||
from plane.app.permissions import ROLE
|
||||
from plane.db.models import WorkspaceMember
|
||||
|
||||
|
||||
Admin = 20
|
||||
Member = 15
|
||||
def get_workspace_slug(view):
|
||||
"""Resolve the workspace slug from the view, returning None when it is absent.
|
||||
|
||||
Accessing ``view.workspace_slug`` directly would raise ``AttributeError`` (and a
|
||||
500) on a view that does not expose it; returning None lets the caller deny cleanly.
|
||||
"""
|
||||
return getattr(view, "workspace_slug", None)
|
||||
|
||||
|
||||
class WorkspaceAdminOnlyPermission(BasePermission):
|
||||
@@ -17,13 +23,18 @@ class WorkspaceAdminOnlyPermission(BasePermission):
|
||||
"""
|
||||
|
||||
def has_permission(self, request, view):
|
||||
"""Allow only active workspace admins."""
|
||||
if request.user.is_anonymous:
|
||||
return False
|
||||
|
||||
workspace_slug = get_workspace_slug(view)
|
||||
if not workspace_slug:
|
||||
return False
|
||||
|
||||
return WorkspaceMember.objects.filter(
|
||||
member=request.user,
|
||||
workspace__slug=view.workspace_slug,
|
||||
role=Admin,
|
||||
workspace__slug=workspace_slug,
|
||||
role=ROLE.ADMIN.value,
|
||||
is_active=True,
|
||||
).exists()
|
||||
|
||||
@@ -35,20 +46,25 @@ class WorkspaceAdminWriteMemberReadPermission(BasePermission):
|
||||
"""
|
||||
|
||||
def has_permission(self, request, view):
|
||||
"""Allow active members to read and restrict writes to active admins."""
|
||||
if request.user.is_anonymous:
|
||||
return False
|
||||
|
||||
workspace_slug = get_workspace_slug(view)
|
||||
if not workspace_slug:
|
||||
return False
|
||||
|
||||
if request.method in SAFE_METHODS:
|
||||
return WorkspaceMember.objects.filter(
|
||||
member=request.user,
|
||||
workspace__slug=view.workspace_slug,
|
||||
role__in=[Admin, Member],
|
||||
workspace__slug=workspace_slug,
|
||||
role__in=[ROLE.ADMIN.value, ROLE.MEMBER.value],
|
||||
is_active=True,
|
||||
).exists()
|
||||
|
||||
return WorkspaceMember.objects.filter(
|
||||
member=request.user,
|
||||
workspace__slug=view.workspace_slug,
|
||||
role=Admin,
|
||||
workspace__slug=workspace_slug,
|
||||
role=ROLE.ADMIN.value,
|
||||
is_active=True,
|
||||
).exists()
|
||||
|
||||
@@ -837,16 +837,8 @@ class DuplicateAssetEndpoint(BaseAPIView):
|
||||
return Response({"error": "Project not found"}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
storage = S3Storage(request=request)
|
||||
<<<<<<< HEAD
|
||||
# Scope the source asset to workspaces the caller is an active member of,
|
||||
# Scope the source asset lookup to workspaces the caller is a member of,
|
||||
# so a known asset UUID cannot be copied out of another tenant.
|
||||
member_workspace_ids = WorkspaceMember.objects.filter(
|
||||
member=request.user, is_active=True
|
||||
).values_list("workspace_id", flat=True)
|
||||
original_asset = FileAsset.objects.filter(
|
||||
id=asset_id, is_uploaded=True, workspace_id__in=member_workspace_ids
|
||||
=======
|
||||
# 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,
|
||||
@@ -855,13 +847,11 @@ class DuplicateAssetEndpoint(BaseAPIView):
|
||||
id=asset_id,
|
||||
is_uploaded=True,
|
||||
workspace_id__in=user_workspace_ids,
|
||||
>>>>>>> fd16d033fccb37f18e8794df739c8c24ba9d7939
|
||||
).first()
|
||||
|
||||
if not original_asset:
|
||||
return Response({"error": "Asset not found"}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
<<<<<<< HEAD
|
||||
# If the source asset belongs to a project, the caller must be a member of
|
||||
# that project (guards secret-project assets from cross-project duplication).
|
||||
if original_asset.project_id and not ProjectMember.objects.filter(
|
||||
@@ -871,11 +861,8 @@ class DuplicateAssetEndpoint(BaseAPIView):
|
||||
).exists():
|
||||
return Response({"error": "Asset not found"}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
destination_key = f"{workspace.id}/{uuid.uuid4().hex}-{original_asset.attributes.get('name')}"
|
||||
=======
|
||||
sanitized_name = sanitize_filename(original_asset.attributes.get("name")) or "unnamed"
|
||||
destination_key = f"{workspace.id}/{uuid.uuid4().hex}-{sanitized_name}"
|
||||
>>>>>>> fd16d033fccb37f18e8794df739c8c24ba9d7939
|
||||
duplicated_asset = FileAsset.objects.create(
|
||||
attributes={
|
||||
"name": original_asset.attributes.get("name"),
|
||||
|
||||
Reference in New Issue
Block a user