diff --git a/program/serializers.py b/program/serializers.py index 2fd809b7e7246b39e6568a6240e21763f79e0f4f..c7221805f14852c47df905bd871d3e7b881f7da1 100644 --- a/program/serializers.py +++ b/program/serializers.py @@ -23,7 +23,9 @@ from typing import List, TypedDict from drf_jsonschema_serializer import JSONSchemaField from rest_framework import serializers +from rest_framework.permissions import exceptions +from django.conf import settings from django.contrib.auth.models import User from django.core.exceptions import ObjectDoesNotExist from django.utils import timezone @@ -358,22 +360,58 @@ class HostSerializer(serializers.ModelSerializer): def update(self, instance, validated_data): """ Update and return an existing Host instance, given the validated data. + + A `PermissionDenied` exception will be raised if the user is not privileged or the owner of + the host and has the permissions to edit the fields. """ - instance.biography = validated_data.get("biography", instance.biography) - instance.email = validated_data.get("email", instance.email) - instance.image = validated_data.get("image_id", instance.image) - instance.is_active = validated_data.get("is_active", instance.is_active) - instance.name = validated_data.get("name", instance.name) + user = self.context.get("request").user + user_is_privileged = user.groups.filter(name=settings.PRIVILEGED_GROUP).exists() + user_is_owner = user in instance.owners.all() + user_edit_permissions = [ + permission.split("__")[-1] + for permission in user.get_all_permissions() + if permission.startswith("program.edit__host") + ] + + # Only privileged users and owners of a host with edit permissions are allowed to update it + # Being a privileged user overrides the ownership + if not (user_is_privileged or (user_is_owner and len(user_edit_permissions) > 0)): + raise exceptions.PermissionDenied(detail="You are not allowed to update this host.") + + # Only users with edit permissions are allowed to edit these fields + if ( + biography := validated_data.get("biography") + ) and "biography" not in user_edit_permissions: + raise exceptions.PermissionDenied( + detail="You are not allowed to edit the host’s biography." + ) + else: + instance.biography = biography - if links_data := validated_data.get("links"): - instance = delete_links(instance) + if (name := validated_data.get("name")) and "name" not in user_edit_permissions: + raise exceptions.PermissionDenied( + detail="You are not allowed to edit the host’s name." + ) + else: + instance.name = name - for link_data in links_data: - HostLink.objects.create(host=instance, **link_data) + # Only update these fields if the user is privileged, ignore otherwise + if user_is_privileged: + instance.email = validated_data.get("email", instance.email) + instance.image = validated_data.get("image_id", instance.image) + instance.is_active = validated_data.get("is_active", instance.is_active) + + # optional nested objects + if links_data := validated_data.get("links"): + instance = delete_links(instance) - if owners := validated_data.get("owners", []): - instance.owners.set(owners) + for link_data in links_data: + HostLink.objects.create(host=instance, **link_data) + + # optional many-to-many + if owners := validated_data.get("owners"): + instance.owners.set(owners) instance.updated_by = self.context.get("request").user.username @@ -534,54 +572,95 @@ class ShowSerializer(serializers.HyperlinkedModelSerializer): def update(self, instance, validated_data): """ Update and return an existing Show instance, given the validated data. + + A `PermissionDenied` exception will be raised if the user is not privileged or the owner of + a show and has the permissions to edit the fields. """ - instance.cba_series_id = validated_data.get("cba_series_id", instance.cba_series_id) - instance.default_playlist_id = validated_data.get( - "default_playlist_id", instance.default_playlist_id - ) - instance.description = validated_data.get("description", instance.description) - instance.email = validated_data.get("email", instance.email) - instance.funding_category = validated_data.get( - "funding_category_id", instance.funding_category - ) - instance.image = validated_data.get("image_id", instance.image) - instance.internal_note = validated_data.get("internal_note", instance.internal_note) - instance.is_active = validated_data.get("is_active", instance.is_active) - instance.is_public = validated_data.get("is_public", instance.is_public) - instance.logo = validated_data.get("logo_id", instance.logo) - instance.name = validated_data.get("name", instance.name) - instance.predecessor = validated_data.get("predecessor_id", instance.predecessor) - instance.short_description = validated_data.get( - "short_description", instance.short_description - ) - instance.slug = validated_data.get("slug", instance.slug) - instance.type = validated_data.get("type_id", instance.type) + user = self.context.get("request").user + user_is_privileged = user.groups.filter(name=settings.PRIVILEGED_GROUP).exists() + user_is_owner = instance in user.shows.all() + user_edit_permissions = [ + permission.split("__")[-1] + for permission in user.get_all_permissions() + if permission.startswith("program.edit__show") + ] + + # Only privileged users and owners of a show with edit permissions are allowed to update it + # Being a privileged user overrides the ownership + if not (user_is_privileged or (user_is_owner and len(user_edit_permissions) > 0)): + raise exceptions.PermissionDenied(detail="You are not allowed to update this show.") + + # Only users with edit permissions are allowed to update these fields + if ( + description := validated_data.get("description") + ) and "description" not in user_edit_permissions: + raise exceptions.PermissionDenied( + detail="You are not allowed to edit the show’s description." + ) + else: + instance.description = description - # optional many-to-many in PATCH requests - if (category := validated_data.get("category")) is not None: - instance.category.set(category) + if (name := validated_data.get("name")) and "name" not in user_edit_permissions: + raise exceptions.PermissionDenied( + detail="You are not allowed to edit the show’s name." + ) + else: + instance.name = name - if (hosts := validated_data.get("hosts")) is not None: - instance.hosts.set(hosts) + if ( + short_description := validated_data.get("short_description") + ) and "short_description" not in user_edit_permissions: + raise exceptions.PermissionDenied( + detail="You are not allowed to edit the show’s short description." + ) + else: + instance.short_description = short_description - if (language := validated_data.get("language")) is not None: - instance.language.set(language) + # Only update these fields if the user is privileged, ignore otherwise + if user_is_privileged: + instance.cba_series_id = validated_data.get("cba_series_id", instance.cba_series_id) + instance.default_playlist_id = validated_data.get( + "default_playlist_id", instance.default_playlist_id + ) + instance.email = validated_data.get("email", instance.email) + instance.funding_category = validated_data.get( + "funding_category_id", instance.funding_category + ) + instance.image = validated_data.get("image_id", instance.image) + instance.internal_note = validated_data.get("interna_note", instance.internal_note) + instance.is_active = validated_data.get("is_active", instance.is_active) + instance.is_public = validated_data.get("is_public", instance.is_public) + instance.logo = validated_data.get("logo", instance.logo) + instance.predecessor = validated_data.get("predecessor", instance.predecessor) + instance.slug = validated_data.get("slug", instance.slug) + instance.type = validated_data.get("type", instance.type) - if (music_focus := validated_data.get("music_focus")) is not None: - instance.music_focus.set(music_focus) + # optional many-to-many + if category := validated_data.get("category"): + instance.category.set(category) - if (owners := validated_data.get("owners")) is not None: - instance.owners.set(owners) + if hosts := validated_data.get("hosts"): + instance.hosts.set(hosts) - if (topic := validated_data.get("topic")) is not None: - instance.topic.set(topic) + if language := validated_data.get("language"): + instance.language.set(language) - if links_data := validated_data.get("links"): - instance = delete_links(instance) + if music_focus := validated_data.get("music_focus"): + instance.music_focus.set(music_focus) - for link_data in links_data: - ShowLink.objects.create(show=instance, **link_data) + if owners := validated_data.get("owners"): + instance.owners.set(owners) + + if topic := validated_data.get("topic"): + instance.topic.set(topic) + + # optional nested objects + if links_data := validated_data.get("links"): + instance = delete_links(instance) + + for link_data in links_data: + ShowLink.objects.create(show=instance, **link_data) instance.updated_by = self.context.get("request").user.username @@ -858,7 +937,11 @@ class NoteSerializer(serializers.ModelSerializer): ) + read_only_fields def create(self, validated_data): - """Create and return a new Note instance, given the validated data.""" + """Create and return a new Note instance, given the validated data. + + A `PermissionDenied` exception will be raised if the user is not privileged or the owner of + the show. + """ links_data = validated_data.pop("links", []) @@ -871,9 +954,19 @@ class NoteSerializer(serializers.ModelSerializer): validated_data["timeslot"] = validated_data.pop("timeslot_id", timeslot_pk) show = validated_data["timeslot"].schedule.show + user = self.context.get("request").user + user_is_privileged = user.groups.filter(name=settings.PRIVILEGED_GROUP).exists() + user_is_owner = user in show.owners.all() + + # Only privileged users and owners of a show are allowed to create a note + # Being a privileged user overrides the ownership + if not (user_is_privileged or user_is_owner): + raise exceptions.PermissionDenied( + detail="You are not allowed to create a note for this show." + ) # we derive `contributors`, `language` and `topic` from the Show's values if not set - contributors = validated_data.pop("contributors", show.owners.values_list("id", flat=True)) + contributors = validated_data.pop("contributors", show.hosts.values_list("id", flat=True)) language = validated_data.pop("language", show.language.values_list("id", flat=True)) topic = validated_data.pop("topic", show.topic.values_list("id", flat=True)) @@ -903,7 +996,21 @@ class NoteSerializer(serializers.ModelSerializer): return note def update(self, instance, validated_data): - """Update and return an existing Note instance, given the validated data.""" + """Update and return an existing Note instance, given the validated data. + + A `PermissionDenied` exception will be raised if the user is not privileged or the owner of + a show. + """ + + show = validated_data["timeslot"].schedule.show + user = self.context.get("request").user + user_is_privileged = user.groups.filter(name=settings.PRIVILEGED_GROUP).exists() + user_is_owner = user in show.owners + + # Only privileged users and owners of a show are allowed to update a note + # Being a privileged user overrides the ownership + if not (user_is_privileged or user_is_owner): + raise exceptions.PermissionDenied(detail="You are not allowed to update this note.") instance.cba_id = validated_data.get("cba_id", instance.cba_id) instance.content = validated_data.get("content", instance.content) @@ -914,14 +1021,15 @@ class NoteSerializer(serializers.ModelSerializer): instance.tags = validated_data.get("tags", instance.tags) instance.title = validated_data.get("title", instance.title) - # optional many-to-many in PATCH requests - if (contributors := validated_data.get("contributors")) is not None: + # optional many-to-many + if contributors := validated_data.get("contributors"): instance.contributors.set(contributors) - if (language := validated_data.get("language")) is not None: + if language := validated_data.get("language"): instance.language.set(language) - if (topic := validated_data.get("topic")) is not None: + # Only update this field if the user is privileged, ignore otherwise + if topic := validated_data.get("topic") and user_is_privileged: instance.topic.set(topic) if cba_id := validated_data.get("cba_id"):