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:
parent
0999d2f16e
commit
2bbb39860e
3 changed files with 52 additions and 39 deletions
|
@ -47,6 +47,7 @@ class Category(models.Model):
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.title} ({self.owner})"
|
return f"{self.title} ({self.owner})"
|
||||||
|
|
||||||
|
|
||||||
class CategoryPermission(models.Model):
|
class CategoryPermission(models.Model):
|
||||||
category = models.ForeignKey(Category, on_delete=models.CASCADE)
|
category = models.ForeignKey(Category, on_delete=models.CASCADE)
|
||||||
user = models.ForeignKey(get_user_model(), 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)
|
file = models.FileField(upload_to=upload_to)
|
||||||
width = models.IntegerField(blank=True, null=True)
|
width = models.IntegerField(blank=True, null=True)
|
||||||
height = 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):
|
def __str__(self):
|
||||||
return self.title
|
return self.title
|
||||||
|
@ -204,6 +207,18 @@ class OriginalMedia(PolymorphicModel):
|
||||||
def thumbnail(self) -> str:
|
def thumbnail(self) -> str:
|
||||||
return self.resolutions.all().order_by("width").first()
|
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):
|
class OriginalImage(OriginalMedia):
|
||||||
def media_type(self) -> str:
|
def media_type(self) -> str:
|
||||||
|
@ -247,7 +262,9 @@ class Scene(models.Model):
|
||||||
default_x = models.FloatField(default=0.0)
|
default_x = models.FloatField(default=0.0)
|
||||||
default_y = models.FloatField(default=0.0)
|
default_y = models.FloatField(default=0.0)
|
||||||
default_z = 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)
|
public = models.BooleanField(default=True)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -255,11 +272,14 @@ class Scene(models.Model):
|
||||||
return self.base_content.resolutions.order_by("width").first()
|
return self.base_content.resolutions.order_by("width").first()
|
||||||
|
|
||||||
def user_has_permission(self, user):
|
def user_has_permission(self, user):
|
||||||
return (
|
return user.is_authenticated and (
|
||||||
user.is_superuser
|
user.is_superuser
|
||||||
or user.is_staff
|
or user.is_staff
|
||||||
or (self.category and self.category.user_has_permission(user))
|
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):
|
def __str__(self):
|
||||||
return self.title
|
return self.title
|
||||||
|
|
|
@ -9,7 +9,7 @@ from .models import (
|
||||||
TeleportElement,
|
TeleportElement,
|
||||||
TextElement,
|
TextElement,
|
||||||
ImageElement,
|
ImageElement,
|
||||||
Category
|
Category,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -31,7 +31,7 @@ class TeleportElementSerializer(serializers.ModelSerializer):
|
||||||
"destination_y",
|
"destination_y",
|
||||||
"destination_z",
|
"destination_z",
|
||||||
"thetaStart",
|
"thetaStart",
|
||||||
"thetaLength"
|
"thetaLength",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -76,6 +76,13 @@ class OriginalMediaSerializer(serializers.ModelSerializer):
|
||||||
model = OriginalMedia
|
model = OriginalMedia
|
||||||
fields = ["id", "title", "media_type", "width", "height", "resolutions"]
|
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):
|
class SceneSerializer(serializers.ModelSerializer):
|
||||||
base_content = OriginalMediaSerializer()
|
base_content = OriginalMediaSerializer()
|
||||||
|
@ -85,6 +92,13 @@ class SceneSerializer(serializers.ModelSerializer):
|
||||||
model = Scene
|
model = Scene
|
||||||
fields = ["id", "title", "description", "base_content", "elements", "category"]
|
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):
|
class CategorySerializer(serializers.ModelSerializer):
|
||||||
media = OriginalMediaSerializer(many=True, read_only=True)
|
media = OriginalMediaSerializer(many=True, read_only=True)
|
||||||
|
@ -93,3 +107,9 @@ class CategorySerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Category
|
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
|
|
@ -6,7 +6,11 @@ from django.core.exceptions import PermissionDenied
|
||||||
from rest_framework import viewsets
|
from rest_framework import viewsets
|
||||||
|
|
||||||
from .models import Scene, Element, Category
|
from .models import Scene, Element, Category
|
||||||
from .serializers import SceneSerializer, ElementPolymorphicSerializer, CategorySerializer
|
from .serializers import (
|
||||||
|
SceneSerializer,
|
||||||
|
ElementPolymorphicSerializer,
|
||||||
|
CategorySerializer,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class UserPermissionMixin:
|
class UserPermissionMixin:
|
||||||
|
@ -33,24 +37,10 @@ class SceneAPIViewSet(viewsets.GenericViewSet, viewsets.mixins.RetrieveModelMixi
|
||||||
serializer_class = SceneSerializer
|
serializer_class = SceneSerializer
|
||||||
queryset = Scene.objects.all()
|
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):
|
class ElementAPIViewSet(viewsets.ModelViewSet):
|
||||||
serializer_class = ElementPolymorphicSerializer
|
serializer_class = ElementPolymorphicSerializer
|
||||||
|
queryset = Element.objects.all()
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
class SceneView(PublicPermissionMixin, DetailView):
|
class SceneView(PublicPermissionMixin, DetailView):
|
||||||
|
@ -64,14 +54,6 @@ class SceneEditView(UserPermissionMixin, DetailView):
|
||||||
context_object_name = "scene"
|
context_object_name = "scene"
|
||||||
template_name = "tours/scene_edit.html"
|
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")
|
@method_decorator(xframe_options_exempt, name="dispatch")
|
||||||
class SceneEmbedView(SceneView):
|
class SceneEmbedView(SceneView):
|
||||||
|
@ -81,12 +63,3 @@ class SceneEmbedView(SceneView):
|
||||||
class CategoryAPIViewSet(viewsets.ModelViewSet):
|
class CategoryAPIViewSet(viewsets.ModelViewSet):
|
||||||
serializer_class = CategorySerializer
|
serializer_class = CategorySerializer
|
||||||
queryset = Category.objects.all()
|
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
|
|
Loading…
Reference in a new issue