Skip to content

Commit 7196076

Browse files
committed
product type updates and tests
1 parent 26a73be commit 7196076

2 files changed

Lines changed: 169 additions & 0 deletions

File tree

dojo/product_type/api/serializer.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from rest_framework import serializers
2+
from rest_framework.exceptions import PermissionDenied
23

34
from dojo.product_type.models import Product_Type
45

@@ -7,3 +8,44 @@ class ProductTypeSerializer(serializers.ModelSerializer):
78
class Meta:
89
model = Product_Type
910
fields = "__all__"
11+
12+
def validate(self, data):
13+
self._validate_authorized_users_change(data)
14+
return data
15+
16+
def _validate_authorized_users_change(self, data):
17+
"""
18+
Changing ``authorized_users`` requires ``Product_Type_Manage_Members``;
19+
all other fields on this serializer require ``Product_Type_Edit``. No-op
20+
when the field is absent or unchanged (replay-safe). Mirrors
21+
dojo.product.api.serializer.ProductSerializer.
22+
"""
23+
if "authorized_users" not in data:
24+
return
25+
26+
# Field-level validation has already resolved the payload to Dojo_User
27+
# instances at this point.
28+
from dojo.authorization.authorization import ( # noqa: PLC0415 -- lazy import, avoids circular dependency
29+
user_has_permission,
30+
)
31+
from dojo.authorization.roles_permissions import ( # noqa: PLC0415 -- lazy import, avoids circular dependency
32+
Permissions,
33+
)
34+
35+
new_ids = sorted(user.pk for user in (data.get("authorized_users") or []))
36+
current_ids = (
37+
sorted(self.instance.authorized_users.values_list("pk", flat=True))
38+
if self.instance is not None
39+
else []
40+
)
41+
if new_ids == current_ids:
42+
return
43+
44+
request = self.context.get("request")
45+
request_user = getattr(request, "user", None)
46+
if not (
47+
request_user
48+
and user_has_permission(request_user, self.instance, Permissions.Product_Type_Manage_Members)
49+
):
50+
msg = "You do not have permission to manage authorized users for this product type."
51+
raise PermissionDenied(msg)
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
from django.urls import reverse
2+
from rest_framework.authtoken.models import Token
3+
from rest_framework.test import APIClient
4+
5+
from dojo.models import Dojo_User, Product_Type, User
6+
from unittests.dojo_test_case import DojoAPITestCase, versioned_fixtures
7+
8+
9+
@versioned_fixtures
10+
class ProductTypeAuthorizedUsersApiPermissionTest(DojoAPITestCase):
11+
12+
"""
13+
Authorization coverage for the ``authorized_users`` field on the Product
14+
Type API: changing it requires ``Product_Type_Manage_Members``; all other
15+
fields require ``Product_Type_Edit``; no-op submissions are unaffected.
16+
Mirrors unittests.test_product_authorized_users_api_authz.
17+
"""
18+
19+
fixtures = ["dojo_testdata.json"]
20+
21+
@classmethod
22+
def setUpTestData(cls):
23+
cls.product_type = Product_Type.objects.create(name="PTAU-Perm PT")
24+
25+
# Alice: holds Product_Type_Edit on the product type (via authorized_users membership).
26+
cls.alice = User.objects.create_user(
27+
username="ptau_alice",
28+
password="not-a-real-secret", # noqa: S106 - test fixture user
29+
is_staff=False,
30+
)
31+
cls.product_type.authorized_users.add(Dojo_User.objects.get(pk=cls.alice.pk))
32+
33+
# Bob: another user, not initially a member of the product type.
34+
cls.bob = User.objects.create_user(
35+
username="ptau_bob",
36+
password="not-a-real-secret", # noqa: S106 - test fixture user
37+
is_staff=False,
38+
)
39+
40+
# Staff user, who holds Product_Type_Manage_Members.
41+
cls.admin = User.objects.create_user(
42+
username="ptau_admin",
43+
password="not-a-real-secret", # noqa: S106 - test fixture user
44+
is_staff=True,
45+
)
46+
47+
cls.detail_url = reverse("product_type-detail", args=[cls.product_type.id])
48+
49+
def _client_for(self, user):
50+
token, _ = Token.objects.get_or_create(user=user)
51+
client = APIClient()
52+
client.credentials(HTTP_AUTHORIZATION="Token " + token.key)
53+
return client
54+
55+
def test_product_type_edit_cannot_add_authorized_users(self):
56+
client = self._client_for(self.alice)
57+
58+
# Product_Type_Edit alone permits ordinary field updates.
59+
response = client.patch(
60+
self.detail_url, {"description": "edited via product type edit"}, format="json", secure=True,
61+
)
62+
self.assertEqual(200, response.status_code, response.content[:500])
63+
64+
# Adding a user to authorized_users requires Product_Type_Manage_Members.
65+
self.assertFalse(self.product_type.authorized_users.filter(pk=self.bob.pk).exists())
66+
response = client.patch(
67+
self.detail_url,
68+
{"authorized_users": [self.alice.pk, self.bob.pk]},
69+
format="json", secure=True,
70+
)
71+
self.assertEqual(403, response.status_code, response.content[:500])
72+
73+
self.product_type.refresh_from_db()
74+
self.assertFalse(self.product_type.authorized_users.filter(pk=self.bob.pk).exists())
75+
self.assertTrue(self.product_type.authorized_users.filter(pk=self.alice.pk).exists())
76+
77+
def test_product_type_edit_cannot_replace_authorized_users(self):
78+
# PATCH replaces the M2M via .set(); replacing the list is also a
79+
# member-management change and requires Product_Type_Manage_Members.
80+
client = self._client_for(self.alice)
81+
response = client.patch(
82+
self.detail_url, {"authorized_users": [self.bob.pk]}, format="json", secure=True,
83+
)
84+
self.assertEqual(403, response.status_code, response.content[:500])
85+
86+
self.product_type.refresh_from_db()
87+
self.assertTrue(self.product_type.authorized_users.filter(pk=self.alice.pk).exists())
88+
self.assertFalse(self.product_type.authorized_users.filter(pk=self.bob.pk).exists())
89+
90+
def test_product_type_edit_unchanged_authorized_users_is_allowed(self):
91+
# Replay-safe: re-submitting the current membership set unchanged is not
92+
# a member-management change and is accepted.
93+
client = self._client_for(self.alice)
94+
response = client.patch(
95+
self.detail_url, {"authorized_users": [self.alice.pk]}, format="json", secure=True,
96+
)
97+
self.assertEqual(200, response.status_code, response.content[:500])
98+
99+
def test_non_member_cannot_change_authorized_users(self):
100+
# A user who is not a member of the product type cannot retrieve it (the
101+
# viewset queryset is scoped to authorized product types), so the PATCH
102+
# is rejected before the member-management check is reached.
103+
client = self._client_for(self.bob)
104+
105+
response = client.get(self.detail_url, secure=True)
106+
self.assertIn(response.status_code, {403, 404}, response.content[:500])
107+
108+
response = client.patch(
109+
self.detail_url, {"authorized_users": [self.bob.pk]}, format="json", secure=True,
110+
)
111+
self.assertIn(response.status_code, {403, 404}, response.content[:500])
112+
113+
self.product_type.refresh_from_db()
114+
self.assertFalse(self.product_type.authorized_users.filter(pk=self.bob.pk).exists())
115+
116+
def test_manage_members_permission_can_change_authorized_users(self):
117+
# Product_Type_Manage_Members (staff) can update the membership list.
118+
client = self._client_for(self.admin)
119+
response = client.patch(
120+
self.detail_url,
121+
{"authorized_users": [self.alice.pk, self.bob.pk]},
122+
format="json", secure=True,
123+
)
124+
self.assertEqual(200, response.status_code, response.content[:500])
125+
126+
self.product_type.refresh_from_db()
127+
self.assertTrue(self.product_type.authorized_users.filter(pk=self.bob.pk).exists())

0 commit comments

Comments
 (0)