Skip to content

Commit

Permalink
Merge pull request #106 from SELab-2/permissions
Browse files Browse the repository at this point in the history
Permissions
  • Loading branch information
EwoutV authored Mar 14, 2024
2 parents f51096a + d4ed3fe commit dcff399
Show file tree
Hide file tree
Showing 15 changed files with 146 additions and 99 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
.tool-versions
.env
.venv
.idea
.vscode
data/*
data/nginx/ssl/*
data/postres*
data/redis/*

/node_modules
backend/staticfiles/*

!data/nginx/ssl/.gitkeep
Expand Down
4 changes: 4 additions & 0 deletions backend/api/models/teacher.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,7 @@ class Teacher(User):
related_name="teachers",
blank=True,
)

def has_course(self, course: Course) -> bool:
"""Checks if the teacher has the given course."""
return self.courses.contains(course)
23 changes: 23 additions & 0 deletions backend/api/permissions/assistant_permissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from rest_framework.permissions import BasePermission
from rest_framework.request import Request
from rest_framework.viewsets import ViewSet
from api.permissions.role_permissions import is_teacher, is_assistant
from api.models.assistant import Assistant


class AssistantPermission(BasePermission):
"""Permission class used as default policy for assistant endpoint."""
def has_permission(self, request: Request, view: ViewSet) -> bool:
"""Check if user has permission to view a general assistant endpoint."""
user = request.user

if view.action == "list":
# Only teachers can query the assistant list.
return user.is_authenticated and is_teacher(user)

return is_teacher(user) or is_assistant(user)

def has_object_permission(self, request: Request, view: ViewSet, assistant: Assistant) -> bool:
# Teachers can view the details of all assistants.
# Users can view their own assistant object.
return is_teacher(request.user) or request.user.id == assistant.id
6 changes: 3 additions & 3 deletions backend/api/permissions/course_permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ def has_permission(self, request: Request, view: ViewSet) -> bool:

def has_object_permission(self, request: Request, view: ViewSet, course: Course) -> bool:
"""Check if user has permission to view a detailed course endpoint"""
user: User = request.user
user = request.user

# Logged-in users can fetch course details.
if request.method in SAFE_METHODS:
return user.is_authenticated
return is_student(user) or is_teacher(user) or is_assistant(user)

# We only allow teachers and assistants to modify their own courses.
return is_teacher(user) and user.teacher.courses.contains(course) or \
Expand All @@ -44,7 +44,7 @@ def has_object_permission(self, request: Request, view: ViewSet, course: Course)
return user.is_authenticated

# Only teachers can modify assistants of their own courses.
return is_teacher(user) and user.teacher.courses.contains(course)
return is_teacher(user) and user.teacher.has_course(course)


class CourseStudentPermission(CoursePermission):
Expand Down
10 changes: 10 additions & 0 deletions backend/api/permissions/role_permissions.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from rest_framework.permissions import IsAuthenticated
from rest_framework.request import Request
from rest_framework.viewsets import ViewSet
from authentication.models import User
from api.models.student import Student
from api.models.assistant import Assistant
Expand Down Expand Up @@ -37,3 +38,12 @@ def has_permission(self, request, view):
"""Returns true if the request contains a user,
with said user being a student"""
return super().has_permission(request, view) and is_assistant(request.user)


class IsSameUser(IsAuthenticated):
def has_permission(self, request, view):
return False

def has_object_permission(self, request: Request, view: ViewSet, user: User):
"""Returns true if the request's user is the same as the given user"""
return super().has_permission(request, view) and user.id == request.user.id
2 changes: 1 addition & 1 deletion backend/api/serializers/assistant_serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,6 @@ class Meta:


class AssistantIDSerializer(serializers.Serializer):
assistant_id = serializers.PrimaryKeyRelatedField(
assistant = serializers.PrimaryKeyRelatedField(
queryset=Assistant.objects.all()
)
26 changes: 23 additions & 3 deletions backend/api/views/admin_view.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,28 @@
from rest_framework import viewsets
from authentication.serializers import UserSerializer
from django.utils.translation import gettext
from rest_framework.viewsets import ReadOnlyModelViewSet
from rest_framework.response import Response
from rest_framework.request import Request
from rest_framework.permissions import IsAdminUser
from authentication.serializers import UserSerializer, UserIDSerializer
from authentication.models import User


class AdminViewSet(viewsets.ReadOnlyModelViewSet):
class AdminViewSet(ReadOnlyModelViewSet):
queryset = User.objects.filter(is_staff=True)
serializer_class = UserSerializer
permission_classes = [IsAdminUser]

def create(self, request: Request) -> Response:
"""
Make the provided user admin by setting `is_staff` = true.
"""
serializer = UserIDSerializer(
data=request.data
)

if serializer.is_valid(raise_exception=True):
serializer.validated_data["user"].make_admin()

return Response({
"message": gettext("admins.success.add")
})
32 changes: 14 additions & 18 deletions backend/api/views/assistant_view.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,28 @@
from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.viewsets import ReadOnlyModelViewSet
from rest_framework.permissions import IsAdminUser
from api.permissions.assistant_permissions import AssistantPermission
from ..models.assistant import Assistant
from ..serializers.assistant_serializer import AssistantSerializer
from ..serializers.course_serializer import CourseSerializer


class AssistantViewSet(viewsets.ModelViewSet):
class AssistantViewSet(ReadOnlyModelViewSet):

queryset = Assistant.objects.all()
serializer_class = AssistantSerializer
permission_classes = [IsAdminUser | AssistantPermission]

@action(detail=True, methods=["get"])
def courses(self, request, pk=None):
def courses(self, request, **_):
"""Returns a list of courses for the given assistant"""
assistant = self.get_object()
courses = assistant.courses

try:
queryset = Assistant.objects.get(id=pk)
courses = queryset.courses.all()

# Serialize the course objects
serializer = CourseSerializer(
courses, many=True, context={"request": request}
)
return Response(serializer.data)
# Serialize the course objects
serializer = CourseSerializer(
courses, many=True, context={"request": request}
)

except Assistant.DoesNotExist:
# Invalid assistant ID
return Response(
status=status.HTTP_404_NOT_FOUND,
data={"message": "Assistant not found"},
)
return Response(serializer.data)
4 changes: 2 additions & 2 deletions backend/api/views/course_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def _add_assistant(self, request: Request, **_):

if serializer.is_valid(raise_exception=True):
course.assistants.add(
serializer.validated_data["assistant_id"]
serializer.validated_data["assistant"]
)

return Response({
Expand All @@ -70,7 +70,7 @@ def _remove_assistant(self, request: Request, **_):

if serializer.is_valid(raise_exception=True):
course.assistants.remove(
serializer.validated_data["assistant_id"]
serializer.validated_data["assistant"]
)

return Response({
Expand Down
3 changes: 1 addition & 2 deletions backend/api/views/group_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,7 @@ def _remove_student(self, request, **_):
@submissions.mapping.post
@submissions.mapping.put
def _add_submission(self, request: Request, **_):
"""Add an submission to the group"""

"""Add a submission to the group"""
group: Group = self.get_object()

# Add submission to course
Expand Down
16 changes: 6 additions & 10 deletions backend/api/views/project_view.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
from django.utils.translation import gettext
from rest_framework.mixins import CreateModelMixin, RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin
from rest_framework.permissions import IsAdminUser
from rest_framework import viewsets
from rest_framework.viewsets import GenericViewSet
from rest_framework.decorators import action
from rest_framework.response import Response
from api.permissions.project_permissions import ProjectGroupPermission, ProjectPermission
from api.models.group import Group
from api.models.submission import Submission
from ..models.project import Project
from ..serializers.checks_serializer import StructureCheckSerializer, ExtraCheckSerializer
from api.models.project import Project
from api.serializers.checks_serializer import StructureCheckSerializer, ExtraCheckSerializer
from api.serializers.project_serializer import ProjectSerializer, TeacherCreateGroupSerializer, SubmissionStatusSerializer
from api.serializers.group_serializer import GroupSerializer
from api.serializers.submission_serializer import SubmissionSerializer
Expand All @@ -18,7 +18,7 @@ class ProjectViewSet(CreateModelMixin,
RetrieveModelMixin,
UpdateModelMixin,
DestroyModelMixin,
viewsets.GenericViewSet):
GenericViewSet):

queryset = Project.objects.all()
serializer_class = ProjectSerializer
Expand All @@ -39,12 +39,9 @@ def groups(self, request, **_):

return Response(serializer.data)

"""
@action(detail=True, permission_classes=[IsAdminUser])
@action(detail=True)
def submissions(self, request, **_):
# Returns a list of subbmisions for the given project
# This automatically fetches the group from the URL.
# It automatically gives back a 404 HTTP response in case of not found.
"""Returns a list of submissions for the given project"""
project = self.get_object()
submissions = project.submissions.all()

Expand All @@ -54,7 +51,6 @@ def submissions(self, request, **_):
)

return Response(serializer.data)
"""

@groups.mapping.post
def _create_groups(self, request, **_):
Expand Down
64 changes: 26 additions & 38 deletions backend/api/views/student_view.py
Original file line number Diff line number Diff line change
@@ -1,52 +1,40 @@
from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.response import Response
from ..models.student import Student
from ..serializers.student_serializer import StudentSerializer
from ..serializers.course_serializer import CourseSerializer
from ..serializers.group_serializer import GroupSerializer
from rest_framework.permissions import IsAdminUser
from api.permissions.role_permissions import IsSameUser, IsTeacher
from api.models.student import Student
from api.serializers.student_serializer import StudentSerializer
from api.serializers.course_serializer import CourseSerializer
from api.serializers.group_serializer import GroupSerializer


class StudentViewSet(viewsets.ModelViewSet):
queryset = Student.objects.all()
serializer_class = StudentSerializer
permission_classes = [IsAdminUser | IsTeacher | IsSameUser]

@action(detail=True, methods=["get"])
def courses(self, request, pk=None):
@action(detail=True)
def courses(self, request, **_):
"""Returns a list of courses for the given student"""
student = self.get_object()
courses = student.courses.all()

try:
queryset = Student.objects.get(id=pk)
courses = queryset.courses.all()
# Serialize the course objects
serializer = CourseSerializer(
courses, many=True, context={"request": request}
)

# Serialize the course objects
serializer = CourseSerializer(
courses, many=True, context={"request": request}
)
return Response(serializer.data)
return Response(serializer.data)

except Student.DoesNotExist:
# Invalid student ID
return Response(
status=status.HTTP_404_NOT_FOUND, data={"message": "Student not found"}
)

@action(detail=True, methods=["get"])
def groups(self, request, pk=None):
@action(detail=True)
def groups(self, request, **_):
"""Returns a list of groups for the given student"""

try:
queryset = Student.objects.get(id=pk)
groups = queryset.groups.all()

# Serialize the group objects
serializer = GroupSerializer(
groups, many=True, context={"request": request}
)
return Response(serializer.data)

except Student.DoesNotExist:
# Invalid student ID
return Response(
status=status.HTTP_404_NOT_FOUND, data={"message": "Student not found"}
)
student = self.get_object()
groups = student.groups.all()

# Serialize the group objects
serializer = GroupSerializer(
groups, many=True, context={"request": request}
)
return Response(serializer.data)
37 changes: 18 additions & 19 deletions backend/api/views/teacher_view.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,30 @@
from rest_framework import viewsets, status
from rest_framework import status
from rest_framework.decorators import action
from rest_framework.response import Response
from ..models.teacher import Teacher
from ..serializers.teacher_serializer import TeacherSerializer
from ..serializers.course_serializer import CourseSerializer
from rest_framework.viewsets import ReadOnlyModelViewSet
from rest_framework.permissions import IsAdminUser

from api.models.course import Course
from api.models.teacher import Teacher
from api.serializers.teacher_serializer import TeacherSerializer
from api.serializers.course_serializer import CourseSerializer
from api.permissions.role_permissions import IsSameUser

class TeacherViewSet(viewsets.ModelViewSet):

class TeacherViewSet(ReadOnlyModelViewSet):
queryset = Teacher.objects.all()
serializer_class = TeacherSerializer
permission_classes = [IsAdminUser | IsSameUser]

@action(detail=True, methods=["get"])
def courses(self, request, pk=None):
"""Returns a list of courses for the given teacher"""
teacher = self.get_object()
courses = teacher.courses.all()

try:
queryset = Teacher.objects.get(id=pk)
courses = queryset.courses.all()

# Serialize the course objects
serializer = CourseSerializer(
courses, many=True, context={"request": request}
)
return Response(serializer.data)
# Serialize the course objects
serializer = CourseSerializer(
courses, many=True, context={"request": request}
)

except Teacher.DoesNotExist:
# Invalid teacher ID
return Response(
status=status.HTTP_404_NOT_FOUND, data={"message": "Teacher not found"}
)
return Response(serializer.data)
Loading

0 comments on commit dcff399

Please sign in to comment.