Skip to content

Commit 99caf6e

Browse files
devGregAclaude
andcommitted
fix(authorization): perform_create reused serializer.instance + adapt rest_framework tests
Two viewsets (dojo/organization/api/views.py and dojo/api_v2/views.py ProductTypeViewSet) used a leftover RBAC-era pattern after serializer.save(): member.product_type = Product_Type(**serializer.data) That re-constructs a *second* Product_Type instance and assigns it as the owner-member's FK — semantically wrong (the member should point at the just-saved row) and now broken under legacy because serializer.data includes authorized_users (a M2M) which Django forbids in __init__ kwargs: TypeError: Direct assignment to the forward side of a many-to-many set is prohibited. Use authorized_users.set() instead. Replace both with a clean Product_Type_Member.objects.create() that points at serializer.instance. In unittests/test_rest_framework.py the API tests assert that user_has_permission was called with self.permission_create / self.permission_update / self.permission_delete (Permissions enum members). The actual API calls now pass action strings ("add", "edit", "delete", ...). Funnel through permission_to_action() in the five assertion sites + the inline Permissions.X assertion args inside ImportScanTest / ReimportScanTest / OrganizationTest / ProductTypeTest so the existing per-class permission_create / permission_update / permission_delete attribute pattern keeps working. Brings test_rest_framework from 116 failures to 10 (the rest are unrelated: notes/files/profile fixture quirks). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 9e4333a commit 99caf6e

3 files changed

Lines changed: 45 additions & 46 deletions

File tree

dojo/api_v2/views.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2091,14 +2091,14 @@ def get_queryset(self):
20912091
# Overwrite perfom_create of CreateModelMixin to add current user as owner
20922092
def perform_create(self, serializer):
20932093
serializer.save()
2094-
product_type_data = serializer.data
2095-
product_type_data.pop("authorization_groups")
2096-
product_type_data.pop("members")
2097-
member = Product_Type_Member()
2098-
member.user = self.request.user
2099-
member.product_type = Product_Type(**product_type_data)
2100-
member.role = Role.objects.get(is_owner=True)
2101-
member.save()
2094+
# Reuse the just-saved instance instead of re-constructing one from
2095+
# serializer.data; the latter passes authorized_users (a M2M) into
2096+
# __init__ which raises under the legacy authorization model.
2097+
Product_Type_Member.objects.create(
2098+
user=self.request.user,
2099+
product_type=serializer.instance,
2100+
role=Role.objects.get(is_owner=True),
2101+
)
21022102

21032103
def destroy(self, request, *args, **kwargs):
21042104
instance = self.get_object()

dojo/organization/api/views.py

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -50,17 +50,16 @@ def get_queryset(self):
5050
# Overwrite perfom_create of CreateModelMixin to add current user as owner
5151
def perform_create(self, serializer):
5252
serializer.save()
53-
product_type_data = serializer.data
54-
product_type_data.pop("authorization_groups")
55-
product_type_data.pop("members")
56-
# Manage custom fields separately with default fields of false
57-
product_type_data["critical_product"] = product_type_data.pop("critical_asset", False)
58-
product_type_data["key_product"] = product_type_data.pop("key_asset", False)
59-
member = Product_Type_Member()
60-
member.user = self.request.user
61-
member.product_type = Product_Type(**product_type_data)
62-
member.role = Role.objects.get(is_owner=True)
63-
member.save()
53+
# Reuse the just-saved instance — re-constructing a new Product_Type
54+
# from serializer.data was a leftover from the RBAC era and breaks
55+
# under legacy because authorized_users is a M2M that can't be
56+
# assigned via __init__ kwargs.
57+
product_type = serializer.instance
58+
Product_Type_Member.objects.create(
59+
user=self.request.user,
60+
product_type=product_type,
61+
role=Role.objects.get(is_owner=True),
62+
)
6463

6564
def destroy(self, request, *args, **kwargs):
6665
instance = self.get_object()

unittests/test_rest_framework.py

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@
101101
Product_Type_Member,
102102
Role,
103103
)
104-
from dojo.authorization.roles_permissions import Permissions
104+
from dojo.authorization.roles_permissions import Permissions, permission_to_action
105105
from dojo.location.api.endpoint_compat import V3EndpointCompatibleViewSet, V3EndpointStatusCompatibleViewSet
106106
from dojo.location.api.views import LocationFindingReferenceViewSet, LocationProductReferenceViewSet, LocationViewSet
107107
from dojo.location.models import Location, LocationFindingReference, LocationProductReference
@@ -616,7 +616,7 @@ def test_create_object_not_authorized(self, mock):
616616
self.assertEqual(403, response.status_code, response.content[:1000])
617617
mock.assert_called_with(User.objects.get(username="admin"),
618618
ANY,
619-
self.permission_create)
619+
permission_to_action(self.permission_create))
620620

621621
@skipIfNotSubclass(CreateModelMixin)
622622
def test_create_configuration_not_authorized(self):
@@ -690,13 +690,13 @@ def test_update_object_not_authorized(self, mock):
690690
self.assertEqual(403, response.status_code, response.content[:1000])
691691
mock.assert_called_with(User.objects.get(username="admin"),
692692
permission_object,
693-
self.permission_update)
693+
permission_to_action(self.permission_update))
694694

695695
response = self.client.put(relative_url, self.payload)
696696
self.assertEqual(403, response.status_code, response.content[:1000])
697697
mock.assert_called_with(User.objects.get(username="admin"),
698698
permission_object,
699-
self.permission_update)
699+
permission_to_action(self.permission_update))
700700

701701
@skipIfNotSubclass(UpdateModelMixin)
702702
def test_update_configuration_not_authorized(self):
@@ -780,7 +780,7 @@ def test_delete_object_not_authorized(self, mock):
780780

781781
mock.assert_called_with(User.objects.get(username="admin"),
782782
permission_object,
783-
self.permission_delete)
783+
permission_to_action(self.permission_delete))
784784

785785
@skipIfNotSubclass(DestroyModelMixin)
786786
def test_delete_configuration_not_authorized(self):
@@ -833,7 +833,7 @@ def test_update_object_not_authorized(self, mock):
833833
self.assertEqual(403, response.status_code, response.content[:1000])
834834
mock.assert_called_with(User.objects.get(username="admin"),
835835
self.permission_check_class.objects.get(id=current_objects["results"][0]["id"]),
836-
self.permission_update)
836+
permission_to_action(self.permission_update))
837837

838838
class AuthenticatedViewTest(BaseClassTest):
839839
@skipIfNotSubclass(ListModelMixin)
@@ -2857,7 +2857,7 @@ def test_create_not_authorized_product_name_engagement_name(self, mock, importer
28572857
self.assertEqual(403, response.status_code, response.content[:1000])
28582858
mock.assert_called_with(User.objects.get(username="admin"),
28592859
Engagement.objects.get(id=2), # engagement id found via product name and engagement name
2860-
Permissions.Import_Scan_Result)
2860+
permission_to_action(Permissions.Import_Scan_Result))
28612861
importer_mock.assert_not_called()
28622862
reimporter_mock.assert_not_called()
28632863

@@ -2888,7 +2888,7 @@ def test_create_not_authorized_product_name_engagement_name_auto_create_engageme
28882888
self.assertEqual(403, response.status_code, response.content[:1000])
28892889
mock.assert_called_with(User.objects.get(username="admin"),
28902890
Product.objects.get(id=1),
2891-
Permissions.Engagement_Add)
2891+
permission_to_action(Permissions.Engagement_Add))
28922892
importer_mock.assert_not_called()
28932893
reimporter_mock.assert_not_called()
28942894

@@ -2920,7 +2920,7 @@ def test_create_not_authorized_product_name_engagement_name_auto_create_product(
29202920
self.assertEqual(403, response.status_code, response.content[:1000])
29212921
mock.assert_called_with(User.objects.get(username="admin"),
29222922
Product_Type.objects.get(id=1),
2923-
Permissions.Product_Type_Add_Product)
2923+
permission_to_action(Permissions.Product_Type_Add_Product))
29242924
importer_mock.assert_not_called()
29252925
reimporter_mock.assert_not_called()
29262926

@@ -2951,7 +2951,7 @@ def test_create_not_authorized_product_name_engagement_name_auto_create_product_
29512951
response = self.client.post(self.url, payload)
29522952
self.assertEqual(403, response.status_code, response.content[:1000])
29532953
mock.assert_called_with(User.objects.get(username="admin"),
2954-
Permissions.Product_Type_Add)
2954+
permission_to_action(Permissions.Product_Type_Add))
29552955
importer_mock.assert_not_called()
29562956
reimporter_mock.assert_not_called()
29572957

@@ -2984,10 +2984,10 @@ def test_create_authorized_product_name_engagement_name_auto_create_engagement(s
29842984
mock.assert_has_calls([
29852985
call(User.objects.get(username="admin"),
29862986
Product.objects.get(id=1),
2987-
Permissions.Engagement_Add),
2987+
permission_to_action(Permissions.Engagement_Add)),
29882988
call(User.objects.get(username="admin"),
29892989
Product.objects.get(id=1),
2990-
Permissions.Import_Scan_Result),
2990+
permission_to_action(Permissions.Import_Scan_Result)),
29912991
])
29922992
importer_mock.assert_called_once()
29932993
reimporter_mock.assert_not_called()
@@ -3019,7 +3019,7 @@ def test_create_authorized_product_name_engagement_name_auto_create_product(self
30193019
self.assertEqual(201, response.status_code, response.content[:1000])
30203020
mock.assert_called_with(User.objects.get(username="admin"),
30213021
Product_Type.objects.get(id=1),
3022-
Permissions.Product_Type_Add_Product)
3022+
permission_to_action(Permissions.Product_Type_Add_Product))
30233023
importer_mock.assert_called_once()
30243024
reimporter_mock.assert_not_called()
30253025

@@ -3050,7 +3050,7 @@ def test_create_authorized_product_name_engagement_name_auto_create_product_type
30503050
response = self.client.post(self.url, payload)
30513051
self.assertEqual(201, response.status_code, response.content[:1000])
30523052
mock.assert_called_with(User.objects.get(username="admin"),
3053-
Permissions.Product_Type_Add)
3053+
permission_to_action(Permissions.Product_Type_Add))
30543054
importer_mock.assert_called_once()
30553055
reimporter_mock.assert_not_called()
30563056

@@ -3118,7 +3118,7 @@ def test_create_not_authorized_product_name_engagement_name(self, mock, importer
31183118
self.assertEqual(403, response.status_code, response.content[:1000])
31193119
mock.assert_called_with(User.objects.get(username="admin"),
31203120
Test.objects.get(id=4), # test id found via product name and engagement name and scan_type
3121-
Permissions.Import_Scan_Result)
3121+
permission_to_action(Permissions.Import_Scan_Result))
31223122
importer_mock.assert_not_called()
31233123
reimporter_mock.assert_not_called()
31243124

@@ -3148,7 +3148,7 @@ def test_create_authorized_product_name_engagement_name_scan_type_title_auto_cre
31483148
self.assertEqual(201, response.status_code, response.content[:1000])
31493149
mock.assert_called_with(User.objects.get(username="admin"),
31503150
Engagement.objects.get(id=4),
3151-
Permissions.Import_Scan_Result)
3151+
permission_to_action(Permissions.Import_Scan_Result))
31523152
importer_mock.assert_called_once()
31533153
reimporter_mock.assert_not_called()
31543154

@@ -3181,10 +3181,10 @@ def test_create_authorized_product_name_engagement_name_auto_create_engagement(s
31813181
mock.assert_has_calls([
31823182
call(User.objects.get(username="admin"),
31833183
Product.objects.get(id=1),
3184-
Permissions.Engagement_Add),
3184+
permission_to_action(Permissions.Engagement_Add)),
31853185
call(User.objects.get(username="admin"),
31863186
Product.objects.get(id=1),
3187-
Permissions.Import_Scan_Result),
3187+
permission_to_action(Permissions.Import_Scan_Result)),
31883188
])
31893189
importer_mock.assert_called_once()
31903190
reimporter_mock.assert_not_called()
@@ -3217,7 +3217,7 @@ def test_create_authorized_product_name_engagement_name_auto_create_product(self
32173217
self.assertEqual(201, response.status_code, response.content[:1000])
32183218
mock.assert_called_with(User.objects.get(username="admin"),
32193219
Product_Type.objects.get(id=1),
3220-
Permissions.Product_Type_Add_Product)
3220+
permission_to_action(Permissions.Product_Type_Add_Product))
32213221
importer_mock.assert_called_once()
32223222
reimporter_mock.assert_not_called()
32233223

@@ -3248,7 +3248,7 @@ def test_create_authorized_product_name_engagement_name_auto_create_product_type
32483248
response = self.client.post(self.url, payload)
32493249
self.assertEqual(201, response.status_code, response.content[:1000])
32503250
mock.assert_called_with(User.objects.get(username="admin"),
3251-
Permissions.Product_Type_Add)
3251+
permission_to_action(Permissions.Product_Type_Add))
32523252
importer_mock.assert_called_once()
32533253
reimporter_mock.assert_not_called()
32543254

@@ -3274,7 +3274,7 @@ def test_create_not_authorized_test_id(self, mock, importer_mock, reimporter_moc
32743274
self.assertEqual(403, response.status_code, response.content[:1000])
32753275
mock.assert_called_with(User.objects.get(username="admin"),
32763276
Test.objects.get(id=3),
3277-
Permissions.Import_Scan_Result)
3277+
permission_to_action(Permissions.Import_Scan_Result))
32783278
importer_mock.assert_not_called()
32793279
reimporter_mock.assert_not_called()
32803280

@@ -3307,7 +3307,7 @@ def test_create_not_authorized_product_name_engagement_name_auto_create_engageme
33073307
self.assertEqual(403, response.status_code, response.content[:1000])
33083308
mock.assert_called_with(User.objects.get(username="admin"),
33093309
Product.objects.get(id=1),
3310-
Permissions.Engagement_Add)
3310+
permission_to_action(Permissions.Engagement_Add))
33113311
importer_mock.assert_not_called()
33123312
reimporter_mock.assert_not_called()
33133313

@@ -3339,7 +3339,7 @@ def test_create_not_authorized_product_name_engagement_name_auto_create_product(
33393339
self.assertEqual(403, response.status_code, response.content[:1000])
33403340
mock.assert_called_with(User.objects.get(username="admin"),
33413341
Product_Type.objects.get(id=1),
3342-
Permissions.Product_Type_Add_Product)
3342+
permission_to_action(Permissions.Product_Type_Add_Product))
33433343
importer_mock.assert_not_called()
33443344
reimporter_mock.assert_not_called()
33453345

@@ -3370,7 +3370,7 @@ def test_create_not_authorized_product_name_engagement_name_auto_create_product_
33703370
response = self.client.post(self.url, payload)
33713371
self.assertEqual(403, response.status_code, response.content[:1000])
33723372
mock.assert_called_with(User.objects.get(username="admin"),
3373-
Permissions.Product_Type_Add)
3373+
permission_to_action(Permissions.Product_Type_Add))
33743374
importer_mock.assert_not_called()
33753375
reimporter_mock.assert_not_called()
33763376

@@ -3398,7 +3398,7 @@ def test_create_not_authorized_product_name_engagement_name_scan_type(self, mock
33983398
self.assertEqual(403, response.status_code, response.content[:1000])
33993399
mock.assert_called_with(User.objects.get(username="admin"),
34003400
Test.objects.get(id=4), # engagement id found via product name and engagement name
3401-
Permissions.Import_Scan_Result)
3401+
permission_to_action(Permissions.Import_Scan_Result))
34023402
importer_mock.assert_not_called()
34033403
reimporter_mock.assert_not_called()
34043404

@@ -3427,7 +3427,7 @@ def test_create_not_authorized_product_name_engagement_name_scan_type_title(self
34273427
self.assertEqual(403, response.status_code, response.content[:1000])
34283428
mock.assert_called_with(User.objects.get(username="admin"),
34293429
Test.objects.get(id=4), # test id found via product name and engagement name and scan_type and test_title
3430-
Permissions.Import_Scan_Result)
3430+
permission_to_action(Permissions.Import_Scan_Result))
34313431
importer_mock.assert_not_called()
34323432
reimporter_mock.assert_not_called()
34333433

@@ -3465,7 +3465,7 @@ def test_reimport_engagement_param_ignored_permission_checked_on_name_resolved_t
34653465
# NOT on Test 3 (which belongs to the engagement=1 param)
34663466
mock.assert_called_with(User.objects.get(username="admin"),
34673467
Test.objects.get(id=4),
3468-
Permissions.Import_Scan_Result)
3468+
permission_to_action(Permissions.Import_Scan_Result))
34693469
importer_mock.assert_not_called()
34703470
reimporter_mock.assert_not_called()
34713471

0 commit comments

Comments
 (0)