SciPost Code Repository

Skip to content
Snippets Groups Projects
Commit 8e9edeeb authored by Jorran de Wit's avatar Jorran de Wit
Browse files

Important file handling improvements

Files can now be auto-saved into a secure folder on the server.
This will prevent people from opening files that do not copmly with
their permissions.

Further, it's now possible to open all types of files in the partner dashboard.
parent ea688f21
No related branches found
No related tags found
No related merge requests found
...@@ -227,9 +227,13 @@ USE_TZ = True ...@@ -227,9 +227,13 @@ USE_TZ = True
# MEDIA # MEDIA
MEDIA_URL = '/media/' MEDIA_URL = '/media/'
MEDIA_ROOT = 'local_files/media/' MEDIA_URL_SECURE = '/files/secure/'
MAX_UPLOAD_SIZE = "1310720" # Default max attachment size in Bytes 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 files (CSS, JavaScript, Images)
STATIC_URL = '/static/' STATIC_URL = '/static/'
STATIC_ROOT = 'local_files/static/' STATIC_ROOT = 'local_files/static/'
......
...@@ -26,6 +26,7 @@ from .constants import PROSPECTIVE_PARTNER_EVENT_EMAIL_SENT,\ ...@@ -26,6 +26,7 @@ from .constants import PROSPECTIVE_PARTNER_EVENT_EMAIL_SENT,\
from .managers import MembershipAgreementManager, ProspectivePartnerManager, PartnerManager,\ from .managers import MembershipAgreementManager, ProspectivePartnerManager, PartnerManager,\
ContactRequestManager, PartnersAttachmentManager ContactRequestManager, PartnersAttachmentManager
from .storage import SecureFileStorage
from scipost.constants import TITLE_CHOICES from scipost.constants import TITLE_CHOICES
from scipost.fields import ChoiceArrayField from scipost.fields import ChoiceArrayField
...@@ -291,7 +292,8 @@ class PartnersAttachment(models.Model): ...@@ -291,7 +292,8 @@ class PartnersAttachment(models.Model):
An Attachment which can (in the future) be related to a Partner, Contact, MembershipAgreement, An Attachment which can (in the future) be related to a Partner, Contact, MembershipAgreement,
etc. 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) name = models.CharField(max_length=128)
agreement = models.ForeignKey('partners.MembershipAgreement', related_name='attachments', agreement = models.ForeignKey('partners.MembershipAgreement', related_name='attachments',
blank=True) blank=True)
......
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
import mimetypes
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.db import transaction from django.db import transaction
from django.forms import modelformset_factory 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.shortcuts import get_object_or_404, render, reverse, redirect
from django.utils import timezone from django.utils import timezone
...@@ -386,7 +388,11 @@ def agreement_details(request, agreement_id): ...@@ -386,7 +388,11 @@ def agreement_details(request, agreement_id):
def agreement_attachments(request, agreement_id, attachment_id): def agreement_attachments(request, agreement_id, attachment_id):
attachment = get_object_or_404(PartnersAttachment.objects.my_attachments(request.user), attachment = get_object_or_404(PartnersAttachment.objects.my_attachments(request.user),
agreement__id=agreement_id, id=attachment_id) 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) response['Content-Disposition'] = ('filename=%s' % attachment.name)
return response return response
......
from django.conf.urls import include, url from django.conf.urls import url
from django.views.generic import TemplateView from django.views.generic import TemplateView
from . import views from . import views
...@@ -14,6 +14,7 @@ JOURNAL_REGEX = '(?P<doi_label>%s)' % REGEX_CHOICES ...@@ -14,6 +14,7 @@ JOURNAL_REGEX = '(?P<doi_label>%s)' % REGEX_CHOICES
urlpatterns = [ urlpatterns = [
url(r'^$', views.index, name='index'), url(r'^$', views.index, name='index'),
url(r'^files/secure/(?P<path>.*)$', views.protected_serve, name='secure_file'),
# General use pages # General use pages
url(r'^error$', TemplateView.as_view(template_name='scipost/error.html'), name='error'), url(r'^error$', TemplateView.as_view(template_name='scipost/error.html'), name='error'),
......
from django.utils import timezone from django.utils import timezone
from django.shortcuts import get_object_or_404, render from django.shortcuts import get_object_or_404, render
from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.contrib.auth import login, logout, update_session_auth_hash from django.contrib.auth import login, logout, update_session_auth_hash
from django.contrib.auth.decorators import login_required, user_passes_test from django.contrib.auth.decorators import login_required, user_passes_test
...@@ -10,10 +11,12 @@ from django.core.mail import EmailMessage, EmailMultiAlternatives ...@@ -10,10 +11,12 @@ from django.core.mail import EmailMessage, EmailMultiAlternatives
from django.core.paginator import Paginator from django.core.paginator import Paginator
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.db.models import Prefetch from django.db.models import Prefetch
from django.http import Http404
from django.shortcuts import redirect from django.shortcuts import redirect
from django.template import Context, Template from django.template import Context, Template
from django.views.decorators.http import require_POST from django.views.decorators.http import require_POST
from django.views.generic.list import ListView from django.views.generic.list import ListView
from django.views.static import serve
from guardian.decorators import permission_required from guardian.decorators import permission_required
from guardian.shortcuts import assign_perm, get_objects_for_user from guardian.shortcuts import assign_perm, get_objects_for_user
...@@ -85,6 +88,18 @@ def index(request): ...@@ -85,6 +88,18 @@ def index(request):
return render(request, 'scipost/index.html', context) 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 # Information
############### ###############
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment