diff --git a/opentech/apply/funds/models/mixins.py b/opentech/apply/funds/models/mixins.py index 6ca8ac3e050ff521119c9363b663f37fdaababd7..388e238497eb6676d2de8544ecb30afc65446590 100644 --- a/opentech/apply/funds/models/mixins.py +++ b/opentech/apply/funds/models/mixins.py @@ -15,7 +15,13 @@ from opentech.apply.stream_forms.files import StreamFieldFile __all__ = ['AccessFormData'] -submission_storage = get_storage_class(getattr(settings, 'PRIVATE_FILE_STORAGE', None))() +private_file_storage = getattr(settings, 'PRIVATE_FILE_STORAGE', None) +submission_storage_class = get_storage_class(private_file_storage) + +if private_file_storage: + submission_storage = submission_storage_class(is_submission=True) +else: + submission_storage = submission_storage_class() class UnusedFieldException(Exception): diff --git a/opentech/apply/funds/permissions.py b/opentech/apply/funds/permissions.py index 8d7438c54d7673c851d8d6d99288d2696884c602..594fb0fca9ce919c6af52d570e161bee94696f99 100644 --- a/opentech/apply/funds/permissions.py +++ b/opentech/apply/funds/permissions.py @@ -16,3 +16,21 @@ class IsApplyStaffUser(permissions.BasePermission): def has_object_permission(self, request, view, obj): return request.user.is_apply_staff + + +def is_user_has_access_to_view_submission(user, submission): + has_access = False + + if not user.is_authenticated: + pass + + elif user.is_apply_staff or submission.user == user or user.is_reviewer: + has_access = True + + elif user.is_partner and submission.partners.filter(pk=user.pk).exists(): + has_access = True + + elif user.is_community_reviewer and submission.community_review: + has_access = True + + return has_access diff --git a/opentech/apply/funds/urls.py b/opentech/apply/funds/urls.py index 88990380d67ad35c9ec99e2c36042d1cd6aa9e39..dd75bcd4a84bc64ab50dff8c52f85ed9ae0a0155 100644 --- a/opentech/apply/funds/urls.py +++ b/opentech/apply/funds/urls.py @@ -12,6 +12,7 @@ from .views import ( SubmissionOverviewView, SubmissionSealedView, SubmissionDeleteView, + SubmissionPrivateMediaRedirectView, ) from .api_views import ( CommentEdit, @@ -36,6 +37,10 @@ app_name = 'funds' submission_urls = ([ path('', SubmissionOverviewView.as_view(), name="overview"), path('all/', SubmissionListView.as_view(), name="list"), + path( + 'documents/submission/<int:submission_id>/<uuid:field_id>/<str:file_name>/', + SubmissionPrivateMediaRedirectView.as_view(), name='private_media_redirect' + ), path('<int:pk>/', include([ path('', SubmissionDetailView.as_view(), name="detail"), path('edit/', SubmissionEditView.as_view(), name="edit"), diff --git a/opentech/apply/funds/views.py b/opentech/apply/funds/views.py index 5308232895c4f24bda6f10eabaac58b7397fe2e3..176056e1f70314e3fab351b8c9aa6278d4ca96d4 100644 --- a/opentech/apply/funds/views.py +++ b/opentech/apply/funds/views.py @@ -1,8 +1,12 @@ from copy import copy +from django.conf import settings from django.contrib.auth.decorators import login_required, permission_required +from django.contrib.auth.mixins import UserPassesTestMixin +from django.contrib.auth.views import redirect_to_login from django.contrib import messages from django.core.exceptions import PermissionDenied +from django.core.files.storage import get_storage_class from django.db.models import Count, F, Q from django.http import HttpResponseRedirect, Http404 from django.shortcuts import get_object_or_404 @@ -10,7 +14,7 @@ from django.urls import reverse_lazy from django.utils.decorators import method_decorator from django.utils.text import mark_safe from django.utils.translation import ugettext_lazy as _ -from django.views.generic import DetailView, FormView, ListView, UpdateView, DeleteView +from django.views.generic import DetailView, FormView, ListView, UpdateView, DeleteView, RedirectView from django_filters.views import FilterView from django_tables2.views import SingleTableMixin @@ -56,6 +60,9 @@ from .tables import ( SummarySubmissionsTable, ) from .workflow import STAGE_CHANGE_ACTIONS, PHASES_MAPPING, review_statuses +from .permissions import is_user_has_access_to_view_submission + +submission_storage = get_storage_class(getattr(settings, 'PRIVATE_FILE_STORAGE', None))() class BaseAdminSubmissionsTable(SingleTableMixin, FilterView): @@ -812,3 +819,30 @@ class SubmissionDeleteView(DeleteView): ) response = super().delete(request, *args, **kwargs) return response + + +class SubmissionPrivateMediaRedirectView(UserPassesTestMixin, RedirectView): + + def get_redirect_url(self, *args, **kwargs): + submission_id = kwargs['submission_id'] + field_id = kwargs['field_id'] + file_name = kwargs['file_name'] + file_name_with_path = f'submission/{submission_id}/{field_id}/{file_name}' + + return submission_storage.url(file_name_with_path) + + def test_func(self): + submission_id = self.kwargs['submission_id'] + submission = get_object_or_404(ApplicationSubmission, id=submission_id) + + return is_user_has_access_to_view_submission(self.request.user, submission) + + def handle_no_permission(self): + # This method can be removed after upgrading Django to 2.1 + # https://github.com/django/django/commit/9b1125bfc7e2dc747128e6e7e8a2259ff1a7d39f + # In older versions, authenticated users who lacked permissions were + # redirected to the login page (which resulted in a loop) instead of + # receiving an HTTP 403 Forbidden response. + if self.raise_exception or self.request.user.is_authenticated: + raise PermissionDenied(self.get_permission_denied_message()) + return redirect_to_login(self.request.get_full_path(), self.get_login_url(), self.get_redirect_field_name()) diff --git a/opentech/storage_backends.py b/opentech/storage_backends.py index 4dba8b95e81bd05ce6dc79e6a4932e2e91d68428..3a347b4cbf1c2194e2e77dc7fbac240935eb6e62 100644 --- a/opentech/storage_backends.py +++ b/opentech/storage_backends.py @@ -1,6 +1,7 @@ from urllib import parse from django.conf import settings +from django.urls import reverse from django.utils.encoding import filepath_to_uri from storages.backends.s3boto3 import S3Boto3Storage @@ -28,8 +29,21 @@ class PrivateMediaStorage(S3Boto3Storage): file_overwrite = False querystring_auth = True url_protocol = 'https:' + is_submission = False def url(self, name, parameters=None, expire=None): + if self.is_submission: + try: + name_parts = name.split('/') + return reverse( + 'apply:submissions:private_media_redirect', kwargs={ + 'submission_id': name_parts[1], 'field_id': name_parts[2], + 'file_name': name_parts[3] + } + ) + except IndexError: + pass + url = super().url(name, parameters, expire) if hasattr(settings, 'AWS_PRIVATE_CUSTOM_DOMAIN'):