fix rights on albums and next/previous pictures

This commit is contained in:
thomas girod 2024-08-08 13:15:19 +02:00
parent d3b203a4a1
commit 00dc03a235
5 changed files with 110 additions and 112 deletions

View File

@ -41,17 +41,12 @@ class PicturesController(ControllerBase):
cf. https://ae.utbm.fr/user/32663/pictures/) cf. https://ae.utbm.fr/user/32663/pictures/)
""" """
user: User = self.context.request.user user: User = self.context.request.user
if not user.was_subscribed and filters.users_identified != {user.id}: return (
# User can view any moderated picture if he/she is subscribed.
# If not, he/she can view only the one he/she has been identified on
raise PermissionDenied
pictures = list(
filters.filter(Picture.objects.viewable_by(user)) filters.filter(Picture.objects.viewable_by(user))
.distinct() .distinct()
.order_by("-parent__date", "date") .order_by("-parent__date", "date")
.annotate(album=F("parent__name")) .annotate(album=F("parent__name"))
) )
return pictures
@api_controller("/sas/relation", tags="User identification on SAS pictures") @api_controller("/sas/relation", tags="User identification on SAS pictures")

View File

@ -20,6 +20,7 @@ from io import BytesIO
from django.conf import settings from django.conf import settings
from django.core.cache import cache from django.core.cache import cache
from django.db import models from django.db import models
from django.db.models import Exists, OuterRef
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -29,6 +30,36 @@ from core.models import SithFile, User
from core.utils import exif_auto_rotate, resize_image from core.utils import exif_auto_rotate, resize_image
class SasFile(SithFile):
"""Proxy model for any file in the SAS.
May be used to have logic that should be shared by both
[Picture][sas.models.Picture] and [Album][sas.models.Album].
"""
class Meta:
proxy = True
def can_be_viewed_by(self, user):
if user.is_anonymous:
return False
cache_key = (
f"sas:{self._meta.model_name}_viewable_by_{user.id}_in_{self.parent_id}"
)
viewable: list[int] | None = cache.get(cache_key)
if viewable is None:
viewable = list(
self.__class__.objects.filter(parent_id=self.parent_id)
.viewable_by(user)
.values_list("pk", flat=True)
)
cache.set(cache_key, viewable, timeout=10)
return self.id in viewable
def can_be_edited_by(self, user):
return user.is_root or user.is_in_group(pk=settings.SITH_GROUP_SAS_ADMIN_ID)
class PictureQuerySet(models.QuerySet): class PictureQuerySet(models.QuerySet):
def viewable_by(self, user: User) -> PictureQuerySet: def viewable_by(self, user: User) -> PictureQuerySet:
"""Filter the pictures that this user can view. """Filter the pictures that this user can view.
@ -45,17 +76,10 @@ class PictureQuerySet(models.QuerySet):
class SASPictureManager(models.Manager): class SASPictureManager(models.Manager):
def get_queryset(self): def get_queryset(self):
return PictureQuerySet(self.model, using=self._db).filter( return super().get_queryset().filter(is_in_sas=True, is_folder=False)
is_in_sas=True, is_folder=False
)
class SASAlbumManager(models.Manager): class Picture(SasFile):
def get_queryset(self):
return super().get_queryset().filter(is_in_sas=True, is_folder=True)
class Picture(SithFile):
class Meta: class Meta:
proxy = True proxy = True
@ -68,29 +92,6 @@ class Picture(SithFile):
(w, h) = im.size (w, h) = im.size
return (w / h) < 1 return (w / h) < 1
def can_be_edited_by(self, user):
perm = cache.get("%d_can_edit_pictures" % (user.id), None)
if perm is None:
perm = user.is_root or user.is_in_group(pk=settings.SITH_GROUP_SAS_ADMIN_ID)
cache.set("%d_can_edit_pictures" % (user.id), perm, timeout=4)
return perm
def can_be_viewed_by(self, user: User) -> bool:
if user.is_anonymous:
return False
cache_key = f"sas:pictures_viewable_by_{user.id}_in_{self.parent_id}"
viewable: list[int] | None = cache.get(cache_key)
if viewable is None:
viewable = list(
Picture.objects.filter(parent_id=self.parent_id)
.viewable_by(user)
.values_list("pk", flat=True)
)
cache.set(cache_key, viewable, timeout=10)
return self.id in viewable
def get_download_url(self): def get_download_url(self):
return reverse("sas:download", kwargs={"picture_id": self.id}) return reverse("sas:download", kwargs={"picture_id": self.id})
@ -142,48 +143,53 @@ class Picture(SithFile):
def get_next(self): def get_next(self):
if self.is_moderated: if self.is_moderated:
return ( pictures_qs = self.parent.children.filter(
self.parent.children.filter( is_moderated=True,
is_moderated=True, asked_for_removal=False,
asked_for_removal=False, is_folder=False,
is_folder=False, id__gt=self.id,
id__gt=self.id,
)
.order_by("id")
.first()
) )
else: else:
return ( pictures_qs = Picture.objects.filter(id__gt=self.id, is_moderated=False)
Picture.objects.filter(id__gt=self.id, is_moderated=False) return pictures_qs.order_by("id").first()
.order_by("id")
.first()
)
def get_previous(self): def get_previous(self):
if self.is_moderated: if self.is_moderated:
return ( pictures_qs = self.parent.children.filter(
self.parent.children.filter( is_moderated=True,
is_moderated=True, asked_for_removal=False,
asked_for_removal=False, is_folder=False,
is_folder=False, id__lt=self.id,
id__lt=self.id,
)
.order_by("id")
.last()
) )
else: else:
return ( pictures_qs = Picture.objects.filter(id__lt=self.id, is_moderated=False)
Picture.objects.filter(id__lt=self.id, is_moderated=False) return pictures_qs.order_by("-id").first()
.order_by("-id")
.first()
)
class Album(SithFile): class AlbumQuerySet(models.QuerySet):
def viewable_by(self, user: User) -> PictureQuerySet:
"""Filter the albums that this user can view.
Warnings:
Calling this queryset method may add several additional requests.
"""
if user.is_root or user.is_in_group(pk=settings.SITH_GROUP_SAS_ADMIN_ID):
return self.all()
return self.filter(
Exists(Picture.objects.filter(parent_id=OuterRef("pk")).viewable_by(user))
)
class SASAlbumManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(is_in_sas=True, is_folder=True)
class Album(SasFile):
class Meta: class Meta:
proxy = True proxy = True
objects = SASAlbumManager() objects = SASAlbumManager.from_queryset(AlbumQuerySet)()
@property @property
def children_pictures(self): def children_pictures(self):
@ -193,15 +199,6 @@ class Album(SithFile):
def children_albums(self): def children_albums(self):
return Album.objects.filter(parent=self) return Album.objects.filter(parent=self)
def can_be_edited_by(self, user):
return user.is_in_group(pk=settings.SITH_GROUP_SAS_ADMIN_ID)
def can_be_viewed_by(self, user):
# file = SithFile.objects.filter(id=self.id).first()
return self.can_be_edited_by(user) or (
self.is_in_sas and self.is_moderated and user.was_subscribed
) # or user.can_view(file)
def get_absolute_url(self): def get_absolute_url(self):
return reverse("sas:album", kwargs={"album_id": self.id}) return reverse("sas:album", kwargs={"album_id": self.id})

View File

@ -4,22 +4,6 @@
<link rel="stylesheet" href="{{ scss('sas/picture.scss') }}"> <link rel="stylesheet" href="{{ scss('sas/picture.scss') }}">
{%- endblock -%} {%- endblock -%}
{% block head %}
{{ super() }}
{% if picture.get_previous() %}
<link
rel="preload"
as="image"
href="{{ url("sas:download_compressed", picture_id=picture.get_previous().id) }}"
>
{% endif %}
{% if picture.get_next() %}
<link rel="preload" as="image" href="{{ url("sas:download_compressed", picture_id=picture.get_next().id) }}">
{% endif %}
{% endblock %}
{% block title %} {% block title %}
{% trans %}SAS{% endtrans %} {% trans %}SAS{% endtrans %}
{% endblock %} {% endblock %}
@ -124,16 +108,16 @@
<div class="subsection"> <div class="subsection">
<div class="navigation"> <div class="navigation">
<div id="prev"> <div id="prev">
{% if picture.get_previous() %} {% if previous_pict %}
<a href="{{ url( 'sas:picture', picture_id=picture.get_previous().id) }}#pict"> <a href="{{ url( 'sas:picture', picture_id=previous_pict.id) }}#pict">
<div style="background-image: url('{{ picture.get_previous().as_picture.get_download_thumb_url() }}');"></div> <div style="background-image: url('{{ previous_pict.get_download_thumb_url() }}');"></div>
</a> </a>
{% endif %} {% endif %}
</div> </div>
<div id="next"> <div id="next">
{% if picture.get_next() %} {% if next_pict %}
<a href="{{ url( 'sas:picture', picture_id=picture.get_next().id) }}#pict"> <a href="{{ url( 'sas:picture', picture_id=next_pict.id) }}#pict">
<div style="background-image: url('{{ picture.get_next().as_picture.get_download_thumb_url() }}');"></div> <div style="background-image: url('{{ next_pict.get_download_thumb_url() }}');"></div>
</a> </a>
{% endif %} {% endif %}
</div> </div>

View File

@ -89,19 +89,33 @@ class TestPictureSearch(TestSas):
) )
assert [i["id"] for i in res.json()["results"]] == expected assert [i["id"] for i in res.json()["results"]] == expected
# trying to access the pictures of someone else # trying to access the pictures of someone else mixed with owned pictures
res = self.client.get( # should return only owned pictures
reverse("api:pictures") + f"?users_identified={self.user_b.id}"
)
assert res.status_code == 403
# trying to access the pictures of someone else shouldn't success,
# even if mixed with owned pictures
res = self.client.get( res = self.client.get(
reverse("api:pictures") reverse("api:pictures")
+ f"?users_identified={self.user_a.id}&users_identified={self.user_b.id}" + f"?users_identified={self.user_a.id}&users_identified={self.user_b.id}"
) )
assert res.status_code == 403 assert res.status_code == 200
assert [i["id"] for i in res.json()["results"]] == expected
# trying to fetch everything should be the same
# as fetching its own pictures for a non-subscriber
res = self.client.get(reverse("api:pictures"))
assert res.status_code == 200
assert [i["id"] for i in res.json()["results"]] == expected
# trying to access the pictures of someone else should return only
# the ones where the non-subscribed user is identified too
res = self.client.get(
reverse("api:pictures") + f"?users_identified={self.user_b.id}"
)
assert res.status_code == 200
expected = list(
self.user_b.pictures.intersection(self.user_a.pictures.all())
.order_by("-picture__parent__date", "picture__date")
.values_list("picture_id", flat=True)
)
assert [i["id"] for i in res.json()["results"]] == expected
class TestPictureRelation(TestSas): class TestPictureRelation(TestSas):

View File

@ -119,10 +119,11 @@ class SASMainView(FormView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
kwargs = super().get_context_data(**kwargs) kwargs = super().get_context_data(**kwargs)
kwargs["categories"] = Album.objects.filter( albums_qs = Album.objects.viewable_by(self.request.user)
parent__id=settings.SITH_SAS_ROOT_DIR_ID kwargs["categories"] = list(
).order_by("id") albums_qs.filter(parent_id=settings.SITH_SAS_ROOT_DIR_ID).order_by("id")
kwargs["latest"] = Album.objects.filter(is_moderated=True).order_by("-id")[:5] )
kwargs["latest"] = list(albums_qs.order_by("-id")[:5])
return kwargs return kwargs
@ -180,7 +181,14 @@ class PictureView(CanViewMixin, DetailView, FormMixin):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
kwargs = super().get_context_data(**kwargs) kwargs = super().get_context_data(**kwargs)
pictures_qs = Picture.objects.viewable_by(self.request.user)
kwargs["form"] = self.form kwargs["form"] = self.form
kwargs["next_pict"] = (
pictures_qs.filter(id__gt=self.object.id).order_by("id").first()
)
kwargs["previous_pict"] = (
pictures_qs.filter(id__lt=self.object.id).order_by("-id").first()
)
return kwargs return kwargs
def get_success_url(self): def get_success_url(self):