├── .gitignore ├── LICENSE ├── README.rst ├── protected_files ├── __init__.py ├── models.py ├── tests │ ├── __init__.py │ ├── tests.py │ └── urls.py └── views.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *~ 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Lincoln Loop, LLC and individual contributors. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of Lincoln Loop nor the names of its contributors may 15 | be used to endorse or promote products derived from this software 16 | without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Install 2 | ------- 3 | 4 | * ``python setup.py install`` 5 | 6 | Usage 7 | ----- 8 | 9 | * add ``protected_files`` to your ``INSTALLED_APPS`` 10 | * add an URL to your protected resource to your ``urls.py`` (see ``tests.urls`` for examples) 11 | * configure your static server 12 | 13 | Static Server Configuration 14 | --------------------------- 15 | 16 | Nginx 17 | ^^^^^ 18 | 19 | Place this in your Nginx configuration:: 20 | 21 | # this location will only be used by your Django application server 22 | location /protected { 23 | internal; 24 | alias /protected/files/path/; 25 | } 26 | 27 | To Do 28 | ----- 29 | 30 | * Support alternative means of authorization (user, group, is_staff, etc.) 31 | * Support additional static servers (Lighttpd) 32 | 33 | Acknowledgements 34 | ---------------- 35 | 36 | Based on http://www.djangosnippets.org/snippets/491/ -------------------------------------------------------------------------------- /protected_files/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincolnloop/django-protected-files/5a3ac57f035961a6c623d2fc4592da6318394a6e/protected_files/__init__.py -------------------------------------------------------------------------------- /protected_files/models.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lincolnloop/django-protected-files/5a3ac57f035961a6c623d2fc4592da6318394a6e/protected_files/models.py -------------------------------------------------------------------------------- /protected_files/tests/__init__.py: -------------------------------------------------------------------------------- 1 | from tests import * -------------------------------------------------------------------------------- /protected_files/tests/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from django.contrib.auth.models import User, Permission 3 | from django.contrib.contenttypes.models import ContentType 4 | from django.conf import settings 5 | 6 | 7 | class ProtectedFileTest(TestCase): 8 | urls = 'protected_files.tests.urls' 9 | 10 | def setUp(self): 11 | super(ProtectedFileTest, self).setUp() 12 | self.users = [] 13 | u = User.objects.create_user('has_perms', 'has_perms@testserver.com', '!') 14 | ct = ContentType.objects.get(app_label="auth", model="user") 15 | perm = Permission.objects.get(content_type=ct, codename='add_user') 16 | u.user_permissions.add(perm) 17 | self.users.append(u) 18 | 19 | u = User.objects.create_user('no_perms', 'no_perms@testserver.com', '!') 20 | self.users.append(u) 21 | 22 | def tearDown(self): 23 | for u in self.users: 24 | u.delete() 25 | 26 | def testPermUser(self): 27 | "User with permissions has access" 28 | # this test fails with cache middleware enabled 29 | # http://code.djangoproject.com/ticket/5176 30 | response = self.client.login(username='has_perms', password='!') 31 | response = self.client.get('/perms/file.txt') 32 | self.assertEqual(response.status_code, 200) 33 | self.assertEqual(response['X-Accel-Redirect'], '/file.txt') 34 | 35 | def testRedirect(self): 36 | "Redirect to login if user isn't authenticated" 37 | response = self.client.get('/req-login/file.txt') 38 | # this can give a false positive 39 | # self.assertRedirects(response, '%s?next=/req-login/file.txt' % settings.LOGIN_URL) 40 | self.assertEqual(response.status_code, 302) 41 | self.assertEqual(response['Location'], 42 | 'http://testserver%s?next=/req-login/file.txt' % settings.LOGIN_URL) 43 | 44 | def testAnonymousUser(self): 45 | "Anonymous users never have access" 46 | response = self.client.get('/perms/file.txt') 47 | self.assertEqual(response.status_code, 403) 48 | 49 | def testNoPermUser(self): 50 | "User without permissions is forbidden" 51 | response = self.client.login(username='no_perms', password='!') 52 | response = self.client.get('/perms/file.txt') 53 | self.assertEqual(response.status_code, 403) 54 | 55 | def testMultiLevelPath(self): 56 | "We can drill-down inside a directory" 57 | # this test fails with cache middleware enabled 58 | # http://code.djangoproject.com/ticket/5176 59 | response = self.client.login(username='has_perms', password='!') 60 | response = self.client.get('/perms/dir1/dir2/file.txt') 61 | self.assertEqual(response.status_code, 200) 62 | self.assertEqual(response['X-Accel-Redirect'], 63 | '/dir1/dir2/file.txt') 64 | 65 | def testAltRoot(self): 66 | "Specifying an alternate root directory" 67 | # this test fails with cache middleware enabled 68 | # http://code.djangoproject.com/ticket/5176 69 | response = self.client.login(username='has_perms', password='!') 70 | response = self.client.get('/alt-root/file.txt') 71 | self.assertEqual(response.status_code, 200) 72 | self.assertEqual(response['X-Accel-Redirect'], 73 | '/alt/root/path/file.txt') 74 | 75 | def testNoPerms(self): 76 | "Forbidden if permissions aren't specified" 77 | response = self.client.login(username='has_perms', password='!') 78 | response = self.client.get('/no-perms/file.txt') 79 | self.assertEqual(response.status_code, 403) 80 | -------------------------------------------------------------------------------- /protected_files/tests/urls.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse 2 | from django.conf.urls.defaults import * 3 | 4 | urlpatterns = patterns('', 5 | url(r'^perms/(?P.*)', 'protected_files.views.protected_file', {'perm':'auth.add_user'}), 6 | url(r'^req-login/(?P.*)', 'protected_files.views.protected_file', {'perm':'auth.add_user', 'require_login':True}), 7 | url(r'^alt-root/(?P.*)', 'protected_files.views.protected_file', {'perm':'auth.add_user', 'redirect_root':'/alt/root/path'}), 8 | url(r'^no-perms/(?P.*)', 'protected_files.views.protected_file'), 9 | 10 | ) 11 | -------------------------------------------------------------------------------- /protected_files/views.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse, HttpResponseRedirect, HttpResponseForbidden 2 | from django.conf import settings 3 | 4 | 5 | 6 | def protected_file(request, file_path, redirect_root="", 7 | require_login=False, perm=None): 8 | """ 9 | Checks permissions and returns an HttpResponse with the path to the file 10 | modified from http://www.djangosnippets.org/snippets/491/ 11 | 12 | """ 13 | if perm and request.user.has_perm(perm): 14 | response = HttpResponse() 15 | url = '%s/%s' % (redirect_root, file_path) 16 | 17 | # Nginx only 18 | # let nginx determine the correct content type 19 | response['Content-Type']="" 20 | response['X-Accel-Redirect'] = url 21 | return response 22 | 23 | if require_login and not request.user.is_authenticated(): 24 | login_redirect = '%s?next=%s' % (settings.LOGIN_URL, request.path) 25 | return HttpResponseRedirect(login_redirect) 26 | return HttpResponseForbidden() 27 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from distutils.core import setup 4 | 5 | def fullsplit(path, result=None): 6 | """ 7 | Split a pathname into components (the opposite of os.path.join) in a 8 | platform-neutral way. 9 | """ 10 | if result is None: 11 | result = [] 12 | head, tail = os.path.split(path) 13 | if head == "": 14 | return [tail] + result 15 | if head == path: 16 | return result 17 | return fullsplit(head, [tail] + result) 18 | 19 | package_dir = "protected_files" 20 | 21 | packages = [] 22 | for dirpath, dirnames, filenames in os.walk(package_dir): 23 | # ignore dirnames that start with '.' 24 | for i, dirname in enumerate(dirnames): 25 | if dirname.startswith("."): 26 | del dirnames[i] 27 | if "__init__.py" in filenames: 28 | packages.append(".".join(fullsplit(dirpath))) 29 | 30 | setup(name='django-protected-files', 31 | version='0.1', 32 | description='A Django application that lets you serve protected static files via your frontend server after authorizing the user against django.contrib.auth.', 33 | author='Peter Baumgartner', 34 | author_email='pete@lincolnloop.com', 35 | url='http://github.com/lincolnloop/django-protected-files', 36 | packages=packages) --------------------------------------------------------------------------------