Skip to content

Commit 9a895cd

Browse files
authored
Fix reader file upload (#15133)
1 parent b11c831 commit 9a895cd

7 files changed

Lines changed: 90 additions & 26 deletions

File tree

dojo/authorization/api_permissions.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
user_has_permission,
1717
user_is_superuser_or_global_owner,
1818
)
19+
from dojo.authorization.roles_permissions import Permissions
1920
from dojo.importers.auto_create_context import AutoCreateContextManager
2021
from dojo.location.models import Location
2122
from dojo.models import (
@@ -401,6 +402,15 @@ class UserHasEngagementRelatedObjectPermission(BaseRelatedObjectPermission):
401402
}
402403

403404

405+
class UserHasEngagementFilePermission(BaseRelatedObjectPermission):
406+
permission_map = {
407+
"get_permission": Permissions.Product_Tracking_Files_View,
408+
"put_permission": Permissions.Product_Tracking_Files_Edit,
409+
"delete_permission": Permissions.Product_Tracking_Files_Delete,
410+
"post_permission": Permissions.Product_Tracking_Files_Add,
411+
}
412+
413+
404414
class UserHasEngagementNotePermission(BaseRelatedObjectPermission):
405415
permission_map = {
406416
"get_permission": "view",
@@ -462,6 +472,15 @@ class UserHasFindingRelatedObjectPermission(BaseRelatedObjectPermission):
462472
}
463473

464474

475+
class UserHasFindingFilePermission(BaseRelatedObjectPermission):
476+
permission_map = {
477+
"get_permission": Permissions.Product_Tracking_Files_View,
478+
"put_permission": Permissions.Product_Tracking_Files_Edit,
479+
"delete_permission": Permissions.Product_Tracking_Files_Delete,
480+
"post_permission": Permissions.Product_Tracking_Files_Add,
481+
}
482+
483+
465484
class UserHasFindingNotePermission(BaseRelatedObjectPermission):
466485
permission_map = {
467486
"get_permission": "view",
@@ -778,6 +797,15 @@ class UserHasTestRelatedObjectPermission(BaseRelatedObjectPermission):
778797
}
779798

780799

800+
class UserHasTestFilePermission(BaseRelatedObjectPermission):
801+
permission_map = {
802+
"get_permission": Permissions.Product_Tracking_Files_View,
803+
"put_permission": Permissions.Product_Tracking_Files_Edit,
804+
"delete_permission": Permissions.Product_Tracking_Files_Delete,
805+
"post_permission": Permissions.Product_Tracking_Files_Add,
806+
}
807+
808+
781809
class UserHasTestNotePermission(BaseRelatedObjectPermission):
782810
permission_map = {
783811
"get_permission": "view",

dojo/engagement/api/views.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ def notes(self, request, pk=None):
219219
responses={status.HTTP_201_CREATED: api_v2_serializers.FileSerializer},
220220
)
221221
@action(
222-
detail=True, methods=["get", "post"], parser_classes=(MultiPartParser,), permission_classes=[IsAuthenticated, permissions.UserHasEngagementRelatedObjectPermission],
222+
detail=True, methods=["get", "post"], parser_classes=(MultiPartParser,), permission_classes=[IsAuthenticated, permissions.UserHasEngagementFilePermission],
223223
)
224224
def files(self, request, pk=None):
225225
engagement = self.get_object()

dojo/finding/api/views.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -480,7 +480,7 @@ def notes(self, request, pk=None):
480480
responses={status.HTTP_201_CREATED: api_v2_serializers.FileSerializer},
481481
)
482482
@action(
483-
detail=True, methods=["get", "post"], parser_classes=(MultiPartParser,), permission_classes=(IsAuthenticated, permissions.UserHasFindingRelatedObjectPermission),
483+
detail=True, methods=["get", "post"], parser_classes=(MultiPartParser,), permission_classes=(IsAuthenticated, permissions.UserHasFindingFilePermission),
484484
)
485485
def files(self, request, pk=None):
486486
finding = self.get_object()
@@ -518,7 +518,7 @@ def files(self, request, pk=None):
518518
@action(
519519
detail=True,
520520
methods=["get"],
521-
url_path=r"files/download/(?P<file_id>\d+)", permission_classes=(IsAuthenticated, permissions.UserHasFindingRelatedObjectPermission),
521+
url_path=r"files/download/(?P<file_id>\d+)", permission_classes=(IsAuthenticated, permissions.UserHasFindingFilePermission),
522522
)
523523
def download_file(self, request, file_id, pk=None):
524524
finding = self.get_object()

dojo/forms.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -572,6 +572,7 @@ def clean(self):
572572

573573

574574
ManageFileFormSet = modelformset_factory(FileUpload, extra=3, max_num=10, fields=["title", "file"], can_delete=True, formset=BaseManageFileFormSet)
575+
AddOnlyManageFileFormSet = modelformset_factory(FileUpload, extra=3, max_num=10, fields=["title", "file"], can_delete=False, formset=BaseManageFileFormSet)
575576

576577

577578
# Risk acceptance forms live in dojo/risk_acceptance/ui/forms.py. Re-exported here for

dojo/test/api/views.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ def notes(self, request, pk=None):
194194
responses={status.HTTP_201_CREATED: api_v2_serializers.FileSerializer},
195195
)
196196
@action(
197-
detail=True, methods=["get", "post"], parser_classes=(MultiPartParser,), permission_classes=(IsAuthenticated, permissions.UserHasTestRelatedObjectPermission),
197+
detail=True, methods=["get", "post"], parser_classes=(MultiPartParser,), permission_classes=(IsAuthenticated, permissions.UserHasTestFilePermission),
198198
)
199199
def files(self, request, pk=None):
200200
test = self.get_object()
@@ -233,7 +233,7 @@ def files(self, request, pk=None):
233233
detail=True,
234234
methods=["get"],
235235
url_path=r"files/download/(?P<file_id>\d+)",
236-
permission_classes=(IsAuthenticated, permissions.UserHasTestRelatedObjectPermission),
236+
permission_classes=(IsAuthenticated, permissions.UserHasTestFilePermission),
237237
)
238238
def download_file(self, request, file_id, pk=None):
239239
test = self.get_object()

dojo/views.py

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@
1010
from django.shortcuts import get_object_or_404, render
1111
from django.urls import reverse
1212

13-
from dojo.authorization.authorization import user_has_permission_or_403
14-
from dojo.forms import ManageFileFormSet
13+
from dojo.authorization.authorization import user_has_permission, user_has_permission_or_403
14+
from dojo.authorization.roles_permissions import Permissions
15+
from dojo.forms import AddOnlyManageFileFormSet, ManageFileFormSet
1516
from dojo.models import (
1617
Engagement,
1718
FileUpload,
@@ -42,34 +43,39 @@ def custom_bad_request_view(request, exception=None):
4243
def manage_files(request, oid, obj_type):
4344
if obj_type == "Engagement":
4445
obj = get_object_or_404(Engagement, pk=oid)
45-
user_has_permission_or_403(request.user, obj, "edit")
4646
obj_vars = ("view_engagement", "engagement_set")
4747
elif obj_type == "Test":
4848
obj = get_object_or_404(Test, pk=oid)
49-
user_has_permission_or_403(request.user, obj, "edit")
5049
obj_vars = ("view_test", "test_set")
5150
elif obj_type == "Finding":
5251
obj = get_object_or_404(Finding, pk=oid)
53-
user_has_permission_or_403(request.user, obj, "edit")
5452
obj_vars = ("view_finding", "finding_set")
5553
else:
5654
raise Http404
5755

58-
files_formset = ManageFileFormSet(queryset=obj.files.all())
56+
has_file_add_permission = user_has_permission(request.user, obj, Permissions.Product_Tracking_Files_Add)
57+
has_file_edit_permission = user_has_permission(request.user, obj, Permissions.Product_Tracking_Files_Edit)
58+
if not (has_file_add_permission or has_file_edit_permission):
59+
raise PermissionDenied
60+
61+
formset_class = ManageFileFormSet if has_file_edit_permission else AddOnlyManageFileFormSet
62+
files_queryset = obj.files.all() if has_file_edit_permission else FileUpload.objects.none()
63+
files_formset = formset_class(queryset=files_queryset)
5964
error = False
6065

6166
if request.method == "POST":
62-
files_formset = ManageFileFormSet(
63-
request.POST, request.FILES, queryset=obj.files.all())
67+
files_formset = formset_class(
68+
request.POST, request.FILES, queryset=files_queryset)
6469
if files_formset.is_valid():
6570
# remove all from database and disk
6671

6772
files_formset.save()
6873

69-
for o in files_formset.deleted_objects:
70-
logger.debug("removing file: %s", o.file.name)
71-
with suppress(FileNotFoundError):
72-
(Path(settings.MEDIA_ROOT) / o.file.name).unlink()
74+
for o in getattr(files_formset, "deleted_objects", []):
75+
if has_file_edit_permission:
76+
logger.debug("removing file: %s", o.file.name)
77+
with suppress(FileNotFoundError):
78+
(Path(settings.MEDIA_ROOT) / o.file.name).unlink()
7379

7480
for o in files_formset.new_objects:
7581
logger.debug("adding file: %s", o.file.name)

unittests/test_permissions_audit.py

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1406,6 +1406,9 @@ def _client_for_user(self, user):
14061406
client.credentials(HTTP_AUTHORIZATION="Token " + token.key)
14071407
return client
14081408

1409+
def _authorize_reader_user(self):
1410+
self.product.authorized_users.add(self.reader_user)
1411+
14091412
def setUp(self):
14101413
super().setUp()
14111414
# Legacy auth collapses Reader/Writer/Maintainer/Owner into
@@ -1493,11 +1496,13 @@ def test_engagement_notes_post_writer_allowed(self):
14931496

14941497
# ── Engagement: files ──────────────────────────────────────────────
14951498

1496-
def test_engagement_files_post_reader_denied(self):
1499+
def test_engagement_files_post_member_allowed(self):
1500+
self._authorize_reader_user()
14971501
client = self._client_for_user(self.reader_user)
14981502
url = reverse("engagement-files", args=(self.engagement.id,))
1499-
response = client.post(url, data={}, format="json")
1500-
self.assertEqual(response.status_code, 403, response.content)
1503+
test_file = SimpleUploadedFile("reader-proof.txt", b"engagement file content", content_type="text/plain")
1504+
response = client.post(url, data={"title": "reader proof", "file": test_file}, format="multipart")
1505+
self.assertEqual(response.status_code, 201, response.content)
15011506

15021507
def test_engagement_files_post_writer_allowed(self):
15031508
client = self._client_for_user(self.writer_user)
@@ -1587,11 +1592,13 @@ def test_finding_notes_post_writer_allowed(self):
15871592

15881593
# ── Finding: files ─────────────────────────────────────────────────
15891594

1590-
def test_finding_files_post_reader_denied(self):
1595+
def test_finding_files_post_member_allowed(self):
1596+
self._authorize_reader_user()
15911597
client = self._client_for_user(self.reader_user)
15921598
url = reverse("finding-files", args=(self.finding.id,))
1593-
response = client.post(url, data={}, format="json")
1594-
self.assertEqual(response.status_code, 403, response.content)
1599+
test_file = SimpleUploadedFile("reader-evidence.txt", b"finding file content", content_type="text/plain")
1600+
response = client.post(url, data={"title": "reader evidence", "file": test_file}, format="multipart")
1601+
self.assertEqual(response.status_code, 201, response.content)
15951602

15961603
def test_finding_files_post_writer_allowed(self):
15971604
client = self._client_for_user(self.writer_user)
@@ -1600,6 +1607,26 @@ def test_finding_files_post_writer_allowed(self):
16001607
response = client.post(url, data={"title": "test evidence", "file": test_file}, format="multipart")
16011608
self.assertEqual(response.status_code, 201, response.content)
16021609

1610+
def test_manage_files_member_can_upload_to_finding(self):
1611+
self._authorize_reader_user()
1612+
client = Client()
1613+
client.login(username="relobjperm_reader", password="testTEST1234!@#$") # noqa: S106
1614+
test_file = SimpleUploadedFile("reader-ui-evidence.txt", b"finding file content", content_type="text/plain")
1615+
response = client.post(
1616+
reverse("manage_files", args=(self.finding.id, "Finding")),
1617+
data={
1618+
"form-TOTAL_FORMS": "3",
1619+
"form-INITIAL_FORMS": "0",
1620+
"form-MIN_NUM_FORMS": "0",
1621+
"form-MAX_NUM_FORMS": "10",
1622+
"form-0-title": "reader ui evidence",
1623+
"form-0-file": test_file,
1624+
},
1625+
)
1626+
1627+
self.assertEqual(response.status_code, 302, response.content)
1628+
self.assertTrue(self.finding.files.filter(title="reader ui evidence").exists())
1629+
16031630
# ── Finding: remove_note (NotePermission — PATCH uses Edit) ────────
16041631

16051632
def test_finding_remove_note_reader_denied(self):
@@ -1691,11 +1718,13 @@ def test_test_notes_post_writer_allowed(self):
16911718

16921719
# ── Test: files ────────────────────────────────────────────────────
16931720

1694-
def test_test_files_post_reader_denied(self):
1721+
def test_test_files_post_member_allowed(self):
1722+
self._authorize_reader_user()
16951723
client = self._client_for_user(self.reader_user)
16961724
url = reverse("test-files", args=(self.test.id,))
1697-
response = client.post(url, data={}, format="json")
1698-
self.assertEqual(response.status_code, 403, response.content)
1725+
test_file = SimpleUploadedFile("reader-results.txt", b"test file content", content_type="text/plain")
1726+
response = client.post(url, data={"title": "reader results", "file": test_file}, format="multipart")
1727+
self.assertEqual(response.status_code, 201, response.content)
16991728

17001729
def test_test_files_post_writer_allowed(self):
17011730
client = self._client_for_user(self.writer_user)

0 commit comments

Comments
 (0)