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