Merge branch 'dfsg_clean'
Sunil Mohan Adapa
6 years ago
2 | 2 | - "2.6" |
3 | 3 | - "2.7" |
4 | 4 | - "3.4" |
5 | - "3.5" | |
6 | - "3.6" | |
5 | 7 | env: |
6 | - DJANGO_VERSION=1.4.13 | |
7 | - DJANGO_VERSION=1.5.8 | |
8 | - DJANGO_VERSION=1.6.5 | |
9 | - DJANGO_VERSION=1.7.1 | |
8 | - DJANGO_VERSION="django>=1.4,<1.5" | |
9 | - DJANGO_VERSION="django>=1.5,<1.6" | |
10 | - DJANGO_VERSION="django>=1.6,<1.7" | |
11 | - DJANGO_VERSION="django>=1.7,<1.8" | |
12 | - DJANGO_VERSION="django>=1.8,<1.9" | |
13 | - DJANGO_VERSION="django>=1.9,<1.10" | |
14 | - DJANGO_VERSION="django>=1.10,<1.11" | |
15 | - DJANGO_VERSION="django>=1.11,<1.12" | |
16 | - DJANGO_VERSION="django>=2.0,<2.1" | |
10 | 17 | matrix: |
11 | 18 | exclude: |
12 | 19 | - python: "2.6" |
13 | env: DJANGO_VERSION=1.7.1 | |
20 | env: DJANGO_VERSION="django>=1.7,<1.8" | |
21 | - python: "2.6" | |
22 | env: DJANGO_VERSION="django>=1.8,<1.9" | |
23 | - python: "2.6" | |
24 | env: DJANGO_VERSION="django>=1.9,<1.10" | |
25 | - python: "2.6" | |
26 | env: DJANGO_VERSION="django>=1.10,<1.11" | |
27 | - python: "2.6" | |
28 | env: DJANGO_VERSION="django>=1.11,<1.12" | |
29 | - python: "2.6" | |
30 | env: DJANGO_VERSION="django>=2.0,<2.1" | |
31 | - python: "2.7" | |
32 | env: DJANGO_VERSION="django>=2.0,<2.1" | |
14 | 33 | - python: "3.4" |
15 | env: DJANGO_VERSION=1.4.13 | |
34 | env: DJANGO_VERSION="django>=1.4,<1.5" | |
35 | - python: "3.4" | |
36 | env: DJANGO_VERSION="django>=1.5,<1.6" | |
37 | - python: "3.4" | |
38 | env: DJANGO_VERSION="django>=1.6,<1.7" | |
39 | - python: "3.4" | |
40 | env: DJANGO_VERSION="django>=1.7,<1.8" | |
41 | - python: "3.5" | |
42 | env: DJANGO_VERSION="django>=1.4,<1.5" | |
43 | - python: "3.5" | |
44 | env: DJANGO_VERSION="django>=1.5,<1.6" | |
45 | - python: "3.5" | |
46 | env: DJANGO_VERSION="django>=1.6,<1.7" | |
47 | - python: "3.5" | |
48 | env: DJANGO_VERSION="django>=1.7,<1.8" | |
49 | - python: "3.6" | |
50 | env: DJANGO_VERSION="django>=1.4,<1.5" | |
51 | - python: "3.6" | |
52 | env: DJANGO_VERSION="django>=1.5,<1.6" | |
53 | - python: "3.6" | |
54 | env: DJANGO_VERSION="django>=1.6,<1.7" | |
55 | - python: "3.6" | |
56 | env: DJANGO_VERSION="django>=1.7,<1.8" | |
16 | 57 | install: |
17 | 58 | - pip install -r requirements.txt |
18 | - pip install Django==$DJANGO_VERSION | |
59 | - pip install $DJANGO_VERSION | |
19 | 60 | - python setup.py install |
20 | 61 | script: make test |
0 | ![travis](https://travis-ci.org/mgrouchy/django-stronghold.png?branch=master) | |
0 | [![Build Status](https://travis-ci.org/mgrouchy/django-stronghold.svg?branch=master)](https://travis-ci.org/mgrouchy/django-stronghold) | |
1 | 1 | |
2 | #Stronghold | |
2 | # Stronghold | |
3 | 3 | |
4 | 4 | Get inside your stronghold and make all your Django views default login_required |
5 | 5 | |
7 | 7 | |
8 | 8 | WARNING: still in development, so some of the DEFAULTS and such will be changing without notice. |
9 | 9 | |
10 | ##Installation | |
10 | ## Installation | |
11 | 11 | |
12 | 12 | Install via pip. |
13 | 13 | |
35 | 35 | |
36 | 36 | ``` |
37 | 37 | |
38 | ##Usage | |
38 | ## Usage | |
39 | 39 | |
40 | 40 | If you followed the installation instructions now all your views are defaulting to require a login. |
41 | 41 | To make a view public again you can use the public decorator provided in `stronghold.decorators` like so: |
42 | 42 | |
43 | ###For function based views | |
43 | ### For function based views | |
44 | 44 | ```python |
45 | 45 | from stronghold.decorators import public |
46 | 46 | |
52 | 52 | |
53 | 53 | ``` |
54 | 54 | |
55 | ###for class based views | |
55 | ### For class based views (decorator) | |
56 | 56 | |
57 | 57 | ```python |
58 | 58 | from django.utils.decorators import method_decorator |
69 | 69 | return super(SomeView, self).dispatch(*args, **kwargs) |
70 | 70 | ``` |
71 | 71 | |
72 | ##Configuration (optional) | |
72 | ### For class based views (mixin) | |
73 | ||
74 | ```python | |
75 | from stronghold.views import StrongholdPublicMixin | |
73 | 76 | |
74 | 77 | |
75 | ###STRONGHOLD_DEFAULTS | |
78 | class SomeView(StrongholdPublicMixin, View): | |
79 | pass | |
80 | ``` | |
81 | ||
82 | ## Configuration (optional) | |
83 | ||
84 | ||
85 | ### STRONGHOLD_DEFAULTS | |
76 | 86 | |
77 | 87 | Use Strongholds defaults in addition to your own settings. |
78 | 88 | |
87 | 97 | will be made public without using the `@public` decorator. |
88 | 98 | |
89 | 99 | |
90 | ###STRONGHOLD_PUBLIC_URLS | |
100 | ### STRONGHOLD_PUBLIC_URLS | |
91 | 101 | |
92 | 102 | **Default**: |
93 | 103 | ```python |
108 | 118 | |
109 | 119 | > Note: Public URL regexes are matched against [HttpRequest.path_info](https://docs.djangoproject.com/en/dev/ref/request-response/#django.http.HttpRequest.path_info). |
110 | 120 | |
111 | ###STRONGHOLD_PUBLIC_NAMED_URLS | |
121 | ### STRONGHOLD_PUBLIC_NAMED_URLS | |
112 | 122 | You can add a tuple of url names in your settings file with the |
113 | 123 | `STRONGHOLD_PUBLIC_NAMED_URLS` setting. Names in this setting will be reversed using |
114 | 124 | `django.core.urlresolvers.reverse` and any url matching the output of the reverse |
122 | 132 | If STRONGHOLD_DEFAULTS is True additionally we search for `django.contrib.auth` |
123 | 133 | if it exists, we add the login and logout view names to `STRONGHOLD_PUBLIC_NAMED_URLS` |
124 | 134 | |
125 | ###STRONGHOLD_PERMISSIONS_DECORATOR | |
126 | Optionally configure STRONGHOLD_PERMISSIONS_DECORATOR to be something besides | |
127 | `login_required`. This allows the developer to set this to an alternative | |
128 | decorator like `staff_member_required` or a user created decorator that | |
129 | processes a view function and returns `None` or a `HTTPResponse`. | |
135 | ### STRONGHOLD_USER_TEST_FUNC | |
136 | Optionally, set STRONGHOLD_USER_TEST_FUNC to a callable to limit access to users | |
137 | that pass a custom test. The callback receives a `User` object and should | |
138 | return `True` if the user is authorized. This is equivalent to decorating a | |
139 | view with `user_passes_test`. | |
140 | ||
141 | **Example**: | |
142 | ||
143 | ```python | |
144 | STRONGHOLD_USER_TEST_FUNC = lambda user: user.is_staff | |
145 | ``` | |
130 | 146 | |
131 | 147 | **Default**: |
148 | ||
132 | 149 | ```python |
133 | STRONGHOLD_PERMISSIONS_DECORATOR = login_required | |
150 | STRONGHOLD_USER_TEST_FUNC = lambda user: user.is_authenticated | |
134 | 151 | ``` |
135 | 152 | |
136 | 153 | ##Compatiblity |
140 | 157 | * Django 1.5.x |
141 | 158 | * Django 1.6.x |
142 | 159 | * Django 1.7.x |
160 | * Django 1.8.x | |
161 | * Django 1.9.x | |
162 | * Django 1.10.x | |
163 | * Django 1.11.x | |
164 | * Django 2.0.x | |
143 | 165 | |
144 | ##Contribute | |
166 | ## Contribute | |
145 | 167 | |
146 | 168 | See CONTRIBUTING.md |
61 | 61 | # do some work |
62 | 62 | #... |
63 | 63 | |
64 | for class based views | |
65 | ~~~~~~~~~~~~~~~~~~~~~ | |
64 | for class based views (decorator) | |
65 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
66 | 66 | |
67 | 67 | .. code:: python |
68 | 68 | |
78 | 78 | @method_decorator(public) |
79 | 79 | def dispatch(self, *args, **kwargs): |
80 | 80 | return super(SomeView, self).dispatch(*args, **kwargs) |
81 | ||
82 | for class based views (mixin) | |
83 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
84 | ||
85 | .. code:: python | |
86 | ||
87 | from stronghold import StrongholdPublicMixin | |
88 | ||
89 | class SomeView(StrongholdPublicMixin, View): | |
90 | pass | |
81 | 91 | |
82 | 92 | Configuration (optional) |
83 | 93 | ------------------------ |
142 | 152 | ``django.contrib.auth`` if it exists, we add the login and logout view |
143 | 153 | names to ``STRONGHOLD_PUBLIC_NAMED_URLS`` |
144 | 154 | |
145 | STRONGHOLD\_PERMISSIONS\_DECORATOR | |
146 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
155 | STRONGHOLD\_USER\_TEST\_FUNC | |
156 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
157 | Optionally, set STRONGHOLD_USER_TEST_FUNC to a callable to limit access to users | |
158 | that pass a custom test. The callback receives a ``User`` object and should | |
159 | return ``True`` if the user is authorized. This is equivalent to decorating a | |
160 | view with ``user_passes_test``. | |
147 | 161 | |
148 | Optionally configure STRONGHOLD_PERMISSIONS_DECORATOR to be something besides | |
149 | ``login_required``. This allows the developer to set this to an alternative | |
150 | decorator like ``staff_member_required`` or a user created decorator that | |
151 | processes a view function and returns ``None`` or a ``HTTPResponse``. | |
162 | **Example**: | |
163 | ||
164 | .. code:: python | |
165 | STRONGHOLD_USER_TEST_FUNC = lambda user: user.is_staff | |
152 | 166 | |
153 | 167 | **Default**: |
154 | 168 | |
155 | 169 | .. code:: python |
156 | STRONGHOLD_PERMISSIONS_DECORATOR = login_required | |
170 | STRONGHOLD_USER_TEST_FUNC = lambda user: user.is_authenticated | |
157 | 171 | |
158 | 172 | |
159 | 173 | Compatiblity |
165 | 179 | - Django 1.5.x |
166 | 180 | - Django 1.6.x |
167 | 181 | - Django 1.7.x |
182 | - Django 1.8.x | |
183 | - Django 1.9.x | |
184 | - Django 1.10.x | |
185 | - Django 1.11.x | |
186 | - Django 2.0.x | |
168 | 187 | |
169 | 188 | Contribute |
170 | 189 | ---------- |
11 | 11 | |
12 | 12 | setup( |
13 | 13 | name='django-stronghold', |
14 | version='0.2.7', | |
14 | version='0.3.0', | |
15 | 15 | description='Get inside your stronghold and make all your Django views default login_required', |
16 | 16 | url='https://github.com/mgrouchy/django-stronghold', |
17 | 17 | author='Mike Grouchy', |
24 | 24 | install_requires=dependencies, |
25 | 25 | tests_require=test_dependencies, |
26 | 26 | long_description=open('README.rst').read(), |
27 | classifiers=( | |
27 | classifiers=[ | |
28 | 28 | 'Development Status :: 5 - Production/Stable', |
29 | 29 | 'Intended Audience :: Developers', |
30 | 30 | 'Natural Language :: English', |
34 | 34 | 'Programming Language :: Python :: 2.7', |
35 | 35 | 'Programming Language :: Python :: 3', |
36 | 36 | 'Programming Language :: Python :: 3.4', |
37 | ), | |
37 | 'Programming Language :: Python :: 3.5', | |
38 | 'Programming Language :: Python :: 3.6', | |
39 | ], | |
38 | 40 | ) |
0 | 0 | import re |
1 | 1 | |
2 | from django.core.urlresolvers import reverse, NoReverseMatch | |
2 | try: | |
3 | from django.urls import reverse, NoReverseMatch | |
4 | except ImportError: | |
5 | from django.core.urlresolvers import reverse, NoReverseMatch | |
3 | 6 | from django.conf import settings |
4 | 7 | from django.contrib.auth.decorators import login_required |
5 | ||
6 | 8 | |
7 | 9 | STRONGHOLD_PUBLIC_URLS = getattr(settings, 'STRONGHOLD_PUBLIC_URLS', ()) |
8 | 10 | STRONGHOLD_DEFAULTS = getattr(settings, 'STRONGHOLD_DEFAULTS', True) |
9 | 11 | STRONGHOLD_PUBLIC_NAMED_URLS = getattr(settings, 'STRONGHOLD_PUBLIC_NAMED_URLS', ()) |
10 | STRONGHOLD_PERMISSIONS_DECORATOR = getattr(settings, 'STRONGHOLD_PERMISSIONS_DECORATOR', login_required) | |
12 | ||
13 | def is_authenticated(user): | |
14 | """ make compatible with django 1 and 2 """ | |
15 | try: | |
16 | return user.is_authenticated() | |
17 | except TypeError: | |
18 | return user.is_authenticated | |
19 | ||
20 | STRONGHOLD_USER_TEST_FUNC = getattr(settings, 'STRONGHOLD_USER_TEST_FUNC', is_authenticated) | |
11 | 21 | |
12 | 22 | |
13 | 23 | if STRONGHOLD_DEFAULTS: |
14 | 24 | if 'django.contrib.auth' in settings.INSTALLED_APPS: |
15 | 25 | STRONGHOLD_PUBLIC_NAMED_URLS += ('login', 'logout') |
16 | ||
26 | ||
17 | 27 | # Do not login protect the logout url, causes an infinite loop |
18 | 28 | logout_url = getattr(settings, 'LOGOUT_URL', None) |
19 | 29 | if logout_url: |
0 | from django.contrib.auth.decorators import user_passes_test | |
0 | 1 | from stronghold import conf, utils |
1 | 2 | |
3 | try: | |
4 | from django.utils.deprecation import MiddlewareMixin | |
5 | except ImportError: | |
6 | MiddlewareMixin = object | |
2 | 7 | |
3 | class LoginRequiredMiddleware(object): | |
8 | ||
9 | class LoginRequiredMiddleware(MiddlewareMixin): | |
4 | 10 | """ |
5 | Force all views to use the permissions defined by | |
6 | STRONGHOLD_PERMISSIONS_DECORATOR. Default is login_required, but can use | |
7 | staff_member_required or a user defined decorator | |
11 | Restrict access to users that for which STRONGHOLD_USER_TEST_FUNC returns | |
12 | True. Default is to check if the user is authenticated. | |
8 | 13 | |
9 | 14 | View is deemed to be public if the @public decorator is applied to the view |
10 | 15 | |
15 | 20 | """ |
16 | 21 | |
17 | 22 | def __init__(self, *args, **kwargs): |
23 | if MiddlewareMixin != object: | |
24 | super(LoginRequiredMiddleware, self).__init__(*args, **kwargs) | |
18 | 25 | self.public_view_urls = getattr(conf, 'STRONGHOLD_PUBLIC_URLS', ()) |
19 | 26 | |
20 | 27 | def process_view(self, request, view_func, view_args, view_kwargs): |
21 | if request.user.is_authenticated() or utils.is_view_func_public(view_func) \ | |
28 | if conf.STRONGHOLD_USER_TEST_FUNC(request.user) \ | |
29 | or utils.is_view_func_public(view_func) \ | |
22 | 30 | or self.is_public_url(request.path_info): |
23 | 31 | return None |
24 | 32 | |
25 | return conf.STRONGHOLD_PERMISSIONS_DECORATOR(view_func)(request, *view_args, **view_kwargs) | |
33 | decorator = user_passes_test(conf.STRONGHOLD_USER_TEST_FUNC) | |
34 | return decorator(view_func)(request, *view_args, **view_kwargs) | |
26 | 35 | |
27 | 36 | def is_public_url(self, url): |
28 | 37 | return any(public_url.match(url) for public_url in self.public_view_urls) |
0 | 0 | from stronghold.tests.testdecorators import * |
1 | 1 | from stronghold.tests.testmiddleware import * |
2 | from stronghold.tests.testmixins import * | |
2 | 3 | from stronghold.tests.testutils import * |
1 | 1 | |
2 | 2 | from stronghold import decorators |
3 | 3 | |
4 | from django.utils import unittest | |
4 | import django | |
5 | if django.VERSION[:2] < (1, 9): | |
6 | from django.utils import unittest | |
7 | else: | |
8 | import unittest | |
5 | 9 | |
6 | 10 | |
7 | 11 | class StrongholdDecoratorTests(unittest.TestCase): |
3 | 3 | from stronghold import conf |
4 | 4 | from stronghold.middleware import LoginRequiredMiddleware |
5 | 5 | |
6 | from django.core.urlresolvers import reverse | |
6 | try: | |
7 | from django.urls import reverse | |
8 | except ImportError: | |
9 | from django.core.urlresolvers import reverse | |
10 | ||
7 | 11 | from django.http import HttpResponse |
8 | 12 | from django.test import TestCase |
9 | 13 | from django.test.client import RequestFactory |
35 | 39 | 'request': self.request, |
36 | 40 | } |
37 | 41 | |
42 | def set_authenticated(self, is_authenticated): | |
43 | """Set whether user is authenticated in the request.""" | |
44 | user = self.request.user | |
45 | user.is_authenticated.return_value = is_authenticated | |
46 | ||
47 | # In Django >= 1.10, is_authenticated acts as property and method | |
48 | user.is_authenticated.__bool__ = lambda self: is_authenticated | |
49 | user.is_authenticated.__nonzero__ = lambda self: is_authenticated | |
50 | ||
38 | 51 | def test_redirects_to_login_when_not_authenticated(self): |
39 | self.request.user.is_authenticated.return_value = False | |
52 | self.set_authenticated(False) | |
40 | 53 | |
41 | 54 | response = self.middleware.process_view(**self.kwargs) |
42 | 55 | |
43 | 56 | self.assertEqual(response.status_code, 302) |
44 | 57 | |
45 | 58 | def test_returns_none_when_authenticated(self): |
46 | self.request.user.is_authenticated.return_value = True | |
59 | self.set_authenticated(True) | |
47 | 60 | |
48 | 61 | response = self.middleware.process_view(**self.kwargs) |
49 | 62 | |
50 | 63 | self.assertEqual(response, None) |
51 | 64 | |
52 | 65 | def test_returns_none_when_url_is_in_public_urls(self): |
53 | self.request.user.is_authenticated.return_value = False | |
66 | self.set_authenticated(False) | |
54 | 67 | self.middleware.public_view_urls = [re.compile(r'/test-protected-url/')] |
55 | 68 | |
56 | 69 | response = self.middleware.process_view(**self.kwargs) |
58 | 71 | self.assertEqual(response, None) |
59 | 72 | |
60 | 73 | def test_returns_none_when_url_is_decorated_public(self): |
61 | self.request.user.is_authenticated.return_value = False | |
74 | self.set_authenticated(False) | |
62 | 75 | |
63 | 76 | self.kwargs['view_func'].STRONGHOLD_IS_PUBLIC = True |
64 | 77 | response = self.middleware.process_view(**self.kwargs) |
65 | 78 | |
66 | 79 | self.assertEqual(response, None) |
80 | ||
81 | def test_redirects_to_login_when_not_passing_custom_test(self): | |
82 | with mock.patch('stronghold.conf.STRONGHOLD_USER_TEST_FUNC', lambda u: u.is_staff): | |
83 | self.request.user.is_staff = False | |
84 | ||
85 | response = self.middleware.process_view(**self.kwargs) | |
86 | ||
87 | self.assertEqual(response.status_code, 302) | |
88 | ||
89 | def test_returns_none_when_passing_custom_test(self): | |
90 | with mock.patch('stronghold.conf.STRONGHOLD_USER_TEST_FUNC', lambda u: u.is_staff): | |
91 | self.request.user.is_staff = True | |
92 | ||
93 | response = self.middleware.process_view(**self.kwargs) | |
94 | ||
95 | self.assertEqual(response, None) |
0 | from stronghold.views import StrongholdPublicMixin | |
1 | ||
2 | import django | |
3 | from django.views.generic import View | |
4 | from django.views.generic.base import TemplateResponseMixin | |
5 | ||
6 | if django.VERSION[:2] < (1, 9): | |
7 | from django.utils import unittest | |
8 | else: | |
9 | import unittest | |
10 | ||
11 | ||
12 | class StrongholdMixinsTests(unittest.TestCase): | |
13 | ||
14 | def test_public_mixin_sets_attr(self): | |
15 | ||
16 | class TestView(StrongholdPublicMixin, View): | |
17 | pass | |
18 | ||
19 | self.assertTrue(TestView.dispatch.STRONGHOLD_IS_PUBLIC) | |
20 | ||
21 | def test_public_mixin_sets_attr_with_multiple_mixins(self): | |
22 | ||
23 | class TestView(StrongholdPublicMixin, TemplateResponseMixin, View): | |
24 | template_name = 'dummy.html' | |
25 | ||
26 | self.assertTrue(TestView.dispatch.STRONGHOLD_IS_PUBLIC) |
0 | 0 | from stronghold import utils |
1 | 1 | |
2 | from django.utils import unittest | |
2 | import django | |
3 | if django.VERSION[:2] < (1, 9): | |
4 | from django.utils import unittest | |
5 | else: | |
6 | import unittest | |
3 | 7 | |
4 | 8 | |
5 | 9 | class IsViewFuncPublicTests(unittest.TestCase): |
0 | from django.utils.decorators import method_decorator | |
1 | from stronghold.decorators import public | |
2 | ||
3 | ||
4 | class StrongholdPublicMixin(object): | |
5 | ||
6 | @method_decorator(public) | |
7 | def dispatch(self, *args, **kwargs): | |
8 | return super(StrongholdPublicMixin, self).dispatch(*args, **kwargs) |
64 | 64 | 'stronghold.middleware.LoginRequiredMiddleware', |
65 | 65 | ) |
66 | 66 | |
67 | MIDDLEWARE = MIDDLEWARE_CLASSES | |
68 | ||
67 | 69 | ROOT_URLCONF = 'test_project.urls' |
68 | 70 | |
69 | 71 | # Python dotted path to the WSGI application used by Django's runserver. |
0 | from django.conf.urls import patterns, url | |
0 | from django.conf.urls import url | |
1 | 1 | from . import views |
2 | 2 | |
3 | 3 | |
4 | urlpatterns = patterns( | |
5 | '', | |
4 | urlpatterns = [ | |
6 | 5 | url(r'^protected/$', views.ProtectedView.as_view(), name="protected_view"), |
7 | 6 | url(r'^public/$', views.PublicView.as_view(), name="public_view"), |
8 | ) | |
7 | ] |