[WEB-8066] refactor: return bool from asset access helper, build Response in views

Address review (Saurabhkmr98): rename project_membership_denied ->
has_project_asset_access, returning a boolean (True = allowed) instead of a
Response. Each of get/patch/delete now builds the 403 Response based on the
returned value. Behaviour is unchanged (same 403 + message; workspace-level
assets with project_id=None still allowed).

Co-authored-by: Plane AI <noreply@plane.so>
This commit is contained in:
Manish Gupta
2026-07-10 15:38:56 +05:30
parent 90b6416ebf
commit be67aab7f9

View File

@@ -312,35 +312,29 @@ class WorkspaceFileAssetEndpoint(BaseAPIView):
else:
return
def project_membership_denied(self, request, asset):
"""Enforce project-level access on a workspace-scoped asset lookup.
def has_project_asset_access(self, request, asset):
"""Return whether the user may access a workspace-scoped asset.
This endpoint is authorized at the WORKSPACE level, so a workspace
member/guest could otherwise reach an asset that belongs to a project
they are not a member of. For project-bound assets, require an active
ProjectMember of the asset's project. Workspace-level entity types
(WORKSPACE_LOGO, USER_AVATAR, USER_COVER) have project_id=None and are
exempt. Returns a 403 Response when access is denied, else None.
always allowed.
"""
if asset.project_id is None:
return None
return True
# Scope the membership lookup to the asset's workspace as well as its
# project, mirroring allow_permission's PROJECT branch. This prevents a
# member of the same project in a different workspace from passing the
# check should an asset row ever be inconsistent (asset.workspace_id !=
# asset.project.workspace_id).
is_project_member = ProjectMember.objects.filter(
return ProjectMember.objects.filter(
member=request.user,
workspace_id=asset.workspace_id,
project_id=asset.project_id,
is_active=True,
).exists()
if not is_project_member:
return Response(
{"error": "You don't have access to this asset."},
status=status.HTTP_403_FORBIDDEN,
)
return None
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
def post(self, request, slug):
@@ -424,9 +418,11 @@ class WorkspaceFileAssetEndpoint(BaseAPIView):
# get the asset id
asset = FileAsset.objects.get(id=asset_id, workspace__slug=slug)
# enforce project-level access for project-bound assets
denied = self.project_membership_denied(request, asset)
if denied is not None:
return denied
if not self.has_project_asset_access(request, asset):
return Response(
{"error": "You don't have access to this asset."},
status=status.HTTP_403_FORBIDDEN,
)
# get the storage metadata
asset.is_uploaded = True
# get the storage metadata
@@ -449,9 +445,11 @@ class WorkspaceFileAssetEndpoint(BaseAPIView):
def delete(self, request, slug, asset_id):
asset = FileAsset.objects.get(id=asset_id, workspace__slug=slug)
# enforce project-level access for project-bound assets
denied = self.project_membership_denied(request, asset)
if denied is not None:
return denied
if not self.has_project_asset_access(request, asset):
return Response(
{"error": "You don't have access to this asset."},
status=status.HTTP_403_FORBIDDEN,
)
asset.is_deleted = True
asset.deleted_at = timezone.now()
# get the entity and save the asset id for the request field
@@ -464,9 +462,11 @@ class WorkspaceFileAssetEndpoint(BaseAPIView):
# get the asset id
asset = FileAsset.objects.get(id=asset_id, workspace__slug=slug)
# enforce project-level access for project-bound assets
denied = self.project_membership_denied(request, asset)
if denied is not None:
return denied
if not self.has_project_asset_access(request, asset):
return Response(
{"error": "You don't have access to this asset."},
status=status.HTTP_403_FORBIDDEN,
)
# Check if the asset is uploaded
if not asset.is_uploaded: