Skip to content

Commit

Permalink
fix endpoint permissions
Browse files Browse the repository at this point in the history
  • Loading branch information
felixrindt committed Nov 10, 2024
1 parent aaa65d3 commit ca72d81
Show file tree
Hide file tree
Showing 6 changed files with 115 additions and 39 deletions.
27 changes: 24 additions & 3 deletions ephios/api/filters.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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):
Expand All @@ -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()
Expand Down
18 changes: 1 addition & 17 deletions ephios/api/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,27 +24,11 @@ class ViewObjectPermissions(ViewPermissionsMixin, DjangoObjectPermissions):
pass


class UserModelObjectPermissions(ViewObjectPermissions):
class ViewUserModelObjectPermissions(ViewObjectPermissions):
"""
Like the default DjangoObjectPermissions, but force the
permission model to be UserProfile.
"""

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
18 changes: 16 additions & 2 deletions ephios/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)

Expand All @@ -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")
Expand Down Expand Up @@ -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]
14 changes: 12 additions & 2 deletions ephios/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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<user>[\d]+)/participations", UserParticipationView, basename="user-participations"
)
Expand Down
42 changes: 34 additions & 8 deletions ephios/api/views/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -61,15 +67,35 @@ 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 = (
AbstractParticipation.objects.all()
.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")
)
35 changes: 28 additions & 7 deletions ephios/api/views/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand All @@ -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"]

Expand All @@ -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):
Expand Down

0 comments on commit ca72d81

Please sign in to comment.