added permissions and authentication to API

This commit is contained in:
Faraphel 2023-11-30 19:00:18 +01:00
parent 6dcc3a448e
commit 0a39a9a1b9
8 changed files with 288 additions and 218 deletions

View file

@ -1,7 +1,14 @@
from django.urls import path, include
from rest_framework_simplejwt.views import TokenRefreshView, TokenObtainPairView, TokenVerifyView
import Palto.Palto.api.v1.urls as v1_urls
urlpatterns = [
# Authentification (JWT)
path('auth/jwt/token/', TokenObtainPairView.as_view(), name='token'),
path('auth/jwt/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
path('auth/jwt/token/verify/', TokenVerifyView.as_view(), name='token_verify'),
# API
path('v1/', include(v1_urls)),
]

View file

@ -0,0 +1,205 @@
from rest_framework import permissions
from Palto.Palto.models import (Department, TeachingUnit, StudentCard, StudentGroup, User, TeachingSession, Attendance,
Absence, AbsenceAttachment)
class UserPermission(permissions.BasePermission):
def has_object_permission(self, request, view, obj: User) -> bool:
# if the requesting user is admin, allow all
if request.user.is_superuser:
return True
if request.method in permissions.SAFE_METHODS:
# if the user is in one of the same department as the requesting user, allow read
if obj in Department.multiple_related_users(request.user.related_departments):
return True
return False
class DepartmentPermission(permissions.BasePermission):
def has_object_permission(self, request, view, obj: Department) -> bool:
# if the requesting user is admin, allow all
if request.user.is_superuser:
return True
# if the group department is managed by the user, allow all
if obj in request.user.managing_departments:
return True
if request.method in permissions.SAFE_METHODS:
# allow read to everybody
return True
return False
class StudentGroupPermission(permissions.BasePermission):
def has_object_permission(self, request, view, obj: StudentGroup) -> bool:
# if the requesting user is admin, allow all
if request.user.is_superuser:
return True
# if the group department is managed by the user, allow all
if obj.department in request.user.managing_departments:
return True
# if the user is the owner of the group, allow all
if obj.owner is request.user:
return True
if request.method in permissions.SAFE_METHODS:
# if the student is in the group, allow read
if obj in request.user.student_groups:
return True
# if the user is a teacher from the same department, allow read
if obj.department in request.user.teaching_departments:
return True
return False
class TeachingUnitPermission(permissions.BasePermission):
def has_object_permission(self, request, view, obj: TeachingUnit) -> bool:
# if the requesting user is admin, allow all
if request.user.is_superuser:
return True
# if the teaching unit department is managed by the user, allow all
if obj.department in request.user.managing_departments:
return True
# if the teaching unit is managed by the user, allow all
if obj in request.user.managing_units:
return True
if request.method in permissions.SAFE_METHODS:
# if the user is related to the department, allow read
if obj.department in request.user.related_departments:
return True
return False
class StudentCardPermission(permissions.BasePermission):
def has_object_permission(self, request, view, obj: StudentCard) -> bool:
# if the requesting user is admin, allow all
if request.user.is_superuser:
return True
# if the card department is managed by the user, allow all
if obj.department in request.user.managing_departments:
return True
if request.method in permissions.SAFE_METHODS:
# if the owner of the card is the user, allow read
if obj.owner is request.user:
return True
return False
class TeachingSessionPermission(permissions.BasePermission):
def has_object_permission(self, request, view, obj: TeachingSession) -> bool:
# if the requesting user is admin, allow all
if request.user.is_superuser:
return True
# if the teacher is the user, allow all
if obj.teacher is request.user:
return True
# if the unit of the session is managed by the user, allow all
if obj.unit in request.user.managing_units:
return True
# if the department of the session is managed by the user, allow all
if obj.unit.department in request.user.managing_departments:
return True
if request.method in permissions.SAFE_METHODS:
# if the user was one of the student, allow read
if request.user in obj.group.students:
return True
return False
class AttendancePermission(permissions.BasePermission):
def has_object_permission(self, request, view, obj: Attendance) -> bool:
# if the requesting user is admin, allow all
if request.user.is_superuser:
return True
# if the teacher is the user, allow all
if obj.session.teacher is request.user:
return True
# if the unit of the session is managed by the user, allow all
if obj.session.unit in request.user.managing_units:
return True
# if the department of the session is managed by the user, allow all
if obj.session.unit.department in request.user.managing_departments:
return True
if request.method in permissions.SAFE_METHODS:
# if the user was the student, allow read
if obj.student is request.user:
return True
return False
class AbsencePermission(permissions.BasePermission):
def has_object_permission(self, request, view, obj: Absence) -> bool:
# if the requesting user is admin, allow all
if request.user.is_superuser:
return True
# if the department of the session is managed by the user, allow all
if obj.session.unit.department in request.user.managing_departments:
return True
# if the user was the student, allow all
if obj.student is request.user:
return True
if request.method in permissions.SAFE_METHODS:
# if the unit of the session is managed by the user, allow read
if obj.session.unit in request.user.managing_units:
return True
# if the teacher is the user, allow read
if obj.session.teacher is request.user:
return True
return False
class AbsenceAttachmentPermission(permissions.BasePermission):
def has_object_permission(self, request, view, obj: AbsenceAttachment) -> bool:
# if the requesting user is admin, allow all
if request.user.is_superuser:
return True
# if the department of the session is managed by the user, allow all
if obj.absence.session.unit.department in request.user.managing_departments:
return True
# if the user was the student, allow all
if obj.absence.student is request.user:
return True
if request.method in permissions.SAFE_METHODS:
# if the unit of the session is managed by the user, allow read
if obj.absence.session.unit in request.user.managing_units:
return True
# if the teacher is the user, allow read
if obj.absence.session.teacher is request.user:
return True
return False

View file

@ -4,39 +4,40 @@ from Palto.Palto.models import (User, Department, TeachingUnit, StudentCard, Tea
AbsenceAttachment, StudentGroup)
# TODO(Raphaël): Les champs sont-ils sûr ? (carte uid ?)
# TODO(Raphaël): Connection à l'API avec token ?
# TODO(Raphaël): Voir pour les relations
# TODO(Raphaël): Voir pour les related_name
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'username', 'first_name', 'last_name']
fields = ['id', 'username', 'first_name', 'last_name', 'email']
class DepartmentSerializer(serializers.ModelSerializer):
class Meta:
model = Department
fields = ['id', 'name', 'mail']
fields = ['id', 'name', 'email', 'managers']
# NOTE: teachers, students
class StudentGroupSerializer(serializers.ModelSerializer):
class Meta:
model = StudentGroup
fields = ['id', 'name']
fields = ['id', 'name', 'owner', 'department']
# NOTE: students
class TeachingUnitSerializer(serializers.ModelSerializer):
class Meta:
model = TeachingUnit
fields = ['id', 'name', 'department']
# NOTE: managers, teachers, student_groups
class StudentCardSerializer(serializers.ModelSerializer):
class Meta:
model = StudentCard
fields = ['id', 'uid', 'owner']
fields = ['id', 'uid', 'department', 'owner']
class TeachingSessionSerializer(serializers.ModelSerializer):
@ -48,13 +49,13 @@ class TeachingSessionSerializer(serializers.ModelSerializer):
class AttendanceSerializer(serializers.ModelSerializer):
class Meta:
model = Attendance
fields = ['id', 'date', 'student']
fields = ['id', 'date', 'student', 'session']
class AbsenceSerializer(serializers.ModelSerializer):
class Meta:
model = Absence
fields = ['id', 'message', 'student']
fields = ['id', 'message', 'student', 'session']
class AbsenceAttachmentSerializer(serializers.ModelSerializer):

View file

@ -1,6 +1,9 @@
from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticated
from .permissions import (UserPermission, DepartmentPermission, StudentGroupPermission, TeachingUnitPermission,
StudentCardPermission, TeachingSessionPermission, AttendancePermission, AbsencePermission,
AbsenceAttachmentPermission)
from .serializers import (UserSerializer, AbsenceAttachmentSerializer, AbsenceSerializer, AttendanceSerializer,
TeachingSessionSerializer, StudentCardSerializer, StudentGroupSerializer,
DepartmentSerializer, TeachingUnitSerializer)
@ -10,72 +13,53 @@ from ...models import (User, AbsenceAttachment, Absence, Attendance, TeachingSes
class UserViewSet(viewsets.ModelViewSet):
serializer_class = UserSerializer
def get_queryset(self):
return User.all_visible_to(self.request.user)
def get_permissions(self):
return User.permissions_for(self.request.user, self.request.method)
queryset = User.objects.all()
permission_classes = [IsAuthenticated, UserPermission]
class DepartmentViewSet(UserViewSet):
serializer_class = DepartmentSerializer
def get_queryset(self):
return Department.all_visible_to(self.request.user)
queryset = Department.objects.all()
permission_classes = [DepartmentPermission]
class StudentGroupViewSet(UserViewSet):
serializer_class = StudentGroupSerializer
permission_classes = [IsAuthenticated]
def get_queryset(self):
return StudentGroup.all_visible_to(self.request.user)
queryset = StudentGroup.objects.all()
permission_classes = [IsAuthenticated, StudentGroupPermission]
class TeachingUnitViewSet(UserViewSet):
serializer_class = TeachingUnitSerializer
permission_classes = [IsAuthenticated]
def get_queryset(self):
return TeachingUnit.all_visible_to(self.request.user)
queryset = TeachingUnit.objects.all()
permission_classes = [IsAuthenticated, TeachingUnitPermission]
class StudentCardViewSet(UserViewSet):
serializer_class = StudentCardSerializer
permission_classes = [IsAuthenticated]
def get_queryset(self):
return StudentCard.all_visible_to(self.request.user)
queryset = StudentCard.objects.all()
permission_classes = [IsAuthenticated, StudentCardPermission]
class TeachingSessionViewSet(UserViewSet):
serializer_class = TeachingSessionSerializer
permission_classes = [IsAuthenticated]
def get_queryset(self):
return TeachingSession.all_visible_to(self.request.user)
queryset = TeachingSession.objects.all()
permission_classes = [IsAuthenticated, TeachingSessionPermission]
class AttendanceViewSet(UserViewSet):
serializer_class = AttendanceSerializer
permission_classes = [IsAuthenticated]
def get_queryset(self):
return Attendance.all_visible_to(self.request.user)
queryset = Attendance.objects.all()
permission_classes = [IsAuthenticated, AttendancePermission]
class AbsenceViewSet(UserViewSet):
serializer_class = AbsenceSerializer
permission_classes = [IsAuthenticated]
def get_queryset(self):
return Absence.all_visible_to(self.request.user)
queryset = Absence.objects.all()
permission_classes = [IsAuthenticated, AbsencePermission]
class AbsenceAttachmentViewSet(UserViewSet):
serializer_class = AbsenceAttachmentSerializer
permission_classes = [IsAuthenticated]
def get_queryset(self):
return AbsenceAttachment.all_visible_to(self.request.user)
queryset = AbsenceAttachment.objects.all()
permission_classes = [IsAuthenticated, AbsenceAttachmentPermission]

View file

@ -1,5 +1,4 @@
import uuid
from abc import abstractmethod, ABC
from datetime import datetime, timedelta
from typing import Iterable
@ -7,27 +6,9 @@ from django.contrib.auth import get_user_model
from django.contrib.auth.models import AbstractUser
from django.db import models
from django.db.models import QuerySet, Q
from rest_framework import permissions
# Create your models here.
class ModelApiMixin(ABC):
@classmethod
@abstractmethod
def all_visible_to(cls, user: "User") -> QuerySet:
"""
Return all the objects visible to a user.
"""
@classmethod
@abstractmethod
def permissions_for(cls, user: "User", method: str) -> permissions.BasePermission:
"""
Return the permissions for a user and the method used to access the object.
"""
class User(AbstractUser, ModelApiMixin):
class User(AbstractUser):
"""
A user.
@ -59,25 +40,8 @@ class User(AbstractUser, ModelApiMixin):
return self.multiple_related_departments([self])
@classmethod
def all_visible_to(cls, user: "User") -> QuerySet["User"]:
if user.is_superuser:
queryset = User.objects.all()
else:
queryset = Department.multiple_related_users(user.related_departments)
return queryset.order_by("pk")
@classmethod
def permissions_for(cls, user: "User", method: str) -> permissions.BasePermission:
# TODO: ???
if method in permissions.SAFE_METHODS:
return permissions.AllowAny()
return permissions.IsAdminUser()
class Department(models.Model, ModelApiMixin):
class Department(models.Model):
"""
A scholar department.
@ -87,7 +51,7 @@ class Department(models.Model, ModelApiMixin):
id: uuid.UUID = models.UUIDField(default=uuid.uuid4, primary_key=True, editable=False, max_length=36)
name: str = models.CharField(max_length=64)
mail: str = models.EmailField()
email: str = models.EmailField()
managers = models.ManyToManyField(to=get_user_model(), blank=True, related_name="managing_departments")
teachers = models.ManyToManyField(to=get_user_model(), blank=True, related_name="teaching_departments")
@ -119,12 +83,8 @@ class Department(models.Model, ModelApiMixin):
return self.multiple_related_users([self])
@classmethod
def all_visible_to(cls, user: "User") -> QuerySet["User"]:
return cls.objects.all().order_by("pk")
class StudentGroup(models.Model, ModelApiMixin):
class StudentGroup(models.Model):
"""
A student group.
@ -137,6 +97,7 @@ class StudentGroup(models.Model, ModelApiMixin):
id: uuid.UUID = models.UUIDField(default=uuid.uuid4, primary_key=True, editable=False, max_length=36)
name: str = models.CharField(max_length=128)
owner = models.ForeignKey(to=get_user_model(), on_delete=models.CASCADE, related_name="owning_groups")
department = models.ForeignKey(to=Department, on_delete=models.CASCADE, related_name="student_groups")
students = models.ManyToManyField(to=get_user_model(), blank=True, related_name="student_groups")
@ -146,24 +107,8 @@ class StudentGroup(models.Model, ModelApiMixin):
def __str__(self):
return self.name
@classmethod
def all_visible_to(cls, user: "User") -> QuerySet["User"]:
if user.is_superuser:
queryset = cls.objects.all()
else:
queryset = cls.objects.filter(
# get all the groups where the user is
Q(students=user) |
# get all the groups where the department is managed by the user
Q(department=user.managing_departments)
# TODO: prof ? rôle créateur du groupe ?
).distinct()
return queryset.order_by("pk")
class TeachingUnit(models.Model, ModelApiMixin):
class TeachingUnit(models.Model):
"""
A teaching unit.
@ -188,21 +133,8 @@ class TeachingUnit(models.Model, ModelApiMixin):
def __str__(self):
return self.name
@classmethod
def all_visible_to(cls, user: "User") -> QuerySet["User"]:
if user.is_superuser:
queryset = cls.objects.all()
else:
queryset = cls.objects.filter(
# get all the units with a common department with the user
Q(department__in=user.related_departments)
)
return queryset.order_by("pk")
class StudentCard(models.Model, ModelApiMixin):
class StudentCard(models.Model):
"""
A student card.
@ -212,28 +144,14 @@ class StudentCard(models.Model, ModelApiMixin):
id: uuid.UUID = models.UUIDField(default=uuid.uuid4, primary_key=True, editable=False, max_length=36)
uid: bytes = models.BinaryField(max_length=7)
department: Department = models.ForeignKey(to=Department, on_delete=models.CASCADE, related_name="student_cards")
owner: User = models.ForeignKey(to=get_user_model(), on_delete=models.CASCADE, related_name="student_cards")
def __repr__(self):
return f"<{self.__class__.__name__} id={str(self.id)[:8]} owner={self.owner.username!r}>"
@classmethod
def all_visible_to(cls, user: "User") -> QuerySet["User"]:
if user.is_superuser:
queryset = cls.objects.all()
else:
queryset = cls.objects.filter(
# get all the cards that are owned by the user
Q(owner=user) |
# get all the cards where the owner is studying in a department where the user is a manager
Q(owner__studying_departments__managers=user)
).distinct()
return queryset.order_by("pk")
class TeachingSession(models.Model, ModelApiMixin):
class TeachingSession(models.Model):
"""
A session of a teaching unit.
@ -262,27 +180,8 @@ class TeachingSession(models.Model, ModelApiMixin):
def end(self) -> datetime:
return self.start + self.duration
@classmethod
def all_visible_to(cls, user: "User") -> QuerySet["User"]:
if user.is_superuser:
queryset = cls.objects.all()
else:
queryset = cls.objects.filter(
# get all the sessions where the user is a teacher
Q(teacher=user) |
# get all the sessions where the user is in the group
Q(group__students=user) |
# get all the sessions where the user is managing the unit
Q(unit__managers=user) |
# get all the sessions where the user is managing the department
Q(unit__department__managers=user)
).distinct()
return queryset.order_by("pk")
class Attendance(models.Model, ModelApiMixin):
class Attendance(models.Model):
"""
A student attendance to a session.
@ -312,27 +211,8 @@ class Attendance(models.Model, ModelApiMixin):
f">"
)
@classmethod
def all_visible_to(cls, user: "User") -> QuerySet["User"]:
if user.is_superuser:
queryset = cls.objects.all()
else:
queryset = cls.objects.filter(
# get all the session where the user was the teacher
Q(session__teacher=user) |
# get all the session where the user was the student
Q(student=user) |
# get all the sessions where the user is managing the unit
Q(session__unit__managers=user) |
# get all the sessions where the user is managing the department
Q(session__unit__department__managers=user)
).distinct()
return queryset.order_by("pk")
class Absence(models.Model, ModelApiMixin):
class Absence(models.Model):
"""
A student justified absence to a session.
@ -357,27 +237,8 @@ class Absence(models.Model, ModelApiMixin):
def __str__(self):
return f"[{str(self.id)[:8]}] {self.student}"
@classmethod
def all_visible_to(cls, user: "User") -> QuerySet["User"]:
if user.is_superuser:
queryset = cls.objects.all()
else:
queryset = cls.objects.filter(
# get all the absence where the user was the teacher
Q(session__teacher=user) |
# get all the absence where the user was the student
Q(student=user) |
# get all the absences where the user is managing the unit
Q(session__unit__managers=user) |
# get all the absences where the user is managing the department
Q(session__unit__department__managers=user)
).distinct()
return queryset.order_by("pk")
class AbsenceAttachment(models.Model, ModelApiMixin):
class AbsenceAttachment(models.Model):
"""
An attachment to a student justified absence.
@ -391,22 +252,3 @@ class AbsenceAttachment(models.Model, ModelApiMixin):
def __repr__(self):
return f"<{self.__class__.__name__} id={str(self.id)[:8]} content={self.content!r}>"
@classmethod
def all_visible_to(cls, user: "User") -> QuerySet["User"]:
if user.is_superuser:
queryset = cls.objects.all()
else:
queryset = cls.objects.filter(
# get all the absence attachments where the user was the teacher
Q(absence__session__teacher=user) |
# get all the absence attachments where the user was the student
Q(absence__student=user) |
# get all the absence attachments where the user is managing the unit
Q(absence__session__unit__managers=user) |
# get all the absence attachments where the user is managing the department
Q(absence__session__unit__department__managers=user)
).distinct()
return queryset.order_by("pk")

View file

@ -10,6 +10,7 @@ For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.2/ref/settings/
"""
import os
from datetime import timedelta
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
@ -50,12 +51,15 @@ INSTALLED_APPS = [
'rest_framework',
"debug_toolbar",
'django_extensions',
'rest_framework_simplejwt',
'corsheaders',
"Palto.Palto",
]
MIDDLEWARE = [
"debug_toolbar.middleware.DebugToolbarMiddleware",
'corsheaders.middleware.CorsMiddleware',
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
@ -147,6 +151,13 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
# Rest API configuration
# https://www.django-rest-framework.org/
REST_FRAMEWORK = {
# Default way to authenticate to the REST api.
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework_simplejwt.authentication.JWTAuthentication',
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.BasicAuthentication',
],
# Use Django's standard `django.contrib.auth` permissions,
# or allow read-only access for unauthenticated users.
'DEFAULT_PERMISSION_CLASSES': [
@ -168,5 +179,23 @@ REST_FRAMEWORK = {
'PAGE_SIZE': 30
}
# JWT Authentification
SIMPLE_JWT = {
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=15),
"REFRESH_TOKEN_LIFETIME": timedelta(days=1),
}
# User model
AUTH_USER_MODEL = "Palto.User"
# CORS settings
# TODO(Raphaël): Only in debug !
CORS_ORIGIN_ALLOW_ALL = True
# Login
LOGIN_URL = "/admin/login/"
LOGIN_REDIRECT_URL = "/"
LOGOUT_REDIRECT_URL = "/"

View file

@ -14,7 +14,7 @@ Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
import debug_toolbar
from django.contrib import admin
from django.urls import path, re_path, include
from django.views.static import serve
@ -32,7 +32,7 @@ urlpatterns = [
# Debug
path('admin/', admin.site.urls), # Admin page
path("__debug__/", include(debug_toolbar.urls)), # Debug toolbar
path("__debug__/", include('debug_toolbar.urls')), # Debug toolbar
]

View file

@ -2,6 +2,8 @@ django
djangorestframework
django-debug-toolbar
django-extensions
djangorestframework-simplejwt[crypto]
django-cors-headers
Werkzeug
markdown