From ca72d816a7eb5015f9d5f0faebd7c9d02bf5923c Mon Sep 17 00:00:00 2001 From: Felix Rindt Date: Sun, 10 Nov 2024 21:09:48 +0100 Subject: [PATCH] fix endpoint permissions --- ephios/api/filters.py | 27 +++++++++++++++++++++--- ephios/api/permissions.py | 18 +--------------- ephios/api/serializers.py | 18 ++++++++++++++-- ephios/api/urls.py | 14 +++++++++++-- ephios/api/views/events.py | 42 ++++++++++++++++++++++++++++++-------- ephios/api/views/users.py | 35 ++++++++++++++++++++++++------- 6 files changed, 115 insertions(+), 39 deletions(-) diff --git a/ephios/api/filters.py b/ephios/api/filters.py index a38b80de4..a9681c519 100644 --- a/ephios/api/filters.py +++ b/ephios/api/filters.py @@ -1,4 +1,5 @@ import django_filters +from django.db.models import Q from django_filters import FilterSet, IsoDateTimeFilter, ModelMultipleChoiceFilter from guardian.shortcuts import get_objects_for_user from rest_framework.filters import BaseFilterBackend @@ -8,8 +9,28 @@ class ParticipationPermissionFilter(BaseFilterBackend): def filter_queryset(self, request, queryset, view): - events = get_objects_for_user(request.user, "core.view_event") - return queryset.filter(shift__event__in=events) + # to view public participation information (excl. email) you need to + # be able to see the event + viewable_events = get_objects_for_user(request.user, "core.view_event") + return queryset.filter(shift__event__in=viewable_events) + + +class UserinfoParticipationPermissionFilter(ParticipationPermissionFilter): + def filter_queryset(self, request, queryset, view): + # to also see user info of participations (incl. email) you need to + # * see the event AND + # * ANY of + # * has view_userprofile permission + # * can view user object + # * refers to request.user + qs = super().filter_queryset(request, queryset, view) + if not request.user.has_perm("core.view_userprofile"): + viewable_users = get_objects_for_user(request.user, "core.view_userprofile") + qs = qs.filter( + Q(LocalParticipation___user=request.user) + | Q(LocalParticipation___user__in=viewable_users) + ) + return qs class ShiftPermissionFilter(BaseFilterBackend): @@ -34,7 +55,7 @@ class StartEndTimeFilterSet(FilterSet): ) -class AbstractParticipationFilterSet(StartEndTimeFilterSet): +class ParticipationFilterSet(StartEndTimeFilterSet): # we cannot use gettext_lazy as it breaks sphinxcontrib.openapi (https://github.com/sphinx-contrib/openapi/issues/153) event_type = ModelMultipleChoiceFilter( field_name="shift__event__type", label="event type", queryset=EventType.objects.all() diff --git a/ephios/api/permissions.py b/ephios/api/permissions.py index 5628de782..11beff8f5 100644 --- a/ephios/api/permissions.py +++ b/ephios/api/permissions.py @@ -24,7 +24,7 @@ class ViewObjectPermissions(ViewPermissionsMixin, DjangoObjectPermissions): pass -class UserModelObjectPermissions(ViewObjectPermissions): +class ViewUserModelObjectPermissions(ViewObjectPermissions): """ Like the default DjangoObjectPermissions, but force the permission model to be UserProfile. @@ -32,19 +32,3 @@ class UserModelObjectPermissions(ViewObjectPermissions): def get_required_permissions(self, method, model_cls): return super().get_required_permissions(method, get_user_model()) - - -class ParticipationPermissions(UserModelObjectPermissions): - """ - For viewing participations, require viewing the user. - Additionally, assume view permission on the own user model for - authenticated users. - """ - - def has_object_permission(self, request, view, obj): - if super().has_object_permission(request, view, obj): - return True - if request.user and request.user.is_authenticated: - if getattr(obj, "user", None) == request.user: - return True - return False diff --git a/ephios/api/serializers.py b/ephios/api/serializers.py index 4e917ae82..8d619ca1e 100644 --- a/ephios/api/serializers.py +++ b/ephios/api/serializers.py @@ -2,7 +2,7 @@ from django.utils import timezone from rest_framework import serializers from rest_framework.exceptions import MethodNotAllowed -from rest_framework.fields import SerializerMethodField +from rest_framework.fields import BooleanField, SerializerMethodField from rest_framework.relations import SlugRelatedField from rest_framework.serializers import ModelSerializer @@ -130,6 +130,7 @@ class ParticipantSerializer(serializers.Serializer): email = serializers.EmailField(allow_null=True) date_of_birth = serializers.DateField() age = serializers.IntegerField(source="get_age") + is_minor = BooleanField() type = serializers.SerializerMethodField() qualifications = QualificationSerializer(many=True) @@ -144,7 +145,7 @@ def create(self, validated_data): raise MethodNotAllowed("create") -class AbstractParticipationSerializer(ModelSerializer): +class UserinfoParticipationSerializer(ModelSerializer): state = ChoiceDisplayField(choices=AbstractParticipation.States.choices) duration = serializers.SerializerMethodField() event_title = serializers.CharField(source="shift.event.title") @@ -178,3 +179,16 @@ class Meta: "user", "participant", ] + + +class ParticipationSerializer(UserinfoParticipationSerializer): + """ + redact confidential fields + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + del self.fields["comment"] + participant_field = self.fields["participant"] + for field_name in ["email", "age", "date_of_birth"]: + del participant_field.fields[field_name] diff --git a/ephios/api/urls.py b/ephios/api/urls.py index 0a75f2934..a7dbce08d 100644 --- a/ephios/api/urls.py +++ b/ephios/api/urls.py @@ -13,8 +13,14 @@ ApplicationDetail, ApplicationUpdate, ) -from ephios.api.views.events import EventViewSet, ParticipationViewSet, ShiftViewSet +from ephios.api.views.events import ( + EventViewSet, + ParticipationViewSet, + ShiftViewSet, + UserinfoParticipationViewSet, +) from ephios.api.views.users import ( + OwnParticipationsViewSet, UserByMailView, UserParticipationView, UserProfileMeView, @@ -25,9 +31,13 @@ router = routers.DefaultRouter() router.register(r"events", EventViewSet) router.register(r"shifts", ShiftViewSet) -router.register(r"participations", ParticipationViewSet) +router.register(r"participations", ParticipationViewSet, basename="participations") +router.register( + r"participations-userinfo", UserinfoParticipationViewSet, basename="userinfo-participations" +) router.register(r"users/by_email", UserByMailView, basename="user-by-email") router.register(r"users", UserViewSet) +router.register(r"users/me/participations", OwnParticipationsViewSet, basename="participations-me") router.register( r"users/(?P[\d]+)/participations", UserParticipationView, basename="user-participations" ) diff --git a/ephios/api/views/events.py b/ephios/api/views/events.py index ce52a0bb2..5c7479f34 100644 --- a/ephios/api/views/events.py +++ b/ephios/api/views/events.py @@ -7,14 +7,20 @@ from rest_framework_guardian import filters as guardian_filters from ephios.api.filters import ( - AbstractParticipationFilterSet, EventFilterSet, + ParticipationFilterSet, ParticipationPermissionFilter, ShiftPermissionFilter, StartEndTimeFilterSet, + UserinfoParticipationPermissionFilter, +) +from ephios.api.permissions import ViewUserModelObjectPermissions +from ephios.api.serializers import ( + EventSerializer, + ParticipationSerializer, + ShiftSerializer, + UserinfoParticipationSerializer, ) -from ephios.api.permissions import ParticipationPermissions -from ephios.api.serializers import AbstractParticipationSerializer, EventSerializer, ShiftSerializer from ephios.core.models import AbstractParticipation, Event, Shift @@ -61,11 +67,11 @@ class EventViewSet(viewsets.ReadOnlyModelViewSet): ) -class ParticipationViewSet(viewsets.ReadOnlyModelViewSet): - serializer_class = AbstractParticipationSerializer - permission_classes = [ParticipationPermissions, IsAuthenticatedOrTokenHasScope] - filter_backends = [ParticipationPermissionFilter, DjangoFilterBackend] - filterset_class = AbstractParticipationFilterSet +class UserinfoParticipationViewSet(viewsets.ReadOnlyModelViewSet): + serializer_class = UserinfoParticipationSerializer + permission_classes = [ViewUserModelObjectPermissions, IsAuthenticatedOrTokenHasScope] + filter_backends = [UserinfoParticipationPermissionFilter, DjangoFilterBackend] + filterset_class = ParticipationFilterSet required_scopes = ["CONFIDENTIAL_READ"] queryset = ( @@ -73,3 +79,23 @@ class ParticipationViewSet(viewsets.ReadOnlyModelViewSet): .select_related("shift", "shift__event", "shift__event__type") .order_by("id") ) + + +class ParticipationViewSet(UserinfoParticipationViewSet): + """ + Remove information that would not be visible in the web version, + i.e. no email and date of birth of participants + """ + + serializer_class = ParticipationSerializer + filter_backends = [ParticipationPermissionFilter, DjangoFilterBackend] + permission_classes = [IsAuthenticatedOrTokenHasScope] + required_scopes = ["CONFIDENTIAL_READ"] + + queryset = ( + AbstractParticipation.objects.filter( + state__in=AbstractParticipation.States.REQUESTED_AND_CONFIRMED + ) + .select_related("shift", "shift__event", "shift__event__type") + .order_by("id") + ) diff --git a/ephios/api/views/users.py b/ephios/api/views/users.py index 3e808333f..8c63d06cd 100644 --- a/ephios/api/views/users.py +++ b/ephios/api/views/users.py @@ -7,9 +7,17 @@ from rest_framework.mixins import RetrieveModelMixin from rest_framework.viewsets import GenericViewSet -from ephios.api.filters import AbstractParticipationFilterSet, ParticipationPermissionFilter -from ephios.api.permissions import ParticipationPermissions, ViewPermissions -from ephios.api.serializers import AbstractParticipationSerializer, UserProfileSerializer +from ephios.api.filters import ( + ParticipationFilterSet, + ParticipationPermissionFilter, + UserinfoParticipationPermissionFilter, +) +from ephios.api.permissions import ViewObjectPermissions, ViewPermissions +from ephios.api.serializers import ( + ParticipationSerializer, + UserinfoParticipationSerializer, + UserProfileSerializer, +) from ephios.core.models import LocalParticipation, UserProfile @@ -25,10 +33,23 @@ def get_object(self): return self.request.user +class OwnParticipationsViewSet(viewsets.ReadOnlyModelViewSet): + serializer_class = UserinfoParticipationSerializer + permission_classes = [IsAuthenticatedOrTokenHasScope] + filter_backends = [UserinfoParticipationPermissionFilter, DjangoFilterBackend] + filterset_class = ParticipationFilterSet + required_scopes = ["ME_READ"] + + def get_queryset(self): + return LocalParticipation.objects.filter(user=self.request.user).select_related( + "shift", "shift__event", "shift__event__type" + ) + + class UserViewSet(viewsets.ReadOnlyModelViewSet): serializer_class = UserProfileSerializer queryset = UserProfile.objects.all() - permission_classes = [IsAuthenticatedOrTokenHasScope, ViewPermissions] + permission_classes = [IsAuthenticatedOrTokenHasScope, ViewObjectPermissions] required_scopes = ["CONFIDENTIAL_READ"] search_fields = ["display_name", "email"] @@ -49,10 +70,10 @@ class UserByMailView(RetrieveModelMixin, GenericViewSet): class UserParticipationView(viewsets.ReadOnlyModelViewSet): - serializer_class = AbstractParticipationSerializer - permission_classes = [ParticipationPermissions, IsAuthenticatedOrTokenHasScope] + serializer_class = ParticipationSerializer + permission_classes = [IsAuthenticatedOrTokenHasScope] filter_backends = [ParticipationPermissionFilter, DjangoFilterBackend] - filterset_class = AbstractParticipationFilterSet + filterset_class = ParticipationFilterSet required_scopes = ["CONFIDENTIAL_READ"] def get_queryset(self):