diff --git a/Palto/Palto/api/v1/permissions.py b/Palto/Palto/api/v1/permissions.py index 46b5f91..c0bb9ee 100644 --- a/Palto/Palto/api/v1/permissions.py +++ b/Palto/Palto/api/v1/permissions.py @@ -4,206 +4,49 @@ Permissions for the Palto project's API v1. 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 Palto.Palto import models -class UserPermission(permissions.BasePermission): - # TODO: has_permission check for authentication +def permission_from_helper_class(model: Type[models.ModelPermissionHelper]) -> Type[permissions.BasePermission]: + """ + Create a permission class from a model if it implements ModelPermissionHelper. + This make creating permission easier to understand and less redundant. + """ - def has_permission(self, request, view) -> bool: - if request.method in permissions.SAFE_METHODS: - # for reading, allow everybody - return True + class Permission(permissions.BasePermission): + def has_permission(self, request, view) -> bool: + if request.method in permissions.SAFE_METHODS: + # for reading, allow everybody + return True - if models.User.can_user_create(request.user): - # for writing, only allowed users - return True + if model.can_user_create(request.user): + # for writing, only allowed users + return True - return False + 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.User.all_visible_by_user(request.user) + 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 model.all_visible_by_user(request.user) - else: - # for writing, only allow if the user can edit the object - return obj in models.User.all_editable_by_user(request.user) + else: + # for writing, only allow if the user can edit the object + return obj in model.all_editable_by_user(request.user) + + return Permission -class DepartmentPermission(permissions.BasePermission): - def has_permission(self, request, view) -> bool: - if request.method in permissions.SAFE_METHODS: - # for reading, allow everybody - return True - - if models.Department.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.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) \ No newline at end of file +UserPermission = permission_from_helper_class(models.User) +DepartmentPermission = permission_from_helper_class(models.Department) +StudentGroupPermission = permission_from_helper_class(models.StudentGroup) +TeachingUnitPermission = permission_from_helper_class(models.TeachingUnit) +StudentCardPermission = permission_from_helper_class(models.StudentCard) +TeachingSessionPermission = permission_from_helper_class(models.TeachingSession) +AttendancePermission = permission_from_helper_class(models.Attendance) +AbsencePermission = permission_from_helper_class(models.Absence) +AbsenceAttachmentPermission = permission_from_helper_class(models.AbsenceAttachment) diff --git a/Palto/Palto/api/v1/tests.py b/Palto/Palto/api/v1/tests.py index 00c2921..1fa5f0b 100644 --- a/Palto/Palto/api/v1/tests.py +++ b/Palto/Palto/api/v1/tests.py @@ -7,7 +7,7 @@ Everything to test the API v1 is described here. from rest_framework import status from rest_framework import test -from Palto.Palto import factories +from Palto.Palto import factories, models from Palto.Palto.api.v1 import serializers @@ -39,6 +39,7 @@ class UserApiTestCase(test.APITestCase): # check for a get request response = self.client.get("/api/v1/users/") self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.json()["count"], models.User.objects.count()) # check for a post request response = self.client.post("/api/v1/users/", data=self.USER_CREATION_DATA) @@ -93,7 +94,82 @@ class UserApiTestCase(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): diff --git a/Palto/Palto/api/v1/views.py b/Palto/Palto/api/v1/views.py index 486e9cf..632225b 100644 --- a/Palto/Palto/api/v1/views.py +++ b/Palto/Palto/api/v1/views.py @@ -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. """ +from typing import Type 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 serializers from ... import models -class UserViewSet(viewsets.ModelViewSet): - serializer_class = serializers.UserSerializer - permission_classes = [IsAuthenticated, permissions.UserPermission] +def view_from_helper_class( + model_class: Type[models.ModelPermissionHelper], + 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. + """ - def get_queryset(self): - return models.User.all_visible_by_user(self.request.user) + 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): + return model_class.all_visible_by_user(self.request.user) + + return ViewSet -class DepartmentViewSet(viewsets.ModelViewSet): - serializer_class = serializers.DepartmentSerializer - permission_classes = [permissions.DepartmentPermission] - - def get_queryset(self): - return models.Department.all_visible_by_user(self.request.user) - - -class StudentGroupViewSet(viewsets.ModelViewSet): - serializer_class = serializers.StudentGroupSerializer - permission_classes = [IsAuthenticated, permissions.StudentGroupPermission] - - def get_queryset(self): - return models.StudentGroup.all_visible_by_user(self.request.user) - - -class TeachingUnitViewSet(viewsets.ModelViewSet): - serializer_class = serializers.TeachingUnitSerializer - permission_classes = [IsAuthenticated, permissions.TeachingUnitPermission] - - def get_queryset(self): - return models.TeachingUnit.all_visible_by_user(self.request.user) - - -class StudentCardViewSet(viewsets.ModelViewSet): - serializer_class = serializers.StudentCardSerializer - permission_classes = [IsAuthenticated, permissions.StudentCardPermission] - - def get_queryset(self): - return models.StudentCard.all_visible_by_user(self.request.user) - - -class TeachingSessionViewSet(viewsets.ModelViewSet): - serializer_class = serializers.TeachingSessionSerializer - permission_classes = [IsAuthenticated, permissions.TeachingSessionPermission] - - def get_queryset(self): - return models.TeachingSession.all_visible_by_user(self.request.user) - - -class AttendanceViewSet(viewsets.ModelViewSet): - serializer_class = serializers.AttendanceSerializer - permission_classes = [IsAuthenticated, permissions.AttendancePermission] - - 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) +UserViewSet = view_from_helper_class( + model_class=models.User, + serializer_class=serializers.UserSerializer, + permission_classes=[IsAuthenticated, permissions.UserPermission] +) +DepartmentViewSet = view_from_helper_class( + model_class=models.Department, + serializer_class=serializers.DepartmentSerializer, + permission_classes=[IsAuthenticated, permissions.DepartmentPermission] +) +StudentGroupViewSet = view_from_helper_class( + model_class=models.StudentGroup, + serializer_class=serializers.StudentGroupSerializer, + permission_classes=[IsAuthenticated, permissions.StudentGroupPermission] +) +TeachingUnitViewSet = view_from_helper_class( + model_class=models.TeachingUnit, + serializer_class=serializers.TeachingUnitSerializer, + permission_classes=[IsAuthenticated, permissions.TeachingUnitPermission] +) +StudentCardViewSet = view_from_helper_class( + model_class=models.StudentCard, + serializer_class=serializers.StudentCardSerializer, + permission_classes=[IsAuthenticated, permissions.StudentCardPermission] +) +TeachingSessionViewSet = view_from_helper_class( + model_class=models.TeachingSession, + serializer_class=serializers.TeachingSessionSerializer, + permission_classes=[IsAuthenticated, permissions.TeachingSessionPermission] +) +AttendanceViewSet = view_from_helper_class( + model_class=models.Attendance, + serializer_class=serializers.AttendanceSerializer, + permission_classes=[IsAuthenticated, permissions.AttendancePermission] +) +AbsenceViewSet = view_from_helper_class( + model_class=models.Absence, + serializer_class=serializers.AbsenceSerializer, + permission_classes=[IsAuthenticated, permissions.AbsencePermission] +) +AbsenceAttachmentViewSet = view_from_helper_class( + model_class=models.AbsenceAttachment, + serializer_class=serializers.AbsenceAttachmentSerializer, + permission_classes=[IsAuthenticated, permissions.AbsenceAttachmentPermission] +)