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):
|
||||
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
|
||||
|
|
|
@ -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
|
|
@ -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
|
Loading…
Reference in a new issue