Skip to content

Commit 2a9a747

Browse files
authored
Refactor engagement and risk acceptance permissions (#14155)
* Refactor engagement permissions: introduce BaseRelatedObjectPermission and update related views * Refactor permission classes for risk acceptance and findings in views * Refactor permission classes: introduce UserHasDevelopmentEnvironmentPermission, UserHasRegulationPermission, and UserHasSLAPermission; update views accordingly * Refactor BaseDjangoModelPermission: short circuit permission evaluation for unsupported request methods * Refactor RiskAcceptanceViewSet: simplify download_proof method by moving permission_classes to the decorator * Add global role fixture and enhance test setup for permissions * Refactor test setup in BaseClass: consolidate user authentication logic into a reusable method * Create new user rather than hijacking an existing one * More user fun :)
1 parent 891cf5d commit 2a9a747

5 files changed

Lines changed: 333 additions & 159 deletions

File tree

dojo/api_v2/permissions.py

Lines changed: 149 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import re
21

32
from django.db.models import Model
43
from django.shortcuts import get_object_or_404
@@ -20,13 +19,16 @@
2019
from dojo.importers.auto_create_context import AutoCreateContextManager
2120
from dojo.models import (
2221
Cred_Mapping,
22+
Development_Environment,
2323
Dojo_Group,
2424
Endpoint,
2525
Engagement,
2626
Finding,
2727
Finding_Group,
2828
Product,
2929
Product_Type,
30+
Regulation,
31+
SLA_Configuration,
3032
Test,
3133
)
3234

@@ -60,6 +62,72 @@ def check_object_permission(
6062
return False
6163

6264

65+
class BaseRelatedObjectPermission(permissions.BasePermission):
66+
67+
"""
68+
An "abstract" base class for related object permissions (like notes, metadata, etc.)
69+
that only need object permissions, not general permissions. This class will serve as
70+
the base class for other more aptly named permission classes.
71+
"""
72+
73+
permission_map: dict[str, int] = {
74+
"get_permission": None,
75+
"put_permission": None,
76+
"delete_permission": None,
77+
"post_permission": None,
78+
}
79+
80+
def has_permission(self, request: Request, view):
81+
# related object only need object permission
82+
return True
83+
84+
def has_object_permission(self, request: Request, view, obj):
85+
return check_object_permission(
86+
request,
87+
obj,
88+
**self.permission_map,
89+
)
90+
91+
92+
class BaseDjangoModelPermission(permissions.BasePermission):
93+
94+
"""
95+
An "abstract" base class for Django model permissions.
96+
This class will serve as the base class for other more aptly named permission classes.
97+
"""
98+
99+
django_model: Model = None
100+
request_method_permission_map: dict[str, str] = {
101+
"GET": "view",
102+
"POST": "add",
103+
"PUT": "change",
104+
"PATCH": "change",
105+
"DELETE": "delete",
106+
}
107+
108+
def _evaluate_permissions(self, request: Request, permissions: dict[str, str]) -> bool:
109+
# Short circuit if the request method is not in the expected methods
110+
if request.method not in permissions:
111+
return True
112+
# Evaluate the permissions as usual
113+
for method, permission in permissions.items():
114+
if request.method == method:
115+
return user_has_configuration_permission(
116+
request.user,
117+
f"{self.django_model._meta.app_label}.{permission}_{self.django_model._meta.model_name}",
118+
)
119+
return False
120+
121+
def has_permission(self, request: Request, view):
122+
# First restrict the mapping got GET/POST only
123+
expected_request_method_permission_map = {k: v for k, v in self.request_method_permission_map.items() if k in {"GET", "POST"}}
124+
# Evaluate the permissions
125+
return self._evaluate_permissions(request, expected_request_method_permission_map)
126+
127+
def has_object_permission(self, request: Request, view, obj):
128+
return self._evaluate_permissions(request, self.request_method_permission_map)
129+
130+
63131
class UserHasAppAnalysisPermission(permissions.BasePermission):
64132
def has_permission(self, request, view):
65133
return check_post_permission(
@@ -279,132 +347,82 @@ def has_object_permission(self, request, view, obj):
279347

280348

281349
class UserHasEngagementPermission(permissions.BasePermission):
282-
# Permission checks for related objects (like notes or metadata) can be moved
283-
# into a seperate class, when the legacy authorization will be removed.
284-
path_engagement_post = re.compile(r"^/api/v2/engagements/$")
285-
path_engagement = re.compile(r"^/api/v2/engagements/\d+/$")
286-
287350
def has_permission(self, request, view):
288-
if UserHasEngagementPermission.path_engagement_post.match(
289-
request.path,
290-
) or UserHasEngagementPermission.path_engagement.match(request.path):
291-
return check_post_permission(
351+
return check_post_permission(
292352
request, Product, "product", Permissions.Engagement_Add,
293353
)
294-
# related object only need object permission
295-
return True
296354

297355
def has_object_permission(self, request, view, obj):
298-
if UserHasEngagementPermission.path_engagement_post.match(
299-
request.path,
300-
) or UserHasEngagementPermission.path_engagement.match(request.path):
301-
return check_object_permission(
302-
request,
303-
obj,
304-
Permissions.Engagement_View,
305-
Permissions.Engagement_Edit,
306-
Permissions.Engagement_Delete,
307-
)
308356
return check_object_permission(
309357
request,
310358
obj,
311359
Permissions.Engagement_View,
312360
Permissions.Engagement_Edit,
313-
Permissions.Engagement_Edit,
314-
Permissions.Engagement_Edit,
361+
Permissions.Engagement_Delete,
315362
)
316363

317364

318-
class UserHasRiskAcceptancePermission(permissions.BasePermission):
319-
# Permission checks for related objects (like notes or metadata) can be moved
320-
# into a seperate class, when the legacy authorization will be removed.
321-
path_risk_acceptance_post = re.compile(r"^/api/v2/risk_acceptances/$")
322-
path_risk_acceptance = re.compile(r"^/api/v2/risk_acceptances/\d+/$")
365+
class UserHasEngagementRelatedObjectPermission(BaseRelatedObjectPermission):
366+
permission_map = {
367+
"get_permission": Permissions.Engagement_View,
368+
"put_permission": Permissions.Engagement_Edit,
369+
"delete_permission": Permissions.Engagement_Edit,
370+
"post_permission": Permissions.Engagement_Edit,
371+
}
372+
323373

374+
class UserHasRiskAcceptancePermission(permissions.BasePermission):
324375
def has_permission(self, request, view):
325-
if UserHasRiskAcceptancePermission.path_risk_acceptance_post.match(
326-
request.path,
327-
) or UserHasRiskAcceptancePermission.path_risk_acceptance.match(
328-
request.path,
329-
):
330-
return check_post_permission(
331-
request, Product, "product", Permissions.Risk_Acceptance,
332-
)
333-
# related object only need object permission
376+
# The previous implementation only checked for the object permission if the path was
377+
# /api/v2/risk_acceptances/, but the path has always been /api/v2/risk_acceptance/ (notice the missing "s")
378+
# So there really has not been a notion of a post permission check for risk acceptances.
379+
# It would be best to leave as is to not break any existing implementations.
334380
return True
335381

336382
def has_object_permission(self, request, view, obj):
337-
if UserHasRiskAcceptancePermission.path_risk_acceptance_post.match(
338-
request.path,
339-
) or UserHasRiskAcceptancePermission.path_risk_acceptance.match(
340-
request.path,
341-
):
342-
return check_object_permission(
343-
request,
344-
obj,
345-
Permissions.Risk_Acceptance,
346-
Permissions.Risk_Acceptance,
347-
Permissions.Risk_Acceptance,
348-
)
349383
return check_object_permission(
350384
request,
351385
obj,
352386
Permissions.Risk_Acceptance,
353387
Permissions.Risk_Acceptance,
354388
Permissions.Risk_Acceptance,
355-
Permissions.Risk_Acceptance,
356389
)
357390

358391

359-
class UserHasFindingPermission(permissions.BasePermission):
360-
# Permission checks for related objects (like notes or metadata) can be moved
361-
# into a seperate class, when the legacy authorization will be removed.
362-
path_finding_post = re.compile(r"^/api/v2/findings/$")
363-
path_finding = re.compile(r"^/api/v2/findings/\d+/$")
364-
path_stub_finding_post = re.compile(r"^/api/v2/stub_findings/$")
365-
path_stub_finding = re.compile(r"^/api/v2/stub_findings/\d+/$")
392+
class UserHasRiskAcceptanceRelatedObjectPermission(BaseRelatedObjectPermission):
393+
permission_map = {
394+
"get_permission": Permissions.Risk_Acceptance,
395+
"put_permission": Permissions.Risk_Acceptance,
396+
"delete_permission": Permissions.Risk_Acceptance,
397+
"post_permission": Permissions.Risk_Acceptance,
398+
}
399+
366400

401+
class UserHasFindingPermission(permissions.BasePermission):
367402
def has_permission(self, request, view):
368-
if (
369-
UserHasFindingPermission.path_finding_post.match(request.path)
370-
or UserHasFindingPermission.path_finding.match(request.path)
371-
or UserHasFindingPermission.path_stub_finding_post.match(
372-
request.path,
373-
)
374-
or UserHasFindingPermission.path_stub_finding.match(request.path)
375-
):
376-
return check_post_permission(
377-
request, Test, "test", Permissions.Finding_Add,
378-
)
379-
# related object only need object permission
380-
return True
403+
return check_post_permission(
404+
request, Test, "test", Permissions.Finding_Add,
405+
)
381406

382407
def has_object_permission(self, request, view, obj):
383-
if (
384-
UserHasFindingPermission.path_finding_post.match(request.path)
385-
or UserHasFindingPermission.path_finding.match(request.path)
386-
or UserHasFindingPermission.path_stub_finding_post.match(
387-
request.path,
388-
)
389-
or UserHasFindingPermission.path_stub_finding.match(request.path)
390-
):
391-
return check_object_permission(
392-
request,
393-
obj,
394-
Permissions.Finding_View,
395-
Permissions.Finding_Edit,
396-
Permissions.Finding_Delete,
397-
)
398408
return check_object_permission(
399409
request,
400410
obj,
401411
Permissions.Finding_View,
402412
Permissions.Finding_Edit,
403-
Permissions.Finding_Edit,
404-
Permissions.Finding_Edit,
413+
Permissions.Finding_Delete,
405414
)
406415

407416

417+
class UserHasFindingRelatedObjectPermission(BaseRelatedObjectPermission):
418+
permission_map = {
419+
"get_permission": Permissions.Finding_View,
420+
"put_permission": Permissions.Finding_Edit,
421+
"delete_permission": Permissions.Finding_Edit,
422+
"post_permission": Permissions.Finding_Edit,
423+
}
424+
425+
408426
class UserHasImportPermission(permissions.BasePermission):
409427
def has_permission(self, request, view):
410428
# permission check takes place before validation, so we don't have access to serializer.validated_data()
@@ -761,42 +779,30 @@ def has_permission(self, request, view):
761779

762780

763781
class UserHasTestPermission(permissions.BasePermission):
764-
# Permission checks for related objects (like notes or metadata) can be moved
765-
# into a seperate class, when the legacy authorization will be removed.
766-
path_tests_post = re.compile(r"^/api/v2/tests/$")
767-
path_tests = re.compile(r"^/api/v2/tests/\d+/$")
768-
769782
def has_permission(self, request, view):
770-
if UserHasTestPermission.path_tests_post.match(
771-
request.path,
772-
) or UserHasTestPermission.path_tests.match(request.path):
773-
return check_post_permission(
774-
request, Engagement, "engagement", Permissions.Test_Add,
775-
)
776-
# related object only need object permission
777-
return True
783+
return check_post_permission(
784+
request, Engagement, "engagement", Permissions.Test_Add,
785+
)
778786

779787
def has_object_permission(self, request, view, obj):
780-
if UserHasTestPermission.path_tests_post.match(
781-
request.path,
782-
) or UserHasTestPermission.path_tests.match(request.path):
783-
return check_object_permission(
784-
request,
785-
obj,
786-
Permissions.Test_View,
787-
Permissions.Test_Edit,
788-
Permissions.Test_Delete,
789-
)
790788
return check_object_permission(
791789
request,
792790
obj,
793791
Permissions.Test_View,
794792
Permissions.Test_Edit,
795-
Permissions.Test_Edit,
796-
Permissions.Test_Edit,
793+
Permissions.Test_Delete,
797794
)
798795

799796

797+
class UserHasTestRelatedObjectPermission(BaseRelatedObjectPermission):
798+
permission_map = {
799+
"get_permission": Permissions.Test_View,
800+
"put_permission": Permissions.Test_Edit,
801+
"delete_permission": Permissions.Test_Edit,
802+
"post_permission": Permissions.Test_Edit,
803+
}
804+
805+
800806
class UserHasTestImportPermission(permissions.BasePermission):
801807
def has_permission(self, request, view):
802808
return check_post_permission(
@@ -1023,6 +1029,36 @@ def has_object_permission(self, request, view, obj):
10231029
)
10241030

10251031

1032+
class UserHasSLAPermission(BaseDjangoModelPermission):
1033+
django_model = SLA_Configuration
1034+
1035+
1036+
class UserHasDevelopmentEnvironmentPermission(BaseDjangoModelPermission):
1037+
django_model = Development_Environment
1038+
# https://github.com/DefectDojo/django-DefectDojo/blob/963d4a35bfd8f5138330f0d70595a755fa4999b0/dojo/user/utils.py#L93
1039+
# It looks like view permission was explicitly not supported, so I assume
1040+
# reading these endpoints are not necessarily restricted (unless you're auth'd of course)
1041+
request_method_permission_map = {
1042+
"POST": "add",
1043+
"PUT": "change",
1044+
"PATCH": "change",
1045+
"DELETE": "delete",
1046+
}
1047+
1048+
1049+
class UserHasRegulationPermission(BaseDjangoModelPermission):
1050+
django_model = Regulation
1051+
# https://github.com/DefectDojo/django-DefectDojo/blob/963d4a35bfd8f5138330f0d70595a755fa4999b0/dojo/user/utils.py#L104
1052+
# It looks like view permission was explicitly not supported, so I assume
1053+
# reading these endpoints are not necessarily restricted (unless you're auth'd of course)
1054+
request_method_permission_map = {
1055+
"POST": "add",
1056+
"PUT": "change",
1057+
"PATCH": "change",
1058+
"DELETE": "delete",
1059+
}
1060+
1061+
10261062
def raise_no_auto_create_import_validation_error(
10271063
test_title,
10281064
scan_type,

0 commit comments

Comments
 (0)