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.
This commit is contained in:
Kumi 2024-03-16 07:26:49 +01:00
parent 0999d2f16e
commit 2bbb39860e
Signed by: kumi
GPG key ID: ECBCC9082395383F
3 changed files with 52 additions and 39 deletions

View file

@ -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

View file

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

View file

@ -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