From 2bbb39860e98e7d7d9e432c1d72cd4e0376300c1 Mon Sep 17 00:00:00 2001 From: Kumi Date: Sat, 16 Mar 2024 07:26:49 +0100 Subject: [PATCH] feat: refine access control for media and scenes Introduced new permission checks in the models, serializers, and views across the application to enhance user access control and data privacy. This update ensures that sensitive media and scene information are only accessible to users with appropriate permissions, including public access checks and enhanced controls for superusers and staff. These changes streamline permission logic, making it more robust and maintainable. Serialization now conditionally includes media and scenes based on user permissions, significantly improving data security and user experience by preventing unauthorized access. This enhancement aligns with our ongoing efforts to bolster security measures and ensure compliance with data protection regulations. --- quackscape/tours/models.py | 26 +++++++++++++++++++--- quackscape/tours/serializers.py | 26 +++++++++++++++++++--- quackscape/tours/views.py | 39 +++++---------------------------- 3 files changed, 52 insertions(+), 39 deletions(-) diff --git a/quackscape/tours/models.py b/quackscape/tours/models.py index 8ceb5b5..21c6dc2 100644 --- a/quackscape/tours/models.py +++ b/quackscape/tours/models.py @@ -47,6 +47,7 @@ class Category(models.Model): def __str__(self): return f"{self.title} ({self.owner})" + class CategoryPermission(models.Model): category = models.ForeignKey(Category, on_delete=models.CASCADE) user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE) @@ -191,7 +192,9 @@ class OriginalMedia(PolymorphicModel): file = models.FileField(upload_to=upload_to) width = models.IntegerField(blank=True, null=True) height = models.IntegerField(blank=True, null=True) - category = models.ForeignKey(Category, related_name="media", on_delete=models.SET_NULL, null=True) + category = models.ForeignKey( + Category, related_name="media", on_delete=models.SET_NULL, null=True + ) def __str__(self): return self.title @@ -204,6 +207,18 @@ class OriginalMedia(PolymorphicModel): def thumbnail(self) -> str: return self.resolutions.all().order_by("width").first() + def user_has_permission(self, user): + return user.is_authenticated and ( + user.is_superuser + or user.is_staff + or (self.category and self.category.user_has_permission(user)) + ) + + def user_has_view_permission(self, user): + return self.user_has_permission(user) or any( + [scene.user_has_view_permission(user) for scene in self.scenes.all()] + ) + class OriginalImage(OriginalMedia): def media_type(self) -> str: @@ -247,7 +262,9 @@ class Scene(models.Model): default_x = models.FloatField(default=0.0) default_y = models.FloatField(default=0.0) default_z = models.FloatField(default=0.0) - category = models.ForeignKey(Category, related_name="scenes", on_delete=models.SET_NULL, null=True) + category = models.ForeignKey( + Category, related_name="scenes", on_delete=models.SET_NULL, null=True + ) public = models.BooleanField(default=True) @property @@ -255,11 +272,14 @@ class Scene(models.Model): return self.base_content.resolutions.order_by("width").first() def user_has_permission(self, user): - return ( + return user.is_authenticated and ( user.is_superuser or user.is_staff or (self.category and self.category.user_has_permission(user)) ) + def user_has_view_permission(self, user): + return self.public or (user.is_authenticated and self.user_has_permission(user)) + def __str__(self): return self.title diff --git a/quackscape/tours/serializers.py b/quackscape/tours/serializers.py index e6dae2a..81ae034 100644 --- a/quackscape/tours/serializers.py +++ b/quackscape/tours/serializers.py @@ -9,7 +9,7 @@ from .models import ( TeleportElement, TextElement, ImageElement, - Category + Category, ) @@ -31,7 +31,7 @@ class TeleportElementSerializer(serializers.ModelSerializer): "destination_y", "destination_z", "thetaStart", - "thetaLength" + "thetaLength", ] @@ -76,6 +76,13 @@ class OriginalMediaSerializer(serializers.ModelSerializer): model = OriginalMedia fields = ["id", "title", "media_type", "width", "height", "resolutions"] + def to_representation(self, instance): + request = self.context.get("request") + if request and instance.user_has_view_permission(request.user): + return super().to_representation(instance) + else: + return None + class SceneSerializer(serializers.ModelSerializer): base_content = OriginalMediaSerializer() @@ -85,6 +92,13 @@ class SceneSerializer(serializers.ModelSerializer): model = Scene fields = ["id", "title", "description", "base_content", "elements", "category"] + def to_representation(self, instance): + request = self.context.get("request") + if request and instance.user_has_view_permission(request.user): + return super().to_representation(instance) + else: + return None + class CategorySerializer(serializers.ModelSerializer): media = OriginalMediaSerializer(many=True, read_only=True) @@ -92,4 +106,10 @@ class CategorySerializer(serializers.ModelSerializer): class Meta: model = Category - fields = ["id", "title", "media", "scenes"] \ No newline at end of file + fields = ["id", "title", "media", "scenes"] + + def to_representation(self, instance): + ret = super().to_representation(instance) + ret['media'] = [m for m in ret['media'] if m is not None] + ret['scenes'] = [s for s in ret['scenes'] if s is not None] + return ret \ No newline at end of file diff --git a/quackscape/tours/views.py b/quackscape/tours/views.py index c0221f8..3c4823b 100644 --- a/quackscape/tours/views.py +++ b/quackscape/tours/views.py @@ -6,7 +6,11 @@ from django.core.exceptions import PermissionDenied from rest_framework import viewsets from .models import Scene, Element, Category -from .serializers import SceneSerializer, ElementPolymorphicSerializer, CategorySerializer +from .serializers import ( + SceneSerializer, + ElementPolymorphicSerializer, + CategorySerializer, +) class UserPermissionMixin: @@ -33,24 +37,10 @@ class SceneAPIViewSet(viewsets.GenericViewSet, viewsets.mixins.RetrieveModelMixi serializer_class = SceneSerializer queryset = Scene.objects.all() - def get_object(self): - obj = super().get_object() - - if not (obj.public or obj.user_has_permission(self.request.user)): - raise PermissionDenied() - - return obj - class ElementAPIViewSet(viewsets.ModelViewSet): serializer_class = ElementPolymorphicSerializer - - def get_queryset(self): - scene = Scene.objects.get(id=self.kwargs["scene"]) - if not scene.user_has_permission(self.request.user): - raise PermissionDenied - - return scene.elements + queryset = Element.objects.all() class SceneView(PublicPermissionMixin, DetailView): @@ -64,14 +54,6 @@ class SceneEditView(UserPermissionMixin, DetailView): context_object_name = "scene" template_name = "tours/scene_edit.html" - def get_object(self, queryset=None): - obj = super().get_object(queryset) - - if not obj.user_has_permission(self.request.user): - raise PermissionDenied() - - return obj - @method_decorator(xframe_options_exempt, name="dispatch") class SceneEmbedView(SceneView): @@ -81,12 +63,3 @@ class SceneEmbedView(SceneView): class CategoryAPIViewSet(viewsets.ModelViewSet): serializer_class = CategorySerializer queryset = Category.objects.all() - - def get_queryset(self): - categories = Category.objects.all() - - for category in categories: - if not category.user_has_permission(self.request.user): - categories = categories.exclude(id=category.id) - - return categories \ No newline at end of file