Skip to content

Commit ab76561

Browse files
authored
Merge pull request #95 from silverlogic/BA-1338-drf-nested
BA-1338: Add drf nested router and implement permission management route
2 parents 639b143 + e53c1af commit ab76561

File tree

8 files changed

+155
-8
lines changed

8 files changed

+155
-8
lines changed

baseapp-auth/baseapp_auth/rest_framework/routers/account.py

+8
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,13 @@
2424
from baseapp_auth.rest_framework.change_email.views import (
2525
ChangeEmailViewSet,
2626
) # noqa
27+
from baseapp_auth.rest_framework.users.views import PermissionsViewSet, UsersViewSet # noqa
28+
from rest_framework_nested.routers import NestedSimpleRouter # noqa
2729

2830
account_router.register(r"change-email", ChangeEmailViewSet, basename="change-email")
31+
32+
33+
account_router.register(r"users", UsersViewSet, basename="users")
34+
35+
users_router_nested = NestedSimpleRouter(account_router, r"users", lookup="user")
36+
users_router_nested.register(r"permissions", PermissionsViewSet, basename="user-permissions")

baseapp-auth/baseapp_auth/rest_framework/users/serializers.py

+45
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
from baseapp_core.rest_framework.serializers import ModelSerializer
66
from baseapp_referrals.utils import get_referral_code, get_user_from_referral_code
77
from django.contrib.auth import get_user_model
8+
from django.contrib.auth.models import Permission
9+
from django.contrib.contenttypes.models import ContentType
810
from django.utils import timezone
911
from django.utils.translation import gettext_lazy as _
1012
from rest_framework import serializers
@@ -141,3 +143,46 @@ def update(self, instance, validated_data):
141143

142144
class UserPermissionSerializer(serializers.Serializer):
143145
perm = serializers.CharField(required=True)
146+
147+
148+
class UserContentTypeSerializer(serializers.ModelSerializer):
149+
class Meta:
150+
model = ContentType
151+
fields = ["app_label", "model"]
152+
153+
154+
class UserManagePermissionSerializer(serializers.ModelSerializer):
155+
content_type = UserContentTypeSerializer(read_only=True)
156+
permissions = serializers.ListField(
157+
child=serializers.CharField(), write_only=True, required=False
158+
)
159+
160+
class Meta:
161+
model = Permission
162+
fields = ["id", "codename", "content_type", "permissions"]
163+
extra_kwargs = {
164+
"codename": {"required": False},
165+
}
166+
167+
def create(self, validated_data):
168+
user = self.context["user"]
169+
if not user:
170+
raise serializers.ValidationError({"user": "User does not exist."})
171+
if not self.context["request"].user.has_perm("users.change_user"):
172+
raise serializers.ValidationError(
173+
{"detail": "You do not have permission to perform this action."}
174+
)
175+
perms = validated_data.pop("permissions", [])
176+
if len(perms) > 0:
177+
permissions = Permission.objects.filter(codename__in=perms)
178+
user.user_permissions.set(permissions)
179+
180+
if validated_data.get("codename"):
181+
permission = Permission.objects.filter(codename=validated_data["codename"]).first()
182+
if not permission:
183+
raise serializers.ValidationError(
184+
{"codename": "Permission with this codename does not exist."}
185+
)
186+
user.user_permissions.add(permission)
187+
return permission
188+
return validated_data

baseapp-auth/baseapp_auth/rest_framework/users/views.py

+32
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from baseapp_core.rest_framework.decorators import action
22
from django.contrib.auth import get_user_model
3+
from django.contrib.auth.models import Permission
34
from django.http import Http404
5+
from django.shortcuts import get_object_or_404
46
from django.utils.translation import gettext_lazy as _
57
from rest_framework import (
68
filters,
@@ -11,13 +13,15 @@
1113
status,
1214
viewsets,
1315
)
16+
from rest_framework_nested.viewsets import NestedViewSetMixin
1417

1518
User = get_user_model()
1619

1720
from .parsers import SafeJSONParser
1821
from .serializers import (
1922
ChangePasswordSerializer,
2023
ConfirmEmailSerializer,
24+
UserManagePermissionSerializer,
2125
UserPermissionSerializer,
2226
UserSerializer,
2327
)
@@ -112,3 +116,31 @@ def permissions(self, request):
112116
serializer.is_valid(raise_exception=True)
113117

114118
return response.Response({"has_perm": user.has_perm(serializer.data["perm"])})
119+
120+
121+
class PermissionsViewSet(
122+
NestedViewSetMixin, viewsets.GenericViewSet, mixins.ListModelMixin, mixins.CreateModelMixin
123+
):
124+
pagination_class = None
125+
serializer_class = UserManagePermissionSerializer
126+
permission_classes = [
127+
permissions.IsAuthenticated,
128+
]
129+
parent_lookup_kwargs = {"user_pk": "user__id"}
130+
131+
def get_user(self):
132+
user_pk = self.kwargs.get("user_pk", None)
133+
return get_object_or_404(User, pk=user_pk) if user_pk else None
134+
135+
def get_queryset(self):
136+
user = self.get_user()
137+
if user:
138+
if not self.request.user.has_perm("users.change_user"):
139+
raise serializers.ValidationError(
140+
{"detail": "You do not have permission to perform this action."}
141+
)
142+
return user.user_permissions.all().select_related("content_type")
143+
return Permission.objects.all().select_related("content_type")
144+
145+
def get_serializer_context(self):
146+
return {**super().get_serializer_context(), "user": self.get_user()}

baseapp-auth/baseapp_auth/tests/integration/test_users.py

+63-6
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33
import baseapp_auth.tests.helpers as h
44
import pytest
55
from avatar.models import Avatar
6-
from baseapp_auth.rest_framework.routers.account import account_router
7-
from baseapp_auth.rest_framework.users.views import UsersViewSet
86
from baseapp_auth.tests.factories import PasswordValidationFactory
97
from baseapp_auth.tests.mixins import ApiMixin
108
from baseapp_auth.tokens import ConfirmEmailTokenGenerator
@@ -13,6 +11,7 @@
1311
from django.conf import settings
1412
from django.contrib.auth import get_user_model
1513
from django.contrib.auth.models import Permission
14+
from django.contrib.contenttypes.models import ContentType
1615
from django.utils import timezone
1716

1817
User = get_user_model()
@@ -26,10 +25,6 @@
2625

2726
UserReferral = get_user_referral_model()
2827

29-
account_router.register(
30-
r"users", UsersViewSet, basename="users"
31-
) # We expect the main app to register the viewset
32-
3328

3429
class TestUsersRetrieve(ApiMixin):
3530
view_name = "users-detail"
@@ -321,3 +316,65 @@ def test_user_get_false_without_permission(self, user_client):
321316
r = user_client.post(self.reverse(), {"perm": "admin.test_perm"})
322317
h.responseOk(r)
323318
assert not r.data["has_perm"]
319+
320+
321+
class TestUserPermissionList(ApiMixin):
322+
view_name = "user-permissions-list"
323+
324+
def test_guest_cannot_get_user_permissions(self, client):
325+
content_type = ContentType.objects.all().first()
326+
perm = Permission.objects.filter(content_type_id=content_type).first()
327+
user = UserFactory()
328+
user.user_permissions.add(perm)
329+
r = client.get(self.reverse(kwargs={"user_pk": user.pk}))
330+
h.responseUnauthorized(r)
331+
332+
def test_user_without_perm_cannot_get_user_permissions(self, user_client):
333+
content_type = ContentType.objects.all().first()
334+
perm = Permission.objects.filter(content_type_id=content_type).first()
335+
user = UserFactory()
336+
user.user_permissions.add(perm)
337+
r = user_client.get(self.reverse(kwargs={"user_pk": user.pk}))
338+
h.responseBadRequest(r)
339+
assert "You do not have permission to perform this action." == r.data["detail"]
340+
341+
def test_user_with_perm_can_get_user_permissions(self, user_client):
342+
content_type = ContentType.objects.all().first()
343+
perm = Permission.objects.filter(content_type_id=content_type).first()
344+
user = UserFactory()
345+
user.user_permissions.add(perm)
346+
p = Permission.objects.get(codename="change_user")
347+
p.content_type.app_label = "users"
348+
p.content_type.save()
349+
user_client.user.user_permissions.add(p)
350+
user_client.user.refresh_from_db()
351+
r = user_client.get(self.reverse(kwargs={"user_pk": user.pk}))
352+
h.responseOk(r)
353+
354+
def test_user_with_perm_can_up_user_permissions(self, user_client):
355+
content_type = ContentType.objects.all().first()
356+
perm = Permission.objects.filter(content_type_id=content_type).first()
357+
user = UserFactory()
358+
user.user_permissions.add(perm)
359+
p = Permission.objects.get(codename="change_user")
360+
p.content_type.app_label = "users"
361+
p.content_type.save()
362+
user_client.user.user_permissions.add(p)
363+
r = user_client.post(
364+
self.reverse(kwargs={"user_pk": user.pk}), data={"codename": "delete_user"}
365+
)
366+
h.responseCreated(r)
367+
assert user.user_permissions.count() == 2
368+
369+
def test_user_with_perm_can_set_user_permissions(self, user_client):
370+
user = UserFactory()
371+
p = Permission.objects.get(codename="change_user")
372+
p.content_type.app_label = "users"
373+
p.content_type.save()
374+
user_client.user.user_permissions.add(p)
375+
r = user_client.post(
376+
self.reverse(kwargs={"user_pk": user.pk}),
377+
data={"permissions": ["change_user", "delete_user"]},
378+
)
379+
h.responseCreated(r)
380+
assert user.user_permissions.count() == 2

baseapp-auth/testproject/urls.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
import baseapp_auth.rest_framework.urls.auth_mfa as auth_mfa_urls
44
import baseapp_auth.rest_framework.urls.auth_mfa_jwt as auth_mfa_jwt_urls
55
import baseapp_auth.rest_framework.urls.pre_auth as pre_auth_urls
6-
from baseapp_auth.rest_framework.routers.account import account_router
6+
from baseapp_auth.rest_framework.routers.account import (
7+
account_router,
8+
users_router_nested,
9+
)
710
from baseapp_core.graphql import GraphQLView
811
from django.contrib import admin
912
from django.urls import include, re_path
@@ -14,6 +17,7 @@
1417

1518
v1_urlpatterns = [
1619
re_path(r"", include(account_router.urls)),
20+
re_path(r"", include(users_router_nested.urls)),
1721
re_path(r"auth/authtoken/", include(auth_authtoken_urls)),
1822
re_path(r"auth/jwt/", include(auth_jwt_urls)),
1923
re_path(r"auth/mfa/", include(auth_mfa_urls)),

baseapp-core/setup.cfg

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ install_requires =
2222
swapper >= 1.3
2323
django-model-utils >= 4.3
2424
djangorestframework >= 3.14
25+
drf-nested-routers >= 0.93.5
2526
djangorestframework-expander >= 0.2
2627
drf-extra-fields >= 3.5
2728
psycopg2-binary >= 2.9

baseapp-core/testproject/requirements.txt

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
-e ./baseapp-core
33

44
djangorestframework-simplejwt[crypto]>=5.2.2
5+
drf-nested-routers==0.93.5
56

67
# test
78
freezegun==1.2.1

baseapp-e2e/baseapp_e2e/tests/integration/api/test_e2e_endpoints.py

-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ def data(self):
4242
def test_load_data(self, data, client):
4343
assert User.objects.count() == 0
4444
r = client.post(self.endpoint_url, data=json.dumps(data), content_type="application/json")
45-
print(r.data)
4645
h.responseOk(r)
4746

4847
assert User.objects.count() == len(data["objects"])

0 commit comments

Comments
 (0)