From 9f17d6282bb94fd764255d142a97f73fbc09ba2f Mon Sep 17 00:00:00 2001
From: Hangzhi Yu <hangzhi@protonmail.com>
Date: Fri, 13 Sep 2024 23:23:47 +0200
Subject: [PATCH] Check for permissions when accessing lesson events in backend

---
 aleksis/apps/chronos/managers.py              |  2 ++
 .../migrations/0018_add_global_permissions.py | 17 +++++++++++++++
 aleksis/apps/chronos/model_extensions.py      |  7 ++++++-
 aleksis/apps/chronos/models.py                | 16 ++++++++++++++
 aleksis/apps/chronos/util/chronos_helpers.py  |  5 ++++-
 aleksis/apps/chronos/util/predicates.py       | 21 +++++++++++++++++++
 6 files changed, 66 insertions(+), 2 deletions(-)
 create mode 100644 aleksis/apps/chronos/migrations/0018_add_global_permissions.py

diff --git a/aleksis/apps/chronos/managers.py b/aleksis/apps/chronos/managers.py
index 6b2f4c3c..1b41f62f 100644
--- a/aleksis/apps/chronos/managers.py
+++ b/aleksis/apps/chronos/managers.py
@@ -79,7 +79,9 @@ class TimetableType(Enum):
 
     GROUP = "group"
     TEACHER = "teacher"
+    PARTICIPANT = "participant"
     ROOM = "room"
+    COURSE = "course"
 
     @classmethod
     def from_string(cls, s: Optional[str]):
diff --git a/aleksis/apps/chronos/migrations/0018_add_global_permissions.py b/aleksis/apps/chronos/migrations/0018_add_global_permissions.py
new file mode 100644
index 00000000..c51cd016
--- /dev/null
+++ b/aleksis/apps/chronos/migrations/0018_add_global_permissions.py
@@ -0,0 +1,17 @@
+# Generated by Django 5.0.7 on 2024-09-13 21:20
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('chronos', '0017_optional_slot_number'),
+    ]
+
+    operations = [
+        migrations.AlterModelOptions(
+            name='chronosglobalpermissions',
+            options={'managed': False, 'permissions': (('view_all_room_timetables', 'Can view all room timetables'), ('view_all_group_timetables', 'Can view all group timetables'), ('view_all_person_timetables', 'Can view all person timetables'), ('view_all_course_timetables', 'Can view all course timetables'), ('view_timetable_overview', 'Can view timetable overview'), ('view_lessons_day', 'Can view all lessons per day'), ('view_supervisions_day', 'Can view all supervisions per day'))},
+        ),
+    ]
diff --git a/aleksis/apps/chronos/model_extensions.py b/aleksis/apps/chronos/model_extensions.py
index 8f43350e..80468991 100644
--- a/aleksis/apps/chronos/model_extensions.py
+++ b/aleksis/apps/chronos/model_extensions.py
@@ -6,6 +6,7 @@ from django.utils.translation import gettext_lazy as _
 
 from reversion.models import Revision
 
+from aleksis.apps.cursus.models import Course
 from aleksis.core.models import Announcement, Group, Person
 from aleksis.core.util.core_helpers import get_site_preferences
 
@@ -138,7 +139,7 @@ def for_timetables(cls):
 
 Announcement.class_method(for_timetables)
 
-# Dynamically add extra permissions to Group and Person models in core
+# Dynamically add extra permissions to Group and Person models in core and Course model in cursus
 # Note: requires migrate afterwards
 Group.add_permission(
     "view_group_timetable",
@@ -148,6 +149,10 @@ Person.add_permission(
     "view_person_timetable",
     _("Can view person timetable"),
 )
+Course.add_permission(
+    "view_course_timetable",
+    _("Can view course timetable"),
+)
 
 
 @receiver(timetable_data_changed)
diff --git a/aleksis/apps/chronos/models.py b/aleksis/apps/chronos/models.py
index 3189999f..1c309a53 100644
--- a/aleksis/apps/chronos/models.py
+++ b/aleksis/apps/chronos/models.py
@@ -71,6 +71,7 @@ from aleksis.core.mixins import (
 )
 from aleksis.core.models import CalendarEvent, Group, Person, Room, SchoolTerm
 from aleksis.core.util.core_helpers import get_site_preferences, has_person
+from aleksis.core.util.predicates import check_global_permission
 
 
 class ValidityRange(ExtensibleModel):
@@ -1293,6 +1294,7 @@ class ChronosGlobalPermissions(GlobalPermissionModel):
             ("view_all_room_timetables", _("Can view all room timetables")),
             ("view_all_group_timetables", _("Can view all group timetables")),
             ("view_all_person_timetables", _("Can view all person timetables")),
+            ("view_all_course_timetables", _("Can view all course timetables")),
             ("view_timetable_overview", _("Can view timetable overview")),
             ("view_lessons_day", _("Can view all lessons per day")),
             ("view_supervisions_day", _("Can view all supervisions per day")),
@@ -1593,6 +1595,20 @@ class LessonEvent(CalendarEvent):
                     objs = objs.related_to_person(request.user.person)
 
             if type_ and obj_id:
+                if request and not (
+                    check_global_permission(request.user, "chronos.view_all_group_timetables")
+                    or check_global_permission(request.user, "chronos.view_all_person_timetables")
+                    or check_global_permission(
+                        request.user, "chronos.chronos.view_all_room_timetables"
+                    )
+                    or check_global_permission(request.user, "chronos.view_all_course_timetables")
+                ):
+                    # inline import needed to avoid circular import
+                    from aleksis.apps.chronos.util.chronos_helpers import get_el_by_pk
+
+                    obj = get_el_by_pk(request, type_.lower(), obj_id)
+                    if not request.user.has_perm("chronos.view_timetable_rule", obj):
+                        raise PermissionDenied()
                 if type_ == "TEACHER":
                     return objs.for_teacher(obj_id)
                 elif type_ == "PARTICIPANT":
diff --git a/aleksis/apps/chronos/util/chronos_helpers.py b/aleksis/apps/chronos/util/chronos_helpers.py
index 8e70ceae..f76fa466 100644
--- a/aleksis/apps/chronos/util/chronos_helpers.py
+++ b/aleksis/apps/chronos/util/chronos_helpers.py
@@ -7,6 +7,7 @@ from django.shortcuts import get_object_or_404
 
 from guardian.core import ObjectPermissionChecker
 
+from aleksis.apps.cursus.models import Course
 from aleksis.core.models import Announcement, Group, Person, Room
 from aleksis.core.util.core_helpers import get_site_preferences
 from aleksis.core.util.predicates import check_global_permission
@@ -39,10 +40,12 @@ def get_el_by_pk(
             Group.objects.prefetch_related("owners", "parent_groups") if prefetch else Group,
             pk=pk,
         )
-    elif type_ == TimetableType.TEACHER.value:
+    elif type_ == TimetableType.TEACHER.value or type_ == TimetableType.PARTICIPANT.value:
         return get_object_or_404(Person, pk=pk)
     elif type_ == TimetableType.ROOM.value:
         return get_object_or_404(Room, pk=pk)
+    elif type_ == TimetableType.COURSE.value:
+        return get_object_or_404(Course, pk=pk)
     else:
         return HttpResponseNotFound()
 
diff --git a/aleksis/apps/chronos/util/predicates.py b/aleksis/apps/chronos/util/predicates.py
index 7657ea96..6bd4a7c7 100644
--- a/aleksis/apps/chronos/util/predicates.py
+++ b/aleksis/apps/chronos/util/predicates.py
@@ -3,6 +3,7 @@ from django.db.models import Model
 
 from rules import predicate
 
+from aleksis.apps.cursus.models import Course
 from aleksis.core.models import Group, Person, Room
 from aleksis.core.util.predicates import has_global_perm, has_object_perm
 
@@ -23,6 +24,8 @@ def has_timetable_perm(user: User, obj: Model) -> bool:
         return has_person_timetable_perm(user, obj)
     elif isinstance(obj, Room):
         return has_room_timetable_perm(user, obj)
+    elif isinstance(obj, Course):
+        return has_course_timetable_perm(user, obj)
     else:
         return False
 
@@ -72,6 +75,24 @@ def has_room_timetable_perm(user: User, obj: Room) -> bool:
     )(user, obj)
 
 
+@predicate
+def has_course_timetable_perm(user: User, obj: Course) -> bool:
+    """
+    Check if can access course timetable.
+
+    Predicate which checks whether the user is allowed
+    to access the requested course timetable.
+    """
+    return (
+        user.person in obj.teachers.all()
+        or obj.groups.all().intersection(user.person.member_of.all()).exists()
+        or user.person.primary_group in obj.groups.all()
+        or obj.groups.all().intersection(user.person.owner_of.all()).exists()
+        or has_global_perm("chronos.view_all_course_timetables")(user)
+        or has_object_perm("cursus.view_course_timetable")(user, obj)
+    )
+
+
 @predicate
 def has_any_timetable_object(user: User) -> bool:
     """Predicate which checks whether there are any timetables the user is allowed to access."""
-- 
GitLab