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'):