diff --git a/aleksis/apps/chronos/managers.py b/aleksis/apps/chronos/managers.py index 07695bb22e7361f3cbfa02b8d5553d2ad6ed5b5d..9689ef0b167a6706de08e037c1a1a747a6f3a1b7 100644 --- a/aleksis/apps/chronos/managers.py +++ b/aleksis/apps/chronos/managers.py @@ -15,7 +15,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/0020_add_global_permissions.py b/aleksis/apps/chronos/migrations/0020_add_global_permissions.py new file mode 100644 index 0000000000000000000000000000000000000000..9fbc5102bd4d4368836ed3a8756b000a8c4ec174 --- /dev/null +++ b/aleksis/apps/chronos/migrations/0020_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', '0019_remove_old_models'), + ] + + 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_substitutions', 'Can view substitutions table'), ('view_all_room_supervisions', 'Can view all room supervisions'), ('view_all_group_supervisions', 'Can view all group supervisions'), ('view_all_person_supervisions', 'Can view all person supervisions'))}, + ), + ] diff --git a/aleksis/apps/chronos/model_extensions.py b/aleksis/apps/chronos/model_extensions.py index 457ef9ef8d0183bf5ddfa5c6799a7e4d14db20d5..7b76336b04e8ea70eb35ca03a0f17f3f1f13f4af 100644 --- a/aleksis/apps/chronos/model_extensions.py +++ b/aleksis/apps/chronos/model_extensions.py @@ -1,8 +1,9 @@ from django.utils.translation import gettext_lazy as _ -from aleksis.core.models import Group, Person +from aleksis.apps.cursus.models import Course +from aleksis.core.models import Group, Person, Room -# Dynamically add extra permissions to Group and Person models in core +# Dynamically add permissions to Group, Person and Room models in core and Course model in cursus # Note: requires migrate afterwards Group.add_permission( "view_group_timetable", @@ -12,3 +13,19 @@ Person.add_permission( "view_person_timetable", _("Can view person timetable"), ) +Group.add_permission( + "view_group_supervisions", + _("Can view group supervisions"), +) +Person.add_permission( + "view_person_supervisions", + _("Can view person supervisions"), +) +Room.add_permission( + "view_room_supervisions", + _("Can view room supervisions"), +) +Course.add_permission( + "view_course_timetable", + _("Can view course timetable"), +) diff --git a/aleksis/apps/chronos/models.py b/aleksis/apps/chronos/models.py index a93a830b36cafd70426cf583334b3ac4ff78302e..f3e7331693b057df03290ebbfd74bfca923e11f8 100644 --- a/aleksis/apps/chronos/models.py +++ b/aleksis/apps/chronos/models.py @@ -7,6 +7,7 @@ from datetime import date from typing import Any from django.contrib.contenttypes.models import ContentType +from django.core.exceptions import PermissionDenied from django.core.validators import MinValueValidator from django.db import models from django.db.models import Q, QuerySet @@ -34,6 +35,7 @@ from aleksis.core.mixins import ( ) from aleksis.core.models import CalendarEvent, Group, Person, Room from aleksis.core.util.core_helpers import get_site_preferences, has_person +from aleksis.core.util.predicates import check_global_permission class AutomaticPlan(LiveDocument): @@ -146,8 +148,12 @@ 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_substitutions", _("Can view substitutions table")), + ("view_all_room_supervisions", _("Can view all room supervisions")), + ("view_all_group_supervisions", _("Can view all group supervisions")), + ("view_all_person_supervisions", _("Can view all person supervisions")), ) @@ -444,6 +450,39 @@ class LessonEvent(CalendarEvent): q = q & LessonEventQuerySet.related_to_person_q(request.user.person) if type_ and obj_id: + if request and not ( + ( + type_ == "GROUP" + and check_global_permission( + request.user, "chronos.view_all_group_timetables" + ) + ) + or ( + type_ == "TEACHER" + or type_ == "PARTICIPANT" + and check_global_permission( + request.user, "chronos.view_all_person_timetables" + ) + ) + or ( + type_ == "ROOM" + and check_global_permission( + request.user, "chronos.view_all_room_timetables" + ) + ) + or ( + type_ == "COURSE" + and 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": q = q & LessonEventQuerySet.for_teacher_q(obj_id) elif type_ == "PARTICIPANT": @@ -523,6 +562,32 @@ class SupervisionEvent(LessonEvent): q = q & SupervisionEventQuerySet.amending_q() if type_ and obj_id: + if request and not ( + ( + type_ == "GROUP" + and check_global_permission( + request.user, "chronos.view_all_group_supervisions" + ) + ) + or ( + type_ == "TEACHER" + and check_global_permission( + request.user, "chronos.view_all_person_supervisions" + ) + ) + or ( + type_ == "ROOM" + and check_global_permission( + request.user, "chronos.view_all_room_supervisions" + ) + ) + ): + # 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_supervisions_rule", obj): + raise PermissionDenied() if type_ == "TEACHER": q = q & SupervisionEventQuerySet.for_teacher_q(obj_id) elif type_ == "GROUP": diff --git a/aleksis/apps/chronos/rules.py b/aleksis/apps/chronos/rules.py index c7d0065c6bc2c6180a0fa81188484472e6ed8af7..ff5172e781c3500ef40b2f5c96bfcb2e322f9a6a 100644 --- a/aleksis/apps/chronos/rules.py +++ b/aleksis/apps/chronos/rules.py @@ -6,7 +6,7 @@ from aleksis.core.util.predicates import ( has_person, ) -from .util.predicates import has_any_timetable_object, has_timetable_perm +from .util.predicates import has_any_timetable_object, has_supervisions_perm, has_timetable_perm # View timetable overview view_timetable_overview_predicate = has_person & ( @@ -18,6 +18,9 @@ add_perm("chronos.view_timetable_overview_rule", view_timetable_overview_predica view_timetable_predicate = has_person & has_timetable_perm add_perm("chronos.view_timetable_rule", view_timetable_predicate) +# View supervisions for group, person or room +view_supervisions_predicate = has_person & has_supervisions_perm +add_perm("chronos.view_supervisions_rule", view_supervisions_predicate) # Edit substition edit_substitution_predicate = has_person & ( diff --git a/aleksis/apps/chronos/util/chronos_helpers.py b/aleksis/apps/chronos/util/chronos_helpers.py index 961fb01460f8c4fa65504d8726d653d223eeef3b..70688e8e585200a6709be110d9770d64f33e971d 100644 --- a/aleksis/apps/chronos/util/chronos_helpers.py +++ b/aleksis/apps/chronos/util/chronos_helpers.py @@ -2,12 +2,16 @@ from datetime import date, datetime, timedelta from typing import TYPE_CHECKING, Optional from django.db.models import Count, Q +from django.http import HttpRequest, HttpResponseNotFound +from django.shortcuts import get_object_or_404 from guardian.shortcuts import get_objects_for_user +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 ..managers import TimetableType from .build import build_substitutions_list if TYPE_CHECKING: @@ -16,6 +20,29 @@ if TYPE_CHECKING: User = get_user_model() # noqa +def get_el_by_pk( + request: HttpRequest, + type_: str, + pk: int, + prefetch: bool = False, + *args, + **kwargs, +): + if type_ == TimetableType.GROUP.value: + return get_object_or_404( + Group.objects.prefetch_related("owners", "parent_groups") if prefetch else Group, + pk=pk, + ) + 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() + + def get_teachers(user: "User"): """Get the teachers whose timetables are allowed to be seen by current user.""" diff --git a/aleksis/apps/chronos/util/predicates.py b/aleksis/apps/chronos/util/predicates.py index 7657ea96becd43be0b92b9ebfda24723b119c44d..b38eec17bdf245ea241363802304859700dcd34a 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,88 @@ 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_supervisions_perm(user: User, obj: Model) -> bool: + """ + Check if can access supervisions of object. + + Predicate which checks whether the user is allowed + to access the requested supervisions of the given + group, person or room. + """ + if isinstance(obj, Group): + return has_group_supervisions_perm(user, obj) + elif isinstance(obj, Person): + return has_person_supervisions_perm(user, obj) + elif isinstance(obj, Room): + return has_room_supervisions_perm(user, obj) + else: + return False + + +@predicate +def has_group_supervisions_perm(user: User, obj: Group) -> bool: + """ + Check if can access group supervisions. + + Predicate which checks whether the user is allowed + to access the requested group supervisions. + """ + return ( + obj in user.person.member_of.all() + or user.person.primary_group == obj + or obj in user.person.owner_of.all() + or has_global_perm("chronos.view_all_group_supervisions")(user) + or has_object_perm("core.view_group_supervisions")(user, obj) + ) + + +@predicate +def has_person_supervisions_perm(user: User, obj: Person) -> bool: + """ + Check if can access person supervisions. + + Predicate which checks whether the user is allowed + to access the requested person supervisions. + """ + return ( + user.person == obj + or has_global_perm("chronos.view_all_person_supervisions")(user) + or has_object_perm("core.view_person_supervisions")(user, obj) + ) + + +@predicate +def has_room_supervisions_perm(user: User, obj: Room) -> bool: + """ + Check if can access room supervisions. + + Predicate which checks whether the user is allowed + to access the requested room supervisions. + """ + return has_global_perm("chronos.view_all_room_supervisions")(user) or has_object_perm( + "core.view_room_supervisions" + )(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."""