diff --git a/SciPost_v1/settings/base.py b/SciPost_v1/settings/base.py index 760615bd5f411f88a7d34b7df20f3ac85cd48ccb..f79b99fad368e3cb3d6b9e432a83dca52f0a4a4b 100644 --- a/SciPost_v1/settings/base.py +++ b/SciPost_v1/settings/base.py @@ -227,9 +227,13 @@ USE_TZ = True # MEDIA MEDIA_URL = '/media/' -MEDIA_ROOT = 'local_files/media/' +MEDIA_URL_SECURE = '/files/secure/' MAX_UPLOAD_SIZE = "1310720" # Default max attachment size in Bytes +# -- These MEDIA settings are machine-dependent +MEDIA_ROOT = 'local_files/media/' +MEDIA_ROOT_SECURE = 'local_files/secure/media/' + # Static files (CSS, JavaScript, Images) STATIC_URL = '/static/' STATIC_ROOT = 'local_files/static/' diff --git a/partners/models.py b/partners/models.py index 1d7f39a882f9a0ba0d2aa4c2ae123cc1ccecbe33..ff95d07990170d04286329cc4d70a0c625203715 100644 --- a/partners/models.py +++ b/partners/models.py @@ -26,6 +26,7 @@ from .constants import PROSPECTIVE_PARTNER_EVENT_EMAIL_SENT,\ from .managers import MembershipAgreementManager, ProspectivePartnerManager, PartnerManager,\ ContactRequestManager, PartnersAttachmentManager +from .storage import SecureFileStorage from scipost.constants import TITLE_CHOICES from scipost.fields import ChoiceArrayField @@ -291,7 +292,8 @@ class PartnersAttachment(models.Model): An Attachment which can (in the future) be related to a Partner, Contact, MembershipAgreement, etc. """ - attachment = models.FileField(upload_to='UPLOADS/PARTNERS/ATTACHMENTS') + attachment = models.FileField(upload_to='UPLOADS/PARTNERS/ATTACHMENTS', + storage=SecureFileStorage()) name = models.CharField(max_length=128) agreement = models.ForeignKey('partners.MembershipAgreement', related_name='attachments', blank=True) diff --git a/partners/storage.py b/partners/storage.py new file mode 100644 index 0000000000000000000000000000000000000000..1821e4f48a785229435e4963d81ac33e881aea19 --- /dev/null +++ b/partners/storage.py @@ -0,0 +1,25 @@ +from django.conf import settings +from django.core.files.storage import FileSystemStorage +from django.utils.functional import cached_property + + +class SecureFileStorage(FileSystemStorage): + """ + Inherit default FileStorage system to prevent files from being publicly accessible + from an server location that is permitted to be opened without explicit permissions. + """ + @cached_property + def location(self): + """ + This method determines the storage location for a new file. To secure the file from + 'the public', we'll store it outside the publicly accessible MEDIA_ROOT folder. + + This also means you need to explicitly handle the file reading/opening! + """ + if hasattr(settings, 'MEDIA_ROOT_SECURE'): + return self._value_or_setting(self._location, settings.MEDIA_ROOT_SECURE) + return super().location + + @cached_property + def base_url(self): + return settings.MEDIA_URL_SECURE diff --git a/partners/views.py b/partners/views.py index 04a4c5f1ef0615d1310fb3d420d3a764aad27fe6..a4367d7fdfa4dde37705fb85def385e72b9ce7c6 100644 --- a/partners/views.py +++ b/partners/views.py @@ -1,8 +1,10 @@ +import mimetypes + from django.contrib import messages from django.contrib.auth.decorators import login_required from django.db import transaction from django.forms import modelformset_factory -from django.http import HttpResponse +from django.http import FileResponse, HttpResponse from django.shortcuts import get_object_or_404, render, reverse, redirect from django.utils import timezone @@ -386,7 +388,11 @@ def agreement_details(request, agreement_id): def agreement_attachments(request, agreement_id, attachment_id): attachment = get_object_or_404(PartnersAttachment.objects.my_attachments(request.user), agreement__id=agreement_id, id=attachment_id) - response = HttpResponse(attachment.attachment.read(), content_type='application/pdf') + + content_type, encoding = mimetypes.guess_type(attachment.attachment.path) + content_type = content_type or 'application/octet-stream' + response = HttpResponse(attachment.attachment.read(), content_type=content_type) + response["Content-Encoding"] = encoding response['Content-Disposition'] = ('filename=%s' % attachment.name) return response diff --git a/scipost/urls.py b/scipost/urls.py index f3af0c4a67bc30f8a0c836d7f4692467359db08a..fe4aaefa27501031192cb6ce51fddc74cbe10a87 100644 --- a/scipost/urls.py +++ b/scipost/urls.py @@ -1,4 +1,4 @@ -from django.conf.urls import include, url +from django.conf.urls import url from django.views.generic import TemplateView from . import views @@ -14,6 +14,7 @@ JOURNAL_REGEX = '(?P<doi_label>%s)' % REGEX_CHOICES urlpatterns = [ url(r'^$', views.index, name='index'), + url(r'^files/secure/(?P<path>.*)$', views.protected_serve, name='secure_file'), # General use pages url(r'^error$', TemplateView.as_view(template_name='scipost/error.html'), name='error'), diff --git a/scipost/views.py b/scipost/views.py index 5c09fe3e60b3b05544df1ba5244b9039ef2286a0..ce592b94260700942b745091c88c24f739df2ef3 100644 --- a/scipost/views.py +++ b/scipost/views.py @@ -1,5 +1,6 @@ from django.utils import timezone from django.shortcuts import get_object_or_404, render +from django.conf import settings from django.contrib import messages from django.contrib.auth import login, logout, update_session_auth_hash from django.contrib.auth.decorators import login_required, user_passes_test @@ -10,10 +11,12 @@ from django.core.mail import EmailMessage, EmailMultiAlternatives from django.core.paginator import Paginator from django.core.urlresolvers import reverse from django.db.models import Prefetch +from django.http import Http404 from django.shortcuts import redirect from django.template import Context, Template from django.views.decorators.http import require_POST from django.views.generic.list import ListView +from django.views.static import serve from guardian.decorators import permission_required from guardian.shortcuts import assign_perm, get_objects_for_user @@ -85,6 +88,18 @@ def index(request): return render(request, 'scipost/index.html', context) +def protected_serve(request, path, show_indexes=False): + """ + Serve files that are saved outside the default MEDIA_ROOT folder for superusers only! + This will be usefull eg. in the admin pages. + """ + if not request.user.is_authenticated or not request.user.is_superuser: + # Only superusers may get to see secure files without an explicit serve method! + raise Http404 + document_root = settings.MEDIA_ROOT_SECURE + return serve(request, path, document_root, show_indexes) + + ############### # Information ###############