Repository navigation
fix: block restricted guests from assets of issues hidden from them #10261
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
sriramveeraghanta
merged 1 commit into
preview
from
fix/guest-hidden-issue-asset-downloads
Nov 11, 2026
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
123 changes: 123 additions & 0 deletions
123
apps/api/plane/tests/contract/app/test_guest_asset_download_scope_app.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,123 @@ | ||
| # Copyright (c) 2023-present Plane Software, Inc. and contributors | ||
| # SPDX-License-Identifier: AGPL-3.0-only | ||
| # See the LICENSE file for details. | ||
|
|
||
| # Imported pytest fixtures are requested by parameter name, which F811 misreads as redefinition. | ||
| # ruff: noqa: F811 | ||
|
|
||
| """A restricted guest (``guest_view_all_features=False``) must not read, by asset id, | ||
| the files of issues they did not create: attachments, description images and | ||
| comment images. ``FileAsset.is_project_accessible_to`` applies the rule for every | ||
| asset read route. Own issues, members and unrestricted guests are unaffected. | ||
|
|
||
| Reuses the fixtures of the sibling sub-resource test module. | ||
| """ | ||
|
|
||
| from unittest import mock | ||
| from uuid import uuid4 | ||
|
|
||
| import pytest | ||
| from rest_framework import status | ||
|
|
||
| from plane.db.models import FileAsset | ||
| from plane.tests.contract.app.test_guest_issue_subresource_scope_app import ( # noqa: F401 | ||
| _make_comment, | ||
| foreign_issue, | ||
| guest, | ||
| guest_client, | ||
| own_issue, | ||
| project, | ||
| ) | ||
|
|
||
| S3_STORAGE_PATH = "plane.app.views.asset.v2.S3Storage" | ||
|
|
||
| READ_URLS = [ | ||
| "/api/assets/v2/workspaces/{slug}/download/{asset_id}/", | ||
| "/api/assets/v2/workspaces/{slug}/projects/{project_id}/download/{asset_id}/", | ||
| "/api/assets/v2/workspaces/{slug}/{asset_id}/", | ||
| "/api/assets/v2/workspaces/{slug}/projects/{project_id}/{asset_id}/", | ||
| ] | ||
| CHECK_URL = "/api/assets/v2/workspaces/{slug}/check/{asset_id}/" | ||
| DUPLICATE_URL = "/api/assets/v2/workspaces/{slug}/duplicate-assets/{asset_id}/" | ||
|
|
||
|
|
||
| def _url(tpl, workspace, project, asset): | ||
| return tpl.format(slug=workspace.slug, project_id=project.id, asset_id=asset.id) | ||
|
|
||
|
|
||
|
|
||
| def _attachment(issue): | ||
| return FileAsset.objects.get(issue=issue, entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT) | ||
|
|
||
|
|
||
| def _get_all(client, workspace, project, asset): | ||
| """Status codes of every read route for ``asset``; storage is mocked.""" | ||
| with mock.patch(S3_STORAGE_PATH) as storage: | ||
| storage.return_value.generate_presigned_url.return_value = "https://example.com/s" | ||
| return [client.get(_url(tpl, workspace, project, asset)).status_code for tpl in READ_URLS] | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def foreign_description_asset(workspace, project, foreign_issue): | ||
| return FileAsset.objects.create( | ||
| attributes={"name": "diagram.png", "type": "image/png", "size": 100}, | ||
| asset=f"{workspace.id}/{uuid4().hex}-diagram.png", | ||
| workspace=workspace, | ||
| project=project, | ||
| issue_id=foreign_issue.id, | ||
| entity_type=FileAsset.EntityTypeContext.ISSUE_DESCRIPTION, | ||
| is_uploaded=True, | ||
| ) | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def foreign_comment_asset(workspace, project, foreign_issue, create_user): | ||
| # Comment images carry the comment, not the issue (ProjectBulkAssetEndpoint). | ||
| comment = _make_comment(foreign_issue, project, workspace, create_user) | ||
| return FileAsset.objects.create( | ||
| attributes={"name": "screenshot.png", "type": "image/png", "size": 100}, | ||
| asset=f"{workspace.id}/{uuid4().hex}-screenshot.png", | ||
| workspace=workspace, | ||
| project=project, | ||
| comment_id=comment.id, | ||
| entity_type=FileAsset.EntityTypeContext.COMMENT_DESCRIPTION, | ||
| is_uploaded=True, | ||
| ) | ||
|
|
||
|
|
||
| @pytest.mark.contract | ||
| @pytest.mark.django_db | ||
| class TestGuestHiddenIssueAssets: | ||
| def test_guest_blocked_from_foreign_issue_assets( | ||
| self, guest_client, workspace, project, foreign_issue, foreign_description_asset, foreign_comment_asset | ||
| ): | ||
| for asset in (_attachment(foreign_issue), foreign_description_asset, foreign_comment_asset): | ||
| assert _get_all(guest_client, workspace, project, asset) == [status.HTTP_403_FORBIDDEN] * 4, ( | ||
| asset.entity_type | ||
| ) | ||
|
|
||
| def test_guest_check_and_duplicate_blocked(self, guest_client, workspace, project, foreign_issue): | ||
| asset = _attachment(foreign_issue) | ||
| assert guest_client.get(_url(CHECK_URL, workspace, project, asset)).data == {"exists": False} | ||
|
|
||
| before = FileAsset.objects.count() | ||
| with mock.patch(S3_STORAGE_PATH) as storage: | ||
| response = guest_client.post( | ||
| _url(DUPLICATE_URL, workspace, project, asset), | ||
| {"entity_type": FileAsset.EntityTypeContext.ISSUE_ATTACHMENT, "project_id": str(project.id)}, | ||
| format="json", | ||
| ) | ||
| assert response.status_code == status.HTTP_403_FORBIDDEN | ||
| storage.return_value.copy_object.assert_not_called() | ||
| assert FileAsset.objects.count() == before | ||
|
|
||
| def test_guest_allowed_own_issue_assets(self, guest_client, workspace, project, own_issue): | ||
| assert _get_all(guest_client, workspace, project, _attachment(own_issue)) == [status.HTTP_302_FOUND] * 4 | ||
|
|
||
| def test_member_unaffected(self, session_client, workspace, project, foreign_issue, foreign_comment_asset): | ||
| for asset in (_attachment(foreign_issue), foreign_comment_asset): | ||
| assert _get_all(session_client, workspace, project, asset) == [status.HTTP_302_FOUND] * 4 | ||
|
|
||
| def test_unrestricted_guest_unaffected(self, guest_client, workspace, project, foreign_issue): | ||
| project.guest_view_all_features = True | ||
| project.save(update_fields=["guest_view_all_features"]) | ||
| assert _get_all(guest_client, workspace, project, _attachment(foreign_issue)) == [status.HTTP_302_FOUND] * 4 | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Script executed:
sed -n 95,150p apps/api/plane/db/models/asset.py rg -n 'is_project_accessible_to|COMMENT_DESCRIPTION|comment_id' apps/api/plane/app/views/asset/v2.pyRepository: makeplane/plane
Length of output: 3495
Script executed:
Repository: makeplane/plane
Length of output: 15047
Script executed:
Repository: makeplane/plane
Length of output: 25996
Script executed:
Repository: makeplane/plane
Length of output: 16736
Script executed:
Repository: makeplane/plane
Length of output: 6345
Check issue links before allowing a projectless asset.
Workspace uploads and bulk comment association can leave an issue-linked asset without a project. If the asset is uploaded, a restricted guest with its ID can get a signed URL through the workspace download route, even when another member created the issue.
Resolve the issue and its project before allowing a projectless issue or comment asset. Apply the project membership and guest visibility checks, while preserving access to workspace-level assets. Add a contract test for this upload shape.