7 |
Coming soon...
8 |
9 |
This is not yet available to the public.
10 |
11 | {% if form %}
12 |
16 | {% endif %}
17 |
18 |
19 | {% endblock %}
20 |
--------------------------------------------------------------------------------
/runtests.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import os
4 | import sys
5 |
6 | import django
7 | from django.conf import settings
8 | from django.test.utils import get_runner
9 |
10 |
11 | def runtests(*test_args):
12 | """Set up and run django-lockdowns test suite."""
13 | os.environ['DJANGO_SETTINGS_MODULE'] = 'lockdown.tests.test_settings'
14 |
15 | django.setup()
16 |
17 | if not test_args:
18 | test_args = ['lockdown.tests']
19 |
20 | test_runner = get_runner(settings)()
21 | failures = test_runner.run_tests(test_args)
22 | sys.exit(bool(failures))
23 |
24 |
25 | if __name__ == '__main__':
26 | runtests(*sys.argv[1:])
27 |
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 | Thanks a lot for opening a pull request. :tada:
2 |
3 | To ensure your pull request can get merged as fast as possible, please check
4 | all points in the following list:
5 |
6 | - [ ] When adding new code add corresponding tests as well.
7 | - [ ] Ensure your code follows the [Python Style Guide (PEP8)][1] and the
8 | [Docstring Conventions (PEP257)][2].
9 | - [ ] Update the `AUTHORS.rst`, `CHANGES.rst` and `README.rst` as necessary.
10 | - [ ] Check that [continuous integration (CI)][1] runs succeed after you opened
11 | the pull request and fix the issues it detects.
12 |
13 | Please also replace this template with a description of your changes before you
14 | open the pull request.
15 |
16 | [1]: https://www.python.org/dev/peps/pep-0008/
17 | [2]: https://www.python.org/dev/peps/pep-0257/
18 | [3]: https://github.com/Dunedan/django-lockdown/actions
19 |
--------------------------------------------------------------------------------
/lockdown/tests/test_settings.py:
--------------------------------------------------------------------------------
1 | from random import choice
2 |
3 | DATABASES = {
4 | 'default': {
5 | 'ENGINE': 'django.db.backends.sqlite3'
6 | }
7 | }
8 |
9 | SECRET_KEY = ''.join([choice('abcdefghijklmnopqrstuvwxyz' # nosec
10 | '0123456789!@#$%^&*(-_=+)')
11 | for i in range(64)])
12 |
13 | MIDDLEWARE = [
14 | 'django.contrib.sessions.middleware.SessionMiddleware',
15 | 'django.middleware.common.CommonMiddleware',
16 | 'django.middleware.csrf.CsrfViewMiddleware',
17 | 'django.contrib.auth.middleware.AuthenticationMiddleware'
18 | ]
19 |
20 | INSTALLED_APPS = (
21 | 'django.contrib.sessions',
22 | 'django.contrib.contenttypes',
23 | 'django.contrib.auth',
24 | 'lockdown'
25 | )
26 |
27 | ROOT_URLCONF = 'lockdown.tests.urls'
28 |
29 | TEMPLATES = [
30 | {
31 | 'BACKEND': 'django.template.backends.django.DjangoTemplates',
32 | 'APP_DIRS': True,
33 | 'OPTIONS': {
34 | 'context_processors': [
35 | 'django.template.context_processors.debug',
36 | 'django.template.context_processors.request',
37 | 'django.contrib.auth.context_processors.auth',
38 | 'django.contrib.messages.context_processors.messages',
39 | ],
40 | },
41 | },
42 | ]
43 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) 2009, Carl Meyer
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without
5 | modification, are permitted provided that the following conditions are
6 | met:
7 |
8 | * Redistributions of source code must retain the above copyright
9 | notice, this list of conditions and the following disclaimer.
10 | * Redistributions in binary form must reproduce the above
11 | copyright notice, this list of conditions and the following
12 | disclaimer in the documentation and/or other materials provided
13 | with the distribution.
14 | * Neither the name of the author nor the names of other
15 | contributors may be used to endorse or promote products derived
16 | from this software without specific prior written permission.
17 |
18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | repos:
3 | - repo: https://github.com/pre-commit/pre-commit-hooks
4 | rev: v5.0.0
5 | hooks:
6 | - id: check-added-large-files
7 | - id: check-ast
8 | - id: check-byte-order-marker
9 | - id: check-case-conflict
10 | - id: check-merge-conflict
11 | - id: check-symlinks
12 | - id: check-yaml
13 | - id: debug-statements
14 | - id: detect-aws-credentials
15 | args:
16 | - --allow-missing-credentials
17 | - id: detect-private-key
18 |
19 | - repo: https://github.com/adrienverge/yamllint
20 | rev: v1.37.0
21 | hooks:
22 | - id: yamllint
23 | args:
24 | - -s
25 |
26 | - repo: https://github.com/PyCQA/isort
27 | rev: 6.0.1
28 | hooks:
29 | - id: isort
30 | args:
31 | - --check-only
32 | - --diff
33 |
34 | - repo: https://github.com/prospector-dev/prospector/
35 | rev: v1.16.0
36 | hooks:
37 | - id: prospector
38 | additional_dependencies:
39 | - django
40 | - setuptools
41 |
42 | - repo: https://github.com/Lucas-C/pre-commit-hooks
43 | rev: v1.5.5
44 | hooks:
45 | - id: forbid-crlf
46 | - id: forbid-tabs
47 |
48 | - repo: https://github.com/PyCQA/bandit
49 | rev: 1.8.3
50 | hooks:
51 | - id: bandit
52 | args:
53 | - -l
54 |
55 | - repo: https://github.com/Lucas-C/pre-commit-hooks-safety
56 | rev: v1.4.0
57 | hooks:
58 | - id: python-safety-dependencies-check
59 |
60 | - repo: https://github.com/codespell-project/codespell
61 | rev: v2.4.1
62 | hooks:
63 | - id: codespell
64 |
--------------------------------------------------------------------------------
/lockdown/tests/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path, re_path
2 |
3 | from . import views
4 |
5 | urlpatterns = [
6 | path('a/view/', views.a_view),
7 | path('locked/view/', views.locked_view),
8 | path('overridden/locked/view/', views.overridden_locked_view),
9 | re_path(r'^locked/view/with/exception1/',
10 | views.locked_view_with_exception),
11 | re_path(r'^locked/view/with/exception2/',
12 | views.locked_view_with_exception),
13 | re_path(r'^locked/view/with/ip_exception/',
14 | views.locked_view_with_ip_exception),
15 | re_path(r'^locked/view/with/ip_exception_ipv6/',
16 | views.locked_view_with_ip_exception_ipv6),
17 | re_path(r'^locked/view/with/ip_exception_subnet/',
18 | views.locked_view_with_ip_exception_subnet),
19 | re_path(r'^locked/view/with/ip_exception_ipv6_subnet/',
20 | views.locked_view_with_ip_exception_ipv6_subnet),
21 | re_path(r'^locked/view/with/extra/context/',
22 | views.locked_view_with_extra_context),
23 | re_path(r'^locked/view/until/yesterday/',
24 | views.locked_view_until_yesterday),
25 | re_path(r'^locked/view/until/tomorrow/',
26 | views.locked_view_until_tomorrow),
27 | re_path(r'^locked/view/after/yesterday/',
28 | views.locked_view_after_yesterday),
29 | re_path(r'^locked/view/after/tomorrow/',
30 | views.locked_view_after_tomorrow),
31 | re_path(r'^locked/view/until/and/after/',
32 | views.locked_view_until_and_after),
33 | path('auth/user/locked/view/', views.user_locked_view),
34 | path('auth/staff/locked/view/', views.staff_locked_view),
35 | path('auth/superuser/locked/view/', views.superuser_locked_view),
36 | ]
37 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | ---
2 | name: CI
3 | on:
4 | - push
5 | - pull_request
6 | jobs:
7 | test:
8 | runs-on: ubuntu-22.04
9 | strategy:
10 | fail-fast: false
11 | matrix:
12 | python-version:
13 | - "3.9"
14 | - "3.10"
15 | - "3.11"
16 | - "3.12"
17 | - "3.13"
18 | - "pypy3.10"
19 | django-version:
20 | - Django>=4.2,<5.0
21 | - Django>=5.0,<5.1
22 | - Django>=5.1,<5.2
23 | - Django>=5.2,<6.0
24 | exclude:
25 | - python-version: "3.9"
26 | django-version: "Django>=5.0,<5.1"
27 | - python-version: "3.9"
28 | django-version: "Django>=5.1,<5.2"
29 | - python-version: "3.9"
30 | django-version: "Django>=5.2,<6.0"
31 | - python-version: "3.13"
32 | django-version: "Django>=4.2,<5.0"
33 | - python-version: "3.13"
34 | django-version: "Django>=5.0,<5.1"
35 | steps:
36 | - uses: actions/checkout@v4
37 | - uses: actions/setup-python@v5
38 | with:
39 | python-version: ${{ matrix.python-version }}
40 | - name: Install Django
41 | run: pip install "${{ matrix.django-version}}"
42 | - name: Install coveralls
43 | run: pip install coveralls
44 | - name: Run tests
45 | run: coverage run ./runtests.py
46 | - name: Run coveralls
47 | run: coveralls --service=github
48 | env:
49 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
50 | pre-commit:
51 | runs-on: ubuntu-22.04
52 | steps:
53 | - uses: actions/checkout@v4
54 | - uses: actions/setup-python@v5
55 | with:
56 | python-version: "3.11"
57 | - uses: pre-commit/action@v3.0.1
58 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import find_packages, setup
2 |
3 | with open('README.rst', encoding='utf-8') as rfd:
4 | with open('CHANGES.rst', encoding='utf-8') as cfd:
5 | LONG_DESCRIPTION = "\n".join([rfd.read(), cfd.read()])
6 |
7 | setup(
8 | name='django-lockdown',
9 | version='5.0.0',
10 | description=('Lock down a Django site or individual views, with '
11 | 'configurable preview authorization'),
12 | long_description=LONG_DESCRIPTION,
13 | long_description_content_type='text/x-rst',
14 | author='Carl Meyer',
15 | author_email='carl@dirtcircle.com',
16 | maintainer='Daniel Roschka',
17 | maintainer_email='danielroschka@phoenitydawn.de',
18 | url='https://github.com/Dunedan/django-lockdown/',
19 | packages=find_packages(exclude=['lockdown.tests']),
20 | python_requires='>=3.9',
21 | install_requires=['Django>=4.2'],
22 | classifiers=[
23 | 'Development Status :: 5 - Production/Stable',
24 | 'Environment :: Web Environment',
25 | 'Intended Audience :: Developers',
26 | 'License :: OSI Approved :: BSD License',
27 | 'Operating System :: OS Independent',
28 | 'Programming Language :: Python',
29 | 'Programming Language :: Python :: 3.9',
30 | 'Programming Language :: Python :: 3.10',
31 | 'Programming Language :: Python :: 3.11',
32 | 'Programming Language :: Python :: 3.12',
33 | 'Programming Language :: Python :: 3.13',
34 | 'Programming Language :: Python :: Implementation :: CPython',
35 | 'Programming Language :: Python :: Implementation :: PyPy',
36 | 'Framework :: Django',
37 | 'Framework :: Django :: 4.2',
38 | 'Framework :: Django :: 5.0',
39 | 'Framework :: Django :: 5.1',
40 | 'Framework :: Django :: 5.2',
41 | ],
42 | zip_safe=False,
43 | test_suite='runtests.runtests',
44 | package_data={'lockdown': ['templates/lockdown/*.html']},
45 | )
46 |
--------------------------------------------------------------------------------
/lockdown/tests/views.py:
--------------------------------------------------------------------------------
1 | # pylint: disable=invalid-name
2 |
3 | import datetime
4 |
5 | from django.http import HttpResponse
6 |
7 | from lockdown.decorators import lockdown
8 | from lockdown.forms import AuthForm
9 |
10 | YESTERDAY = datetime.datetime.now() - datetime.timedelta(days=1)
11 | TOMORROW = datetime.datetime.now() + datetime.timedelta(days=1)
12 |
13 |
14 | def a_view(request):
15 | """Regular unlocked view."""
16 | return HttpResponse('A view.')
17 |
18 |
19 | @lockdown()
20 | def locked_view(request):
21 | """View, locked by the default lockdown decorator."""
22 | return HttpResponse('A locked view.')
23 |
24 |
25 | @lockdown(passwords=('squirrel',))
26 | def overridden_locked_view(request):
27 | """View, locked by the decorator with a custom password."""
28 | return HttpResponse('A locked view.')
29 |
30 |
31 | @lockdown(url_exceptions=(r'^/locked/view/with/exception2/',))
32 | def locked_view_with_exception(request):
33 | """View, locked by the decorator with url exceptions."""
34 | return HttpResponse('A locked view.')
35 |
36 |
37 | @lockdown(remote_addr_exceptions=['192.168.0.1'])
38 | def locked_view_with_ip_exception(request):
39 | """View, locked except for the configured IPv4-address."""
40 | return HttpResponse('A locked view.')
41 |
42 |
43 | @lockdown(remote_addr_exceptions=['fd::1'])
44 | def locked_view_with_ip_exception_ipv6(request):
45 | """View, locked except for the configured IPv6-address."""
46 | return HttpResponse('A locked view.')
47 |
48 |
49 | @lockdown(remote_addr_exceptions=['192.168.0.0/24'])
50 | def locked_view_with_ip_exception_subnet(request):
51 | """View, locked except for the configured IPv4-subnet."""
52 | return HttpResponse('A locked view.')
53 |
54 |
55 | @lockdown(remote_addr_exceptions=['fd::0/80'])
56 | def locked_view_with_ip_exception_ipv6_subnet(request):
57 | """View, locked except for the configured IPv6-subnet."""
58 | return HttpResponse('A locked view.')
59 |
60 |
61 | @lockdown(extra_context={'foo': 'bar'})
62 | def locked_view_with_extra_context(request):
63 | """View, locked by the decorator with extra context."""
64 | return HttpResponse('A locked view.')
65 |
66 |
67 | @lockdown(until_date=YESTERDAY)
68 | def locked_view_until_yesterday(request):
69 | """View, locked till yesterday."""
70 | return HttpResponse('A locked view.')
71 |
72 |
73 | @lockdown(until_date=TOMORROW)
74 | def locked_view_until_tomorrow(request):
75 | """View, locked till tomorrow."""
76 | return HttpResponse('A locked view.')
77 |
78 |
79 | @lockdown(after_date=YESTERDAY)
80 | def locked_view_after_yesterday(request):
81 | """View, locked since yesterday."""
82 | return HttpResponse('A locked view.')
83 |
84 |
85 | @lockdown(after_date=TOMORROW)
86 | def locked_view_after_tomorrow(request):
87 | """View, locked starting from tomorrow."""
88 | return HttpResponse('A locked view.')
89 |
90 |
91 | @lockdown(until_date=YESTERDAY, after_date=TOMORROW)
92 | def locked_view_until_and_after(request):
93 | """View, only not looked between yesterday and tomorrow."""
94 | return HttpResponse('A locked view.')
95 |
96 |
97 | @lockdown(form=AuthForm, staff_only=False)
98 | def user_locked_view(request):
99 | """View, locked by the decorator with access for known users only."""
100 | return HttpResponse('A locked view.')
101 |
102 |
103 | @lockdown(form=AuthForm)
104 | def staff_locked_view(request):
105 | """View, locked by the decorator with access for staff users only."""
106 | return HttpResponse('A locked view.')
107 |
108 |
109 | @lockdown(form=AuthForm, superusers_only=True)
110 | def superuser_locked_view(request):
111 | """View, locked by the decorator with access for superusers only."""
112 | return HttpResponse('A locked view.')
113 |
--------------------------------------------------------------------------------
/lockdown/forms.py:
--------------------------------------------------------------------------------
1 | from django import forms
2 | from django.conf import settings
3 | from django.contrib import auth
4 | from django.contrib.auth.forms import AuthenticationForm
5 |
6 |
7 | class LockdownForm(forms.Form):
8 | """Defines a form to enter a password for accessing locked down content."""
9 |
10 | password = forms.CharField(widget=forms.PasswordInput())
11 |
12 | # pylint: disable=keyword-arg-before-vararg
13 | def __init__(self, passwords=None, *args, **kwargs):
14 | """Initialize the form by setting the valid passwords."""
15 | super().__init__(*args, **kwargs)
16 | if passwords is None:
17 | passwords = getattr(settings, 'LOCKDOWN_PASSWORDS', ())
18 | if not isinstance(passwords, (tuple, list)):
19 | passwords = (passwords,) if passwords else ()
20 |
21 | self.valid_passwords = passwords
22 |
23 | def clean_password(self):
24 | """Check that the password is valid."""
25 | value = self.cleaned_data.get('password')
26 | if value not in self.valid_passwords:
27 | raise forms.ValidationError('Incorrect password.')
28 | return value
29 |
30 | def generate_token(self):
31 | """Save the password as the authentication token.
32 |
33 | It's acceptable to store the password raw, as it is stored server-side
34 | in the user's session.
35 | """
36 | return self.cleaned_data['password']
37 |
38 | def authenticate(self, token_value):
39 | """Check that the password is valid.
40 |
41 | This allows for revoking of a user's preview rights by changing the
42 | valid passwords.
43 | """
44 | return token_value in self.valid_passwords
45 |
46 | def show_form(self):
47 | """Show the form if there are any valid passwords."""
48 | return bool(self.valid_passwords)
49 |
50 |
51 | class AuthForm(AuthenticationForm):
52 | """Defines a form using Djangos authentication to access locked content.
53 |
54 | This form is a sample implementation of how to use a custom form to provide
55 | access to locked down content.
56 | """
57 |
58 | # pylint: disable=keyword-arg-before-vararg
59 | def __init__(self, staff_only=None, superusers_only=None, *args,
60 | **kwargs):
61 | """Initialize the form by setting permissions needed for access."""
62 | super().__init__(*args, **kwargs)
63 | if staff_only is None:
64 | staff_only = getattr(settings,
65 | 'LOCKDOWN_AUTHFORM_STAFF_ONLY',
66 | True)
67 | if superusers_only is None:
68 | superusers_only = getattr(settings,
69 | 'LOCKDOWN_AUTHFORM_SUPERUSERS_ONLY',
70 | False)
71 | self.staff_only = staff_only
72 | self.superusers_only = superusers_only
73 |
74 | def clean(self):
75 | """When receiving the filled out form, check for valid access."""
76 | cleaned_data = super().clean()
77 | user = self.get_user()
78 | if self.staff_only and (not user or not user.is_staff):
79 | raise forms.ValidationError('Sorry, only staff are allowed.')
80 | if self.superusers_only and (not user or not user.is_superuser):
81 | raise forms.ValidationError('Sorry, only superusers are allowed.')
82 | return cleaned_data
83 |
84 | def generate_token(self):
85 | """Save the password as the authentication token.
86 |
87 | It's acceptable to store the password raw, as it is stored server-side
88 | in the user's session.
89 | """
90 | user = self.get_user()
91 | return f'{user.backend}:{user.pk}'
92 |
93 | def authenticate(self, token_value):
94 | """Check that the password is valid.
95 |
96 | This allows for revoking of a user's preview rights by changing the
97 | valid passwords.
98 | """
99 | try:
100 | backend_path, user_id = token_value.split(':', 1)
101 | except (ValueError, AttributeError):
102 | return False
103 | backend = auth.load_backend(backend_path)
104 | return bool(backend.get_user(user_id))
105 |
106 | def show_form(self):
107 | """Determine if the form should be shown on locked pages."""
108 | return True
109 |
--------------------------------------------------------------------------------
/CHANGES.rst:
--------------------------------------------------------------------------------
1 | CHANGES
2 | =======
3 |
4 | 5.0.0 (2025-04-25)
5 | ------------------
6 |
7 | - Remove support for end-of-life Django versions (from 2.2 until 4.1).
8 |
9 | - Remove support for end-of-life Python versions (3.6, 3.7 and 3.8).
10 |
11 | - Add support for Django 4.2, 5.0, 5.1 and 5.2.
12 |
13 | - Add support for Python 3.10, 3.11, 3.12 and 3.13.
14 |
15 | - Updated pre-commit hooks and modernize code style. Most notably started to
16 | use f-strings.
17 |
18 | 4.0.0 (2021-02-14)
19 | ------------------
20 |
21 | - Remove support for end-of-life Django versions (1.11, 2.0 and 2.1).
22 |
23 | - Add support for Python 3.9 and remove support for Python 3.5.
24 |
25 | - Add support for Django 3.1.
26 |
27 | 3.0.0. (2020-01-01)
28 | -------------------
29 |
30 | - Added support for Python 3.8.
31 |
32 | - Added support for Django 3.0.
33 |
34 | - Removed support for Python 2.7 and 3.4.
35 |
36 | 2.0.0 (2019-05-26)
37 | ------------------
38 |
39 | - Added support for proxies when using IP-address based lockdown exceptions.
40 |
41 | - This introduces a breaking change: Installations running behind a proxy will
42 | need to set the newly introduced ``LOCKDOWN_TRUSTED_PROXIES``, otherwise
43 | access won't be granted anymore, when accessing the site through a proxy.
44 |
45 | - Added the ability to whitelist views when locking down a whole site using
46 | the middleware.
47 |
48 | - Added support for Django 2.2.
49 |
50 | - Only require ``mock`` as separate third-party test dependency for
51 | Python <3.3.
52 |
53 | - Fix detection of compacted IP-addresses.
54 |
55 | - This introduces a breaking change for users which make use of the
56 | ``REMOTE_ADDR_EXCEPTIONS`` feature and passed the IP-addresses to except as
57 | byte strings in the configuration. While it's unlikely somebody did that
58 | with Python 3, it's the default for Python 2. With this version, byte
59 | strings don't work anymore, but using unicode strings is required.
60 |
61 | - Add the ability to specify IP-subnets for remote addresses exception.
62 |
63 | 1.6.0 (2018-11-25)
64 | ------------------
65 |
66 | - Drops support for Django <=1.10.
67 |
68 | - Drops support for Python 3.3.
69 |
70 | - Add the ability to bypass the lockdown for configured IP-addresses.
71 |
72 | - Integrate pre-commit for code style checks during commit and CI.
73 |
74 | - Added support for Django 2.1.
75 |
76 | - Add support for Python 3.7.
77 |
78 | - Add support for PyPy.
79 |
80 | 1.5.0 (2017-12-05)
81 | ------------------
82 |
83 | - Add support for Django 2.0
84 |
85 | - Improve the code style in some areas
86 |
87 | 1.4.2 (2017-04-07)
88 | ------------------
89 |
90 | - Fix formatting for PyPi
91 |
92 |
93 | 1.4.1 (2017-04-07)
94 | ------------------
95 |
96 | - Fix problem with upload for PyPi
97 |
98 |
99 | 1.4.0 (2017-04-06)
100 | ------------------
101 |
102 | - Refactor tests to use Mocks
103 |
104 | - Add support for Python 3.6
105 |
106 | - Add support for Django 1.11
107 |
108 |
109 | 1.3 (2016-08-07)
110 | ----------------
111 |
112 | - Adds support for Django 1.10.
113 |
114 | - Adds support for providing additional context data to the lockdown template.
115 |
116 |
117 | 1.2 (2015-12-03)
118 | ----------------
119 |
120 | - Adds support for Python 3.5.
121 |
122 | - Adds support for Django 1.9.
123 |
124 | - Drops support for Django <=1.7.
125 |
126 | - Fixes not working URL exceptions when specifying them in the decorator
127 | arguments.
128 |
129 | - Improves tests.
130 |
131 | 1.1 (2015-04-06)
132 | ----------------
133 |
134 | - Proper new version after 0.1.2 and 0.1.3 have been tagged after the release
135 | of 1.0. Contains all new features of 0.1.2 and 0.1.3, most notably support
136 | for Python 3.
137 |
138 | - Last version of django-lockdown with support for Django 1.3, 1.5 and 1.6.
139 | Upcoming versions will only support Django versions with official security
140 | support. For the time being these are Django 1.4 LTS, 1.7 and 1.8 LTS.
141 |
142 | - Fixes testing for Django >=1.7
143 |
144 | 0.1.3 (2014-03-15) (never released)
145 | -----------------------------------
146 |
147 | - Added ``LOCKDOWN_ENABLED`` setting.
148 |
149 | - Removed Django 1.1 backport of ``decorator_from_middleware_with_args``.
150 |
151 | 0.1.2 (2014-03-15) (never released)
152 | -----------------------------------
153 |
154 | - Require at least Django 1.3.
155 |
156 | - Fixed the test runner script to work with recent Django versions.
157 |
158 | - Added the csrf_token template tag to the included form template.
159 |
160 | - Minor syntax adjustments for Python 3 compatibility.
161 |
162 | 1.0 (2013-07-10)
163 | ----------------
164 |
165 | - BACKWARDS INCOMPATIBLE: Allow multiple passwords (the passwords setting has
166 | changed from ``LOCKDOWN_PASSWORD`` to ``LOCKDOWN_PASSWORDS``).
167 |
168 | - Decorator changed to a callable decorator (so settings can be overridden for
169 | an individual decorator).
170 |
171 | - Add ``AuthForm`` which can be used to allow previewing from authenticated
172 | users (via ``django.contrib.auth``).
173 |
174 | - Allow locking up until or only after certain dates.
175 |
176 | 0.1.1 (2009-11-24)
177 | ------------------
178 |
179 | - Fix setup.py so ``tests`` package is not installed.
180 |
181 | 0.1 (2009-11-16)
182 | ----------------
183 |
184 | - Initial release.
185 |
--------------------------------------------------------------------------------
/lockdown/middleware.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | import ipaddress
3 | import re
4 | from importlib import import_module
5 |
6 | from django.conf import settings
7 | from django.core.exceptions import ImproperlyConfigured
8 | from django.http import HttpResponseRedirect
9 | from django.shortcuts import render
10 | from django.urls import Resolver404, resolve
11 |
12 |
13 | def compile_url_exceptions(url_exceptions):
14 | """Return a list of compiled regex objects, containing the url exceptions.
15 |
16 | All URLs in that list returned won't be considered as locked.
17 | """
18 | return [re.compile(p) for p in url_exceptions]
19 |
20 |
21 | def get_lockdown_form(form_path):
22 | """Return a form class for a given string pointing to a lockdown form."""
23 | if not form_path:
24 | raise ImproperlyConfigured('No LOCKDOWN_FORM specified.')
25 | form_path_list = form_path.split(".")
26 | new_module = ".".join(form_path_list[:-1])
27 | attr = form_path_list[-1]
28 | try:
29 | mod = import_module(new_module)
30 | except (ImportError, ValueError):
31 | # pylint: disable=raise-missing-from
32 | raise ImproperlyConfigured("Module configured in LOCKDOWN_FORM "
33 | f"({new_module}) to contain the form class "
34 | "couldn't be " "found.")
35 | try:
36 | form = getattr(mod, attr)
37 | except AttributeError:
38 | # pylint: disable=raise-missing-from
39 | raise ImproperlyConfigured('The module configured in LOCKDOWN_FORM '
40 | f" ({new_module}) doesn't define a '{attr}'"
41 | " form.")
42 | return form
43 |
44 |
45 | # pylint: disable=too-many-instance-attributes
46 | class LockdownMiddleware(object):
47 | """Middleware to lock down a whole Django site."""
48 |
49 | # pylint: disable=too-many-arguments,too-many-positional-arguments
50 | def __init__(self, get_response=None, form=None, until_date=None,
51 | after_date=None, logout_key=None, session_key=None,
52 | url_exceptions=None, remote_addr_exceptions=None,
53 | trusted_proxies=None, extra_context=None, **form_kwargs):
54 | """Initialize the middleware, by setting the configuration values."""
55 | if logout_key is None:
56 | logout_key = getattr(settings,
57 | 'LOCKDOWN_LOGOUT_KEY',
58 | 'preview-logout')
59 | if session_key is None:
60 | session_key = getattr(settings,
61 | 'LOCKDOWN_SESSION_KEY',
62 | 'lockdown-allow')
63 | self.get_response = get_response
64 | self.form = form
65 | self.form_kwargs = form_kwargs
66 | self.until_date = until_date
67 | self.after_date = after_date
68 | self.logout_key = logout_key
69 | self.session_key = session_key
70 | self.url_exceptions = url_exceptions
71 | self.remote_addr_exceptions = remote_addr_exceptions
72 | self.trusted_proxies = trusted_proxies
73 | self.extra_context = extra_context
74 |
75 | def __call__(self, request):
76 | """Handle calls to the class instance."""
77 | response = self.process_request(request)
78 |
79 | if not response:
80 | response = self.get_response(request)
81 |
82 | return response
83 |
84 | # pylint: disable=too-many-locals,too-many-return-statements
85 | # pylint: disable=too-many-statements,too-many-branches
86 | def process_request(self, request):
87 | """Check if each request is allowed to access the current resource."""
88 | try:
89 | session = request.session
90 | except AttributeError as exc:
91 | raise ImproperlyConfigured('django-lockdown requires the Django '
92 | 'sessions framework') from exc
93 |
94 | # Don't lock down if django-lockdown is disabled altogether.
95 | if getattr(settings, 'LOCKDOWN_ENABLED', True) is False:
96 | return None
97 |
98 | # Don't lock down if the client REMOTE_ADDR matched and is part of the
99 | # exception list.
100 | if self.remote_addr_exceptions:
101 | remote_addr_exceptions = self.remote_addr_exceptions
102 | else:
103 | remote_addr_exceptions = getattr(settings,
104 | 'LOCKDOWN_REMOTE_ADDR_EXCEPTIONS',
105 | [])
106 |
107 | remote_addr_exceptions = [ipaddress.ip_network(ip)
108 | for ip in remote_addr_exceptions]
109 | if remote_addr_exceptions:
110 | # If forwarding proxies are used they must be listed as trusted
111 | trusted_proxies = self.trusted_proxies or \
112 | getattr(settings, 'LOCKDOWN_TRUSTED_PROXIES', [])
113 | trusted_proxies = [ipaddress.ip_network(ip)
114 | for ip in trusted_proxies]
115 |
116 | remote_addr = ipaddress.ip_address(request.META.get('REMOTE_ADDR'))
117 | if any(remote_addr for ip_exceptions in remote_addr_exceptions
118 | if remote_addr in ip_exceptions):
119 | return None
120 |
121 | if any(remote_addr for proxy in trusted_proxies
122 | if remote_addr in proxy):
123 | # If REMOTE_ADDR is a trusted proxy check x-forwarded-for
124 | x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
125 | if x_forwarded_for:
126 | remote_addr = ipaddress.ip_address(
127 | x_forwarded_for.split(',')[-1].strip())
128 | if any(remote_addr for ip_exceptions in
129 | remote_addr_exceptions
130 | if remote_addr in ip_exceptions):
131 | return None
132 |
133 | # Don't lock down if the URL matches an exception pattern.
134 | if self.url_exceptions:
135 | url_exceptions = compile_url_exceptions(self.url_exceptions)
136 | else:
137 | url_exceptions = compile_url_exceptions(
138 | getattr(settings, 'LOCKDOWN_URL_EXCEPTIONS', ()))
139 | for pattern in url_exceptions:
140 | if pattern.search(request.path):
141 | return None
142 |
143 | # Don't lock down if the URL resolves to a whitelisted view.
144 | try:
145 | resolved_path = resolve(request.path)
146 | except Resolver404:
147 | pass
148 | else:
149 | if resolved_path.func in getattr(
150 | settings, 'LOCKDOWN_VIEW_EXCEPTIONS', []):
151 | return None
152 |
153 | # Don't lock down if outside of the lockdown dates.
154 | if self.until_date:
155 | until_date = self.until_date
156 | else:
157 | until_date = getattr(settings, "LOCKDOWN_UNTIL_DATE", None)
158 |
159 | if self.after_date:
160 | after_date = self.after_date
161 | else:
162 | after_date = getattr(settings, "LOCKDOWN_AFTER_DATE", None)
163 |
164 | if until_date or after_date:
165 | locked_date = False
166 | if until_date and datetime.datetime.now() < until_date:
167 | locked_date = True
168 | if after_date and datetime.datetime.now() > after_date:
169 | locked_date = True
170 | if not locked_date:
171 | return None
172 |
173 | form_data = request.POST if request.method == 'POST' else None
174 | if self.form:
175 | form_class = self.form
176 | else:
177 | form_class = get_lockdown_form(
178 | getattr(settings,
179 | 'LOCKDOWN_FORM',
180 | 'lockdown.forms.LockdownForm'))
181 | form = form_class(data=form_data, **self.form_kwargs)
182 |
183 | authorized = False
184 | token = session.get(self.session_key)
185 | if hasattr(form, 'authenticate'):
186 | if form.authenticate(token):
187 | authorized = True
188 | elif token is True:
189 | authorized = True
190 |
191 | if authorized and self.logout_key and self.logout_key in request.GET:
192 | if self.session_key in session:
193 | del session[self.session_key]
194 | querystring = request.GET.copy()
195 | del querystring[self.logout_key]
196 | return self.redirect(request)
197 |
198 | # Don't lock down if the user is already authorized for previewing.
199 | if authorized:
200 | return None
201 |
202 | if form.is_valid():
203 | if hasattr(form, 'generate_token'):
204 | token = form.generate_token()
205 | else:
206 | token = True
207 | session[self.session_key] = token
208 | return self.redirect(request)
209 |
210 | page_data = {'until_date': until_date, 'after_date': after_date}
211 | if not hasattr(form, 'show_form') or form.show_form():
212 | page_data['form'] = form
213 |
214 | if self.extra_context:
215 | page_data.update(self.extra_context)
216 |
217 | return render(request, 'lockdown/form.html', page_data)
218 |
219 | def redirect(self, request):
220 | """Handle redirects properly."""
221 | url = request.path
222 | querystring = request.GET.copy()
223 | if self.logout_key and self.logout_key in request.GET:
224 | del querystring[self.logout_key]
225 | if querystring:
226 | url = f'{url}?{querystring.urlencode()}'
227 | return HttpResponseRedirect(url)
228 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | ===============
2 | django-lockdown
3 | ===============
4 |
5 | .. image:: https://github.com/Dunedan/django-lockdown/workflows/CI/badge.svg
6 | :target: https://github.com/Dunedan/django-lockdown/actions
7 | :alt: Build Status
8 | .. image:: https://coveralls.io/repos/Dunedan/django-lockdown/badge.svg
9 | :target: https://coveralls.io/r/Dunedan/django-lockdown
10 | :alt: Test Coverage
11 | .. image:: https://img.shields.io/pypi/v/django-lockdown.svg
12 | :target: https://pypi.org/project/django-lockdown/
13 | :alt: Latest Version
14 |
15 | ``django-lockdown`` is a reusable Django application for locking down an entire
16 | site (or particular views), with customizable date ranges and preview
17 | authorization.
18 |
19 | Installation
20 | ============
21 |
22 | Install from PyPI with ``easy_install`` or ``pip``::
23 |
24 | pip install django-lockdown
25 |
26 | To use ``django-lockdown`` in your Django project:
27 |
28 | 1. Add ``'lockdown'`` to your ``INSTALLED_APPS``.
29 | If you want to use one of ``django-lockdowns`` default lock down forms,
30 | you'll additionally have to ensure that you have enabled
31 | ``django.contrib.auth`` as part of to your ``INSTALLED_APPS``.
32 |
33 | 2. To enable admin preview of locked-down sites or views with
34 | passwords, set the `LOCKDOWN_PASSWORDS`_ setting to a tuple of one or
35 | more plain-text passwords.
36 |
37 | 3. Protect the entire site by using middleware, or protect individual views
38 | by applying a decorator to them.
39 |
40 | For more advanced customization of admin preview authorization, see
41 | the `LOCKDOWN_FORM`_ setting.
42 |
43 | Dependencies
44 | ------------
45 |
46 | ``django-lockdown`` requires `Python`_ 3.9 or later and `Django`_ 4.2 or later.
47 |
48 | As an alternative to CPython `PyPy`_ 3.10 is supported as well.
49 |
50 | .. _Python: https://www.python.org/
51 | .. _Django: https://www.djangoproject.com/
52 | .. _PyPy: https://pypy.org/
53 |
54 | Usage
55 | =====
56 |
57 | Using the middleware
58 | --------------------
59 |
60 | To lock down the entire site, add the lockdown middleware to your middlewares::
61 |
62 | MIDDLEWARE = [
63 | # ...
64 | 'lockdown.middleware.LockdownMiddleware',
65 | ]
66 |
67 | Optionally, you may also add URL regular expressions to a
68 | `LOCKDOWN_URL_EXCEPTIONS`_ setting.
69 |
70 | Using the decorator
71 | -------------------
72 |
73 | - Import the decorator::
74 |
75 | from lockdown.decorators import lockdown
76 |
77 | - Apply the decorator to individual views you want to protect. For example::
78 |
79 | @lockdown()
80 | def secret_page(request):
81 | # ...
82 |
83 | The decorator accepts seven arguments:
84 |
85 | ``form``
86 | The form to use for providing an admin preview, rather than the form
87 | referenced by `LOCKDOWN_FORM`_. Note that this must be an actual form class,
88 | not a module reference like the setting.
89 |
90 | ``until_date``
91 | The date to use rather than the date provided by `LOCKDOWN_UNTIL`_.
92 |
93 | ``after_date``
94 | The date to use rather than the date provided by `LOCKDOWN_AFTER`_.
95 |
96 | ``logout_key``
97 | A preview logout key to use, rather than the one provided by
98 | `LOCKDOWN_LOGOUT_KEY`_.
99 |
100 | ``session_key``
101 | The session key to use, rather than the one provided by
102 | `LOCKDOWN_SESSION_KEY`_.
103 |
104 | ``url_exceptions``
105 | A list of regular expressions for which matching urls can bypass the lockdown
106 | (rather than using those defined in `LOCKDOWN_URL_EXCEPTIONS`_).
107 |
108 | ``remote_addr_exceptions``
109 | A list of IP-addresses or IP-subnets for which matching URLs can bypass the
110 | lockdown (rather than using those defined in
111 | `LOCKDOWN_REMOTE_ADDR_EXCEPTIONS`_).
112 |
113 | ``extra_context``
114 | A dictionary of context data that will be added to the default context data
115 | passed to the template.
116 |
117 | Any further keyword arguments are passed to the admin preview form. The default
118 | form accepts one argument:
119 |
120 | ``passwords``
121 | A tuple of passwords to use, rather than the ones provided by
122 | `LOCKDOWN_PASSWORDS`_.
123 |
124 |
125 | Settings
126 | ========
127 |
128 | LOCKDOWN_ENABLED
129 | ----------------
130 |
131 | An optional boolean value that, if set to False, disables
132 | ``django-lockdown`` globally. Defaults to True (lock down enabled).
133 |
134 |
135 | LOCKDOWN_PASSWORDS
136 | ------------------
137 |
138 | One or more plain-text passwords which allow the previewing of the site or
139 | views protected by django-lockdown::
140 |
141 | LOCKDOWN_PASSWORDS = ('letmein', 'beta')
142 |
143 | If this setting is not provided (and the default `LOCKDOWN_FORM`_ is being
144 | used), there will be no admin preview for locked-down pages.
145 |
146 | If a `LOCKDOWN_FORM`_ other than the default is used, this setting has no
147 | effect.
148 |
149 | LOCKDOWN_URL_EXCEPTIONS
150 | -----------------------
151 |
152 | An optional list/tuple of regular expressions to be matched against incoming
153 | URLs. If a URL matches a regular expression in this list, it will not be
154 | locked. For example::
155 |
156 | LOCKDOWN_URL_EXCEPTIONS = (
157 | r'^/about/$', # unlock /about/
158 | r'\.json$', # unlock JSON API
159 | )
160 |
161 | LOCKDOWN_VIEW_EXCEPTIONS
162 | ------------------------
163 |
164 | An optional list of regular expressions to be matched against the
165 | resolved views of incoming requests. If the URL of an incoming request
166 | resolves to one of the views in the list, it will not be locked.
167 | That's useful if you want to lock down a whole site using the middleware,
168 | but want to whitelist some localized URLs.
169 |
170 | For example::
171 |
172 | from yourapp import one_view_to_unlock, another_view_to_unlock
173 |
174 | LOCKDOWN_VIEW_EXCEPTIONS = [
175 | one_view_to_unlock,
176 | another_view_to_unlock
177 | ]
178 |
179 | LOCKDOWN_REMOTE_ADDR_EXCEPTIONS
180 | -------------------------------
181 |
182 | An optional list of IP-addresses or IP-subnets to be matched against the
183 | requesting IP-address (from `requests.META['REMOTE_ADDR']`). If the
184 | requesting IP-address is in this list, it will not be locked. For example::
185 |
186 | LOCKDOWN_REMOTE_ADDR_EXCEPTIONS = [
187 | '127.0.0.1',
188 | '::1',
189 | ]
190 |
191 | LOCKDOWN_TRUSTED_PROXIES
192 | -------------------------------
193 |
194 | A list of trusted proxy IP-addresses to be used in conjunction with
195 | `LOCKDOWN_REMOTE_ADDR_EXCEPTIONS` when a reverse-proxy or load balancer is used.
196 | If the requesting IP address is from the trusted proxies list the last address from
197 | the `X-Forwared-For` header (from `requests.META['HTTP_X_FORWARDED_FOR']`) will be
198 | checked against `LOCKDOWN_REMOTE_ADDR_EXCEPTIONS` and locked or unlocked accordingly.
199 |
200 | For example::
201 |
202 | LOCKDOWN_TRUSTED_PROXIES = [
203 | '172.17.0.1',
204 | ]
205 |
206 | LOCKDOWN_REMOTE_ADDR_EXCEPTIONS = [
207 | '172.17.0.5',
208 | ]
209 |
210 | LOCKDOWN_UNTIL
211 | --------------
212 |
213 | Used to lock the site down up until a certain date. Set to a
214 | ``datetime.datetime`` object.
215 |
216 | If neither ``LOCKDOWN_UNTIL`` nor `LOCKDOWN_AFTER`_ is provided (the default),
217 | the site or views will always be locked.
218 |
219 | LOCKDOWN_AFTER
220 | --------------
221 |
222 | Used to lock the site down after a certain date. Set to a ``datetime.datetime``
223 | object.
224 |
225 | See also: `LOCKDOWN_UNTIL`_.
226 |
227 | LOCKDOWN_LOGOUT_KEY
228 | -------------------
229 |
230 | A key which, if provided in the query string of a locked URL, will log out the
231 | user from the preview.
232 |
233 | LOCKDOWN_FORM
234 | -------------
235 |
236 | The default lockdown form allows admin preview by entering a preset
237 | plain-text password (checked, by default, against the `LOCKDOWN_PASSWORDS`_
238 | setting). To set up more advanced methods of authenticating access to
239 | locked-down pages, set ``LOCKDOWN_FORM`` to the Python dotted path to a Django
240 | ``Form`` subclass. This form will be displayed on the lockout page. If the form
241 | validates when submitted, the user will be allowed access to locked pages::
242 |
243 | LOCKDOWN_FORM = 'path.to.my.CustomLockdownForm'
244 |
245 | A form for authenticating against ``django.contrib.auth`` users is provided
246 | with django-lockdown (use ``LOCKDOWN_FORM = 'lockdown.forms.AuthForm'``). It
247 | accepts two keyword arguments (in the ``lockdown`` decorator):
248 |
249 | ``staff_only``
250 | Only allow staff members to preview. Defaults to ``True`` (but the default
251 | can be provided as a `LOCKDOWN_AUTHFORM_STAFF_ONLY`_ setting).
252 |
253 | ``superusers_only``
254 | Only allow superusers to preview. Defaults to ``False`` (but the default
255 | can be provided as a `LOCKDOWN_AUTHFORM_SUPERUSERS_ONLY`_ setting).
256 |
257 | LOCKDOWN_AUTHFORM_STAFF_ONLY
258 | ----------------------------
259 |
260 | If using ``lockdown.forms.AuthForm`` and this setting is ``True``, only staff
261 | users will be allowed to preview (True by default).
262 |
263 | Has no effect if not using ``lockdown.forms.AuthForm``.
264 |
265 | LOCKDOWN_AUTHFORM_SUPERUSERS_ONLY
266 | ---------------------------------
267 |
268 | If using ``lockdown.forms.AuthForm`` and this setting is ``True``, only
269 | superusers will be allowed to preview (False by default). Has no effect if not
270 | using ``lockdown.forms.AuthForm``.
271 |
272 | LOCKDOWN_SESSION_KEY
273 | --------------------
274 |
275 | Once a client is authorized for admin preview, they will continue to
276 | be authorized for the remainder of their browsing session (using
277 | Django's built-in session support). ``LOCKDOWN_SESSION_KEY`` defines
278 | the session key used; the default is ``'lockdown-allow'``.
279 |
280 |
281 | Templates
282 | =========
283 |
284 | ``django-lockdown`` uses a single template, ``lockdown/form.html``. The
285 | default template displays a simple "coming soon" message and the
286 | preview authorization form, if a password via `LOCKDOWN_PASSWORDS`_ is set.
287 |
288 | If you want to use a different template, you can use Djangos template
289 | `loaders`_ to specify a path inside your project to search for templates,
290 | before searching for templates included in ``django-lockdown``.
291 |
292 | In your overwritten template the lockdown preview form is available in the
293 | template context as ``form``.
294 |
295 | .. _loaders: https://docs.djangoproject.com/en/2.1/ref/templates/api/#template-loaders
296 |
--------------------------------------------------------------------------------
/lockdown/tests/tests.py:
--------------------------------------------------------------------------------
1 | # pylint: disable=invalid-name,too-many-public-methods
2 |
3 | import datetime
4 |
5 | from django.conf import settings as django_settings
6 | from django.contrib.auth.models import \
7 | User # pylint: disable=imported-auth-user
8 | from django.core.exceptions import ImproperlyConfigured
9 | from django.test import TestCase, override_settings
10 |
11 | from lockdown import middleware
12 | from lockdown.forms import AuthForm
13 | from lockdown.tests.views import a_view
14 |
15 |
16 | class BaseTests(TestCase):
17 | """Base tests for lockdown functionality.
18 |
19 | These base tests are used for testing lockdowns decorator and middleware
20 | functionality.
21 |
22 | Subclasses should provide ``locked_url`` and ``locked_contents``
23 | attributes.
24 | """
25 |
26 | locked_url = '/locked/view/'
27 | locked_contents = b'A locked view.'
28 |
29 | def test_lockdown_template_used(self):
30 | """Test if the login form template is used on locked pages."""
31 | response = self.client.get(self.locked_url)
32 | self.assertTemplateUsed(response, 'lockdown/form.html')
33 |
34 | @override_settings(LOCKDOWN_PASSWORDS=('letmein',))
35 | def test_form_in_context(self):
36 | """Test if the login form contains a proper password field."""
37 | response = self.client.get(self.locked_url)
38 | form = response.context['form']
39 | self.assertIn('password', form.fields)
40 |
41 | @override_settings(LOCKDOWN_ENABLED=False)
42 | def test_global_disable(self):
43 | """Test that a page isn't locked when LOCKDOWN_ENABLED=False."""
44 | response = self.client.get(self.locked_url)
45 | self.assertEqual(response.content, self.locked_contents)
46 |
47 | @override_settings(LOCKDOWN_URL_EXCEPTIONS=(r'/view/$',))
48 | def test_url_exceptions(self):
49 | """Test that a page isn't locked when its URL is in the exception list.
50 |
51 | The excepted URLs are determined by the LOCKDOWN_URL_EXCEPTIONS
52 | setting.
53 | """
54 | response = self.client.get(self.locked_url)
55 | self.assertEqual(response.content, self.locked_contents)
56 |
57 | @override_settings(LOCKDOWN_REMOTE_ADDR_EXCEPTIONS=['192.168.0.1'])
58 | def test_remote_addr_exc_lock(self):
59 | """Test that a page is locked when client IP is not in exception list.
60 |
61 | The excepted IP-addresses are determined by the
62 | LOCKDOWN_REMOTE_ADDR_EXCEPTIONS setting.
63 | """
64 | response = self.client.get(self.locked_url,
65 | REMOTE_ADDR='192.168.0.100')
66 | self.assertNotEqual(response.content, self.locked_contents)
67 |
68 | @override_settings(LOCKDOWN_REMOTE_ADDR_EXCEPTIONS=['192.168.0.1'])
69 | def test_remote_addr_exc_unlock(self):
70 | """Test that a page isn't locked when client IP is in exception list.
71 |
72 | The excepted IP-addresses are determined by the
73 | LOCKDOWN_REMOTE_ADDR_EXCEPTIONS setting.
74 | """
75 | response = self.client.get(self.locked_url, REMOTE_ADDR='192.168.0.1')
76 | self.assertEqual(response.content, self.locked_contents)
77 |
78 | @override_settings(LOCKDOWN_REMOTE_ADDR_EXCEPTIONS=['fd::1'])
79 | def test_remote_addr_exc_unlock_ipv6(self):
80 | """Test that a page isn't locked when client IP is in exception list.
81 |
82 | The excepted IP-addresses are determined by the
83 | LOCKDOWN_REMOTE_ADDR_EXCEPTIONS setting.
84 | """
85 | response = self.client.get(
86 | self.locked_url,
87 | REMOTE_ADDR='fd:0000:0000:0000:0000:0000:0000:0001')
88 | self.assertEqual(response.content, self.locked_contents)
89 |
90 | @override_settings(LOCKDOWN_REMOTE_ADDR_EXCEPTIONS=['192.168.0.0/24'])
91 | def test_remote_addr_exc_unlock_subnet(self):
92 | """Test that a page isn't locked when client IP is in exception list.
93 |
94 | The excepted IP-subnets are determined by the
95 | LOCKDOWN_REMOTE_ADDR_EXCEPTIONS setting.
96 | """
97 | response = self.client.get(self.locked_url, REMOTE_ADDR='192.168.0.1')
98 | self.assertEqual(response.content, self.locked_contents)
99 |
100 | @override_settings(LOCKDOWN_REMOTE_ADDR_EXCEPTIONS=['fd::0/80'])
101 | def test_remote_addr_exc_unlock_ipv6_subnet(self):
102 | """Test that a page isn't locked when client IP is in exception list.
103 |
104 | The excepted IP-subnets are determined by the
105 | LOCKDOWN_REMOTE_ADDR_EXCEPTIONS setting.
106 | """
107 | response = self.client.get(
108 | self.locked_url,
109 | REMOTE_ADDR='fd:0000:0000:0000:0000:0000:0000:0001')
110 | self.assertEqual(response.content, self.locked_contents)
111 |
112 | @override_settings(LOCKDOWN_REMOTE_ADDR_EXCEPTIONS=['192.168.0.1'],
113 | LOCKDOWN_TRUSTED_PROXIES=['127.0.0.1'])
114 | def test_untrusted_proxy_lock(self):
115 | """Test that a page is locked when proxy is not trusted.
116 |
117 | The excepted IP-addresses are determined by the
118 | LOCKDOWN_REMOTE_ADDR_EXCEPTIONS and TRUSTED_PROXIES settings.
119 | """
120 | response = self.client.get(self.locked_url,
121 | REMOTE_ADDR='172.17.0.1',
122 | HTTP_X_FORWARDED_FOR='192.168.0.1')
123 | self.assertNotEqual(response.content, self.locked_contents)
124 |
125 | @override_settings(LOCKDOWN_REMOTE_ADDR_EXCEPTIONS=['fd::2'],
126 | LOCKDOWN_TRUSTED_PROXIES=['fd::1'])
127 | def test_untrusted_proxy_lock_ipv6(self):
128 | """Test that a page is locked when proxy is not trusted.
129 |
130 | The excepted IP-addresses are determined by the
131 | LOCKDOWN_REMOTE_ADDR_EXCEPTIONS and TRUSTED_PROXIES settings.
132 | """
133 | response = self.client.get(
134 | self.locked_url, REMOTE_ADDR='2001:db8::ff00:42:8329',
135 | HTTP_X_FORWARDED_FOR='fd:0000:0000:0000:0000:0000:0000:0001')
136 | self.assertNotEqual(response.content, self.locked_contents)
137 |
138 | @override_settings(LOCKDOWN_REMOTE_ADDR_EXCEPTIONS=['192.168.0.0/24'],
139 | LOCKDOWN_TRUSTED_PROXIES=['127.0.0.0/24'])
140 | def test_untrusted_proxy_lock_subnet(self):
141 | """Test that a page is locked when proxy is not trusted.
142 |
143 | The excepted IP-subnets are determined by the
144 | LOCKDOWN_REMOTE_ADDR_EXCEPTIONS and TRUSTED_PROXIES settings.
145 | """
146 | response = self.client.get(self.locked_url,
147 | REMOTE_ADDR='172.17.0.1',
148 | HTTP_X_FORWARDED_FOR='192.168.0.1')
149 | self.assertNotEqual(response.content, self.locked_contents)
150 |
151 | @override_settings(LOCKDOWN_REMOTE_ADDR_EXCEPTIONS=['fd::2'],
152 | LOCKDOWN_TRUSTED_PROXIES=['fd::1'])
153 | def test_untrusted_proxy_lock_ipv6_subnet(self):
154 | """Test that a page is locked when proxy is not trusted.
155 |
156 | The excepted IP-subnets are determined by the
157 | LOCKDOWN_REMOTE_ADDR_EXCEPTIONS and TRUSTED_PROXIES settings.
158 | """
159 | response = self.client.get(
160 | self.locked_url, REMOTE_ADDR='2001:db8::ff00:42:8329',
161 | HTTP_X_FORWARDED_FOR='fd:0000:0000:0000:0000:0000:0000:0001')
162 | self.assertNotEqual(response.content, self.locked_contents)
163 |
164 | @override_settings(LOCKDOWN_REMOTE_ADDR_EXCEPTIONS=['192.168.0.1'])
165 | def test_no_trusted_proxy_lock(self):
166 | """Test that a page is locked when x-forwarded-for is used w/o proxies.
167 |
168 | The excepted IP-addresses are determined by the
169 | LOCKDOWN_REMOTE_ADDR_EXCEPTIONS and TRUSTED_PROXIES settings.
170 | """
171 | response = self.client.get(self.locked_url,
172 | REMOTE_ADDR='10.0.0.1',
173 | HTTP_X_FORWARDED_FOR='192.168.0.1')
174 | self.assertNotEqual(response.content, self.locked_contents)
175 |
176 | @override_settings(LOCKDOWN_REMOTE_ADDR_EXCEPTIONS=['fd::0/80'])
177 | def test_no_trusted_proxy_lock_ipv6(self):
178 | """Test that a page is locked when x-forwarded-for is used w/o proxies.
179 |
180 | The excepted IP-addresses are determined by the
181 | LOCKDOWN_REMOTE_ADDR_EXCEPTIONS and TRUSTED_PROXIES settings.
182 | """
183 | response = self.client.get(
184 | self.locked_url,
185 | REMOTE_ADDR='2001:db8::ff00:42:8329',
186 | HTTP_X_FORWARDED_FOR='fd:0000:0000:0000:0000:0000:0000:0001')
187 | self.assertNotEqual(response.content, self.locked_contents)
188 |
189 | @override_settings(LOCKDOWN_REMOTE_ADDR_EXCEPTIONS=['192.168.0.0/24'])
190 | def test_no_trusted_proxy_lock_subnet(self):
191 | """Test that a page is locked when x-forwarded-for is used w/o proxies.
192 |
193 | The excepted IP-subnets are determined by the
194 | LOCKDOWN_REMOTE_ADDR_EXCEPTIONS and TRUSTED_PROXIES settings.
195 | """
196 | response = self.client.get(self.locked_url,
197 | REMOTE_ADDR='10.0.0.1',
198 | HTTP_X_FORWARDED_FOR='192.168.0.100')
199 | self.assertNotEqual(response.content, self.locked_contents)
200 |
201 | @override_settings(LOCKDOWN_REMOTE_ADDR_EXCEPTIONS=['fd::0/80'])
202 | def test_no_trusted_proxy_lock_ipv6_subnet(self):
203 | """Test that a page is locked when x-forwarded-for is used w/o proxies.
204 |
205 | The excepted IP-subnets are determined by the
206 | LOCKDOWN_REMOTE_ADDR_EXCEPTIONS and TRUSTED_PROXIES settings.
207 | """
208 | response = self.client.get(
209 | self.locked_url,
210 | REMOTE_ADDR='fe::1',
211 | HTTP_X_FORWARDED_FOR='fd::2')
212 | self.assertNotEqual(response.content, self.locked_contents)
213 |
214 | @override_settings(LOCKDOWN_REMOTE_ADDR_EXCEPTIONS=['192.168.0.1'],
215 | LOCKDOWN_TRUSTED_PROXIES=['10.0.0.1'])
216 | def test_trusted_proxy_unlock(self):
217 | """Test that a page isn't locked when client IP is in exception list.
218 |
219 | The excepted IP-addresses are determined by the
220 | LOCKDOWN_REMOTE_ADDR_EXCEPTIONS and TRUSTED_PROXIES settings.
221 | """
222 | response = self.client.get(self.locked_url,
223 | REMOTE_ADDR='10.0.0.1',
224 | HTTP_X_FORWARDED_FOR='192.168.0.1')
225 | self.assertEqual(response.content, self.locked_contents)
226 |
227 | @override_settings(
228 | LOCKDOWN_REMOTE_ADDR_EXCEPTIONS=[
229 | 'fd:0000:0000:0000:0000:0000:0000:0001'],
230 | LOCKDOWN_TRUSTED_PROXIES=['fd:0000:0000:0000:0000:0000:0000:0002'])
231 | def test_trusted_proxy_unlock_ipv6(self):
232 | """Test that a page isn't locked when client IP is in exception list.
233 |
234 | The excepted IP-addresses are determined by the
235 | LOCKDOWN_REMOTE_ADDR_EXCEPTIONS and TRUSTED_PROXIES settings.
236 | """
237 | response = self.client.get(
238 | self.locked_url,
239 | REMOTE_ADDR='fd:0000:0000:0000:0000:0000:0000:0002',
240 | HTTP_X_FORWARDED_FOR='fd:0000:0000:0000:0000:0000:0000:0001')
241 | self.assertEqual(response.content, self.locked_contents)
242 |
243 | @override_settings(LOCKDOWN_REMOTE_ADDR_EXCEPTIONS=['192.168.0.0/24'],
244 | LOCKDOWN_TRUSTED_PROXIES=['127.0.0.0/24'])
245 | def test_trusted_proxy_unlock_subnet(self):
246 | """Test that a page isn't locked when client IP is in exception list.
247 |
248 | The excepted IP-subnets are determined by the
249 | LOCKDOWN_REMOTE_ADDR_EXCEPTIONS and TRUSTED_PROXIES settings.
250 | """
251 | response = self.client.get(self.locked_url,
252 | REMOTE_ADDR='127.0.0.1',
253 | HTTP_X_FORWARDED_FOR='192.168.0.1')
254 | self.assertEqual(response.content, self.locked_contents)
255 |
256 | @override_settings(LOCKDOWN_REMOTE_ADDR_EXCEPTIONS=['fd::0/80'],
257 | LOCKDOWN_TRUSTED_PROXIES=['fe::0/80'])
258 | def test_trusted_proxy_unlock_ipv6_subnet(self):
259 | """Test that a page isn't locked when client IP is in exception list.
260 |
261 | The excepted IP-subnets are determined by the
262 | LOCKDOWN_REMOTE_ADDR_EXCEPTIONS and TRUSTED_PROXIES settings.
263 | """
264 | response = self.client.get(
265 | self.locked_url,
266 | REMOTE_ADDR='fe::2',
267 | HTTP_X_FORWARDED_FOR='fd::1')
268 | self.assertEqual(response.content, self.locked_contents)
269 |
270 | @override_settings(LOCKDOWN_PASSWORDS=('letmein',))
271 | def test_submit_password(self):
272 | """Test that access to locked content works with a correct password."""
273 | response = self.client.post(self.locked_url, {'password': 'letmein'},
274 | follow=True)
275 | self.assertEqual(response.content, self.locked_contents)
276 |
277 | @override_settings(LOCKDOWN_PASSWORDS=('letmein',))
278 | def test_submit_wrong_password(self):
279 | """Test access to locked content is denied for wrong passwords."""
280 | response = self.client.post(self.locked_url, {'password': 'imacrook'})
281 | self.assertContains(response, 'Incorrect password.')
282 |
283 | @override_settings(LOCKDOWN_FORM='lockdown.tests.forms.CustomLockdownForm')
284 | def test_custom_form(self):
285 | """Test if access using a custom lockdown form works."""
286 | response = self.client.post(self.locked_url, {'answer': '42'},
287 | follow=True)
288 | self.assertEqual(response.content, self.locked_contents)
289 |
290 | def test_invalid_custom_form(self):
291 | """Test that pointing to an invalid form properly produces an error."""
292 | # no form configured at all
293 | self.assertRaises(ImproperlyConfigured,
294 | middleware.get_lockdown_form, None)
295 | # invalid module name in the configured form
296 | self.assertRaises(ImproperlyConfigured,
297 | middleware.get_lockdown_form, 'invalidform')
298 | # not existing module for form
299 | self.assertRaises(ImproperlyConfigured,
300 | middleware.get_lockdown_form, 'invalid.form')
301 | # existing module, but no form with that name in the module
302 | self.assertRaises(ImproperlyConfigured,
303 | middleware.get_lockdown_form, 'lockdown.forms.foo')
304 |
305 | def test_locked_until(self):
306 | """Test locking until a certain date."""
307 | yesterday = datetime.datetime.now() - datetime.timedelta(days=1)
308 | tomorrow = datetime.datetime.now() + datetime.timedelta(days=1)
309 |
310 | with self.settings(LOCKDOWN_UNTIL_DATE=tomorrow):
311 | response = self.client.get(self.locked_url)
312 | self.assertTemplateUsed(response, 'lockdown/form.html')
313 |
314 | with self.settings(LOCKDOWN_UNTIL_DATE=yesterday):
315 | response = self.client.get(self.locked_url)
316 | self.assertEqual(response.content, self.locked_contents)
317 |
318 | def test_locked_after(self):
319 | """Test locking starting at a certain date."""
320 | yesterday = datetime.datetime.now() - datetime.timedelta(days=1)
321 | tomorrow = datetime.datetime.now() + datetime.timedelta(days=1)
322 |
323 | with self.settings(LOCKDOWN_AFTER_DATE=yesterday):
324 | response = self.client.get(self.locked_url)
325 | self.assertTemplateUsed(response, 'lockdown/form.html')
326 |
327 | with self.settings(LOCKDOWN_AFTER_DATE=tomorrow):
328 | response = self.client.get(self.locked_url)
329 | self.assertEqual(response.content, self.locked_contents)
330 |
331 | def test_locked_until_and_after(self):
332 | """Test locking until a certain date and starting at another date."""
333 | yesterday = datetime.datetime.now() - datetime.timedelta(days=1)
334 | tomorrow = datetime.datetime.now() + datetime.timedelta(days=1)
335 |
336 | with self.settings(LOCKDOWN_UNTIL_DATE=yesterday,
337 | LOCKDOWN_AFTER_DATE=yesterday):
338 | response = self.client.get(self.locked_url)
339 | self.assertTemplateUsed(response, 'lockdown/form.html')
340 |
341 | with self.settings(LOCKDOWN_UNTIL_DATE=tomorrow,
342 | LOCKDOWN_AFTER_DATE=tomorrow):
343 | response = self.client.get(self.locked_url)
344 | self.assertTemplateUsed(response, 'lockdown/form.html')
345 |
346 | with self.settings(LOCKDOWN_UNTIL_DATE=yesterday,
347 | LOCKDOWN_AFTER_DATE=tomorrow):
348 | response = self.client.get(self.locked_url)
349 | self.assertEqual(response.content, self.locked_contents)
350 |
351 | def test_missing_session_middleware(self):
352 | """Test behavior with missing session middleware.
353 |
354 | When the session middleware isn't present an ImproperlyConfigured error
355 | is expected.
356 | """
357 | middleware_remove = {
358 | 'remove': [
359 | 'django.contrib.sessions.middleware.SessionMiddleware',
360 | 'django.contrib.auth.middleware.AuthenticationMiddleware'
361 | ]
362 | }
363 |
364 | with self.modify_settings(MIDDLEWARE=middleware_remove):
365 | self.assertRaises(ImproperlyConfigured,
366 | self.client.get,
367 | self.locked_url)
368 |
369 |
370 | class DecoratorTests(BaseTests):
371 | """Tests for using lockdown via decorators."""
372 |
373 | def test_overridden_password(self):
374 | """Test that locking works when overriding the password."""
375 | url = '/overridden/locked/view/'
376 |
377 | response = self.client.post(url, {'password': 'letmein'}, follow=True)
378 | self.assertTemplateUsed(response, 'lockdown/form.html')
379 |
380 | response = self.client.post(url, {'password': 'squirrel'}, follow=True)
381 | self.assertTemplateNotUsed(response, 'lockdown/form.html')
382 | self.assertEqual(response.content, self.locked_contents)
383 |
384 | def test_overridden_url_exceptions(self):
385 | """Test that locking works when overriding the url exceptions."""
386 | url = '/locked/view/with/exception1/'
387 | response = self.client.post(url, follow=True)
388 | self.assertTemplateUsed(response, 'lockdown/form.html')
389 |
390 | url = '/locked/view/with/exception2/'
391 | response = self.client.post(url, follow=True)
392 | self.assertTemplateNotUsed(response, 'lockdown/form.html')
393 | self.assertEqual(response.content, self.locked_contents)
394 |
395 | def test_overridden_ip_exceptions(self):
396 | """Test that locking works with overwritten remote_addr exceptions."""
397 | url = '/locked/view/with/ip_exception/'
398 | response = self.client.post(url, REMOTE_ADDR='192.168.0.100',
399 | follow=True)
400 | self.assertTemplateUsed(response, 'lockdown/form.html')
401 |
402 | url = '/locked/view/with/ip_exception/'
403 | response = self.client.post(url, REMOTE_ADDR='192.168.0.1',
404 | follow=True)
405 | self.assertTemplateNotUsed(response, 'lockdown/form.html')
406 | self.assertEqual(response.content, self.locked_contents)
407 |
408 | def test_overridden_ip_exceptions_ipv6(self):
409 | """Test that locking works with overwritten remote_addr exceptions."""
410 | url = '/locked/view/with/ip_exception_ipv6/'
411 | response = self.client.post(
412 | url, REMOTE_ADDR='fd:0000:0000:0000:0000:0000:0000:0002',
413 | follow=True)
414 | self.assertTemplateUsed(response, 'lockdown/form.html')
415 |
416 | url = '/locked/view/with/ip_exception_ipv6/'
417 | response = self.client.post(
418 | url, REMOTE_ADDR='fd:0000:0000:0000:0000:0000:0000:0001',
419 | follow=True)
420 | self.assertTemplateNotUsed(response, 'lockdown/form.html')
421 | self.assertEqual(response.content, self.locked_contents)
422 |
423 | def test_overridden_ip_exceptions_subnet(self):
424 | """Test that locking works with overwritten remote_addr exceptions."""
425 | url = '/locked/view/with/ip_exception_subnet/'
426 | response = self.client.post(url, REMOTE_ADDR='172.0.0.1',
427 | follow=True)
428 | self.assertTemplateUsed(response, 'lockdown/form.html')
429 |
430 | url = '/locked/view/with/ip_exception_subnet/'
431 | response = self.client.post(url, REMOTE_ADDR='192.168.0.1',
432 | follow=True)
433 | self.assertTemplateNotUsed(response, 'lockdown/form.html')
434 | self.assertEqual(response.content, self.locked_contents)
435 |
436 | def test_overridden_ip_exceptions_subnet_ipv6(self):
437 | """Test that locking works with overwritten remote_addr exceptions."""
438 | url = '/locked/view/with/ip_exception_ipv6_subnet/'
439 | response = self.client.post(url, REMOTE_ADDR='2001:db8::ff00:42:8329',
440 | follow=True)
441 | self.assertTemplateUsed(response, 'lockdown/form.html')
442 |
443 | url = '/locked/view/with/ip_exception_ipv6_subnet/'
444 | response = self.client.post(url, REMOTE_ADDR='fd::1',
445 | follow=True)
446 | self.assertTemplateNotUsed(response, 'lockdown/form.html')
447 | self.assertEqual(response.content, self.locked_contents)
448 |
449 | def test_overridden_until_date(self):
450 | """Test that locking works when overriding the until date."""
451 | url = '/locked/view/until/yesterday/'
452 | response = self.client.post(url, follow=True)
453 | self.assertTemplateNotUsed(response, 'lockdown/form.html')
454 | self.assertEqual(response.content, self.locked_contents)
455 |
456 | url = '/locked/view/until/tomorrow/'
457 | response = self.client.post(url, follow=True)
458 | self.assertTemplateUsed(response, 'lockdown/form.html')
459 |
460 | def test_overridden_after_date(self):
461 | """Test that locking works when overriding the after date."""
462 | url = '/locked/view/after/yesterday/'
463 | response = self.client.post(url, follow=True)
464 | self.assertTemplateUsed(response, 'lockdown/form.html')
465 |
466 | url = '/locked/view/after/tomorrow/'
467 | response = self.client.post(url, follow=True)
468 | self.assertTemplateNotUsed(response, 'lockdown/form.html')
469 | self.assertEqual(response.content, self.locked_contents)
470 |
471 | def test_overridden_both_dates(self):
472 | """Test that locking works when overriding the after date."""
473 | url = '/locked/view/until/and/after/'
474 | response = self.client.post(url, follow=True)
475 | self.assertTemplateNotUsed(response, 'lockdown/form.html')
476 | self.assertEqual(response.content, self.locked_contents)
477 |
478 | def test_overridden_extra_context(self):
479 | """Test that locking works when overriding the extra context."""
480 | url = '/locked/view/with/extra/context/'
481 | response = self.client.get(url)
482 | self.assertIn('foo', response.context)
483 |
484 |
485 | class MiddlewareTests(BaseTests):
486 | """Tests for using lockdown via its middleware."""
487 |
488 | locked_url = '/a/view/'
489 | locked_contents = b'A view.'
490 |
491 | def setUp(self):
492 | """Additional setup for middleware tests."""
493 | super().setUp()
494 | self._old_middleware_classes = django_settings.MIDDLEWARE
495 | django_settings.MIDDLEWARE.append(
496 | 'lockdown.middleware.LockdownMiddleware',
497 | )
498 |
499 | @override_settings(LOCKDOWN_VIEW_EXCEPTIONS=[a_view])
500 | def test_view_exceptions(self):
501 | """Test that a page isn't locked when its view whitelisted.
502 |
503 | The excepted URLs are determined by the
504 | LOCKDOWN_VIEW_EXCEPTIONS setting.
505 | """
506 | response = self.client.get(self.locked_url)
507 | self.assertEqual(response.content, self.locked_contents)
508 |
509 | def tearDown(self):
510 | """Additional tear down for middleware tests."""
511 | django_settings.MIDDLEWARE = self._old_middleware_classes
512 | super().tearDown()
513 |
514 |
515 | class AuthFormTests(TestCase):
516 | """Tests for using the auth form for previewing locked pages."""
517 |
518 | def test_using_form(self):
519 | """Test unauthorized access to locked page.
520 |
521 | Unauthorized access to a to locked page should show the auth form
522 | """
523 | url = '/auth/user/locked/view/'
524 | response = self.client.get(url)
525 | self.assertTemplateUsed(response, 'lockdown/form.html')
526 |
527 | form = response.context['form']
528 | self.assertTrue(isinstance(form, AuthForm))
529 |
530 | def add_user(self, username='test', password='pw', **kwargs): # nosec
531 | """Add a user used for testing the auth form."""
532 | user = User(username=username, **kwargs)
533 | user.set_password(password)
534 | user.save()
535 |
536 | def test_inactive_user(self):
537 | """Test access to a locked page with an inactive user."""
538 | url = '/auth/user/locked/view/'
539 | self.add_user(is_active=False)
540 |
541 | post_data = {'username': 'test', 'password': 'pw'}
542 | response = self.client.post(url, post_data, follow=True)
543 | self.assertTemplateUsed(response, 'lockdown/form.html')
544 |
545 | def test_user(self):
546 | """Test access to a locked page which requires authorization."""
547 | url = '/auth/user/locked/view/'
548 | self.add_user()
549 |
550 | # Incorrect password.
551 | post_data = {'username': 'test', 'password': 'bad'}
552 | response = self.client.post(url, post_data, follow=True)
553 | self.assertTemplateUsed(response, 'lockdown/form.html')
554 |
555 | # Correct password.
556 | post_data = {'username': 'test', 'password': 'pw'}
557 | response = self.client.post(url, post_data, follow=True)
558 | self.assertTemplateNotUsed(response, 'lockdown/form.html')
559 |
560 | def test_staff(self):
561 | """Test access to a locked page which requires a staff user."""
562 | url = '/auth/staff/locked/view/'
563 | self.add_user(username='user')
564 | self.add_user(username='staff', is_staff=True)
565 |
566 | # Non-staff member.
567 | post_data = {'username': 'user', 'password': 'pw'}
568 | response = self.client.post(url, post_data, follow=True)
569 | self.assertTemplateUsed(response, 'lockdown/form.html')
570 |
571 | # Incorrect password.
572 | post_data = {'username': 'staff', 'password': 'bad'}
573 | response = self.client.post(url, post_data, follow=True)
574 | self.assertTemplateUsed(response, 'lockdown/form.html')
575 |
576 | # Correct password.
577 | post_data = {'username': 'staff', 'password': 'pw'}
578 | response = self.client.post(url, post_data, follow=True)
579 | self.assertTemplateNotUsed(response, 'lockdown/form.html')
580 |
581 | def test_superuser(self):
582 | """Test access to a locked page which requires a superuser."""
583 | url = '/auth/superuser/locked/view/'
584 | self.add_user(username='staff', is_staff=True)
585 | self.add_user(username='superuser', is_staff=True, is_superuser=True)
586 |
587 | # Non-superuser.
588 | post_data = {'username': 'staff', 'password': 'pw'}
589 | response = self.client.post(url, post_data, follow=True)
590 | self.assertTemplateUsed(response, 'lockdown/form.html')
591 |
592 | # Incorrect password.
593 | post_data = {'username': 'superuser', 'password': 'bad'}
594 | response = self.client.post(url, post_data, follow=True)
595 | self.assertTemplateUsed(response, 'lockdown/form.html')
596 |
597 | # Correct password.
598 | post_data = {'username': 'superuser', 'password': 'pw'}
599 | response = self.client.post(url, post_data, follow=True)
600 | self.assertTemplateNotUsed(response, 'lockdown/form.html')
601 |
602 |
603 | # Remove the BaseTests class from the module namespace, so it won't get picked
604 | # up by unittest.
605 | del BaseTests
606 |
--------------------------------------------------------------------------------