├── tests
├── testapp
│ ├── models.py
│ ├── __init__.py
│ ├── templates
│ │ └── registration
│ │ │ └── login.html
│ ├── views.py
│ ├── sixmock.py
│ ├── urls.py
│ └── tests.py
└── setup.py
├── docs
├── .gitignore
├── license.rst
├── contributing.rst
├── quickstart.rst
├── authors.rst
├── index.rst
├── supported-versions.rst
├── extension-points.rst
├── release-notes.rst
├── Makefile
└── conf.py
├── setup.cfg
├── djactasauth
├── __init__.py
├── models.py
├── util.py
├── views.py
└── backends.py
├── MANIFEST.in
├── manage.py
├── setup.py
├── .gitignore
├── .github
└── workflows
│ └── tox.yml
├── tox.ini
├── pyproject.toml
├── LICENSE
├── .travis.yml
├── settings.py
├── tox2travis.py
├── README.rst
└── Makefile
/tests/testapp/models.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/.gitignore:
--------------------------------------------------------------------------------
1 | _build
2 |
--------------------------------------------------------------------------------
/tests/testapp/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [wheel]
2 | universal = 1
--------------------------------------------------------------------------------
/djactasauth/__init__.py:
--------------------------------------------------------------------------------
1 | __version__ = '0.6.0'
2 |
--------------------------------------------------------------------------------
/djactasauth/models.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
--------------------------------------------------------------------------------
/docs/license.rst:
--------------------------------------------------------------------------------
1 | License
2 | -------
3 |
4 | .. include:: ../LICENSE
5 |
--------------------------------------------------------------------------------
/docs/contributing.rst:
--------------------------------------------------------------------------------
1 | .. include:: ../README.rst
2 | :start-after: .. contributing start
3 | :end-before: .. contributing end
4 |
--------------------------------------------------------------------------------
/docs/quickstart.rst:
--------------------------------------------------------------------------------
1 | .. include:: ../README.rst
2 | :start-after: .. quickstart start
3 | :end-before: .. quickstart end
4 |
5 |
--------------------------------------------------------------------------------
/tests/testapp/templates/registration/login.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/tests/testapp/views.py:
--------------------------------------------------------------------------------
1 | from django.http import HttpResponse
2 |
3 |
4 | def whoami(request):
5 | return HttpResponse(request.user.username)
6 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include AUTHORS.rst
2 | include CONTRIBUTING.rst
3 | include HISTORY.rst
4 | include LICENSE
5 | include README.rst
6 | recursive-include djactasauth *.html *.png *.gif *js *.css *jpg *jpeg *svg *py
7 |
--------------------------------------------------------------------------------
/tests/testapp/sixmock.py:
--------------------------------------------------------------------------------
1 | try:
2 | from unittest.mock import call, patch, Mock, PropertyMock, MagicMock # noqa: E501
3 | except ImportError:
4 | from mock import call, patch, Mock, PropertyMock, MagicMock # noqa: F401
5 |
--------------------------------------------------------------------------------
/tests/testapp/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 | from djactasauth.views import PrefillLoginView
3 | from testapp.views import whoami
4 |
5 |
6 | urlpatterns = [
7 | path(r'login/', PrefillLoginView.as_view(), {}, 'login'),
8 | path(r'whoami/', whoami),
9 | ]
10 |
--------------------------------------------------------------------------------
/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import os
3 | import sys
4 |
5 | if __name__ == "__main__":
6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings")
7 |
8 | from django.core.management import execute_from_command_line
9 |
10 | execute_from_command_line(sys.argv)
11 |
--------------------------------------------------------------------------------
/docs/authors.rst:
--------------------------------------------------------------------------------
1 | Authors
2 | =======
3 |
4 | * `Paessler AG `_ https://www.paessler.com
5 | * `Peter Zsoldos `_ http://zsoldosp.eu
6 | * `Kai Richard König `_
7 | * `Michael Zeidler `_
8 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | from setuptools import setup
4 | import os
5 |
6 | use_unsupported_django = os.environ.get('DJANGO_ACT_AS_AUTH_USE_UNSUPPORTED_DJANGO', '0') == '1'
7 |
8 | dependencies = ['Django'] if use_unsupported_django else ["Django>=4.2, <6.0"]
9 |
10 |
11 | if __name__ == "__main__":
12 | setup(install_requires=dependencies)
13 |
--------------------------------------------------------------------------------
/djactasauth/util.py:
--------------------------------------------------------------------------------
1 | from django.urls import reverse
2 | import urllib.parse
3 | from djactasauth.backends import ActAsBackend
4 |
5 |
6 | def act_as_login_url(auth, act_as, **query):
7 | username = ActAsBackend.sepchar.join([auth, act_as])
8 | return get_login_url(username=username, **query)
9 |
10 |
11 | def get_login_url(**query):
12 | return reverse('login') + '?' + urllib.parse.urlencode(query)
13 |
--------------------------------------------------------------------------------
/djactasauth/views.py:
--------------------------------------------------------------------------------
1 | from django.contrib.auth.views import LoginView
2 |
3 |
4 | class PrefillLoginView(LoginView):
5 |
6 | query2initial = ('username',)
7 |
8 | def get_initial(self):
9 | initial = super(PrefillLoginView, self).get_initial()
10 | field_names = set(self.query2initial)
11 | for field_name in field_names:
12 | val_from_query = self.request.GET.get(field_name, None)
13 | if val_from_query:
14 | initial.setdefault(field_name, val_from_query)
15 | return initial
16 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.py[cod]
2 |
3 | # C extensions
4 | *.so
5 |
6 | # Packages
7 | *.egg
8 | *.egg-info
9 | dist
10 | build
11 | eggs
12 | parts
13 | bin
14 | var
15 | sdist
16 | develop-eggs
17 | .installed.cfg
18 | lib
19 | lib64
20 |
21 | # Installer logs
22 | pip-log.txt
23 |
24 | # Unit test / coverage reports
25 | .coverage
26 | .tox
27 | nosetests.xml
28 |
29 | # Translations
30 | *.mo
31 |
32 | # Mr Developer
33 | .mr.developer.cfg
34 | .project
35 | .pydevproject
36 |
37 | # Complexity
38 | output/*.html
39 | output/*/index.html
40 |
41 | # Sphinx
42 | docs/_build
43 | no-readme-errors
44 | .travis.yml.generated
45 |
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | .. Django Act As Auth documentation master file, created by
2 | sphinx-quickstart on Thu Mar 10 09:22:19 2016.
3 | You can adapt this file completely to your liking, but it should at least
4 | contain the root `toctree` directive.
5 |
6 | Welcome to Django Act As Auth's documentation!
7 | ==============================================
8 |
9 | .. include:: ../README.rst
10 | :start-after: .. sales pitch start
11 | :end-before: .. sales pitch end
12 |
13 | ----
14 |
15 | Contents:
16 |
17 | .. toctree::
18 | :maxdepth: 2
19 |
20 | quickstart
21 | extension-points
22 | supported-versions
23 | license
24 | release-notes
25 | contributing
26 | authors
27 |
28 |
29 | Indices and tables
30 | ==================
31 |
32 | * :ref:`genindex`
33 | * :ref:`search`
34 |
35 |
--------------------------------------------------------------------------------
/.github/workflows/tox.yml:
--------------------------------------------------------------------------------
1 | name: Tox
2 |
3 | on:
4 | push:
5 | branches: [ "master" ]
6 | pull_request:
7 | branches: [ "master" ]
8 |
9 | jobs:
10 | build:
11 |
12 | runs-on: ubuntu-latest
13 | strategy:
14 | max-parallel: 4
15 | matrix:
16 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
17 |
18 | steps:
19 | - uses: actions/checkout@v3
20 | - name: Set up Python ${{ matrix.python-version }}
21 | uses: actions/setup-python@v3
22 | with:
23 | python-version: ${{ matrix.python-version }}
24 | - name: Install Dependencies
25 | run: |
26 | set -xu
27 | python -m pip install --upgrade pip
28 | pip install tox==4.6.3
29 | - name: Run Tests
30 | run: |
31 | tox --skip-missing-interpreters
32 |
33 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | # See README.rst supported versions
2 | [tox]
3 | envlist =
4 | py{39,310,311,312}-django42,
5 | py{310,311,312,313}-django51,
6 | py{310,311,312,313}-django52,
7 | requires =
8 | tox >= 4.6.3
9 | setuptools >= 61.0.0
10 | setuptools-scm[toml] >= 5.0.0
11 |
12 |
13 | [testenv]
14 | commands =
15 | pip install -e tests
16 | make test lint docs
17 | setenv =
18 | DJANGO_SETTINGS_MODULE = settings
19 | PIP_INDEX_URL = https://pypi.python.org/simple/
20 | deps =
21 | django42: Django>=4.2,<4.3
22 | django51: Django>=5.1,<5.2
23 | django52: Django>=5.2,<5.3
24 | py39,py310,py311: flake8==3.8.4
25 | py312: flake8==5.0
26 | py313: flake8==7.1.0
27 | # TODO: duplicated from pyproject.toml
28 | py313: setuptools>=61.0.0
29 | py313: setuptools-scm[toml]>=5.0.0
30 | docutils==0.15
31 | pyhamcrest<2.0
32 |
33 | whitelist_externals = make
34 | allowlist_externals = make
35 |
36 | [flake8]
37 | exclude = docs
38 |
--------------------------------------------------------------------------------
/docs/supported-versions.rst:
--------------------------------------------------------------------------------
1 | Supported Versions
2 | ==================
3 |
4 | Version Numbers
5 | ---------------
6 |
7 | The project is versioned in the spirit of `Semantic Versioning`_.
8 | Note however that currently it's pre 1.0, thus `minor` version
9 | changes can be backwards incompatible. I.e.: ``0.1.3`` and ``0.1.2``
10 | are compatible, but ``0.2.0`` and ``0.1.3`` are not.
11 |
12 | Django Versions Support Philosophy
13 | ----------------------------------
14 |
15 | The project aims to support the versions Django itself supports.
16 |
17 | Supported version of djactasauth
18 | --------------------------------
19 |
20 | The project itself has only a single supported version, that is the latest
21 | stable release.
22 |
23 | I.e.: bugfixes are not backported, i.e.: if the current stable release is ``1.2.3``,
24 | but the bug applies to all versions since ``0.1.2``, the bug will only be fixed in
25 | ``1.2.4``.
26 |
27 | Supported Django and Python versions
28 | ------------------------------------
29 |
30 | See ``tox.ini``.
31 |
32 | .. include:: ../tox.ini
33 | :start-after: [tox]
34 | :end-before: [testenv]
35 |
36 |
37 | .. _Semantic Versioning: http://semver.org/
38 |
--------------------------------------------------------------------------------
/tests/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | import sys
5 |
6 | try:
7 | from setuptools import setup
8 | except ImportError:
9 | from distutils.core import setup
10 |
11 | if sys.argv[-1] in ('publish', 'release'):
12 | raise Exception('this is a test app, do not release it!')
13 |
14 | readme = 'A simple test application to test djactasauth'
15 |
16 | setup(
17 | name='testapp',
18 | version='0.0.0',
19 | description=readme,
20 | long_description=readme,
21 | author='Paessler AG',
22 | url='https://github.com/PaesslerAG/django-act-as-auth',
23 | packages=[
24 | 'testapp',
25 | ],
26 | include_package_data=True,
27 | install_requires=[
28 | ],
29 | license="BSD",
30 | zip_safe=False,
31 | keywords='django-act-as-auth',
32 | classifiers=[
33 | 'Development Status :: 2 - Pre-Alpha',
34 | 'Framework :: Django',
35 | 'Intended Audience :: Developers',
36 | 'License :: OSI Approved :: BSD License',
37 | 'Natural Language :: English',
38 | 'Programming Language :: Python :: 2',
39 | 'Programming Language :: Python :: 2.7',
40 | 'Programming Language :: Python :: 3',
41 | ],
42 | )
43 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["setuptools>=61.0.0", "setuptools-scm[toml]>=5.0.0"]
3 | build-backend = "setuptools.build_meta"
4 |
5 | [project]
6 | name = "djactasauth"
7 | dynamic = ["version", "readme", "dependencies"]
8 | authors = [{ name = "Peter Zsoldos", email = "hello@zsoldosp.eu" }]
9 | maintainers = [{ name = "Peter Zsoldos", email = "hello@zsoldosp.eu" }]
10 | description = "Django authentication backend allowing admins to login as another user."
11 | requires-python = ">=3.9"
12 | classifiers = [
13 | "Development Status :: 4 - Beta",
14 | "Intended Audience :: Developers",
15 | "License :: OSI Approved :: BSD License",
16 | "Operating System :: OS Independent",
17 | "Natural Language :: English",
18 | "Programming Language :: Python :: 3.9",
19 | "Programming Language :: Python :: 3.10",
20 | "Programming Language :: Python :: 3.11",
21 | "Programming Language :: Python :: 3.12",
22 | "Programming Language :: Python :: 3.13",
23 | "Framework :: Django",
24 | "Framework :: Django :: 4.2",
25 | "Framework :: Django :: 5.1",
26 | "Framework :: Django :: 5.2",
27 | ]
28 |
29 | [project.urls]
30 | "Homepage" = "https://github.com/zsoldosp/django-django-act-as-auth"
31 | "Bug Tracker" = "https://github.com/zsoldosp/django-django-act-as-auth/issues"
32 |
33 | [tool.setuptools]
34 | packages = ["djactasauth"]
35 |
36 | [tool.setuptools.dynamic]
37 | version = { attr = "djactasauth.__version__" }
38 | readme = { file = ["README.rst"] }
39 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2016, Paessler AG
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
5 |
6 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
7 |
8 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
9 |
10 | * Neither the name of djactasauth nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
11 |
12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | dist: xenial
2 | language: python
3 | before_install:
4 | - sudo apt-get -qq update
5 | - sudo apt-get install -y make sed
6 | install:
7 | - pip install tox
8 | matrix:
9 | include:
10 | - python: "2.7"
11 | env: TOX_ENVS=py27-django111
12 | - python: "3.5"
13 | env: TOX_ENVS=py35-django111,py35-django21,py35-django22
14 | - python: "3.6"
15 | env: TOX_ENVS=py36-django111,py36-django21,py36-django22
16 | - python: "3.7"
17 | env: TOX_ENVS=py37-django111,py37-django21,py37-django22
18 | script:
19 | - tox -e $TOX_ENVS
20 | before_deploy: "make clean"
21 | deploy:
22 | provider: pypi
23 | user: "paessler_bis"
24 | password:
25 | secure: "N+ark/iPL3Y8/ew9i3y0IQITVFRN6AQmUirG6YBIaNt5jw+N081qYuPtco7AwcbOWBiYz9JX6k6xfFcWGw0+bDUkpGZQkVgBdqAaQUj6kEzpyNU5vsyHY7jqBAYdkjReTXf7s+ZtNCIs/qLuhgipYIOEwCtv5cUkC5WMFa1/wIWKq7LwkS6TmrjbFxC0+fXna9xwa6hdUSkz3t0B8d81tEln5TbJovlViJObM1GqxQPrU8UUoGvdOWzaTdLLWB70Z1M70Gy+XwPba+Ce6tJsRzoKpELCEYuNyTPivPAbNmzqpUB+LzBNg90X7WPO2cfI1mlHBOOV1l8ogac/wEJvxQyNMg08z07JQUJfg6sbBsQMSc7EWj46owCnvvPZ6xQ+wkz3h+HEwYTxTFuoO/9/2LIpXvMqmO6n7WJ/jpBlJA/2ejWX1Eb8EXWBsNm6/8EVZLz5JSnFhyxZ6XxB83rsGGSGtQy4CR2JZisies8RqWNATF+6UGXd4ydPi9WXr4/BsPRMnObWAZrFUzzqgoLpkKQ/YTSqn55Id0PfL9veCWVwrWel5fvBB8Pkad3eG+VVhszgTzlcQyni8hZbI1StCpLIjGqWRxhp7F7fPBs+MBUKTX7P/wrCwAhn67i54rzvdc0Tk1BQuBVa7b6T+o1dO9xtMnz811Pj817bhF9oVvM="
26 | distributions: sdist bdist_wheel
27 | on:
28 | tags: true
29 | branch: master
30 | skip_existing: true
31 |
--------------------------------------------------------------------------------
/settings.py:
--------------------------------------------------------------------------------
1 | # Django settings for autodata project.
2 | import django
3 |
4 | DEBUG = True
5 |
6 | DATABASES = {
7 | 'default': {
8 | 'ENGINE': 'django.db.backends.sqlite3',
9 | }
10 | }
11 |
12 | # Make this unique, and don't share it with anybody.
13 | SECRET_KEY = 'mq%31q+sjj^)m^tvy(klwqw6ksv7du2yzdf9yn78iga*r%8w^t-djactasauth'
14 |
15 | INSTALLED_APPS = (
16 | 'django.contrib.sessions',
17 | 'django.contrib.auth',
18 | 'django.contrib.contenttypes',
19 | 'djactasauth',
20 | 'testapp',
21 | )
22 |
23 | MIDDLEWARE = [
24 | 'django.contrib.sessions.middleware.SessionMiddleware',
25 | 'django.contrib.auth.middleware.AuthenticationMiddleware',
26 | ]
27 |
28 | STATIC_URL = '/static/'
29 |
30 | ROOT_URLCONF = 'testapp.urls'
31 |
32 | TEMPLATES = [
33 | {
34 | 'BACKEND': 'django.template.backends.django.DjangoTemplates',
35 | 'DIRS': [],
36 | 'APP_DIRS': True,
37 | 'OPTIONS': {
38 | 'context_processors': [
39 | # Insert your TEMPLATE_CONTEXT_PROCESSORS here or use this
40 | # list if you haven't customized them:
41 | 'django.contrib.auth.context_processors.auth',
42 | 'django.template.context_processors.debug',
43 | 'django.template.context_processors.i18n',
44 | 'django.template.context_processors.media',
45 | 'django.template.context_processors.static',
46 | 'django.template.context_processors.tz',
47 | 'django.contrib.messages.context_processors.messages',
48 | ],
49 | },
50 | },
51 | ]
52 |
--------------------------------------------------------------------------------
/docs/extension-points.rst:
--------------------------------------------------------------------------------
1 | Extension points
2 | ================
3 |
4 | Authentication Backends
5 | -----------------------
6 |
7 | FilteredModelBackend
8 | ....................
9 |
10 | If a subclass of ``djactasauth.backends.FilteredModelBackend`` has a class
11 | or instance level ``filter_kwargs`` field, then those filters would be applied
12 | in the ``FilteredModelBackend.get_user`` method.
13 |
14 | If there is no such field, it's ignored, and the behaviour is the same
15 | as its parent, ``django.contrib.auth.backends.ModelBackend``.
16 |
17 | An empty dictionary (``{}``) is also a valid value for filters, again,
18 | the behavior is the same as if no such field was specifiec.
19 |
20 | ``ActAsBackend``
21 | .....................
22 |
23 | You can have precise control over which user can act as which other kind
24 | of user, by subclassing ``djactasauth.backends.ActAsBackend``, and describing your policy
25 | by overwriting the ``can_act_as(self, auth_user, user)`` method. For an
26 | example, see ``djactasauth.backends.OnlySuperuserCanActAsBackend``.
27 |
28 | ``ActAsBackend`` by default doesn't allow anyone to act-as, so there
29 | is no chance for misconfiguration.
30 |
31 |
32 | Views
33 | -----
34 |
35 | ``act_as_login_view``
36 | .....................
37 |
38 |
39 | You can extend ``djactasauth.views.act_as_login_view`` through the
40 | standard ``kwargs``, as you would extend
41 | ``django.contrib.auth.views.login``, or you can create your own view
42 | method that eventually delegates to it - the same way this implementation
43 | does for Django's own :-)
44 |
45 | Forms
46 | -----
47 |
48 | ``get_login_form``
49 | ..................
50 |
51 | ``djactasauth.views.get_login_form``
52 |
53 | This is used by ``djactasauth.views.act_as_login_view``. On the one hand,
54 | it backports a Django 1.6 feature to 1.5 (pass in ``request`` as an argument
55 | to the form), and if needed, it mixes in
56 | ``djactasauth.forms.InitialValuesFromRequestGetFormMixin``, so the username
57 | can be prefilled for act-as-auth links from the ``GET`` request.
58 |
59 | ``InitialValuesFromRequestGetFormMixin``
60 | ........................................
61 |
62 | ``djactasauth.forms.InitialValuesFromRequestGetFormMixin`` is a
63 | ``Form`` mixin, which - given one of its super`s has initialized
64 | the form's ``self.request``, will got through ``self.request.GET``, and
65 | copy over the values to ``self.initial`` - unless ``self.initial`` already
66 | has a value for the given field names you declared in your class's
67 | ``query2initial`` property (``tuple``).
68 |
69 | This is needed for a feature here, but you might find it useful in other
70 | parts of your code too :-)
71 |
72 | Other
73 | -----
74 |
75 | ``djactasauth.util.act_as_login_url``
76 | .....................................
77 |
78 | Convenience method to encapsulate how the act as auth username should be
79 | constructed from the two usernames.
80 |
--------------------------------------------------------------------------------
/docs/release-notes.rst:
--------------------------------------------------------------------------------
1 | Release Notes
2 | =============
3 |
4 | 0.6.0
5 | -----
6 |
7 | * version upgrade release
8 | * drop support for Django 1.11, 2.1, 2.2
9 | * add support for Django 4.2, 5.1, 5.2
10 | * drop support for Python 2.7, 3.5, 3.6, 3.7
11 | * add support for Python 3.9, 3.10, 3.11, 3.12, 3.13
12 |
13 | 0.3.0
14 | -----
15 |
16 | * drop support for deprecated python and django versions
17 | * conform to latest flake8
18 |
19 | 0.2.1
20 | -----
21 |
22 | * bugfix: #12 - won't crash if username contains multiple act as auth
23 | sepchars (e.g.: ``admin/user/`` (note the trailing slash)
24 | * bugfix: #13 - wrapping ``act_as_auth_view`` in ``sensitive_post_parameters``
25 |
26 | 0.2.0
27 | -----
28 |
29 | * BACKWARDS INCOMPATIBLE: not inheriting from ``ModelBackend``,
30 | but rather working in addition to the existing
31 | ``settings.AUTHENTICATION_BACKENDS``
32 | * BACKWARDS INCOMPATIBLE: only one act-as-auth backend can be
33 | configured for ``settings.AUTHENTICATION_BACKENDS``
34 |
35 | 0.1.7
36 | -----
37 |
38 | * add support for Django 1.11 and thus python 3.6
39 |
40 | 0.1.6
41 | -----
42 |
43 | * add support for Django 1.10
44 |
45 | 0.1.5
46 | -----
47 |
48 | * fix ``description`` on https://pypi.python.org
49 |
50 | 0.1.4
51 | -----
52 |
53 | * first public release to pypi
54 | * fixed ``README.rst`` to look OK on https://pypi.python.org
55 |
56 | 0.1.3
57 | -----
58 |
59 | * explicitly add support for Django 1.6 and 1.7
60 | * use Django's own bundled ``six`` instead of installing the external version
61 | * explicity add support for Django's own supported Python version, i.e.:
62 | Python 3.3 and 3.5 too (dropped 3.2 support as the travis build failed
63 | during setup)
64 |
65 | 0.1.2
66 | -----
67 |
68 | * introduce
69 |
70 | * ``act_as_login_view``
71 | * ``act_as_login_url``
72 | * ``get_login_form``
73 | * ``InitialValuesFromRequestGetFormMixin``
74 |
75 | as part of the public api
76 |
77 | * "backport" to Django 1.5: ``authentication_form`` has ``request`` even
78 | on ``POST``
79 | * can prefill ``username`` from query string
80 | * bugfix: when user to act as is ``None``, don't crash the process (e.g.:
81 | when ``can_act_as`` checked some property of the user, thus generating
82 | an ``AttributeError``)
83 |
84 | 0.1.1
85 | -----
86 |
87 | * bugfix: ``ActAsModelBackend.is_act_as_username`` used to fail when
88 | ``username`` argument was ``None``, now it returns ``False``
89 | * explicitly regression testing for login redirecting to
90 | value provided in ``REDIRECT_FIELD_NAME``
91 | * bugfix: ``setup.py`` now lists its dependencies (and added ``six``)
92 |
93 | 0.1.0
94 | -----
95 |
96 | * initial release
97 | * supports Django 1.5, 1.8 and 1.9 on python 2.7 and 3.4
98 | * introduce ``FilteredModelBackend``, ``ActAsModelBackend``,
99 | and ``OnlySuperuserCanActAsModelBackend``
100 |
--------------------------------------------------------------------------------
/tox2travis.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import os
3 | import subprocess
4 |
5 |
6 | class ToxToTravis:
7 |
8 | def __init__(self, cwd):
9 | self.cwd = cwd
10 |
11 | def parse_tox(self):
12 | proc = subprocess.Popen(
13 | "tox -l", shell=True, stdout=subprocess.PIPE, cwd=self.cwd,
14 | universal_newlines=True)
15 | self.tox_lines = proc.stdout.read().strip().split('\n')
16 | self.parse_python_versions()
17 |
18 | def parse_python_versions(self):
19 | tox_pys = set([])
20 | djangos = set([])
21 | tox_py_to_djangos = {}
22 | for tox_line in self.tox_lines:
23 | py, env = tox_line.split('-')
24 | tox_pys.add(py)
25 | djangos.add(env)
26 | tox_py_to_djangos.setdefault(py, [])
27 | tox_py_to_djangos[py].append(env)
28 |
29 | self.djangos = sorted(djangos)
30 | self.tox_pys = sorted(tox_pys)
31 | self.tox_py_to_djangos = tox_py_to_djangos
32 |
33 | def write_travis(self):
34 | lines = self.setup_python() + self.matrix() + self.test_command()
35 | print('\n'.join(lines))
36 |
37 | def setup_python(self):
38 | return [
39 | 'dist: xenial',
40 | 'language: python',
41 | 'before_install:',
42 | ' - sudo apt-get -qq update',
43 | ' - sudo apt-get install -y make sed',
44 | 'install:',
45 | ' - pip install tox',
46 | ]
47 |
48 | def matrix(self):
49 | self.tox2travis_py = dict(
50 | py27='2.7',
51 | py35='3.5',
52 | py36='3.6',
53 | py37='3.7',
54 | )
55 | output = [
56 | 'matrix:',
57 | ' include:',
58 | ]
59 | for tox_py, djangos in sorted(self.tox_py_to_djangos.items()):
60 | tox_envs_gen = ('-'.join((tox_py, d)) for d in sorted(djangos))
61 | item = [
62 | ' - python: "%s"' % self.tox2travis_py[tox_py],
63 | ' env: TOX_ENVS=%s' % ','.join(tox_envs_gen),
64 | ]
65 | output += item
66 | return output
67 |
68 | def test_command(self):
69 | return [
70 | 'script:',
71 | ' - tox -e $TOX_ENVS',
72 | 'before_deploy: "make clean"',
73 | 'deploy:',
74 | ' provider: pypi',
75 | ' user: "paessler_bis"',
76 | ' password:',
77 | ' secure: "N+ark/iPL3Y8/ew9i3y0IQITVFRN6AQmUirG6YBIaNt5jw+N081qYuPtco7AwcbOWBiYz9JX6k6xfFcWGw0+bDUkpGZQkVgBdqAaQUj6kEzpyNU5vsyHY7jqBAYdkjReTXf7s+ZtNCIs/qLuhgipYIOEwCtv5cUkC5WMFa1/wIWKq7LwkS6TmrjbFxC0+fXna9xwa6hdUSkz3t0B8d81tEln5TbJovlViJObM1GqxQPrU8UUoGvdOWzaTdLLWB70Z1M70Gy+XwPba+Ce6tJsRzoKpELCEYuNyTPivPAbNmzqpUB+LzBNg90X7WPO2cfI1mlHBOOV1l8ogac/wEJvxQyNMg08z07JQUJfg6sbBsQMSc7EWj46owCnvvPZ6xQ+wkz3h+HEwYTxTFuoO/9/2LIpXvMqmO6n7WJ/jpBlJA/2ejWX1Eb8EXWBsNm6/8EVZLz5JSnFhyxZ6XxB83rsGGSGtQy4CR2JZisies8RqWNATF+6UGXd4ydPi9WXr4/BsPRMnObWAZrFUzzqgoLpkKQ/YTSqn55Id0PfL9veCWVwrWel5fvBB8Pkad3eG+VVhszgTzlcQyni8hZbI1StCpLIjGqWRxhp7F7fPBs+MBUKTX7P/wrCwAhn67i54rzvdc0Tk1BQuBVa7b6T+o1dO9xtMnz811Pj817bhF9oVvM="', # noqa: E501
78 | ' distributions: sdist bdist_wheel',
79 | ' on:',
80 | ' tags: true',
81 | ' branch: master',
82 | ' skip_existing: true',
83 | ]
84 |
85 |
86 | def main():
87 | cwd = os.path.abspath(os.path.dirname(__file__))
88 | ttt = ToxToTravis(cwd)
89 | ttt.parse_tox()
90 | ttt.write_travis()
91 |
92 |
93 | if __name__ == '__main__':
94 | main()
95 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | Django Act As Auth Backend
2 | ==========================
3 |
4 | .. sales pitch start
5 |
6 | Django authentication back-end that allows one to login as someone else
7 | (an existing Django user allowed to login) without having to know their
8 | password.
9 |
10 | Great for customer support and testing scenarios!
11 |
12 | .. sales pitch end
13 |
14 | .. image:: https://readthedocs.org/projects/django-act-as-auth/badge/?version=latest
15 | :target: http://django-act-as-auth.readthedocs.org/
16 |
17 | .. quickstart start
18 |
19 | Quickstart
20 | ----------
21 |
22 | Install ``djactasauth``::
23 |
24 | pip install djactasauth
25 |
26 | Add it to your auth backends in ``settings``::
27 |
28 | import djactasauth
29 | AUTHENTICATION_BACKENDS = (
30 | ...,
31 | 'djactasauth.backends.OnlySuperuserCanActAsBackend',
32 | ...,
33 | )
34 |
35 | Configure the custom login view to take advantage of all the features
36 | in your ``urls.py``::
37 |
38 | from django.conf.urls import patterns, url
39 | from djactasauth.views import PrefillLoginView
40 | from testapp.views import whoami
41 |
42 |
43 | urlpatterns = patterns(
44 | '',
45 | url(r'^login/$', PrefillLoginView.as_view(), {}, 'login'),
46 | )
47 |
48 |
49 | Then you can log in with username ``your_superuser_name/customer`` and password
50 | ``yourpassword``.
51 |
52 | The full `documentation `_ including release notes on read the docs.
53 | .. quickstart end
54 |
55 |
56 | .. contributing start
57 |
58 | Contributing
59 | ------------
60 |
61 | As an open source project, we welcome contributions.
62 |
63 | The code lives on `github `_.
64 |
65 | Reporting issues/improvements
66 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
67 |
68 | Please open an `issue on github `_
69 | or provide a `pull request `_
70 | whether for code or for the documentation.
71 |
72 | For non-trivial changes, we kindly ask you to open an issue, as it might be rejected.
73 | However, if the diff of a pull request better illustrates the point, feel free to make
74 | it a pull request anyway.
75 |
76 | Pull Requests
77 | ~~~~~~~~~~~~~
78 |
79 | * for code changes
80 |
81 | * it must have tests covering the change. You might be asked to cover missing scenarios
82 | * the latest ``flake8`` will be run and shouldn't produce any warning
83 | * if the change is significant enough, documentation has to be provided
84 |
85 | * if you are not there already, add yourself to the `Authors `_ file
86 |
87 | To trigger packaging, run `make release` on the master branch.
88 |
89 | Setting up all Python versions
90 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
91 |
92 | ::
93 |
94 | sudo apt-get -y install software-properties-common
95 | sudo add-apt-repository ppa:fkrull/deadsnakes
96 | sudo apt-get update
97 | for version in 3.9 3.10 3.11 3.12 3.13; do
98 | py=python$version
99 | if ! which ${py}; then
100 | sudo apt-get -y install ${py} ${py}-dev
101 | fi
102 | done
103 | sudo add-apt-repository --remove ppa:deadsnakes/ppa
104 | sudo apt-get update
105 |
106 | Code of Conduct
107 | ~~~~~~~~~~~~~~~
108 |
109 | As it is a Django extension, it follows
110 | `Django's own Code of Conduct `_.
111 | As there is no mailing list yet, please just email one of the main authors
112 | (see ``setup.py`` file)
113 |
114 |
115 | .. contributing end
116 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: clean-python clean-build docs clean-tox
2 | #PYPI_SERVER?=pypi
3 | PYPI_SERVER?=testpypi
4 | ifeq ($(PYPI_SERVER),testpypi)
5 | TWINE_PASSWORD=${TEST_TWINE_PASSWORD}
6 | else
7 | TWINE_PASSWORD=${ACTASAUTH_TWINE_PASSWORD}
8 | endif
9 | RELEASE_PYTHON=python3.13
10 | RELEASE_VENV=release-venv-${RELEASE_PYTHON}
11 | RELEASE_PYTHON_ACTIVATE=${RELEASE_VENV}/bin/activate
12 | GIT_REMOTE_NAME?=origin
13 | SHELL=/bin/bash
14 | PACKAGE_NAME=djactasauth
15 | VERSION=$(shell ${RELEASE_PYTHON} -c"import ${PACKAGE_NAME} as m; print(m.__version__)")
16 | PACKAGE_FILE_TGZ=dist/${PACKAGE_NAME}-${VERSION}.tar.gz
17 | PACKAGE_FILE_WHL=dist/${PACKAGE_NAME}-${VERSION}-py3-none-any.whl
18 |
19 | help:
20 | @echo "clean-build - remove build artifacts"
21 | @echo "clean-python - remove Python file artifacts"
22 | @echo "clean-tox - remove test artifacts"
23 | @echo "lint - check style with flake8"
24 | @echo "test - run tests quickly with the default Python"
25 | @echo "test-all - run tests on every Python version with tox"
26 | @echo "coverage - check code coverage quickly with the default Python"
27 | @echo "docs - generate Sphinx HTML documentation, including API docs"
28 | @echo "tag - git tag the current version which creates a new pypi package with travis-ci's help"
29 | @echo "package- build the sdist/wheel"
30 | @echo "release- package, tag, and publush"
31 |
32 | clean: clean-build clean-python clean-tox
33 |
34 | clean-build:
35 | rm -fr build/
36 | rm -fr dist/
37 | find -name *.egg-info -type d | xargs rm -rf
38 |
39 | clean-python:
40 | find . -name '*.pyc' -exec rm -f {} +
41 | find . -name '*.pyo' -exec rm -f {} +
42 | find . -name '*~' -exec rm -f {} +
43 | find . -name '__pycache__' -type d -exec rm -rf {} +
44 |
45 | clean-tox:
46 | if [[ -d .tox ]]; then rm -r .tox; fi
47 |
48 | lint:
49 | flake8 ${PACKAGE_NAME} tests --max-complexity=10
50 |
51 | test:
52 | python manage.py test testapp --traceback
53 |
54 | test-all: clean-tox
55 | tox
56 |
57 | coverage:
58 | coverage run --source ${PACKAGE_NAME} setup.py test
59 | coverage report -m
60 | coverage html
61 | open htmlcov/index.html
62 |
63 | docs: outfile=/tmp/readme-errors
64 | docs:
65 | rst2html.py README.rst > /dev/null 2> ${outfile}
66 | cat ${outfile}
67 | test 0 -eq `cat ${outfile} | wc -l`
68 |
69 | tag: TAG:=v${VERSION}
70 | tag: exit_code=$(shell git ls-remote ${GIT_REMOTE_NAME} | grep -q tags/${TAG}; echo $$?)
71 | tag:
72 | ifeq ($(exit_code),0)
73 | @echo "Tag ${TAG} already present"
74 | else
75 | @echo "git tag -a ${TAG} -m"${TAG}"; git push --tags ${GIT_REMOTE_NAME}"
76 | endif
77 |
78 | ${RELEASE_VENV}:
79 | virtualenv --python ${RELEASE_PYTHON} ${RELEASE_VENV}
80 |
81 | build-deps: ${RELEASE_VENV}
82 | source ${RELEASE_PYTHON_ACTIVATE} && python -m pip install --upgrade build
83 | source ${RELEASE_PYTHON_ACTIVATE} && python -m pip install --upgrade twine
84 |
85 |
86 | ${PACKAGE_FILE_TGZ}: ${PACKAGE_NAME}/ pyproject.toml Makefile setup.py setup.cfg
87 | ${PACKAGE_FILE_WHL}: ${PACKAGE_NAME}/ pyproject.toml Makefile setup.py setup.cfg
88 | source ${RELEASE_PYTHON_ACTIVATE} && python -m build
89 |
90 | package: build-deps clean-build clean-python ${PACKAGE_FILE} ${PACKAGE_FILE_WHL}
91 |
92 |
93 | release: package
94 | ifeq ($(TWINE_PASSWORD),)
95 | echo TWINE_PASSWORD empty
96 | echo "USE env vars TEST_TWINE_PASSWORD/ACTASAUTH_TWINE_PASSWORD env vars before invoking make"
97 | false
98 | endif
99 | twine check dist/*
100 | echo "if the release fails, setup a ~/pypirc file as per https://packaging.python.org/en/latest/tutorials/packaging-projects/"
101 | # env | grep TWINE
102 | source ${RELEASE_PYTHON_ACTIVATE} && TWINE_PASSWORD=${TWINE_PASSWORD} python -m twine upload --repository ${PYPI_SERVER} dist/* --verbose
103 |
--------------------------------------------------------------------------------
/djactasauth/backends.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import logging
3 |
4 | from django.contrib.auth.backends import ModelBackend
5 | from django.contrib import auth
6 |
7 | log = logging.getLogger(__name__)
8 |
9 |
10 | class FilteredModelBackend(ModelBackend):
11 | def get_user(self, user_id):
12 | user = super(FilteredModelBackend, self).get_user(user_id)
13 | return self.filter_user(user)
14 |
15 | def authenticate(self, request, **kwargs):
16 | user = super(FilteredModelBackend, self).authenticate(
17 | request=request, **kwargs)
18 | return self.filter_user(user)
19 |
20 | def filter_user(self, user):
21 | if not user:
22 | return user
23 | filters = getattr(self, 'filter_kwargs', None)
24 | if filters:
25 | qs = type(user)._default_manager.filter(
26 | pk=user.pk).filter(**filters)
27 | if not qs.exists():
28 | return None
29 | return user
30 |
31 |
32 | class ActAsBackend(object):
33 |
34 | sepchar = '/'
35 | too_many_sepchar_msg = 'Username holds more than one separation char "{}"'\
36 | '.'.format(sepchar)
37 |
38 | @classmethod
39 | def is_act_as_username(cls, username):
40 | if not username:
41 | return False
42 | if username.count(ActAsBackend.sepchar) > 1:
43 | log.warn(cls.too_many_sepchar_msg)
44 | return False
45 | return cls.sepchar in username
46 |
47 | def authenticate(self, request, username=None, password=None, **kwargs):
48 | self.fail_unless_one_aaa_backend_is_configured()
49 | assert password is not None
50 | if not self.is_act_as_username(username):
51 | return None
52 | auth_username, act_as_username = username.split(self.sepchar)
53 | backends = [b for b in auth.get_backends() if not
54 | isinstance(b, ActAsBackend)]
55 | for backend in backends:
56 | auth_user = backend.authenticate(
57 | request=request, username=auth_username, password=password,
58 | **kwargs)
59 | if auth_user:
60 | return self.get_act_as_user(
61 | auth_user=auth_user, act_as_username=act_as_username)
62 |
63 | def fail_unless_one_aaa_backend_is_configured(self):
64 | aaa_backends = list(
65 | type(backend) for backend in auth.get_backends()
66 | if isinstance(backend, ActAsBackend))
67 | if len(aaa_backends) != 1:
68 | raise ValueError(
69 | 'There should be exactly one AAA backend configured, '
70 | 'but there were {}'.format(aaa_backends))
71 |
72 | def get_act_as_user(self, auth_user, act_as_username):
73 | if auth_user.username != act_as_username:
74 | UserModel = auth.get_user_model()
75 | try:
76 | user = self._get_user_manager().get_by_natural_key(
77 | act_as_username)
78 | except UserModel.DoesNotExist:
79 | return None
80 | if not self.can_act_as(auth_user=auth_user, user=user):
81 | return None
82 | else:
83 | user = auth_user
84 | return user
85 |
86 | def _get_user_manager(self):
87 | UserModel = auth.get_user_model()
88 | return UserModel._default_manager
89 |
90 | def can_act_as(self, auth_user, user):
91 | return False
92 |
93 | def get_user(self, user_id):
94 | return self._get_user_manager().get(pk=user_id)
95 |
96 |
97 | class OnlySuperuserCanActAsBackend(ActAsBackend):
98 | def can_act_as(self, auth_user, user):
99 | return auth_user.is_superuser and not user.is_superuser
100 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line.
5 | SPHINXOPTS =
6 | SPHINXBUILD = sphinx-build
7 | PAPER =
8 | BUILDDIR = _build
9 |
10 | # User-friendly check for sphinx-build
11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
13 | endif
14 |
15 | # Internal variables.
16 | PAPEROPT_a4 = -D latex_paper_size=a4
17 | PAPEROPT_letter = -D latex_paper_size=letter
18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
19 | # the i18n builder cannot share the environment and doctrees with the others
20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
21 |
22 | .PHONY: help
23 | help:
24 | @echo "Please use \`make ' where is one of"
25 | @echo " html to make standalone HTML files"
26 | @echo " dirhtml to make HTML files named index.html in directories"
27 | @echo " singlehtml to make a single large HTML file"
28 | @echo " pickle to make pickle files"
29 | @echo " json to make JSON files"
30 | @echo " htmlhelp to make HTML files and a HTML help project"
31 | @echo " qthelp to make HTML files and a qthelp project"
32 | @echo " applehelp to make an Apple Help Book"
33 | @echo " devhelp to make HTML files and a Devhelp project"
34 | @echo " epub to make an epub"
35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
36 | @echo " latexpdf to make LaTeX files and run them through pdflatex"
37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
38 | @echo " text to make text files"
39 | @echo " man to make manual pages"
40 | @echo " texinfo to make Texinfo files"
41 | @echo " info to make Texinfo files and run them through makeinfo"
42 | @echo " gettext to make PO message catalogs"
43 | @echo " changes to make an overview of all changed/added/deprecated items"
44 | @echo " xml to make Docutils-native XML files"
45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes"
46 | @echo " linkcheck to check all external links for integrity"
47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)"
48 | @echo " coverage to run coverage check of the documentation (if enabled)"
49 |
50 | .PHONY: clean
51 | clean:
52 | rm -rf $(BUILDDIR)/*
53 |
54 | .PHONY: html
55 | html:
56 | $(SPHINXBUILD) -nWT -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
57 | @echo
58 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
59 |
60 | .PHONY: dirhtml
61 | dirhtml:
62 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
63 | @echo
64 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
65 |
66 | .PHONY: singlehtml
67 | singlehtml:
68 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
69 | @echo
70 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
71 |
72 | .PHONY: pickle
73 | pickle:
74 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
75 | @echo
76 | @echo "Build finished; now you can process the pickle files."
77 |
78 | .PHONY: json
79 | json:
80 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
81 | @echo
82 | @echo "Build finished; now you can process the JSON files."
83 |
84 | .PHONY: htmlhelp
85 | htmlhelp:
86 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
87 | @echo
88 | @echo "Build finished; now you can run HTML Help Workshop with the" \
89 | ".hhp project file in $(BUILDDIR)/htmlhelp."
90 |
91 | .PHONY: qthelp
92 | qthelp:
93 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
94 | @echo
95 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \
96 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
97 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/DjangoActAsAuth.qhcp"
98 | @echo "To view the help file:"
99 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/DjangoActAsAuth.qhc"
100 |
101 | .PHONY: applehelp
102 | applehelp:
103 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp
104 | @echo
105 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp."
106 | @echo "N.B. You won't be able to view it unless you put it in" \
107 | "~/Library/Documentation/Help or install it in your application" \
108 | "bundle."
109 |
110 | .PHONY: devhelp
111 | devhelp:
112 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
113 | @echo
114 | @echo "Build finished."
115 | @echo "To view the help file:"
116 | @echo "# mkdir -p $$HOME/.local/share/devhelp/DjangoActAsAuth"
117 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/DjangoActAsAuth"
118 | @echo "# devhelp"
119 |
120 | .PHONY: epub
121 | epub:
122 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
123 | @echo
124 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
125 |
126 | .PHONY: latex
127 | latex:
128 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
129 | @echo
130 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
131 | @echo "Run \`make' in that directory to run these through (pdf)latex" \
132 | "(use \`make latexpdf' here to do that automatically)."
133 |
134 | .PHONY: latexpdf
135 | latexpdf:
136 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
137 | @echo "Running LaTeX files through pdflatex..."
138 | $(MAKE) -C $(BUILDDIR)/latex all-pdf
139 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
140 |
141 | .PHONY: latexpdfja
142 | latexpdfja:
143 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
144 | @echo "Running LaTeX files through platex and dvipdfmx..."
145 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
146 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
147 |
148 | .PHONY: text
149 | text:
150 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
151 | @echo
152 | @echo "Build finished. The text files are in $(BUILDDIR)/text."
153 |
154 | .PHONY: man
155 | man:
156 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
157 | @echo
158 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
159 |
160 | .PHONY: texinfo
161 | texinfo:
162 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
163 | @echo
164 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
165 | @echo "Run \`make' in that directory to run these through makeinfo" \
166 | "(use \`make info' here to do that automatically)."
167 |
168 | .PHONY: info
169 | info:
170 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
171 | @echo "Running Texinfo files through makeinfo..."
172 | make -C $(BUILDDIR)/texinfo info
173 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
174 |
175 | .PHONY: gettext
176 | gettext:
177 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
178 | @echo
179 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
180 |
181 | .PHONY: changes
182 | changes:
183 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
184 | @echo
185 | @echo "The overview file is in $(BUILDDIR)/changes."
186 |
187 | .PHONY: linkcheck
188 | linkcheck:
189 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
190 | @echo
191 | @echo "Link check complete; look for any errors in the above output " \
192 | "or in $(BUILDDIR)/linkcheck/output.txt."
193 |
194 | .PHONY: doctest
195 | doctest:
196 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
197 | @echo "Testing of doctests in the sources finished, look at the " \
198 | "results in $(BUILDDIR)/doctest/output.txt."
199 |
200 | .PHONY: coverage
201 | coverage:
202 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
203 | @echo "Testing of coverage in the sources finished, look at the " \
204 | "results in $(BUILDDIR)/coverage/python.txt."
205 |
206 | .PHONY: xml
207 | xml:
208 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
209 | @echo
210 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml."
211 |
212 | .PHONY: pseudoxml
213 | pseudoxml:
214 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
215 | @echo
216 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
217 |
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Django Act As Auth documentation build configuration file, created by
4 | # sphinx-quickstart on Thu Mar 10 09:22:19 2016.
5 | #
6 | # This file is execfile()d with the current directory set to its
7 | # containing dir.
8 | #
9 | # Note that not all possible configuration values are present in this
10 | # autogenerated file.
11 | #
12 | # All configuration values have a default; values that are commented out
13 | # serve to show the default.
14 |
15 | import sys
16 | import os
17 |
18 | # If extensions (or modules to document with autodoc) are in another directory,
19 | # add these directories to sys.path here. If the directory is relative to the
20 | # documentation root, use os.path.abspath to make it absolute, like shown here.
21 | #sys.path.insert(0, os.path.abspath('.'))
22 |
23 | # -- General configuration ------------------------------------------------
24 |
25 | # If your documentation needs a minimal Sphinx version, state it here.
26 | #needs_sphinx = '1.0'
27 |
28 | # Add any Sphinx extension module names here, as strings. They can be
29 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
30 | # ones.
31 | extensions = []
32 |
33 | # Add any paths that contain templates here, relative to this directory.
34 | templates_path = []
35 |
36 | # The suffix(es) of source filenames.
37 | # You can specify multiple suffix as a list of string:
38 | # source_suffix = ['.rst', '.md']
39 | source_suffix = '.rst'
40 |
41 | # The encoding of source files.
42 | #source_encoding = 'utf-8-sig'
43 |
44 | # The master toctree document.
45 | master_doc = 'index'
46 |
47 | # General information about the project.
48 | project = u'Django Act As Auth'
49 | copyright = u'2016, Paessler AG'
50 | author = u'Paessler AG'
51 |
52 | # The version info for the project you're documenting, acts as replacement for
53 | # |version| and |release|, also used in various other places throughout the
54 | # built documents.
55 | #
56 | # The short X.Y version.
57 | version = u'0.1.3'
58 | # The full version, including alpha/beta/rc tags.
59 | release = u'0.1.3'
60 |
61 | # The language for content autogenerated by Sphinx. Refer to documentation
62 | # for a list of supported languages.
63 | #
64 | # This is also used if you do content translation via gettext catalogs.
65 | # Usually you set "language" from the command line for these cases.
66 | language = None
67 |
68 | # There are two options for replacing |today|: either, you set today to some
69 | # non-false value, then it is used:
70 | #today = ''
71 | # Else, today_fmt is used as the format for a strftime call.
72 | #today_fmt = '%B %d, %Y'
73 |
74 | # List of patterns, relative to source directory, that match files and
75 | # directories to ignore when looking for source files.
76 | exclude_patterns = ['_build']
77 |
78 | # The reST default role (used for this markup: `text`) to use for all
79 | # documents.
80 | #default_role = None
81 |
82 | # If true, '()' will be appended to :func: etc. cross-reference text.
83 | #add_function_parentheses = True
84 |
85 | # If true, the current module name will be prepended to all description
86 | # unit titles (such as .. function::).
87 | #add_module_names = True
88 |
89 | # If true, sectionauthor and moduleauthor directives will be shown in the
90 | # output. They are ignored by default.
91 | #show_authors = False
92 |
93 | # The name of the Pygments (syntax highlighting) style to use.
94 | pygments_style = 'sphinx'
95 |
96 | # A list of ignored prefixes for module index sorting.
97 | #modindex_common_prefix = []
98 |
99 | # If true, keep warnings as "system message" paragraphs in the built documents.
100 | #keep_warnings = False
101 |
102 | # If true, `todo` and `todoList` produce output, else they produce nothing.
103 | todo_include_todos = False
104 |
105 |
106 | # -- Options for HTML output ----------------------------------------------
107 |
108 | # The theme to use for HTML and HTML Help pages. See the documentation for
109 | # a list of builtin themes.
110 | html_theme = 'alabaster'
111 |
112 | # Theme options are theme-specific and customize the look and feel of a theme
113 | # further. For a list of options available for each theme, see the
114 | # documentation.
115 | #html_theme_options = {}
116 |
117 | # Add any paths that contain custom themes here, relative to this directory.
118 | #html_theme_path = []
119 |
120 | # The name for this set of Sphinx documents. If None, it defaults to
121 | # " v documentation".
122 | #html_title = None
123 |
124 | # A shorter title for the navigation bar. Default is the same as html_title.
125 | #html_short_title = None
126 |
127 | # The name of an image file (relative to this directory) to place at the top
128 | # of the sidebar.
129 | #html_logo = None
130 |
131 | # The name of an image file (relative to this directory) to use as a favicon of
132 | # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
133 | # pixels large.
134 | #html_favicon = None
135 |
136 | # Add any paths that contain custom static files (such as style sheets) here,
137 | # relative to this directory. They are copied after the builtin static files,
138 | # so a file named "default.css" will overwrite the builtin "default.css".
139 | html_static_path = []
140 |
141 | # Add any extra paths that contain custom files (such as robots.txt or
142 | # .htaccess) here, relative to this directory. These files are copied
143 | # directly to the root of the documentation.
144 | #html_extra_path = []
145 |
146 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
147 | # using the given strftime format.
148 | #html_last_updated_fmt = '%b %d, %Y'
149 |
150 | # If true, SmartyPants will be used to convert quotes and dashes to
151 | # typographically correct entities.
152 | #html_use_smartypants = True
153 |
154 | # Custom sidebar templates, maps document names to template names.
155 | #html_sidebars = {}
156 |
157 | # Additional templates that should be rendered to pages, maps page names to
158 | # template names.
159 | #html_additional_pages = {}
160 |
161 | # If false, no module index is generated.
162 | #html_domain_indices = True
163 |
164 | # If false, no index is generated.
165 | #html_use_index = True
166 |
167 | # If true, the index is split into individual pages for each letter.
168 | #html_split_index = False
169 |
170 | # If true, links to the reST sources are added to the pages.
171 | #html_show_sourcelink = True
172 |
173 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
174 | #html_show_sphinx = True
175 |
176 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
177 | #html_show_copyright = True
178 |
179 | # If true, an OpenSearch description file will be output, and all pages will
180 | # contain a tag referring to it. The value of this option must be the
181 | # base URL from which the finished HTML is served.
182 | #html_use_opensearch = ''
183 |
184 | # This is the file name suffix for HTML files (e.g. ".xhtml").
185 | #html_file_suffix = None
186 |
187 | # Language to be used for generating the HTML full-text search index.
188 | # Sphinx supports the following languages:
189 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja'
190 | # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr'
191 | #html_search_language = 'en'
192 |
193 | # A dictionary with options for the search language support, empty by default.
194 | # Now only 'ja' uses this config value
195 | #html_search_options = {'type': 'default'}
196 |
197 | # The name of a javascript file (relative to the configuration directory) that
198 | # implements a search results scorer. If empty, the default will be used.
199 | #html_search_scorer = 'scorer.js'
200 |
201 | # Output file base name for HTML help builder.
202 | htmlhelp_basename = 'DjangoActAsAuthdoc'
203 |
204 | # -- Options for LaTeX output ---------------------------------------------
205 |
206 | latex_elements = {
207 | # The paper size ('letterpaper' or 'a4paper').
208 | #'papersize': 'letterpaper',
209 |
210 | # The font size ('10pt', '11pt' or '12pt').
211 | #'pointsize': '10pt',
212 |
213 | # Additional stuff for the LaTeX preamble.
214 | #'preamble': '',
215 |
216 | # Latex figure (float) alignment
217 | #'figure_align': 'htbp',
218 | }
219 |
220 | # Grouping the document tree into LaTeX files. List of tuples
221 | # (source start file, target name, title,
222 | # author, documentclass [howto, manual, or own class]).
223 | latex_documents = [
224 | (master_doc, 'DjangoActAsAuth.tex', u'Django Act As Auth Documentation',
225 | u'Paessler AG', 'manual'),
226 | ]
227 |
228 | # The name of an image file (relative to this directory) to place at the top of
229 | # the title page.
230 | #latex_logo = None
231 |
232 | # For "manual" documents, if this is true, then toplevel headings are parts,
233 | # not chapters.
234 | #latex_use_parts = False
235 |
236 | # If true, show page references after internal links.
237 | #latex_show_pagerefs = False
238 |
239 | # If true, show URL addresses after external links.
240 | #latex_show_urls = False
241 |
242 | # Documents to append as an appendix to all manuals.
243 | #latex_appendices = []
244 |
245 | # If false, no module index is generated.
246 | #latex_domain_indices = True
247 |
248 |
249 | # -- Options for manual page output ---------------------------------------
250 |
251 | # One entry per manual page. List of tuples
252 | # (source start file, name, description, authors, manual section).
253 | man_pages = [
254 | (master_doc, 'djangoactasauth', u'Django Act As Auth Documentation',
255 | [author], 1)
256 | ]
257 |
258 | # If true, show URL addresses after external links.
259 | #man_show_urls = False
260 |
261 |
262 | # -- Options for Texinfo output -------------------------------------------
263 |
264 | # Grouping the document tree into Texinfo files. List of tuples
265 | # (source start file, target name, title, author,
266 | # dir menu entry, description, category)
267 | texinfo_documents = [
268 | (master_doc, 'DjangoActAsAuth', u'Django Act As Auth Documentation',
269 | author, 'DjangoActAsAuth', 'One line description of project.',
270 | 'Miscellaneous'),
271 | ]
272 |
273 | # Documents to append as an appendix to all manuals.
274 | #texinfo_appendices = []
275 |
276 | # If false, no module index is generated.
277 | #texinfo_domain_indices = True
278 |
279 | # How to display URL addresses: 'footnote', 'no', or 'inline'.
280 | #texinfo_show_urls = 'footnote'
281 |
282 | # If true, do not generate a @detailmenu in the "Top" node's menu.
283 | #texinfo_no_detailmenu = False
284 |
--------------------------------------------------------------------------------
/tests/testapp/tests.py:
--------------------------------------------------------------------------------
1 | import urllib.parse
2 | from django.contrib.auth.backends import ModelBackend
3 | from django.contrib.auth.models import User
4 | from django.contrib.auth import signals as auth_signals, REDIRECT_FIELD_NAME
5 | from django.contrib.auth.forms import AuthenticationForm
6 | from django.test import TransactionTestCase
7 | from django.test.utils import override_settings
8 |
9 | from djactasauth.backends import \
10 | FilteredModelBackend, ActAsBackend, OnlySuperuserCanActAsBackend
11 | from djactasauth.util import act_as_login_url, get_login_url
12 | from testapp.sixmock import patch, call
13 |
14 |
15 | def create_user(
16 | username, password='password', is_superuser=False, is_staff=False):
17 | user = User(username=username, is_superuser=is_superuser)
18 | user.set_password(password)
19 | user.save()
20 | return user
21 |
22 |
23 | def auth_through_backend(backend, **kwargs):
24 | request = None
25 | return backend.authenticate(request, **kwargs)
26 |
27 |
28 | class FilteredBackendTestCase(TransactionTestCase):
29 |
30 | def test_it_is_a_model_backend(self):
31 | self.assertTrue(
32 | issubclass(FilteredModelBackend, ModelBackend),
33 | FilteredModelBackend.__mro__)
34 |
35 | def test_can_declare_filters_which_apply_to_get_user(self):
36 | staff = create_user(
37 | username='staff', is_staff=True, is_superuser=False)
38 | superuser = create_user(
39 | username='superuser', is_staff=True, is_superuser=True)
40 | customer = create_user(
41 | username='customer', is_staff=False, is_superuser=False)
42 | for u in [staff, superuser, customer]:
43 | u.set_password('password')
44 | u.save()
45 |
46 | class TestFilteredBackend(FilteredModelBackend):
47 |
48 | def __init__(self, filter_kwargs):
49 | self.filter_kwargs = filter_kwargs
50 |
51 | def run_scenarios_with(test_method):
52 | self.assertEqual(staff, test_method(staff, dict()))
53 | self.assertEqual(superuser, test_method(superuser, dict()))
54 | self.assertEqual(customer, test_method(customer, dict()))
55 |
56 | self.assertEqual(None, test_method(customer, dict(is_staff=True)))
57 | self.assertEqual(
58 | superuser, test_method(superuser, dict(is_superuser=True)))
59 | self.assertEqual(
60 | customer, test_method(
61 | customer, dict(username__startswith='c')))
62 | self.assertEqual(
63 | None, test_method(superuser, dict(username__startswith='c')))
64 |
65 | def get_user(user, filter_kwargs):
66 | backend = TestFilteredBackend(filter_kwargs)
67 | return backend.get_user(user.pk)
68 |
69 | run_scenarios_with(get_user)
70 |
71 | def authenticate(user, filter_kwargs):
72 | backend = TestFilteredBackend(filter_kwargs)
73 | return auth_through_backend(
74 | backend=backend, username=user.username, password='password')
75 |
76 | run_scenarios_with(authenticate)
77 |
78 |
79 | class TestableBackend(object):
80 |
81 | def __init__(self):
82 | self.reset()
83 |
84 | def authenticate(self, *a, **kw):
85 | kw.pop('request')
86 | self.calls.append((a, kw))
87 | return self.authenticated_user
88 |
89 | def reset(self):
90 | self.calls = []
91 | self.authenticated_user = None
92 |
93 |
94 | def patched_get_backends(backends):
95 | return patch(
96 | 'django.contrib.auth._get_backends',
97 | return_value=backends
98 | )
99 |
100 |
101 | class ActAsBackendAuthenticateTestCase(TransactionTestCase):
102 |
103 | def setUp(self):
104 | super(ActAsBackendAuthenticateTestCase, self).setUp()
105 | self.first_test_backend = TestableBackend()
106 | self.second_test_backend = TestableBackend()
107 | self.third_test_backend_not_in_get_backends = TestableBackend()
108 | self.act_as_auth_backend = ActAsBackend()
109 | self.backends = [
110 | self.first_test_backend,
111 | self.act_as_auth_backend,
112 | self.second_test_backend
113 | ]
114 |
115 | def patched_get_backends(self):
116 | return patched_get_backends(self.backends)
117 |
118 | def test_does_not_inherit_from_any_backend(self):
119 | self.assertEqual(
120 | (ActAsBackend, object),
121 | ActAsBackend.__mro__
122 | )
123 |
124 | def test_fails_if_multiple_act_as_backends_are_configured(self):
125 | """
126 | while I can see how one could like to have multiple rules for
127 | when one can becomes another user, I foresee complexity, unexpected
128 | bugs, corner cases, etc. and thus would much rather place the burden
129 | of managing the complexity/interaction between these various rules
130 | on the user of this library - break the rules apart into multiple
131 | methods, and compose them in your own code, so this library can
132 | remain simple
133 | """
134 | self.backends.append(ActAsBackend())
135 | with self.patched_get_backends():
136 | with self.assertRaises(ValueError):
137 | auth_through_backend(
138 | self.act_as_auth_backend,
139 | username='foo/bar', password='password')
140 |
141 | def test_it_tries_all_other_configured_backends(self):
142 | with self.patched_get_backends():
143 | auth_through_backend(
144 | self.act_as_auth_backend,
145 | username='foo/bar', password='password')
146 | self.assertEqual(
147 | [(tuple(), {'password': 'password', 'username': 'foo'})],
148 | self.first_test_backend.calls)
149 | self.assertEqual(
150 | [(tuple(), {'password': 'password', 'username': 'foo'})],
151 | self.second_test_backend.calls)
152 | self.assertEqual([], self.third_test_backend_not_in_get_backends.calls)
153 |
154 | def test_first_successful_backend_returned_later_ones_not_called(self):
155 | self.first_test_backend.authenticated_user = User()
156 | with self.patched_get_backends():
157 | auth_through_backend(
158 | self.act_as_auth_backend,
159 | username='foo/bar', password='password')
160 | self.assertEqual(
161 | [(tuple(), {'password': 'password', 'username': 'foo'})],
162 | self.first_test_backend.calls)
163 | self.assertEqual([], self.second_test_backend.calls)
164 |
165 | def test_cannot_authenticate_regular_user(self):
166 | with self.patched_get_backends():
167 | self.assertEqual(
168 | None,
169 | auth_through_backend(
170 | self.act_as_auth_backend,
171 | username='foo', password='password'))
172 | self.assertEqual([], self.first_test_backend.calls)
173 | self.assertEqual([], self.second_test_backend.calls)
174 |
175 | def test_can_become_another_user_with_own_password(self):
176 | create_user(username='admin', password='admin password')
177 | user = create_user(username='user', password='user password')
178 | self.assertEqual(
179 | None, self.authenticate(
180 | username='admin/user', password='user password'))
181 | self.assertEqual(
182 | user, self.authenticate(
183 | username='admin/user', password='admin password'))
184 |
185 | @patch("djactasauth.backends.log")
186 | def test_usernames_with_multiple_sepchars_trigger_log_warning(self,
187 | mock_log):
188 | create_user(username='admin', password='foo')
189 | self.assertEqual(None, self.authenticate(username='admin/user/',
190 | password='foo'))
191 | self.assertEqual(None, self.authenticate(username='admin//user',
192 | password='foo'))
193 | self.assertEqual(None, self.authenticate(username='admin/us/er',
194 | password='foo'))
195 | self.assertEqual(None, self.authenticate(username='/admin/user',
196 | password='foo'))
197 | calls = [call(ActAsBackend.too_many_sepchar_msg) for i in range(4)]
198 | mock_log.warn.assert_has_calls(calls)
199 |
200 | def test_cannot_become_nonexistent_user(self):
201 | create_user(username='admin', password='password')
202 | self.assertEqual(
203 | None, self.authenticate(
204 | username='admin/user', password='password'))
205 |
206 | def test_authenticate_does_not_fire_login_signal(self):
207 | def should_not_fire_login_signal(user, **kwargs):
208 | self.fail(
209 | 'should not have fired login signal but did for %r' % user)
210 |
211 | create_user(username='admin', password='admin password')
212 | user = create_user(username='user', password='user password')
213 |
214 | auth_signals.user_logged_in.connect(should_not_fire_login_signal)
215 | try:
216 | self.authenticate(username='admin/user', password='admin password')
217 | finally:
218 | auth_signals.user_logged_in.disconnect(
219 | should_not_fire_login_signal)
220 | self.assertEqual(
221 | user, self.authenticate(
222 | username='admin/user', password='admin password'))
223 |
224 | def test_only_super_user_can_act_as_model_backend_regression(self):
225 | create_user(
226 | username='admin1', password='admin1 password', is_superuser=True)
227 | create_user(
228 | username='admin2', password='admin2 password', is_superuser=True)
229 | user = create_user(
230 | username='user', password='user password', is_superuser=False)
231 |
232 | self.assertEqual(
233 | None, self.authenticate(
234 | username='user/admin1', password='user password',
235 | backend_cls=OnlySuperuserCanActAsBackend))
236 | self.assertEqual(
237 | None, self.authenticate(
238 | username='user/admin2', password='user password',
239 | backend_cls=OnlySuperuserCanActAsBackend))
240 |
241 | self.assertEqual(
242 | user, self.authenticate(
243 | backend_cls=OnlySuperuserCanActAsBackend,
244 | username='admin1/user', password='admin1 password'))
245 | self.assertEqual(
246 | user, self.authenticate(
247 | backend_cls=OnlySuperuserCanActAsBackend,
248 | username='admin2/user', password='admin2 password'))
249 |
250 | self.assertEqual(
251 | None, self.authenticate(
252 | backend_cls=OnlySuperuserCanActAsBackend,
253 | username='admin1/admin2', password='admin1 password'))
254 | self.assertEqual(
255 | None, self.authenticate(
256 | backend_cls=OnlySuperuserCanActAsBackend,
257 | username='admin2/admin1', password='admin2 password'))
258 |
259 | def test_can_customize_can_act_as_policy_by_subclassing(self):
260 | alice = create_user(username='alice', password='alice')
261 | create_user(username='bob', password='bob')
262 |
263 | class OnlyShortUserNamesCanActAs(ActAsBackend):
264 |
265 | def can_act_as(self, auth_user, user):
266 | return len(auth_user.username) <= 3
267 |
268 | self.assertEqual(
269 | None, self.authenticate(
270 | backend_cls=OnlyShortUserNamesCanActAs,
271 | username='alice/bob', password='alice'))
272 | self.assertEqual(
273 | alice, self.authenticate(
274 | backend_cls=OnlyShortUserNamesCanActAs,
275 | username='bob/alice', password='bob'))
276 |
277 | def test_when_users_none_doesnt_crash_process(self):
278 | create_user(username='jane', password='doe')
279 |
280 | class ShouldNotCallCanActAs(ActAsBackend):
281 |
282 | def can_act_as(backend_self, auth_user, user):
283 | self.fail('should not have called')
284 |
285 | self.assertEqual(
286 | None, self.authenticate(
287 | backend_cls=ShouldNotCallCanActAs,
288 | username='jane/non-existent-user', password='doe'))
289 |
290 | def test_is_act_as_username_method(self):
291 | def assert_classification(username, expected_to_be_act_as_username):
292 | self.assertEqual(
293 | expected_to_be_act_as_username,
294 | ActAsBackend.is_act_as_username(username))
295 |
296 | assert_classification(None, False)
297 | assert_classification('', False)
298 | assert_classification('user', False)
299 | assert_classification('user/johndoe', True)
300 |
301 | ###
302 |
303 | def authenticate(self, username, password, backend_cls=None):
304 | if not backend_cls:
305 | class EveryoneCanActAs(ActAsBackend):
306 | def can_act_as(self, auth_user, user):
307 | return True
308 | backend_cls = EveryoneCanActAs
309 |
310 | backend = backend_cls()
311 | with patched_get_backends([backend, ModelBackend()]):
312 | return auth_through_backend(
313 | backend, username=username, password=password)
314 |
315 |
316 | @override_settings(
317 | AUTHENTICATION_BACKENDS=[
318 | 'djactasauth.backends.OnlySuperuserCanActAsBackend',
319 | 'django.contrib.auth.backends.ModelBackend'])
320 | class EndToEndActAsThroughFormAndView(TransactionTestCase):
321 |
322 | def test_login_page_is_set_up_as_expected(self):
323 | self.goto_login_page()
324 | response = self.login_get_response
325 | self.assertEqual(200, response.status_code)
326 | form = response.context['form']
327 | self.assertTrue(
328 | isinstance(form, AuthenticationForm), type(form).__mro__)
329 |
330 | def test_successful_act_as_login_fires_signal_with_act_as_user(self):
331 | logged_in_users = []
332 |
333 | def handle_user_logged_in(user, **kwargs):
334 | logged_in_users.append(user)
335 |
336 | auth_signals.user_logged_in.connect(handle_user_logged_in)
337 | create_user(username='admin', password='admin', is_superuser=True)
338 | user = create_user(
339 | username='user', password='user', is_superuser=False)
340 | try:
341 | self.goto_login_page()
342 | self.submit_login(username='admin/user', password='admin')
343 | self.assertEqual(302, self.login_post_response.status_code)
344 | finally:
345 | auth_signals.user_logged_in.disconnect(handle_user_logged_in)
346 | self.assertEqual([user], logged_in_users)
347 |
348 | def test_after_login_correct_user_is_passed_in_the_request_no_act_as(self):
349 | create_user(username='admin', password='admin', is_superuser=True)
350 | self.assert_logged_in_user_on_next_request(
351 | username='admin', password='admin', display_user='admin')
352 |
353 | def test_after_login_correct_user_is_passed_in_the_request_act_as(self):
354 | create_user(username='admin', password='admin', is_superuser=True)
355 | create_user(username='user', password='user', is_superuser=False)
356 | self.assert_logged_in_user_on_next_request(
357 | username='admin/user', password='admin', display_user='user')
358 |
359 | def test_next_from_GET_is_respected_and_user_is_redirected_there(self):
360 | create_user(username='user', password='user', is_superuser=False)
361 | self.assert_logged_in_user_on_next_request(
362 | username='user', password='user', display_user='user',
363 | **{REDIRECT_FIELD_NAME: '/foo/'})
364 | redir_to = urllib.parse.urlparse(self.login_post_response['Location'])
365 | self.assertEqual('/foo/', redir_to.path)
366 |
367 | def test_on_post_form_has_access_to_request(self):
368 | self.goto_login_page()
369 | self.submit_login(username='foo', password='bar')
370 | response = self.login_post_response
371 | self.assertEqual(200, response.status_code)
372 | form = response.context['form']
373 | self.assertTrue(hasattr(form, 'request'))
374 | self.assertIsNotNone(form.request)
375 |
376 | def test_can_initialize_username_from_querystring(self):
377 | self.goto_login_page(username='foo')
378 | form = self.login_get_response.context['form']
379 | self.assertEqual('foo', form.initial.get('username'))
380 |
381 | ###
382 |
383 | def assert_logged_in_user_on_next_request(
384 | self, username, password, display_user, **query):
385 |
386 | self.goto_login_page(**query)
387 |
388 | self.submit_login(username=username, password=password, **query)
389 | response_content = self.login_post_response.content.decode('ascii')
390 | self.assertEqual(
391 | 302, self.login_post_response.status_code,
392 | (username, password, response_content))
393 |
394 | self.get_whoami_page()
395 | self.assertEqual(
396 | display_user, self.whoami_response.content.decode('ascii'))
397 |
398 | def goto_login_page(self, **query):
399 | url = get_login_url(**query)
400 | self.login_get_response = self.client.get(url)
401 | self.assertEqual(200, self.login_get_response.status_code)
402 |
403 | def submit_login(self, username, password, **query):
404 | url = get_login_url(**query)
405 | self.login_post_response = self.client.post(
406 | url, dict(username=username, password=password))
407 |
408 | def get_whoami_page(self):
409 | self.whoami_response = self.client.get('/whoami/')
410 | self.assertEqual(200, self.whoami_response.status_code)
411 |
412 |
413 | class ActAsUrlGeneratorTestCase(TransactionTestCase):
414 |
415 | def test_generates_the_correct_url(self):
416 | self.assertEqual(
417 | '/login/?username=admin%2Fuser',
418 | act_as_login_url(auth='admin', act_as='user'))
419 |
420 | self.assertEqual(
421 | '/login/?username=foo%2Fbar',
422 | act_as_login_url(auth='foo', act_as='bar'))
423 |
--------------------------------------------------------------------------------