From 433599dc20625b84af9d792f5e720a79d1df1bf0 Mon Sep 17 00:00:00 2001 From: Sean Morley Date: Sat, 18 Jan 2025 17:03:03 -0500 Subject: [PATCH] feat: implement protected media serving and permission checks for adventure images --- backend/nginx.conf | 12 ++- .../utils/check_adventure_image_permisison.py | 33 +++++++ backend/server/main/settings.py | 2 +- backend/server/main/urls.py | 13 ++- backend/server/main/views.py | 30 ++++++- frontend/src/lib/components/Navbar.svelte | 87 ++++++++++--------- frontend/src/routes/search/+page.server.ts | 8 +- 7 files changed, 130 insertions(+), 55 deletions(-) create mode 100644 backend/server/adventures/utils/check_adventure_image_permisison.py diff --git a/backend/nginx.conf b/backend/nginx.conf index b4bad7d..8074aa6 100644 --- a/backend/nginx.conf +++ b/backend/nginx.conf @@ -19,7 +19,7 @@ http { } server { - listen 80; # NGINX listens on port 80 inside the container + listen 80; server_name localhost; location / { @@ -34,8 +34,12 @@ http { alias /code/staticfiles/; # Serve static files directly } - location /media/ { - alias /code/media/; # Serve media files directly + # Serve protected media files with X-Accel-Redirect + location /protectedMedia/ { + internal; # Only internal requests are allowed + alias /code/media/; # This should match Django MEDIA_ROOT + try_files $uri =404; # Return a 404 if the file doesn't exist } + } -} \ No newline at end of file +} diff --git a/backend/server/adventures/utils/check_adventure_image_permisison.py b/backend/server/adventures/utils/check_adventure_image_permisison.py new file mode 100644 index 0000000..ad7df57 --- /dev/null +++ b/backend/server/adventures/utils/check_adventure_image_permisison.py @@ -0,0 +1,33 @@ +from adventures.models import AdventureImage + +def checkAdventureImagePermission(imageId, user): + """ + Checks if the given user has permission to access the specified adventure image. + + Args: + imageId (str): The ID of the image to check permissions for. + user (User): The user object to check permissions against. + + Returns: + bool: True if the user has permission to access the image, False otherwise. + + Raises: + AdventureImage.DoesNotExist: If the image with the specified ID does not exist. + """ + try: + # Construct the full relative path to match the database field + image_path = f"images/{imageId}" + # Fetch the AdventureImage object + adventure = AdventureImage.objects.get(image=image_path).adventure + if adventure.is_public: + return True + elif adventure.user_id == user: + return True + elif adventure.collection: + if adventure.collection.shared_with.filter(id=user.id).exists(): + return True + else: + return False + except AdventureImage.DoesNotExist: + print('No image') + return False diff --git a/backend/server/main/settings.py b/backend/server/main/settings.py index 208b2a1..83ab09a 100644 --- a/backend/server/main/settings.py +++ b/backend/server/main/settings.py @@ -157,7 +157,7 @@ STATIC_ROOT = BASE_DIR / "staticfiles" STATIC_URL = '/static/' MEDIA_URL = '/media/' -MEDIA_ROOT = BASE_DIR / 'media' +MEDIA_ROOT = BASE_DIR / 'media' # This path must match the NGINX root STATICFILES_DIRS = [BASE_DIR / 'static'] STORAGES = { diff --git a/backend/server/main/urls.py b/backend/server/main/urls.py index 571946e..b7bb2a1 100644 --- a/backend/server/main/urls.py +++ b/backend/server/main/urls.py @@ -1,12 +1,9 @@ from django.urls import include, re_path, path from django.contrib import admin from django.views.generic import RedirectView, TemplateView -from django.conf import settings -from django.conf.urls.static import static from users.views import IsRegistrationDisabled, PublicUserListView, PublicUserDetailView, UserMetadataView, UpdateUserMetadataView, EnabledSocialProvidersView -from .views import get_csrf_token, get_public_url +from .views import get_csrf_token, get_public_url, serve_protected_media from drf_yasg.views import get_schema_view - from drf_yasg import openapi schema_view = get_schema_view( @@ -20,6 +17,9 @@ urlpatterns = [ path('api/', include('worldtravel.urls')), path("_allauth/", include("allauth.headless.urls")), + # Serve protected media files + re_path(r'^media/(?P.*)$', serve_protected_media, name='serve-protected-media'), + path('auth/is-registration-disabled/', IsRegistrationDisabled.as_view(), name='is_registration_disabled'), path('auth/users/', PublicUserListView.as_view(), name='public-user-list'), path('auth/user//', PublicUserDetailView.as_view(), name='public-user-detail'), @@ -44,6 +44,5 @@ urlpatterns = [ path("api/integrations/", include("integrations.urls")), - # Include the API endpoints: - -] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + # Include the API endpoints: +] \ No newline at end of file diff --git a/backend/server/main/views.py b/backend/server/main/views.py index f21082b..594017b 100644 --- a/backend/server/main/views.py +++ b/backend/server/main/views.py @@ -1,10 +1,38 @@ from django.http import JsonResponse from django.middleware.csrf import get_token from os import getenv +from django.conf import settings +from django.http import HttpResponse, HttpResponseForbidden +from django.contrib.auth.decorators import login_required +from django.views.static import serve +from adventures.utils.check_adventure_image_permisison import checkAdventureImagePermission def get_csrf_token(request): csrf_token = get_token(request) return JsonResponse({'csrfToken': csrf_token}) def get_public_url(request): - return JsonResponse({'PUBLIC_URL': getenv('PUBLIC_URL')}) \ No newline at end of file + return JsonResponse({'PUBLIC_URL': getenv('PUBLIC_URL')}) + +def serve_protected_media(request, path): + if path.startswith('images/'): + image_id = path.split('/')[1] + user = request.user + if checkAdventureImagePermission(image_id, user): + if settings.DEBUG: + # In debug mode, serve the file directly + return serve(request, path, document_root=settings.MEDIA_ROOT) + else: + # In production, use X-Accel-Redirect + response = HttpResponse() + response['Content-Type'] = '' + response['X-Accel-Redirect'] = '/protectedMedia/' + path + return response + else: + return HttpResponseForbidden() + else: + response = HttpResponse() + response['Content-Type'] = '' + response['X-Accel-Redirect'] = '/protectedMedia/' + path + return response + \ No newline at end of file diff --git a/frontend/src/lib/components/Navbar.svelte b/frontend/src/lib/components/Navbar.svelte index bbfbaf5..13a2e85 100644 --- a/frontend/src/lib/components/Navbar.svelte +++ b/frontend/src/lib/components/Navbar.svelte @@ -120,27 +120,34 @@ {/if} -
- - -
+ + + + + + + + {/if} {/if} -
- - -
+ + {/if}