Luke Plant avatar Luke Plant committed bdfc2ab

Added functionality for securely downloading files to authenticated users.

Comments (0)

Files changed (10)

cciw/officers/urls.py

     (r'^ref/(?P<ref_id>\d+)-(?P<prev_ref_id>\d*)-(?P<hash>.*)/$', 'create_reference_form'),
     (r'^ref/thanks/$', 'create_reference_thanks'),
     (r'^add-officer/$', 'create_officer'),
+    (r'^files/(.*)$', 'officer_files'),
 )

cciw/officers/views.py

 from cciw.officers.utils import camp_officer_list, camp_slacker_list
 from cciw.officers.references import reference_form_info
 from cciw.utils.views import close_window_response
+from securedownload.views import access_folder_securely
 import smtplib
 
 def _copy_application(application):
     return (user.groups.filter(name='Leaders').exists()) \
         or user.camps_as_admin.exists() > 0
 
+def _is_camp_officer(user):
+    return user.is_authenticated() and \
+        user.groups.filter(name='Officers').exists()
+
 def _camps_as_admin_or_leader(user):
     """
     Returns all the camps for which the user is an admin or leader.
          }
     return render_to_response('cciw/officers/create_officer.html',
                               context_instance=template.RequestContext(request, c))
+
+
+officer_files = access_folder_securely("officers",
+                                       lambda request: _is_camp_officer(request.user))
+
     'cciw.utils',
     'django.contrib.messages',
     'mailer',
+    'securedownload',
 )
 
 if DEBUG:
     TEST_DIR = basedir + r'/cciw/cciwmain/tests'
 
 DEFAULT_CONTENT_TYPE = "text/html"
+
+SECURE_FILES_SERVE_URL = "/file/"
+SECURE_FILES_TIMEOUT = 3600
+if DEVBOX:
+    SECURE_FILES_SOURCE = os.path.join(basedir, "../resources/protected_downloads")
+    SECURE_FILES_SERVE_ROOT = os.path.join(basedir, "../protected_downloads")
+else:
+    SECURE_FILES_SOURCE = "/home/cciw/webapps/cciw_protected_downloads_src"
+    SECURE_FILES_SERVE_ROOT = "/home/cciw/webapps/cciw_protected_downloads"
                              {'document_root': settings.MEDIA_ROOT}),
                             (r'^sp_media/(?P<path>.*)$', 'django.views.static.serve',
                              {'document_root': settings.MEDIA_ROOT}),
+                            (r'^file/(?P<path>.*)$', 'django.views.static.serve',
+                             {'document_root': settings.SECURE_FILES_SERVE_ROOT}),
     )
 
 urlpatterns = urlpatterns + patterns('',

securedownload/__init__.py

+"""
+An app that makes it easy to create (relatively) secure download links to
+restricted folders.  To avoid using the Django process to serve the file,
+temporary symlinks are placed into designated directory, which must be served by
+a separate webserver
+"""
+
Add a comment to this file

securedownload/management/__init__.py

Empty file added.

Add a comment to this file

securedownload/management/commands/__init__.py

Empty file added.

securedownload/management/commands/clear_securedownload_links.py

+from django.core.management.base import BaseCommand, CommandError
+from django.conf import settings
+import datetime
+import os
+import shutil
+
+class Command(BaseCommand):
+    help = 'Removes links created for serving secure files if they have expired'
+
+    def handle(self, *args, **kwargs):
+        now = datetime.datetime.now()
+        for d in os.listdir(settings.SECURE_FILES_SERVE_ROOT):
+            parts = d.split('-')
+            if len(parts) != 2:
+                continue
+            try:
+                ts = int(parts[0])
+            except ValueError:
+                continue
+            dt = datetime.datetime.fromtimestamp(ts)
+            td = now - dt
+            if (td.days * 3600 * 24) + td.seconds < settings.SECURE_FILES_TIMEOUT:
+                continue
+
+            # Delete the directory and all contents.
+            shutil.rmtree(os.path.join(settings.SECURE_FILES_SERVE_ROOT, d))

securedownload/models.py

+# Dummy file
+

securedownload/views.py

+import datetime
+import hmac
+import os
+import posixpath
+import urllib
+from django.conf import settings
+from django.http import Http404, HttpResponseRedirect, HttpResponseForbidden
+from django.utils.hashcompat import sha_hmac
+
+def serve_secure_file(filename):
+    """
+    Returns an HTTP redirect to serve the specified file.
+    It will be a redirect to a location under SECURE_FILES_SERVE_URL,
+    which should map to SECURE_FILES_SERVE_ROOT
+
+    filename is relative to SECURE_FILES_SOURCE.
+    """
+    src = os.path.join(settings.SECURE_FILES_SOURCE, filename)
+    if not os.path.isfile(src):
+        raise Http404()
+    # Make a link
+    ts = datetime.datetime.now().strftime("%s")
+    # Make a directory that cannot be guessed, and that contains the timestamp
+    # so that we can remove it easily by timestamp later.
+    key = "cciw.officers.secure_file" + settings.SECRET_KEY
+    nonce = hmac.new(key, "%s-%s" % (ts, filename), sha_hmac).hexdigest()
+    dirname = "%s-%s" % (ts, nonce)
+    abs_destdir = os.path.join(settings.SECURE_FILES_SERVE_ROOT, dirname)
+    if not os.path.isdir(abs_destdir):
+        os.mkdir(abs_destdir)
+    dest = os.path.join(dirname, os.path.basename(filename))
+    os.symlink(src, os.path.join(settings.SECURE_FILES_SERVE_ROOT, dest))
+    return HttpResponseRedirect(os.path.join(settings.SECURE_FILES_SERVE_URL, dest))
+
+def sanitise_path(path):
+    newpath = ''
+    for part in path.split('/'):
+        if not part:
+            # Strip empty path components.
+            continue
+        drive, part = os.path.splitdrive(part)
+        head, part = os.path.split(part)
+        if part in (os.curdir, os.pardir):
+            # Strip '.' and '..' in path.
+            continue
+        newpath = os.path.join(newpath, part).replace('\\', '/')
+    return newpath
+
+def access_folder_securely(folder, check_permission):
+    """
+    Creates a view function for accessing files in a folder.
+    check_permission is a callable that takes a request and
+    returns True if the file should be served.
+
+    folder is relative to SECURE_FILES_SOURCE.
+    """
+    def view(request, filename):
+        if check_permission(request):
+            filename = posixpath.normpath(urllib.unquote(filename))
+            fname = sanitise_path(filename)
+            if fname != filename:
+                raise Http404()
+            return serve_secure_file(os.path.join(folder, fname))
+        else:
+            return HttpResponseForbidden("<h1>Access denied</h1>")
+    return view
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.