|
1 | | -import re |
2 | 1 |
|
3 | 2 | from django.db.models import Model |
4 | 3 | from django.shortcuts import get_object_or_404 |
|
20 | 19 | from dojo.importers.auto_create_context import AutoCreateContextManager |
21 | 20 | from dojo.models import ( |
22 | 21 | Cred_Mapping, |
| 22 | + Development_Environment, |
23 | 23 | Dojo_Group, |
24 | 24 | Endpoint, |
25 | 25 | Engagement, |
26 | 26 | Finding, |
27 | 27 | Finding_Group, |
28 | 28 | Product, |
29 | 29 | Product_Type, |
| 30 | + Regulation, |
| 31 | + SLA_Configuration, |
30 | 32 | Test, |
31 | 33 | ) |
32 | 34 |
|
@@ -60,6 +62,72 @@ def check_object_permission( |
60 | 62 | return False |
61 | 63 |
|
62 | 64 |
|
| 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 | + |
63 | 131 | class UserHasAppAnalysisPermission(permissions.BasePermission): |
64 | 132 | def has_permission(self, request, view): |
65 | 133 | return check_post_permission( |
@@ -279,132 +347,82 @@ def has_object_permission(self, request, view, obj): |
279 | 347 |
|
280 | 348 |
|
281 | 349 | 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 | | - |
287 | 350 | 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( |
292 | 352 | request, Product, "product", Permissions.Engagement_Add, |
293 | 353 | ) |
294 | | - # related object only need object permission |
295 | | - return True |
296 | 354 |
|
297 | 355 | 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 | | - ) |
308 | 356 | return check_object_permission( |
309 | 357 | request, |
310 | 358 | obj, |
311 | 359 | Permissions.Engagement_View, |
312 | 360 | Permissions.Engagement_Edit, |
313 | | - Permissions.Engagement_Edit, |
314 | | - Permissions.Engagement_Edit, |
| 361 | + Permissions.Engagement_Delete, |
315 | 362 | ) |
316 | 363 |
|
317 | 364 |
|
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 | + |
323 | 373 |
|
| 374 | +class UserHasRiskAcceptancePermission(permissions.BasePermission): |
324 | 375 | 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. |
334 | 380 | return True |
335 | 381 |
|
336 | 382 | 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 | | - ) |
349 | 383 | return check_object_permission( |
350 | 384 | request, |
351 | 385 | obj, |
352 | 386 | Permissions.Risk_Acceptance, |
353 | 387 | Permissions.Risk_Acceptance, |
354 | 388 | Permissions.Risk_Acceptance, |
355 | | - Permissions.Risk_Acceptance, |
356 | 389 | ) |
357 | 390 |
|
358 | 391 |
|
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 | + |
366 | 400 |
|
| 401 | +class UserHasFindingPermission(permissions.BasePermission): |
367 | 402 | 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 | + ) |
381 | 406 |
|
382 | 407 | 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 | | - ) |
398 | 408 | return check_object_permission( |
399 | 409 | request, |
400 | 410 | obj, |
401 | 411 | Permissions.Finding_View, |
402 | 412 | Permissions.Finding_Edit, |
403 | | - Permissions.Finding_Edit, |
404 | | - Permissions.Finding_Edit, |
| 413 | + Permissions.Finding_Delete, |
405 | 414 | ) |
406 | 415 |
|
407 | 416 |
|
| 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 | + |
408 | 426 | class UserHasImportPermission(permissions.BasePermission): |
409 | 427 | def has_permission(self, request, view): |
410 | 428 | # 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): |
761 | 779 |
|
762 | 780 |
|
763 | 781 | 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 | | - |
769 | 782 | 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 | + ) |
778 | 786 |
|
779 | 787 | 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 | | - ) |
790 | 788 | return check_object_permission( |
791 | 789 | request, |
792 | 790 | obj, |
793 | 791 | Permissions.Test_View, |
794 | 792 | Permissions.Test_Edit, |
795 | | - Permissions.Test_Edit, |
796 | | - Permissions.Test_Edit, |
| 793 | + Permissions.Test_Delete, |
797 | 794 | ) |
798 | 795 |
|
799 | 796 |
|
| 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 | + |
800 | 806 | class UserHasTestImportPermission(permissions.BasePermission): |
801 | 807 | def has_permission(self, request, view): |
802 | 808 | return check_post_permission( |
@@ -1023,6 +1029,36 @@ def has_object_permission(self, request, view, obj): |
1023 | 1029 | ) |
1024 | 1030 |
|
1025 | 1031 |
|
| 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 | + |
1026 | 1062 | def raise_no_auto_create_import_validation_error( |
1027 | 1063 | test_title, |
1028 | 1064 | scan_type, |
|
0 commit comments