Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Allow users to upload media to shared albums #1256

Draft
wants to merge 1 commit into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 32 additions & 11 deletions api/permissions.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
from typing import Optional

from django.conf import settings
from constance import config as site_config
from rest_framework import permissions

from api.models import User
from api.models import User, Photo


class IsAdminOrSelf(permissions.BasePermission):
Expand Down Expand Up @@ -54,23 +57,41 @@ def has_object_permission(self, request, view, obj):
return obj == request.user


class IsPhotoOrAlbumSharedTo(permissions.BasePermission):
def user_has_photo_permission(user: Optional[User], photo: Photo) -> bool:
"""
Custom permission to only allow owners of an object to edit it.
Checks if user has photo permissions.
"""
# Everybody allowed to see
if photo.public:
return True

def has_object_permission(self, request, view, obj):
if obj.public:
return True
# No user and not public
if user is None:
return False

if obj.owner == request.user or request.user in obj.shared_to.all():
# Allowed for owner
if user == photo.owner or user in photo.shared_to.all():
return True

for album in photo.albumuser_set.only("shared_to"):
# Photo not owned by album owner
# TODO: site_config?
if settings.SHARED_ALBUM_ALL_ADD and user == album.owner:
return True
# Album shared with user
if user in album.shared_to.all():
return True

for album in obj.albumuser_set.only("shared_to"):
if request.user in album.shared_to.all():
return True
return False

return False

class IsPhotoOrAlbumSharedTo(permissions.BasePermission):
"""
Custom permission to only allow owners of an object to edit it.
"""

def has_object_permission(self, request, view, obj):
return user_has_photo_permission(request.user, obj)


class IsRegistrationAllowed(permissions.BasePermission):
Expand Down
17 changes: 15 additions & 2 deletions api/views/albums.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import re

from django.conf import settings
from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
from django.db.models import Count, F, Prefetch, Q
from drf_spectacular.utils import OpenApiParameter, OpenApiTypes, extend_schema
Expand Down Expand Up @@ -281,14 +282,26 @@ class AlbumUserListViewSet(ListViewSet):
def get_queryset(self):
if self.request.user.is_anonymous:
return AlbumUser.objects.none()

if settings.SHARED_ALBUM_ALL_ADD:
query = AlbumUser.objects.filter(
Q(owner=self.request.user) | Q(shared_to__id__exact=self.request.user.id)
)
else:
query = AlbumUser.objects.filter(owner=self.request.user)

return (
AlbumUser.objects.filter(owner=self.request.user)
query
.annotate(
photo_count=Count(
"photos", filter=Q(photos__hidden=False), distinct=True
)
)
.filter(Q(photo_count__gt=0) & Q(owner=self.request.user))
.filter(
Q(photo_count__gt=0),
# TODO: Filtering again needed?
Q(owner=self.request.user) | Q(shared_to__id__exact=self.request.user.id)
)
.order_by("title")
)

Expand Down
79 changes: 34 additions & 45 deletions api/views/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
from api.directory_watcher import scan_photos
from api.ml_models import do_all_models_exist, download_models
from api.models import AlbumUser, LongRunningJob, Photo, User
from api.permissions import user_has_photo_permission
from api.schemas.site_settings import site_settings_schema
from api.serializers.album_user import AlbumUserEditSerializer, AlbumUserListSerializer
from api.util import logger
Expand Down Expand Up @@ -70,7 +71,15 @@ def list(self, *args, **kwargs):
def get_queryset(self):
if self.request.user.is_anonymous:
return AlbumUser.objects.none()
return AlbumUser.objects.filter(owner=self.request.user).order_by("title")

if settings.SHARED_ALBUM_ALL_ADD:
query = AlbumUser.objects.filter(
Q(owner=self.request.user) | Q(shared_to__id__exact=self.request.user.id)
)
else:
query = AlbumUser.objects.filter(owner=self.request.user)

return query.order_by("title")


# API Views
Expand Down Expand Up @@ -305,17 +314,18 @@ def _get_protected_media_url(self, path, fname):
def get(self, request, path, fname, format=None):
jwt = request.COOKIES.get("jwt")
image_hash = fname.split(".")[0].split("_")[0]
ok_response = HttpResponse()
ok_response["Content-Type"] = "image/jpeg"
ok_response["X-Accel-Redirect"] = self._get_protected_media_url(path, fname)

try:
photo = Photo.objects.get(image_hash=image_hash)
except Photo.DoesNotExist:
return HttpResponse(status=404)

# grant access if the requested photo is public
if photo.public:
response = HttpResponse()
response["Content-Type"] = "image/jpeg"
response["X-Accel-Redirect"] = self._get_protected_media_url(path, fname)
return response
if user_has_photo_permission(None, photo):
return ok_response

# forbid access if trouble with jwt
if jwt is not None:
Expand All @@ -330,20 +340,10 @@ def get(self, request, path, fname, format=None):
# or the photo is shared with the user
image_hash = fname.split(".")[0].split("_")[0] # janky alert
user = User.objects.filter(id=token["user_id"]).only("id").first()
if photo.owner == user or user in photo.shared_to.all():
response = HttpResponse()
response["Content-Type"] = "image/jpeg"
response["X-Accel-Redirect"] = self._get_protected_media_url(path, fname)
return response
else:
for album in photo.albumuser_set.only("shared_to"):
if user in album.shared_to.all():
response = HttpResponse()
response["Content-Type"] = "image/jpeg"
response["X-Accel-Redirect"] = self._get_protected_media_url(
path, fname
)
return response

if user_has_photo_permission(user, photo):
return ok_response

return HttpResponse(status=404)


Expand Down Expand Up @@ -536,7 +536,7 @@ def get(self, request, path, fname, format=None):
return HttpResponse(status=404)

# grant access if the requested photo is public
if photo.public:
if user_has_photo_permission(None, photo):
return self._generate_response(photo, path, fname, False)

# forbid access if trouble with jwt
Expand All @@ -556,16 +556,12 @@ def get(self, request, path, fname, format=None):
.only("id", "transcode_videos")
.first()
)
if photo.owner == user or user in photo.shared_to.all():

if user_has_photo_permission(user, photo):
return self._generate_response(
photo, path, fname, user.transcode_videos
)
else:
for album in photo.albumuser_set.only("shared_to"):
if user in album.shared_to.all():
return self._generate_response(
photo, path, fname, user.transcode_videos
)

return HttpResponse(status=404)
else:
jwt = request.COOKIES.get("jwt")
Expand All @@ -589,15 +585,15 @@ def get(self, request, path, fname, format=None):
internal_path = None

internal_path = quote(internal_path)
ok_response = HttpResponse()
mime = magic.Magic(mime=True)
filename = mime.from_file(photo.main_file.path)
ok_response["Content-Type"] = filename
ok_response["X-Accel-Redirect"] = internal_path

# grant access if the requested photo is public
if photo.public:
response = HttpResponse()
mime = magic.Magic(mime=True)
filename = mime.from_file(photo.main_file.path)
response["Content-Type"] = filename
response["X-Accel-Redirect"] = internal_path
return response
if user_has_photo_permission(None, photo):
return ok_response

# forbid access if trouble with jwt
if jwt is not None:
Expand All @@ -614,11 +610,7 @@ def get(self, request, path, fname, format=None):
user = User.objects.filter(id=token["user_id"]).only("id").first()

if internal_path is not None:
response = HttpResponse()
mime = magic.Magic(mime=True)
filename = mime.from_file(photo.main_file.path)
response["Content-Type"] = filename
response["X-Accel-Redirect"] = internal_path
response = ok_response
else:
try:
response = FileResponse(open(photo.main_file.path, "rb"))
Expand All @@ -631,12 +623,9 @@ def get(self, request, path, fname, format=None):
except Exception:
raise

if photo.owner == user or user in photo.shared_to.all():
if user_has_photo_permission(user, photo):
return response
else:
for album in photo.albumuser_set.only("shared_to"):
if user in album.shared_to.all():
return response

return HttpResponse(status=404)


Expand Down
4 changes: 4 additions & 0 deletions librephotos/settings/production.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,3 +280,7 @@

DEFAULT_FAVORITE_MIN_RATING = os.environ.get("DEFAULT_FAVORITE_MIN_RATING", 4)
IMAGE_SIMILARITY_SERVER = "http://localhost:8002"
SHARED_ALBUM_ALL_ADD = os.environ.get(
"SHARED_ALBUM_ALL_ADD", "FALSE"
).upper() == "TRUE"
""" Allow non-owners to add photos to shared albums """
Loading