factorised permissions and views

This commit is contained in:
faraphel 2023-12-04 19:49:54 +01:00
parent a731a03133
commit 93036887c4
3 changed files with 182 additions and 261 deletions

View file

@ -4,20 +4,26 @@ Permissions for the Palto project's API v1.
A permission describe which user is allowed to see and modify which objet with the API A permission describe which user is allowed to see and modify which objet with the API
""" """
from typing import Type
from rest_framework import permissions from rest_framework import permissions
from Palto.Palto import models from Palto.Palto import models
class UserPermission(permissions.BasePermission): def permission_from_helper_class(model: Type[models.ModelPermissionHelper]) -> Type[permissions.BasePermission]:
# TODO: has_permission check for authentication """
Create a permission class from a model if it implements ModelPermissionHelper.
This make creating permission easier to understand and less redundant.
"""
class Permission(permissions.BasePermission):
def has_permission(self, request, view) -> bool: def has_permission(self, request, view) -> bool:
if request.method in permissions.SAFE_METHODS: if request.method in permissions.SAFE_METHODS:
# for reading, allow everybody # for reading, allow everybody
return True return True
if models.User.can_user_create(request.user): if model.can_user_create(request.user):
# for writing, only allowed users # for writing, only allowed users
return True return True
@ -26,184 +32,21 @@ class UserPermission(permissions.BasePermission):
def has_object_permission(self, request, view, obj: models.User) -> bool: def has_object_permission(self, request, view, obj: models.User) -> bool:
if request.method in permissions.SAFE_METHODS: if request.method in permissions.SAFE_METHODS:
# for reading, only allow if the user can see the object # for reading, only allow if the user can see the object
return obj in models.User.all_visible_by_user(request.user) return obj in model.all_visible_by_user(request.user)
else: else:
# for writing, only allow if the user can edit the object # for writing, only allow if the user can edit the object
return obj in models.User.all_editable_by_user(request.user) return obj in model.all_editable_by_user(request.user)
return Permission
class DepartmentPermission(permissions.BasePermission): UserPermission = permission_from_helper_class(models.User)
def has_permission(self, request, view) -> bool: DepartmentPermission = permission_from_helper_class(models.Department)
if request.method in permissions.SAFE_METHODS: StudentGroupPermission = permission_from_helper_class(models.StudentGroup)
# for reading, allow everybody TeachingUnitPermission = permission_from_helper_class(models.TeachingUnit)
return True StudentCardPermission = permission_from_helper_class(models.StudentCard)
TeachingSessionPermission = permission_from_helper_class(models.TeachingSession)
if models.Department.can_user_create(request.user): AttendancePermission = permission_from_helper_class(models.Attendance)
# for writing, only allowed users AbsencePermission = permission_from_helper_class(models.Absence)
return True AbsenceAttachmentPermission = permission_from_helper_class(models.AbsenceAttachment)
return False
def has_object_permission(self, request, view, obj: models.User) -> bool:
if request.method in permissions.SAFE_METHODS:
# for reading, only allow if the user can see the object
return obj in models.Department.all_visible_by_user(request.user)
else:
# for writing, only allow if the user can edit the object
return obj in models.Department.all_editable_by_user(request.user)
class StudentGroupPermission(permissions.BasePermission):
def has_permission(self, request, view) -> bool:
if request.method in permissions.SAFE_METHODS:
# for reading, allow everybody
return True
if models.StudentGroup.can_user_create(request.user):
# for writing, only allowed users
return True
return False
def has_object_permission(self, request, view, obj: models.User) -> bool:
if request.method in permissions.SAFE_METHODS:
# for reading, only allow if the user can see the object
return obj in models.StudentGroup.all_visible_by_user(request.user)
else:
# for writing, only allow if the user can edit the object
return obj in models.StudentGroup.all_editable_by_user(request.user)
class TeachingUnitPermission(permissions.BasePermission):
def has_permission(self, request, view) -> bool:
if request.method in permissions.SAFE_METHODS:
# for reading, allow everybody
return True
if models.TeachingUnit.can_user_create(request.user):
# for writing, only allowed users
return True
return False
def has_object_permission(self, request, view, obj: models.User) -> bool:
if request.method in permissions.SAFE_METHODS:
# for reading, only allow if the user can see the object
return obj in models.TeachingUnit.all_visible_by_user(request.user)
else:
# for writing, only allow if the user can edit the object
return obj in models.TeachingUnit.all_editable_by_user(request.user)
class StudentCardPermission(permissions.BasePermission):
def has_permission(self, request, view) -> bool:
if request.method in permissions.SAFE_METHODS:
# for reading, allow everybody
return True
if models.StudentCard.can_user_create(request.user):
# for writing, only allowed users
return True
return False
def has_object_permission(self, request, view, obj: models.User) -> bool:
if request.method in permissions.SAFE_METHODS:
# for reading, only allow if the user can see the object
return obj in models.StudentCard.all_visible_by_user(request.user)
else:
# for writing, only allow if the user can edit the object
return obj in models.StudentCard.all_editable_by_user(request.user)
class TeachingSessionPermission(permissions.BasePermission):
def has_permission(self, request, view) -> bool:
if request.method in permissions.SAFE_METHODS:
# for reading, allow everybody
return True
if models.TeachingSession.can_user_create(request.user):
# for writing, only allowed users
return True
return False
def has_object_permission(self, request, view, obj: models.User) -> bool:
if request.method in permissions.SAFE_METHODS:
# for reading, only allow if the user can see the object
return obj in models.TeachingSession.all_visible_by_user(request.user)
else:
# for writing, only allow if the user can edit the object
return obj in models.TeachingSession.all_editable_by_user(request.user)
class AttendancePermission(permissions.BasePermission):
def has_permission(self, request, view) -> bool:
if request.method in permissions.SAFE_METHODS:
# for reading, allow everybody
return True
if models.Attendance.can_user_create(request.user):
# for writing, only allowed users
return True
return False
def has_object_permission(self, request, view, obj: models.User) -> bool:
if request.method in permissions.SAFE_METHODS:
# for reading, only allow if the user can see the object
return obj in models.Attendance.all_visible_by_user(request.user)
else:
# for writing, only allow if the user can edit the object
return obj in models.Attendance.all_editable_by_user(request.user)
class AbsencePermission(permissions.BasePermission):
def has_permission(self, request, view) -> bool:
if request.method in permissions.SAFE_METHODS:
# for reading, allow everybody
return True
if models.Absence.can_user_create(request.user):
# for writing, only allowed users
return True
return False
def has_object_permission(self, request, view, obj: models.User) -> bool:
if request.method in permissions.SAFE_METHODS:
# for reading, only allow if the user can see the object
return obj in models.Absence.all_visible_by_user(request.user)
else:
# for writing, only allow if the user can edit the object
return obj in models.Absence.all_editable_by_user(request.user)
class AbsenceAttachmentPermission(permissions.BasePermission):
def has_permission(self, request, view) -> bool:
if request.method in permissions.SAFE_METHODS:
# for reading, allow everybody
return True
if models.AbsenceAttachment.can_user_create(request.user):
# for writing, only allowed users
return True
return False
def has_object_permission(self, request, view, obj: models.User) -> bool:
if request.method in permissions.SAFE_METHODS:
# for reading, only allow if the user can see the object
return obj in models.AbsenceAttachment.all_visible_by_user(request.user)
else:
# for writing, only allow if the user can edit the object
return obj in models.AbsenceAttachment.all_editable_by_user(request.user)

View file

@ -7,7 +7,7 @@ Everything to test the API v1 is described here.
from rest_framework import status from rest_framework import status
from rest_framework import test from rest_framework import test
from Palto.Palto import factories from Palto.Palto import factories, models
from Palto.Palto.api.v1 import serializers from Palto.Palto.api.v1 import serializers
@ -39,6 +39,7 @@ class UserApiTestCase(test.APITestCase):
# check for a get request # check for a get request
response = self.client.get("/api/v1/users/") response = self.client.get("/api/v1/users/")
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.json()["count"], models.User.objects.count())
# check for a post request # check for a post request
response = self.client.post("/api/v1/users/", data=self.USER_CREATION_DATA) response = self.client.post("/api/v1/users/", data=self.USER_CREATION_DATA)
@ -93,7 +94,82 @@ class UserApiTestCase(test.APITestCase):
class DepartmentApiTestCase(test.APITestCase): class DepartmentApiTestCase(test.APITestCase):
pass # fake department creation test
DEPARTMENT_CREATION_DATA: dict = {
"name": "UFR des Sciences",
"email": "ufr.sciences@university.fr",
}
def setUp(self):
self.user_admin = factories.FakeUserFactory(is_superuser=True)
self.user_other = factories.FakeUserFactory()
def test_permission_admin(self):
""" Test the API permission for an administrator """
# TODO: use reverse to get the url ?
self.client.force_login(self.user_admin)
# check for a get request
response = self.client.get("/api/v1/departments/")
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.json()["count"], models.Department.objects.count())
# check for a post request
response = self.client.post("/api/v1/departments/", data=self.DEPARTMENT_CREATION_DATA)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
def test_permission_anonymous(self):
""" Test the API permission for an anonymous user """
# TODO: use reverse to get the url ?
# TODO: use api endpoint as class attribute ?
self.client.logout()
# check for a get request
response = self.client.get("/api/v1/departments/")
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
# check for a post request
response = self.client.post("/api/v1/departments/", data=self.DEPARTMENT_CREATION_DATA)
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
def test_permission_unrelated(self):
""" Test the API permission for an unrelated user """
# TODO: use reverse to get the url ?
self.client.force_login(self.user_other)
# check for a get request and that he can't see anything
response = self.client.get("/api/v1/departments/")
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.json()["count"], 0)
# check for a post request
response = self.client.post("/api/v1/departments/", data=self.DEPARTMENT_CREATION_DATA)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
def test_permission_related(self):
""" Test the API permission for a related user """
# TODO: use reverse to get the url ?
student1, student2 = factories.FakeUserFactory(), factories.FakeUserFactory()
department = factories.FakeDepartmentFactory(students=(student1, student2))
self.client.force_login(student1)
"""
TODO: this test require to show the field students before creating it.
# check for a get request and that he can see the other student
response = self.client.get("/api/v1/departments/")
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertIn(serializers.UserSerializer(student2).data, response.json()["results"])
"""
# check for a post request
response = self.client.post("/api/v1/departments/", data=self.DEPARTMENT_CREATION_DATA)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
class StudentGroupApiTestCase(test.APITestCase): class StudentGroupApiTestCase(test.APITestCase):

View file

@ -3,82 +3,84 @@ Views for the Palto project's API v1.
An API view describe which models should display which files to user with which permissions. An API view describe which models should display which files to user with which permissions.
""" """
from typing import Type
from rest_framework import viewsets from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated, BasePermission
from rest_framework.serializers import BaseSerializer
from . import permissions from . import permissions
from . import serializers from . import serializers
from ... import models from ... import models
class UserViewSet(viewsets.ModelViewSet): def view_from_helper_class(
serializer_class = serializers.UserSerializer model_class: Type[models.ModelPermissionHelper],
permission_classes = [IsAuthenticated, permissions.UserPermission] serializer_class: Type[BaseSerializer],
permission_classes: list[Type[BasePermission]],
) -> Type[viewsets.ModelViewSet]:
"""
Create a view class from a model if it implements ModelPermissionHelper.
This make creating view easier to understand and less redundant.
"""
class ViewSet(viewsets.ModelViewSet):
nonlocal serializer_class, permission_classes, model_class
def get_serializer_class(self):
return serializer_class
def get_permissions(self):
return [permission() for permission in permission_classes]
def get_queryset(self): def get_queryset(self):
return models.User.all_visible_by_user(self.request.user) return model_class.all_visible_by_user(self.request.user)
return ViewSet
class DepartmentViewSet(viewsets.ModelViewSet): UserViewSet = view_from_helper_class(
serializer_class = serializers.DepartmentSerializer model_class=models.User,
permission_classes = [permissions.DepartmentPermission] serializer_class=serializers.UserSerializer,
permission_classes=[IsAuthenticated, permissions.UserPermission]
def get_queryset(self): )
return models.Department.all_visible_by_user(self.request.user) DepartmentViewSet = view_from_helper_class(
model_class=models.Department,
serializer_class=serializers.DepartmentSerializer,
class StudentGroupViewSet(viewsets.ModelViewSet): permission_classes=[IsAuthenticated, permissions.DepartmentPermission]
serializer_class = serializers.StudentGroupSerializer )
permission_classes = [IsAuthenticated, permissions.StudentGroupPermission] StudentGroupViewSet = view_from_helper_class(
model_class=models.StudentGroup,
def get_queryset(self): serializer_class=serializers.StudentGroupSerializer,
return models.StudentGroup.all_visible_by_user(self.request.user) permission_classes=[IsAuthenticated, permissions.StudentGroupPermission]
)
TeachingUnitViewSet = view_from_helper_class(
class TeachingUnitViewSet(viewsets.ModelViewSet): model_class=models.TeachingUnit,
serializer_class = serializers.TeachingUnitSerializer serializer_class=serializers.TeachingUnitSerializer,
permission_classes = [IsAuthenticated, permissions.TeachingUnitPermission] permission_classes=[IsAuthenticated, permissions.TeachingUnitPermission]
)
def get_queryset(self): StudentCardViewSet = view_from_helper_class(
return models.TeachingUnit.all_visible_by_user(self.request.user) model_class=models.StudentCard,
serializer_class=serializers.StudentCardSerializer,
permission_classes=[IsAuthenticated, permissions.StudentCardPermission]
class StudentCardViewSet(viewsets.ModelViewSet): )
serializer_class = serializers.StudentCardSerializer TeachingSessionViewSet = view_from_helper_class(
permission_classes = [IsAuthenticated, permissions.StudentCardPermission] model_class=models.TeachingSession,
serializer_class=serializers.TeachingSessionSerializer,
def get_queryset(self): permission_classes=[IsAuthenticated, permissions.TeachingSessionPermission]
return models.StudentCard.all_visible_by_user(self.request.user) )
AttendanceViewSet = view_from_helper_class(
model_class=models.Attendance,
class TeachingSessionViewSet(viewsets.ModelViewSet): serializer_class=serializers.AttendanceSerializer,
serializer_class = serializers.TeachingSessionSerializer permission_classes=[IsAuthenticated, permissions.AttendancePermission]
permission_classes = [IsAuthenticated, permissions.TeachingSessionPermission] )
AbsenceViewSet = view_from_helper_class(
def get_queryset(self): model_class=models.Absence,
return models.TeachingSession.all_visible_by_user(self.request.user) serializer_class=serializers.AbsenceSerializer,
permission_classes=[IsAuthenticated, permissions.AbsencePermission]
)
class AttendanceViewSet(viewsets.ModelViewSet): AbsenceAttachmentViewSet = view_from_helper_class(
serializer_class = serializers.AttendanceSerializer model_class=models.AbsenceAttachment,
permission_classes = [IsAuthenticated, permissions.AttendancePermission] serializer_class=serializers.AbsenceAttachmentSerializer,
permission_classes=[IsAuthenticated, permissions.AbsenceAttachmentPermission]
def get_queryset(self): )
return models.Attendance.all_visible_by_user(self.request.user)
class AbsenceViewSet(viewsets.ModelViewSet):
serializer_class = serializers.AbsenceSerializer
permission_classes = [IsAuthenticated, permissions.AbsencePermission]
def get_queryset(self):
return models.Absence.all_visible_by_user(self.request.user)
class AbsenceAttachmentViewSet(viewsets.ModelViewSet):
serializer_class = serializers.AbsenceAttachmentSerializer
permission_classes = [IsAuthenticated, permissions.AbsenceAttachmentPermission]
def get_queryset(self):
return models.AbsenceAttachment.all_visible_by_user(self.request.user)