├── docs
├── build
│ └── .hgignore
├── .static
│ ├── logo.png
│ ├── favicon.png
│ ├── authority-object-1to1.png
│ ├── authority-object-1toN.png
│ ├── authority-object-NtoN.png
│ ├── admin-action-permission.png
│ ├── authority-permission-py.png
│ └── authority-scheme-layer.png
├── .theme
│ └── nature
│ │ ├── theme.conf
│ │ └── static
│ │ ├── pygments.css
│ │ └── nature.css_t
├── check_python.txt
├── handling_python.txt
├── handling_template.txt
├── support.txt
├── documentation_guidelines.txt
├── installation.txt
├── configuration.txt
├── check_templates.txt
├── handling_admin.txt
├── index.txt
├── tips_tricks.txt
├── create_basic_permission.txt
├── check_decorator.txt
├── create_per_object_permission.txt
├── create_custom_permission.txt
└── conf.py
├── example
├── __init__.py
├── users
│ ├── __init__.py
│ ├── migrations
│ │ ├── __init__.py
│ │ └── 0001_initial.py
│ ├── admin.py
│ └── models.py
├── exampleapp
│ ├── __init__.py
│ ├── models.py
│ ├── forms.py
│ ├── admin.py
│ ├── views.py
│ └── permissions.py
├── production.py
├── development.py
├── templates
│ ├── registration
│ │ └── login.html
│ └── flatpages
│ │ └── default.html
├── manage.py
├── urls.py
└── settings.py
├── authority
├── migrations
│ ├── __init__.py
│ └── 0001_initial.py
├── templatetags
│ ├── __init__.py
│ └── permissions.py
├── utils.py
├── templates
│ ├── authority
│ │ ├── permission_delete_link.html
│ │ ├── permission_request_approve_link.html
│ │ ├── 403.html
│ │ ├── permission_request_delete_link.html
│ │ └── permission_form.html
│ └── admin
│ │ ├── permission_change_form.html
│ │ └── edit_inline
│ │ └── action_tabular.html
├── exceptions.py
├── fixtures
│ └── tests_custom.json
├── __init__.py
├── urls.py
├── managers.py
├── widgets.py
├── models.py
├── decorators.py
├── forms.py
├── views.py
├── sites.py
├── admin.py
├── permissions.py
└── tests.py
├── .coveragerc
├── .gitignore
├── setup.cfg
├── CONTRIBUTING.md
├── MANIFEST.in
├── AUTHORS
├── tox.ini
├── .github
└── workflows
│ ├── test.yml
│ └── release.yml
├── setup.py
├── LICENSE
├── CODE_OF_CONDUCT.md
└── README.rst
/docs/build/.hgignore:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/example/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/example/users/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/authority/migrations/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/example/exampleapp/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/authority/templatetags/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/example/users/migrations/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/example/production.py:
--------------------------------------------------------------------------------
1 | from example.settings import *
2 |
--------------------------------------------------------------------------------
/example/exampleapp/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 |
3 | # Create your models here.
4 |
--------------------------------------------------------------------------------
/docs/.static/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jazzband/django-authority/HEAD/docs/.static/logo.png
--------------------------------------------------------------------------------
/example/development.py:
--------------------------------------------------------------------------------
1 | from example.settings import *
2 |
3 | DEBUG = True
4 | TEMPLATE_DEBUG = DEBUG
5 |
--------------------------------------------------------------------------------
/.coveragerc:
--------------------------------------------------------------------------------
1 | [run]
2 | source = authority
3 | branch = 1
4 |
5 | [report]
6 | omit = *tests*,*migrations*
7 |
--------------------------------------------------------------------------------
/docs/.static/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jazzband/django-authority/HEAD/docs/.static/favicon.png
--------------------------------------------------------------------------------
/docs/.theme/nature/theme.conf:
--------------------------------------------------------------------------------
1 | [theme]
2 | inherit = basic
3 | stylesheet = nature.css
4 | pygments_style = tango
5 |
--------------------------------------------------------------------------------
/docs/.static/authority-object-1to1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jazzband/django-authority/HEAD/docs/.static/authority-object-1to1.png
--------------------------------------------------------------------------------
/docs/.static/authority-object-1toN.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jazzband/django-authority/HEAD/docs/.static/authority-object-1toN.png
--------------------------------------------------------------------------------
/docs/.static/authority-object-NtoN.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jazzband/django-authority/HEAD/docs/.static/authority-object-NtoN.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 | *.egg-info
3 | *.sql
4 | docs/build/*
5 | .DS_Store
6 | .tox/
7 | dist/
8 | build/
9 | .coverage
10 | .eggs/
11 |
--------------------------------------------------------------------------------
/docs/.static/admin-action-permission.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jazzband/django-authority/HEAD/docs/.static/admin-action-permission.png
--------------------------------------------------------------------------------
/docs/.static/authority-permission-py.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jazzband/django-authority/HEAD/docs/.static/authority-permission-py.png
--------------------------------------------------------------------------------
/docs/.static/authority-scheme-layer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jazzband/django-authority/HEAD/docs/.static/authority-scheme-layer.png
--------------------------------------------------------------------------------
/authority/utils.py:
--------------------------------------------------------------------------------
1 | from authority.sites import (
2 | site,
3 | get_check,
4 | get_choices_for,
5 | register,
6 | unregister,
7 | ) # noqa
8 |
--------------------------------------------------------------------------------
/authority/templates/authority/permission_delete_link.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% if url %}{% trans "Revoke permission" %} {% endif %}
--------------------------------------------------------------------------------
/authority/templates/authority/permission_request_approve_link.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% if url %}{% trans "Approve request" %} {% endif %}
--------------------------------------------------------------------------------
/docs/check_python.txt:
--------------------------------------------------------------------------------
1 | .. _check-python:
2 |
3 | ================================
4 | Check permissions in python code
5 | ================================
6 |
7 | *to be written*
8 |
--------------------------------------------------------------------------------
/authority/templates/authority/403.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 |
3 |
{% trans 'Permission denied (403) ' %}
4 | {% trans "You don't have sufficient permissions for this page." %}
5 |
--------------------------------------------------------------------------------
/docs/handling_python.txt:
--------------------------------------------------------------------------------
1 | .. _handling-python:
2 |
3 | ===================================
4 | Handling permissions in python code
5 | ===================================
6 |
7 | *to be written*
8 |
--------------------------------------------------------------------------------
/docs/handling_template.txt:
--------------------------------------------------------------------------------
1 | .. _handling-template:
2 |
3 | ====================================
4 | Handling permissions using templates
5 | ====================================
6 |
7 | *to be written*
8 |
--------------------------------------------------------------------------------
/example/users/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 | from django.contrib.auth.admin import UserAdmin
3 | from example.users.models import User
4 |
5 |
6 | admin.site.register(User, UserAdmin)
7 |
--------------------------------------------------------------------------------
/authority/templates/authority/permission_request_delete_link.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% if url %}{% if is_requestor %}{% trans "Withdraw request" %}{% else %}{% trans "Deny request" %}{% endif %} {% endif %}
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [egg_info]
2 | #tag_build = dev
3 |
4 | [build_sphinx]
5 | source-dir = docs/
6 | build-dir = docs/build
7 | all_files = 1
8 |
9 | [upload_docs]
10 | upload-dir = docs/build/html
11 |
12 | [wheel]
13 | universal = 1
14 |
--------------------------------------------------------------------------------
/example/templates/registration/login.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Login form
4 |
10 |
11 |
--------------------------------------------------------------------------------
/example/exampleapp/forms.py:
--------------------------------------------------------------------------------
1 | from django import forms
2 | from django.utils.translation import ugettext_lazy as _
3 |
4 | from authority.forms import UserPermissionForm
5 |
6 |
7 | class SpecialUserPermissionForm(UserPermissionForm):
8 | user = forms.CharField(label=_("Special user"), widget=forms.Textarea())
9 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | [](https://jazzband.co/)
2 |
3 | This is a [Jazzband](https://jazzband.co/) project.
4 | By contributing you agree to abide by the [Contributor Code of Conduct](https://jazzband.co/about/conduct)
5 | and follow the [guidelines](https://jazzband.co/about/guidelines).
6 |
--------------------------------------------------------------------------------
/example/exampleapp/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 | from django.contrib.flatpages.models import FlatPage
3 | from django.contrib.flatpages.admin import FlatPageAdmin
4 | from authority.admin import PermissionInline
5 |
6 | admin.site.unregister(FlatPage)
7 | admin.site.register(FlatPage, FlatPageAdmin, inlines=[PermissionInline])
8 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include AUTHORS
2 | include LICENSE
3 | include README.rst
4 | recursive-include authority/templates/authority *.html
5 | recursive-include authority/templates/admin *.html
6 | recursive-include authority/fixtures *.json
7 | recursive-include docs *.txt *.html
8 | recursive-exclude docs/build *.txt
9 | prune docs/build/html/_sources
10 |
--------------------------------------------------------------------------------
/AUTHORS:
--------------------------------------------------------------------------------
1 | Jannis Leidel
2 | Martin Mahner
3 | Diego Búrigo Zacarão
4 | James Pic
5 | Wes Winham
6 | Kyle Gibson
7 | Jason Ward
8 | Travis Chase
9 | Remigiusz Dymecki
10 | Gunnlaugur Thor Briem
11 | Bob Cribbs
12 |
--------------------------------------------------------------------------------
/authority/exceptions.py:
--------------------------------------------------------------------------------
1 | class AuthorityException(Exception):
2 | pass
3 |
4 |
5 | class NotAModel(AuthorityException):
6 | def __init__(self, object):
7 | super(NotAModel, self).__init__("Not a model class or instance")
8 |
9 |
10 | class UnsavedModelInstance(AuthorityException):
11 | def __init__(self, object):
12 | super(UnsavedModelInstance, self).__init__(
13 | "Model instance has no pk, was it saved?"
14 | )
15 |
--------------------------------------------------------------------------------
/docs/support.txt:
--------------------------------------------------------------------------------
1 | .. _support:
2 |
3 | =======
4 | Support
5 | =======
6 |
7 | .. index::
8 | single: Support
9 |
10 | We've created a `google group`_ for django-authority. If you have questions or
11 | suggestions, please drop us a note.
12 |
13 | For more specific issues and bug reports please use the `issue tracker`_ on
14 | django-authority's Github page.
15 |
16 | .. _google group: http://groups.google.com/group/django-authority
17 | .. _issue tracker: https://github.com/jazzband/django-authority/issues/
18 |
--------------------------------------------------------------------------------
/authority/templates/authority/permission_form.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 | {% if form %}
3 |
14 | {% endif %}
15 |
--------------------------------------------------------------------------------
/authority/fixtures/tests_custom.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "pk": 1,
4 | "model": "users.user",
5 | "fields": {
6 | "first_name": "Jez",
7 | "last_name": "Dez",
8 | "is_active": true,
9 | "is_superuser": false,
10 | "is_staff": false,
11 | "last_login": "2009-11-02 03:06:19",
12 | "groups": [],
13 | "user_permissions": [],
14 | "password": "",
15 | "email": "jezdez@github.com",
16 | "date_joined": "2009-11-02 03:06:19",
17 | "greeting_message": "Hello customer user model"
18 | }
19 | }
20 | ]
21 |
--------------------------------------------------------------------------------
/authority/__init__.py:
--------------------------------------------------------------------------------
1 | from pkg_resources import get_distribution, DistributionNotFound
2 |
3 | try:
4 | __version__ = get_distribution("django-authority").version
5 | except DistributionNotFound:
6 | # package is not installed
7 | pass
8 |
9 | LOADING = False
10 |
11 |
12 | def autodiscover():
13 | """
14 | Goes and imports the permissions submodule of every app in INSTALLED_APPS
15 | to make sure the permission set classes are registered correctly.
16 | """
17 | global LOADING
18 | if LOADING:
19 | return
20 | LOADING = True
21 |
22 | from django.utils.module_loading import autodiscover_modules
23 |
24 | autodiscover_modules("permissions")
25 |
--------------------------------------------------------------------------------
/example/exampleapp/views.py:
--------------------------------------------------------------------------------
1 | from __future__ import print_function
2 |
3 | from django.contrib.flatpages.views import flatpage
4 | from django.contrib.flatpages.models import FlatPage
5 |
6 | from authority.decorators import permission_required, permission_required_or_403
7 |
8 | # @permission_required('flatpage_permission.top_secret',
9 | # (FlatPage, 'url__contains', 'url'), (FlatPage, 'url__contains', 'lala'))
10 | # use this to return a 403 page:
11 | @permission_required_or_403(
12 | "flatpage_permission.top_secret", (FlatPage, "url__contains", "url"), "lala"
13 | )
14 | def top_secret(request, url, lala=None):
15 | """
16 | A wrapping view that performs the permission check given in the decorator
17 | """
18 | print("secret!")
19 | return flatpage(request, url)
20 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | skipsdist = True
3 | usedevelop = True
4 | minversion = 1.8
5 | envlist =
6 | py27-dj111
7 | py37-dj{111,22}
8 | {py36,py37,py38}-dj{30,31}
9 | py37-check
10 |
11 | [testenv]
12 | usedevelop = true
13 | commands =
14 | coverage run -a example/manage.py test authority exampleapp
15 | coverage report
16 | coverage xml
17 | deps =
18 | coverage
19 | dj111: Django>=1.11,<2.0
20 | dj22: Django>=2.2,<2.3
21 | dj30: Django>=3.0,<3.1
22 | dj31: Django>=3.1,<3.2
23 |
24 |
25 | [testenv:py37-check]
26 | deps =
27 | twine
28 | wheel
29 | commands =
30 | python setup.py sdist bdist_wheel
31 | twine check dist/*
32 |
33 |
34 | [gh-actions]
35 | python =
36 | 2.7: py27
37 | 3.6: py36
38 | 3.7: py37
39 | 3.8: py38
40 |
--------------------------------------------------------------------------------
/example/users/models.py:
--------------------------------------------------------------------------------
1 | from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin
2 | from django.contrib.auth.models import UserManager
3 | from django.db import models
4 | from django.utils import timezone
5 |
6 |
7 | class User(AbstractBaseUser, PermissionsMixin):
8 | USERNAME_FIELD = "email"
9 | REQUIRED_FIELDS = ["first_name", "last_name"]
10 |
11 | username = models.CharField(max_length=100)
12 | first_name = models.CharField(max_length=50)
13 | last_name = models.CharField(max_length=50)
14 | email = models.EmailField(unique=True)
15 | greeting_message = models.TextField()
16 | is_staff = models.BooleanField(default=False)
17 | is_active = models.BooleanField(default=True)
18 | date_joined = models.DateTimeField(default=timezone.now)
19 |
20 | objects = UserManager()
21 |
--------------------------------------------------------------------------------
/example/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import sys
3 | import os
4 |
5 | from django.core.management import execute_from_command_line
6 |
7 | try:
8 | import settings as settings_mod # Assumed to be in the same directory.
9 | except ImportError:
10 | sys.stderr.write(
11 | "Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n"
12 | % __file__
13 | )
14 | sys.exit(1)
15 |
16 | sys.path.insert(0, settings_mod.PROJECT_ROOT)
17 | sys.path.insert(0, settings_mod.PROJECT_ROOT + "/../")
18 |
19 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings")
20 |
21 | if __name__ == "__main__":
22 | execute_from_command_line(sys.argv)
23 |
--------------------------------------------------------------------------------
/docs/documentation_guidelines.txt:
--------------------------------------------------------------------------------
1 | .. _documentation-guidelines:
2 |
3 | .. warning:: This document is for internal use only.
4 |
5 | ========================
6 | Documentation Guildlines
7 | ========================
8 |
9 | Headline scheme
10 | ===============
11 | ::
12 |
13 | ===================================
14 | First level (equals top and bottom)
15 | ===================================
16 |
17 | Second Level (equals bottom)
18 | ============================
19 |
20 | Third level (dashes botton)
21 | ---------------------------
22 |
23 | Fourth level (drunken dashes bottom)
24 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
25 |
26 | Please try to use not more than 4 levels of headlines.
27 |
28 | Overall salutation guidelines
29 | =============================
30 |
31 | Use the *We* and *you*::
32 |
33 | We think that you should send us a bottle of your local beer.
34 |
35 | Some thoughts
36 | =============
37 |
38 | * Many internal links are good
39 | * Text should not be wider than 80 characters
40 | * Two pages are better than one ultra-long page
41 |
--------------------------------------------------------------------------------
/authority/urls.py:
--------------------------------------------------------------------------------
1 | from django.conf.urls import url
2 | from authority.views import (
3 | add_permission,
4 | delete_permission,
5 | approve_permission_request,
6 | delete_permission,
7 | )
8 |
9 |
10 | urlpatterns = [
11 | url(
12 | r"^permission/add/(?P[\w\-]+)/(?P[\w\-]+)/(?P\d+)/$",
13 | view=add_permission,
14 | name="authority-add-permission",
15 | kwargs={"approved": True},
16 | ),
17 | url(
18 | r"^permission/delete/(?P\d+)/$",
19 | view=delete_permission,
20 | name="authority-delete-permission",
21 | kwargs={"approved": True},
22 | ),
23 | url(
24 | r"^request/add/(?P[\w\-]+)/(?P[\w\-]+)/(?P\d+)/$",
25 | view=add_permission,
26 | name="authority-add-permission-request",
27 | kwargs={"approved": False},
28 | ),
29 | url(
30 | r"^request/approve/(?P\d+)/$",
31 | view=approve_permission_request,
32 | name="authority-approve-permission-request",
33 | ),
34 | url(
35 | r"^request/delete/(?P\d+)/$",
36 | view=delete_permission,
37 | name="authority-delete-permission-request",
38 | kwargs={"approved": False},
39 | ),
40 | ]
41 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 | strategy:
9 | fail-fast: false
10 | max-parallel: 5
11 | matrix:
12 | python-version: ['2.7', '3.6', '3.7', '3.8']
13 |
14 | steps:
15 | - uses: actions/checkout@v2
16 |
17 | - name: Set up Python ${{ matrix.python-version }}
18 | uses: actions/setup-python@v2
19 | with:
20 | python-version: ${{ matrix.python-version }}
21 |
22 | - name: Get pip cache dir
23 | id: pip-cache
24 | run: |
25 | echo "::set-output name=dir::$(pip cache dir)"
26 |
27 | - name: Cache
28 | uses: actions/cache@v2
29 | with:
30 | path: ${{ steps.pip-cache.outputs.dir }}
31 | key:
32 | ${{ matrix.python-version }}-v1-${{ hashFiles('**/setup.py') }}
33 | restore-keys: |
34 | ${{ matrix.python-version }}-v1-
35 |
36 | - name: Install dependencies
37 | run: |
38 | python -m pip install --upgrade pip
39 | python -m pip install --upgrade tox tox-gh-actions
40 |
41 | - name: Tox tests
42 | run: |
43 | tox -v
44 |
45 | - name: Upload coverage
46 | uses: codecov/codecov-action@v1
47 | with:
48 | name: Python ${{ matrix.python-version }}
49 |
--------------------------------------------------------------------------------
/docs/installation.txt:
--------------------------------------------------------------------------------
1 | .. _installation:
2 |
3 | ============
4 | Installation
5 | ============
6 |
7 | The installation of django-authority is easy. Whether you want to use the
8 | latest stable or development version, you have the following options.
9 |
10 | The latest stable version
11 | =========================
12 |
13 | The latest, stable version is always available via the `Python package index_`
14 | (PyPI). You can download the latest version on `the site`_ but most users
15 | would prefer either ``pip`` or ``easy_install``::
16 |
17 | pip install django-authority
18 |
19 | # .. or with easy_install:
20 |
21 | easy_install django-authority
22 |
23 | .. _the site: http://pypi.python.org/pypi/django-authority/
24 | .. _Python package index: http://pypi.python.org/pypi
25 |
26 | Development version
27 | ===================
28 |
29 | The latest development version is located on it's `Github account`_. You
30 | can checkout the package using the Git_ scm::
31 |
32 | git clone https://github.com/jazzband/django-authority
33 |
34 | Then install it manually::
35 |
36 | cd django-authority
37 | python setup.py install
38 |
39 | .. warning:: The development version is not fully tested and may contain
40 | bugs, so we prefer to use the latest package from pypi.
41 |
42 | .. _Github account: https://github.com/jazzband/django-authority/
43 | .. _Git: http://gitscm.org/
44 |
--------------------------------------------------------------------------------
/example/urls.py:
--------------------------------------------------------------------------------
1 | import django.contrib.auth.views
2 | from django.conf.urls import include, handler500, url
3 | from django.conf import settings
4 |
5 | import authority.views
6 | import authority.urls
7 | import example.exampleapp.views
8 |
9 | from exampleapp.forms import SpecialUserPermissionForm
10 |
11 | authority.autodiscover()
12 |
13 | handler500 # flake8
14 |
15 | urlpatterns = (
16 | url(
17 | r"^authority/permission/add/(?P[\w\-]+)/(?P[\w\-]+)/(?P\d+)/$", # noqa
18 | view=authority.views.add_permission,
19 | name="authority-add-permission",
20 | kwargs={"approved": True, "form_class": SpecialUserPermissionForm},
21 | ),
22 | url(
23 | r"^request/add/(?P[\w\-]+)/(?P[\w\-]+)/(?P\d+)/$", # noqa
24 | view=authority.views.add_permission,
25 | name="authority-add-permission-request",
26 | kwargs={"approved": False, "form_class": SpecialUserPermissionForm},
27 | ),
28 | url(r"^authority/", include(authority.urls)),
29 | url(r"^accounts/login/$", django.contrib.auth.views.LoginView.as_view()),
30 | url(
31 | r"^(?P[\/0-9A-Za-z]+)$",
32 | example.exampleapp.views.top_secret,
33 | {"lala": "oh yeah!"},
34 | ),
35 | )
36 |
37 | if settings.DEBUG:
38 | urlpatterns += (
39 | url(
40 | r"^media/(?P.*)$",
41 | django.views.static.serve,
42 | {"document_root": settings.MEDIA_ROOT,},
43 | ),
44 | )
45 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import os
2 | from setuptools import setup, find_packages
3 |
4 |
5 | def read(fname):
6 | return open(os.path.join(os.path.dirname(__file__), fname)).read()
7 |
8 |
9 | setup(
10 | name="django-authority",
11 | use_scm_version=True,
12 | description=(
13 | "A Django app that provides generic per-object-permissions "
14 | "for Django's auth app."
15 | ),
16 | long_description=read("README.rst"),
17 | long_description_content_type="text/x-rst",
18 | author="Jannis Leidel",
19 | author_email="jannis@leidel.info",
20 | license="BSD",
21 | url="https://github.com/jazzband/django-authority/",
22 | packages=find_packages(exclude=("example", "example.*")),
23 | classifiers=[
24 | "Development Status :: 5 - Production/Stable",
25 | "Environment :: Web Environment",
26 | "Intended Audience :: Developers",
27 | "License :: OSI Approved :: BSD License",
28 | "Operating System :: OS Independent",
29 | "Programming Language :: Python",
30 | "Programming Language :: Python :: 2",
31 | "Programming Language :: Python :: 2.7",
32 | "Programming Language :: Python :: 3",
33 | "Programming Language :: Python :: 3.6",
34 | "Programming Language :: Python :: 3.7",
35 | "Programming Language :: Python :: 3.8",
36 | "Framework :: Django",
37 | ],
38 | install_requires=["django"],
39 | setup_requires=["setuptools_scm"],
40 | include_package_data=True,
41 | zip_safe=False,
42 | )
43 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | push:
5 | tags:
6 | - '*'
7 |
8 | jobs:
9 | build:
10 | if: github.repository == 'jazzband/django-authority'
11 | runs-on: ubuntu-latest
12 |
13 | steps:
14 | - uses: actions/checkout@v2
15 | with:
16 | fetch-depth: 0
17 |
18 | - name: Set up Python
19 | uses: actions/setup-python@v2
20 | with:
21 | python-version: 3.8
22 |
23 | - name: Get pip cache dir
24 | id: pip-cache
25 | run: |
26 | echo "::set-output name=dir::$(pip cache dir)"
27 |
28 | - name: Cache
29 | uses: actions/cache@v2
30 | with:
31 | path: ${{ steps.pip-cache.outputs.dir }}
32 | key: release-${{ hashFiles('**/setup.py') }}
33 | restore-keys: |
34 | release-
35 |
36 | - name: Install dependencies
37 | run: |
38 | python -m pip install -U pip
39 | python -m pip install -U setuptools twine wheel
40 |
41 | - name: Build package
42 | run: |
43 | python setup.py --version
44 | python setup.py sdist --format=gztar bdist_wheel
45 | twine check dist/*
46 |
47 | - name: Upload packages to Jazzband
48 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
49 | uses: pypa/gh-action-pypi-publish@master
50 | with:
51 | user: jazzband
52 | password: ${{ secrets.JAZZBAND_RELEASE_KEY }}
53 | repository_url: https://jazzband.co/projects/django-authority/upload
54 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2009-2020, Jannis Leidel
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 |
--------------------------------------------------------------------------------
/docs/configuration.txt:
--------------------------------------------------------------------------------
1 | .. _configuration:
2 |
3 | =============
4 | Configuration
5 | =============
6 |
7 | .. index::
8 | single: urls.py
9 | single: settings.py
10 | single: autodiscover
11 |
12 | settings.py
13 | ===========
14 |
15 | To enable django-authority you just need to add the package to your
16 | ``INSTALLED_APPS`` setting within your ``settings.py``::
17 |
18 | # settings.py
19 | INSTALLED_APPS = (
20 | ...
21 | 'authority',
22 | )
23 |
24 | Make sure your ``settings.py`` contains the following settings to enable the
25 | context processors::
26 |
27 | TEMPLATE_CONTEXT_PROCESSORS = (
28 | 'django.core.context_processors.auth',
29 | 'django.core.context_processors.debug',
30 | 'django.core.context_processors.i18n',
31 | 'django.core.context_processors.media',
32 | 'django.core.context_processors.request',
33 | )
34 |
35 | django-authority defaults to using a smart cache when checking permissions.
36 | This can be disabled by adding the following line to ``settings.py``::
37 |
38 | AUTHORITY_USE_SMART_CACHE = False
39 |
40 | urls.py
41 | =======
42 |
43 | You also have to modify your root URLConf (e.g. ``urls.py``) to include the
44 | app's URL configuration and automatically discover all the permission
45 | classes you defined::
46 |
47 | from django.contrib import admin
48 | import authority
49 |
50 | admin.autodiscover()
51 | authority.autodiscover()
52 |
53 | # ...
54 |
55 | urlpatterns += patterns('',
56 | (r'^authority/', include('authority.urls')),
57 | )
58 |
59 | If you're using Django 1.1 this will automatically add a `site-wide action`_
60 | to the admin site which can be removed as shown here: :ref:`handling-admin`.
61 |
62 | That's all (for now).
63 |
64 | .. _site-wide action: http://docs.djangoproject.com/en/dev/ref/contrib/admin/actions/
65 |
--------------------------------------------------------------------------------
/docs/check_templates.txt:
--------------------------------------------------------------------------------
1 | .. _check-templates:
2 |
3 | ==============================
4 | Check permissions in templates
5 | ==============================
6 |
7 | .. index::
8 | single: ifhasperm
9 | single: get_permissions
10 | single: get_permission
11 |
12 | django-authority provides a couple of template tags which allows you to get
13 | permissions for a user (and a related object).
14 |
15 | ifhasperm
16 | =========
17 |
18 | This function checks whether a permission is True or False for a user and
19 | (optional) a related object.
20 |
21 | Syntax::
22 |
23 | {% ifhasperm [permission_label].[check_name] [user] [*objs] %}
24 | lalala
25 | {% else %}
26 | meh
27 | {% endifhasperm %}
28 |
29 | Example::
30 |
31 | {% ifhasperm "poll_permission.change_poll" request.user %}
32 | lalala
33 | {% else %}
34 | meh
35 | {% endifhasperm %}
36 |
37 |
38 | get_permissions
39 | ===============
40 |
41 | Retrieves all permissions associated with the given obj and user
42 | and assigns the result to a context variable.
43 |
44 | Syntax and example::
45 |
46 | {% get_permissions obj %}
47 | {% for perm in permissions %}
48 | {{ perm }}
49 | {% endfor %}
50 |
51 | {% get_permissions obj as "my_permissions" %}
52 | {% get_permissions obj for request.user as "my_permissions" %}
53 |
54 |
55 | get_permission
56 | ==============
57 |
58 | Performs a permission check with the given signature, user and objects and
59 | assigns the result to a context variable.
60 |
61 | Syntax::
62 |
63 | {% get_permission [permission_label].[check_name] for [user] and [objs] as [varname] %}
64 |
65 | Example::
66 |
67 | {% get_permission "poll_permission.change_poll" for request.user and poll as "is_allowed" %}
68 | {% get_permission "poll_permission.change_poll" for request.user and poll,second_poll as "is_allowed" %}
69 |
70 | {% if is_allowed %}
71 | I've got ze power to change ze pollllllzzz. Muahahaa.
72 | {% else %}
73 | Meh. No power for meeeee.
74 | {% endif %}
75 |
--------------------------------------------------------------------------------
/docs/handling_admin.txt:
--------------------------------------------------------------------------------
1 | .. _handling-admin:
2 |
3 | ===================================================
4 | Handling permissions using Django's admin interface
5 | ===================================================
6 |
7 | *to be written*
8 |
9 | .. note:: Django admin actions are available in Django 1.1 or later.
10 |
11 | Apply permissions using Django's admin actions
12 | ==============================================
13 |
14 | This feature is limited to superusers and users with either the
15 | "Can change permission" (``change_permission``) or the
16 | "Can change foreign permission" (``change_foreign_permission``) `permission`_.
17 |
18 | .. image:: .static/admin-action-permission.png
19 | .. _permission: http://docs.djangoproject.com/en/dev/topics/auth/#permissions
20 |
21 | Disable the admin action site-wide
22 | ----------------------------------
23 |
24 | To disable the action site-wide, place this line somewhere in your code.
25 | One of your app ``admin.py`` files might be a good place::
26 |
27 | admin.site.disable_action('edit_permissions')
28 |
29 | Further informations are available in Django's documentation:
30 | `Disabling a site-wide action`_.
31 |
32 | .. _Disabling a site-wide action: http://docs.djangoproject.com/en/dev/ref/contrib/admin/actions/#disabling-a-site-wide-action
33 |
34 | Disable the admin action per ModelAdmin instance
35 | ------------------------------------------------
36 |
37 | In case you want to disable the permission action per ModelAdmin, delete this
38 | action within the ``get_actions`` method. Here is an example::
39 |
40 | class EntryAdmin(admin.ModelAdmin):
41 |
42 | def get_actions(self, request):
43 | actions = super(EntryAdmin, self).get_actions(request)
44 | del actions['edit_permissions']
45 | return actions
46 |
47 | Further informations are available in Django's documentation:
48 | `Conditionally enabling or disabling actions`_.
49 |
50 | .. _Conditionally enabling or disabling actions: http://docs.djangoproject.com/en/dev/ref/contrib/admin/actions/#conditionally-enabling-or-disabling-actions
51 |
--------------------------------------------------------------------------------
/example/exampleapp/permissions.py:
--------------------------------------------------------------------------------
1 | from django.contrib.flatpages.models import FlatPage
2 | from django.utils.translation import ugettext_lazy as _
3 |
4 | import authority
5 | from authority.permissions import BasePermission
6 |
7 |
8 | class FlatPagePermission(BasePermission):
9 | """
10 | This class contains a bunch of checks:
11 |
12 | 1. the default checks 'add_flatpage', 'browse_flatpage',
13 | 'change_flatpage' and 'delete_flatpage'
14 | 2. the custom checks:
15 | a) 'review_flatpage', which is similar to the default checks
16 | b) 'top_secret', which is represented by the top_secret method
17 |
18 | You can use those checks in your views directly like::
19 |
20 | def review_flatpage(request, url):
21 | flatpage = get_object_or_404(url__contains=url)
22 | check = FlatPagePermission(request.user)
23 | if check.review_flatpage(obj=flatpage):
24 | print "yay, you can review this flatpage!"
25 | return flatpage(request, url)
26 |
27 | Or the same view using the decorator permission_required::
28 |
29 | @permission_required('flatpage_permission.review_flatpage',
30 | ('flatpages.flatpage', 'url__contains', 'url'))
31 | def review_flatpage(request, url):
32 | print "yay, you can review this flatpage!"
33 | return flatpage(request, url)
34 |
35 | Or you can use this permission in your templates like this::
36 |
37 | {% ifhasperm "flatpage_permission.review_flatpage" request.user flatpage %}
38 | Yes, you are allowed to review flatpage '{{ flatpage }}', aren't you?
39 | {% else %}
40 | Nope, sorry. You aren't allowed to review this flatpage.
41 | {% endifhasperm %}
42 |
43 | """
44 |
45 | label = "flatpage_permission"
46 | checks = ("review", "top_secret")
47 |
48 | def top_secret(self, flatpage=None, lala=None):
49 | if flatpage and flatpage.registration_required:
50 | return self.browse_flatpage(obj=flatpage)
51 | return False
52 |
53 | top_secret.short_description = _("Is allowed to see top secret flatpages")
54 |
55 |
56 | authority.sites.register(FlatPage, FlatPagePermission)
57 |
--------------------------------------------------------------------------------
/authority/templates/admin/permission_change_form.html:
--------------------------------------------------------------------------------
1 | {% extends "admin/base_site.html" %}
2 | {% load i18n admin_modify admin_static %}
3 |
4 | {% block extrahead %}{{ block.super }}
5 |
6 | {{ media }}
7 | {% endblock %}
8 |
9 | {% block extrastyle %}{{ block.super }} {% endblock %}
10 |
11 | {% block coltype %}colM{% endblock %}
12 |
13 | {% block bodyclass %}{{ opts.app_label }}-{{ opts.object_name.lower }} change-form{% endblock %}
14 |
15 | {% block breadcrumbs %}{% if not is_popup %}
16 |
22 | {% endif %}{% endblock %}
23 |
24 | {% block content %}
58 | {% endblock %}
59 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Code of Conduct
2 |
3 | As contributors and maintainers of the Jazzband projects, and in the interest of
4 | fostering an open and welcoming community, we pledge to respect all people who
5 | contribute through reporting issues, posting feature requests, updating documentation,
6 | submitting pull requests or patches, and other activities.
7 |
8 | We are committed to making participation in the Jazzband a harassment-free experience
9 | for everyone, regardless of the level of experience, gender, gender identity and
10 | expression, sexual orientation, disability, personal appearance, body size, race,
11 | ethnicity, age, religion, or nationality.
12 |
13 | Examples of unacceptable behavior by participants include:
14 |
15 | - The use of sexualized language or imagery
16 | - Personal attacks
17 | - Trolling or insulting/derogatory comments
18 | - Public or private harassment
19 | - Publishing other's private information, such as physical or electronic addresses,
20 | without explicit permission
21 | - Other unethical or unprofessional conduct
22 |
23 | The Jazzband roadies have the right and responsibility to remove, edit, or reject
24 | comments, commits, code, wiki edits, issues, and other contributions that are not
25 | aligned to this Code of Conduct, or to ban temporarily or permanently any contributor
26 | for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
27 |
28 | By adopting this Code of Conduct, the roadies commit themselves to fairly and
29 | consistently applying these principles to every aspect of managing the jazzband
30 | projects. Roadies who do not follow or enforce the Code of Conduct may be permanently
31 | removed from the Jazzband roadies.
32 |
33 | This code of conduct applies both within project spaces and in public spaces when an
34 | individual is representing the project or its community.
35 |
36 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by
37 | contacting the roadies at `roadies@jazzband.co`. All complaints will be reviewed and
38 | investigated and will result in a response that is deemed necessary and appropriate to
39 | the circumstances. Roadies are obligated to maintain confidentiality with regard to the
40 | reporter of an incident.
41 |
42 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version
43 | 1.3.0, available at [https://contributor-covenant.org/version/1/3/0/][version]
44 |
45 | [homepage]: https://contributor-covenant.org
46 | [version]: https://contributor-covenant.org/version/1/3/0/
47 |
--------------------------------------------------------------------------------
/docs/index.txt:
--------------------------------------------------------------------------------
1 | .. _index:
2 |
3 | ============================================
4 | Welcome to django-authority's documentation!
5 | ============================================
6 |
7 | django-authority is a powerful layer between Django's basic permission system
8 | (provided through ``django.contrib.auth``) and your application:
9 |
10 | .. image:: .static/authority-scheme-layer.png
11 |
12 | This application provides three abilities:
13 |
14 | 1. It gives you the ability to add permissions like Django's generic
15 | permissions to any kind of model without having to add them to the model's
16 | Meta class.
17 |
18 | 2. It provides a very simple way to create per-object-permissions. You might
19 | be more familiar with the term *row level permissions*.
20 |
21 | 3. It wraps Django's generic permissions so you can use the same syntax as
22 | for the options above. But note that django-authority does not add any
23 | voodoo-code to Django's ``contrib.auth`` system, it keeps your existing
24 | permission system intact!
25 |
26 | django-authority uses a cache that is stored on the user object to help improve
27 | performance. However, if the ``Permission`` table changes the cache will need
28 | to be invalidated. More information about this can be found in the tips and
29 | tricks section.
30 |
31 | .. warning:: We have just started with the documentation and it's far from
32 | being perfect. If you find glitches, errors or just have feedback, please
33 | contact the team: :ref:`support`.
34 |
35 | Documentation
36 | =============
37 |
38 | .. note:: The create-permission topics are based on each other. If you are new
39 | to django-authority we encourage to read from top to bottom.
40 |
41 | **Installation topics:**
42 |
43 | .. toctree::
44 | :maxdepth: 1
45 |
46 | installation
47 | configuration
48 |
49 | **Create and check permissions:**
50 |
51 | .. toctree::
52 | :maxdepth: 1
53 |
54 | create_basic_permission
55 | create_per_object_permission
56 | create_custom_permission
57 |
58 | **Permission checks in detail**
59 |
60 | .. toctree::
61 | :maxdepth: 2
62 |
63 | check_python
64 | check_decorator
65 | check_templates
66 |
67 | **Permission assigning and handling**
68 |
69 | .. toctree::
70 | :maxdepth: 1
71 |
72 | handling_python
73 | handling_admin
74 | handling_template
75 |
76 | Other pages
77 | ===========
78 |
79 | * :ref:`search`
80 | * :ref:`genindex`
81 |
82 | .. toctree::
83 | :maxdepth: 1
84 | :glob:
85 |
86 | tips_tricks
87 | support
88 | documentation_guidelines
89 |
--------------------------------------------------------------------------------
/docs/tips_tricks.txt:
--------------------------------------------------------------------------------
1 | .. _tips-tricks:
2 |
3 | ======================
4 | Hints, tips and tricks
5 | ======================
6 |
7 | Within a permission class, you can refer to the user and group using self::
8 |
9 | class CampaignPermission(permissions.BasePermission):
10 | label = 'campaign_permission'
11 | checks = ('do_foo',)
12 |
13 | def do_foo(self, campaign=None):
14 | print self.user
15 | print self.group
16 | # ...
17 |
18 | You can unregister permission classes and re-register them::
19 |
20 | authority.unregister(Campaign)
21 | authority.register(Campaign, CampaignPermission)
22 |
23 | Within a permission class, you can refer to Django's basic permissions::
24 |
25 | class FlagpagePermisson(permissions.BasePermission):
26 | label = 'flatpage_permission'
27 | checks = ('do_foo',)
28 |
29 | def do_foo(self, campaign=None):
30 | if foo and self.change_flatpage():
31 | # ...
32 |
33 | authority.register(Flatpage, FlagpagePermisson)
34 |
35 | If the ``Permission`` table changes during the lifespan of a django-authority
36 | permission instance and the smart cache is being used, you will need to call
37 | invalidate_permissions_cache in order to see that changes::
38 |
39 | class UserPermission(permission.BasePermission):
40 | label = 'user_permission'
41 | checks = ('do_foo',)
42 | authority.register(User, UserPermission)
43 |
44 | user_permission = UserPermission(user)
45 |
46 | # can_foo is False here since the permission has not yet been added.
47 | can_foo = user_permission.has_user_perms('foo', user)
48 |
49 | Permission.objects.create(
50 | content_type=Permission.objects.get_content_type(User),
51 | object_id=user.pk,
52 | codename='foo',
53 | user=user,
54 | approved=True,
55 | )
56 |
57 | # can_foo is still False because the permission cache has not been
58 | invalidated yet.
59 | can_foo = user_permission.has_user_perms('foo', user)
60 |
61 | user_permission.invalidate_permissions_cache()
62 |
63 | # can_foo is now True
64 | can_foo = user_permission.has_user_perms('foo', user)
65 |
66 | This is particularly useful if you are using the permission instances during a
67 | request, where it is unlikely that the state of the ``Permission`` table will
68 | change.
69 |
70 | Although the previous example was only passing in a ``user`` into the
71 | permission, smart caching is used when getting permissions in a ``group`` as
72 | well.
73 |
--------------------------------------------------------------------------------
/authority/managers.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 | from django.db.models import Q
3 | from django.contrib.contenttypes.models import ContentType
4 |
5 |
6 | class PermissionManager(models.Manager):
7 | def get_content_type(self, obj):
8 | return ContentType.objects.get_for_model(obj)
9 |
10 | def get_for_model(self, obj):
11 | return self.filter(content_type=self.get_content_type(obj))
12 |
13 | def for_object(self, obj, approved=True):
14 | return (
15 | self.get_for_model(obj)
16 | .select_related("user", "creator", "group", "content_type")
17 | .filter(object_id=obj.id, approved=approved)
18 | )
19 |
20 | def for_user(self, user, obj, check_groups=True):
21 | perms = self.get_for_model(obj)
22 | if not check_groups:
23 | return perms.select_related("user", "creator").filter(user=user)
24 |
25 | # Hacking user to user__pk to workaround deepcopy bug:
26 | # http://bugs.python.org/issue2460
27 | # Which is triggered by django's deepcopy which backports that fix in
28 | # Django 1.2
29 | return (
30 | perms.select_related("user", "creator")
31 | .prefetch_related("user__groups")
32 | .filter(Q(user__pk=user.pk) | Q(group__in=user.groups.all()))
33 | )
34 |
35 | def user_permissions(self, user, perm, obj, approved=True, check_groups=True):
36 | return self.for_user(user, obj, check_groups,).filter(
37 | codename=perm, approved=approved,
38 | )
39 |
40 | def group_permissions(self, group, perm, obj, approved=True):
41 | """
42 | Get objects that have Group perm permission on
43 | """
44 | return (
45 | self.get_for_model(obj)
46 | .select_related("user", "group", "creator")
47 | .filter(group=group, codename=perm, approved=approved)
48 | )
49 |
50 | def delete_objects_permissions(self, obj):
51 | """
52 | Delete permissions related to an object instance
53 | """
54 | perms = self.for_object(obj)
55 | perms.delete()
56 |
57 | def delete_user_permissions(self, user, perm, obj, check_groups=False):
58 | """
59 | Remove granular permission perm from user on an object instance
60 | """
61 | user_perms = self.user_permissions(user, perm, obj, check_groups=False)
62 | if not user_perms.filter(object_id=obj.id):
63 | return
64 | perms = self.user_permissions(user, perm, obj).filter(object_id=obj.id)
65 | perms.delete()
66 |
--------------------------------------------------------------------------------
/authority/templates/admin/edit_inline/action_tabular.html:
--------------------------------------------------------------------------------
1 | {% load i18n %}
2 |
67 |
--------------------------------------------------------------------------------
/docs/.theme/nature/static/pygments.css:
--------------------------------------------------------------------------------
1 | .c { color: #999988; font-style: italic } /* Comment */
2 | .k { font-weight: bold } /* Keyword */
3 | .o { font-weight: bold } /* Operator */
4 | .cm { color: #999988; font-style: italic } /* Comment.Multiline */
5 | .cp { color: #999999; font-weight: bold } /* Comment.preproc */
6 | .c1 { color: #999988; font-style: italic } /* Comment.Single */
7 | .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */
8 | .ge { font-style: italic } /* Generic.Emph */
9 | .gr { color: #aa0000 } /* Generic.Error */
10 | .gh { color: #999999 } /* Generic.Heading */
11 | .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */
12 | .go { color: #111 } /* Generic.Output */
13 | .gp { color: #555555 } /* Generic.Prompt */
14 | .gs { font-weight: bold } /* Generic.Strong */
15 | .gu { color: #aaaaaa } /* Generic.Subheading */
16 | .gt { color: #aa0000 } /* Generic.Traceback */
17 | .kc { font-weight: bold } /* Keyword.Constant */
18 | .kd { font-weight: bold } /* Keyword.Declaration */
19 | .kp { font-weight: bold } /* Keyword.Pseudo */
20 | .kr { font-weight: bold } /* Keyword.Reserved */
21 | .kt { color: #445588; font-weight: bold } /* Keyword.Type */
22 | .m { color: #009999 } /* Literal.Number */
23 | .s { color: #bb8844 } /* Literal.String */
24 | .na { color: #008080 } /* Name.Attribute */
25 | .nb { color: #999999 } /* Name.Builtin */
26 | .nc { color: #445588; font-weight: bold } /* Name.Class */
27 | .no { color: #ff99ff } /* Name.Constant */
28 | .ni { color: #800080 } /* Name.Entity */
29 | .ne { color: #990000; font-weight: bold } /* Name.Exception */
30 | .nf { color: #990000; font-weight: bold } /* Name.Function */
31 | .nn { color: #555555 } /* Name.Namespace */
32 | .nt { color: #000080 } /* Name.Tag */
33 | .nv { color: purple } /* Name.Variable */
34 | .ow { font-weight: bold } /* Operator.Word */
35 | .mf { color: #009999 } /* Literal.Number.Float */
36 | .mh { color: #009999 } /* Literal.Number.Hex */
37 | .mi { color: #009999 } /* Literal.Number.Integer */
38 | .mo { color: #009999 } /* Literal.Number.Oct */
39 | .sb { color: #bb8844 } /* Literal.String.Backtick */
40 | .sc { color: #bb8844 } /* Literal.String.Char */
41 | .sd { color: #bb8844 } /* Literal.String.Doc */
42 | .s2 { color: #bb8844 } /* Literal.String.Double */
43 | .se { color: #bb8844 } /* Literal.String.Escape */
44 | .sh { color: #bb8844 } /* Literal.String.Heredoc */
45 | .si { color: #bb8844 } /* Literal.String.Interpol */
46 | .sx { color: #bb8844 } /* Literal.String.Other */
47 | .sr { color: #808000 } /* Literal.String.Regex */
48 | .s1 { color: #bb8844 } /* Literal.String.Single */
49 | .ss { color: #bb8844 } /* Literal.String.Symbol */
50 | .bp { color: #999999 } /* Name.Builtin.Pseudo */
51 | .vc { color: #ff99ff } /* Name.Variable.Class */
52 | .vg { color: #ff99ff } /* Name.Variable.Global */
53 | .vi { color: #ff99ff } /* Name.Variable.Instance */
54 | .il { color: #009999 } /* Literal.Number.Integer.Long */
--------------------------------------------------------------------------------
/authority/widgets.py:
--------------------------------------------------------------------------------
1 | from django import forms
2 | from django.conf import settings
3 | from django.utils.safestring import mark_safe
4 | from django.utils.translation import ugettext_lazy as _
5 | from django.contrib.admin.widgets import ForeignKeyRawIdWidget
6 |
7 |
8 | generic_script = """
9 |
19 | """
20 |
21 |
22 | class GenericForeignKeyRawIdWidget(ForeignKeyRawIdWidget):
23 | def __init__(self, ct_field, cts=[], attrs=None):
24 | self.ct_field = ct_field
25 | self.cts = cts
26 | forms.TextInput.__init__(self, attrs)
27 |
28 | def render(self, name, value, attrs=None):
29 | if attrs is None:
30 | attrs = {}
31 | related_url = "../../../"
32 | params = self.url_parameters()
33 | if params:
34 | url = "?" + "&".join(["%s=%s" % (k, v) for k, v in params.iteritems()])
35 | else:
36 | url = ""
37 | if "class" not in attrs:
38 | attrs["class"] = "vForeignKeyRawIdAdminField"
39 | output = [forms.TextInput.render(self, name, value, attrs)]
40 | output.append(
41 | """%(generic_script)s
42 |
47 | """
48 | % {
49 | "generic_script": generic_script,
50 | "related": related_url,
51 | "url": url,
52 | "name": name,
53 | "ct_field": self.ct_field,
54 | }
55 | )
56 | output.append(
57 | ' '
58 | % (settings.STATIC_URL, _("Lookup"))
59 | )
60 |
61 | from django.contrib.contenttypes.models import ContentType
62 |
63 | content_types = """
64 |
68 | """ % (
69 | "\n".join(
70 | [
71 | "content_types[%s] = '%s/%s/';"
72 | % (
73 | ContentType.objects.get_for_model(ct).id,
74 | ct._meta.app_label,
75 | ct._meta.object_name.lower(),
76 | )
77 | for ct in self.cts
78 | ]
79 | )
80 | )
81 | return mark_safe(u"".join(output) + content_types)
82 |
83 | def url_parameters(self):
84 | return {}
85 |
--------------------------------------------------------------------------------
/example/settings.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from django import VERSION
4 |
5 | PROJECT_ROOT = os.path.realpath(os.path.dirname(__file__))
6 |
7 | ADMINS = (
8 | # ('Your Name', 'your_email@domain.com'),
9 | )
10 |
11 | MANAGERS = ADMINS
12 |
13 | DATABASES = {
14 | "default": {
15 | "ENGINE": "django.db.backends.sqlite3",
16 | "NAME": os.path.join(PROJECT_ROOT, "example.db"),
17 | "USER": "",
18 | "PASSWORD": "",
19 | "HOST": "",
20 | "PORT": "",
21 | "TEST": {"NAME": ":memory:", "ENGINE": "django.db.backends.sqlite3",},
22 | }
23 | }
24 |
25 | CACHES = {"default": {"BACKEND": "django.core.cache.backends.dummy.DummyCache",}}
26 |
27 | TIME_ZONE = "America/Chicago"
28 |
29 | LANGUAGE_CODE = "en-us"
30 |
31 | # Absolute path to the directory that holds media.
32 | # Example: "/home/media/media.lawrence.com/"
33 | MEDIA_ROOT = os.path.join(PROJECT_ROOT, "media")
34 |
35 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a
36 | # trailing slash if there is a path component (optional in other cases).
37 | # Examples: "http://media.lawrence.com", "http://example.com/media/"
38 | MEDIA_URL = "/media/"
39 |
40 | # Don't share this with anybody.
41 | SECRET_KEY = "ljlv2lb2d&)#by6th=!v=03-c^(o4lop92i@z4b3f1&ve0yx6d"
42 |
43 | MIDDLEWARE = (
44 | "django.middleware.common.CommonMiddleware",
45 | "django.contrib.sessions.middleware.SessionMiddleware",
46 | "django.contrib.auth.middleware.AuthenticationMiddleware",
47 | "django.contrib.messages.middleware.MessageMiddleware",
48 | # 'django.contrib.flatpages.middleware.FlatpageFallbackMiddleware',
49 | )
50 |
51 | INTERNAL_IPS = ("127.0.0.1",)
52 |
53 |
54 | TEMPLATE_CONTEXT_PROCESSORS = ()
55 |
56 | ROOT_URLCONF = "example.urls"
57 |
58 | SITE_ID = 1
59 |
60 | INSTALLED_APPS = (
61 | "django.contrib.auth",
62 | "django.contrib.contenttypes",
63 | "django.contrib.sessions",
64 | "django.contrib.sites",
65 | "django.contrib.flatpages",
66 | "django.contrib.messages",
67 | "django.contrib.admin",
68 | "authority",
69 | "example.exampleapp",
70 | )
71 |
72 | if VERSION >= (1, 5):
73 | INSTALLED_APPS = INSTALLED_APPS + ("example.users",)
74 | AUTH_USER_MODEL = "users.User"
75 |
76 | TEMPLATES = [
77 | {
78 | "BACKEND": "django.template.backends.django.DjangoTemplates",
79 | "DIRS": [
80 | # insert your TEMPLATE_DIRS here
81 | ],
82 | "APP_DIRS": True,
83 | "OPTIONS": {
84 | "context_processors": [
85 | # Insert your TEMPLATE_CONTEXT_PROCESSORS here or use this
86 | # list if you haven't customized them:
87 | "django.contrib.auth.context_processors.auth",
88 | "django.template.context_processors.debug",
89 | "django.template.context_processors.i18n",
90 | "django.template.context_processors.media",
91 | "django.template.context_processors.static",
92 | "django.template.context_processors.tz",
93 | "django.contrib.messages.context_processors.messages",
94 | ],
95 | },
96 | },
97 | ]
98 |
--------------------------------------------------------------------------------
/authority/models.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 | from django.conf import settings
3 | from django.db import models
4 | from django.contrib.contenttypes.models import ContentType
5 | from django.contrib.contenttypes.fields import GenericForeignKey
6 | from django.contrib.auth.models import Group
7 | from django.utils.translation import ugettext_lazy as _
8 |
9 | from authority.managers import PermissionManager
10 |
11 | USER_MODEL = getattr(settings, "AUTH_USER_MODEL", "auth.User")
12 |
13 |
14 | class Permission(models.Model):
15 | """
16 | A granular permission model, per-object permission in other words.
17 | This kind of permission is associated with a user/group and an object
18 | of any content type.
19 | """
20 |
21 | codename = models.CharField(_("codename"), max_length=100)
22 | content_type = models.ForeignKey(
23 | ContentType, related_name="row_permissions", on_delete=models.CASCADE
24 | )
25 | object_id = models.PositiveIntegerField()
26 | content_object = GenericForeignKey("content_type", "object_id")
27 |
28 | user = models.ForeignKey(
29 | USER_MODEL,
30 | null=True,
31 | blank=True,
32 | related_name="granted_permissions",
33 | on_delete=models.CASCADE,
34 | )
35 | group = models.ForeignKey(Group, null=True, blank=True, on_delete=models.CASCADE)
36 | creator = models.ForeignKey(
37 | USER_MODEL,
38 | null=True,
39 | blank=True,
40 | related_name="created_permissions",
41 | on_delete=models.CASCADE,
42 | )
43 |
44 | approved = models.BooleanField(
45 | _("approved"),
46 | default=False,
47 | help_text=_(
48 | "Designates whether the permission has been approved and treated as active. "
49 | "Unselect this instead of deleting permissions."
50 | ),
51 | )
52 |
53 | date_requested = models.DateTimeField(_("date requested"), default=datetime.now)
54 | date_approved = models.DateTimeField(_("date approved"), blank=True, null=True)
55 |
56 | objects = PermissionManager()
57 |
58 | def __unicode__(self):
59 | return self.codename
60 |
61 | class Meta:
62 | unique_together = ("codename", "object_id", "content_type", "user", "group")
63 | verbose_name = _("permission")
64 | verbose_name_plural = _("permissions")
65 | permissions = (
66 | ("change_foreign_permissions", "Can change foreign permissions"),
67 | ("delete_foreign_permissions", "Can delete foreign permissions"),
68 | ("approve_permission_requests", "Can approve permission requests"),
69 | )
70 |
71 | def save(self, *args, **kwargs):
72 | # Make sure the approval date is always set
73 | if self.approved and not self.date_approved:
74 | self.date_approved = datetime.now()
75 | super(Permission, self).save(*args, **kwargs)
76 |
77 | def approve(self, creator):
78 | """
79 | Approve granular permission request setting a Permission entry as
80 | approved=True for a specific action from an user on an object instance.
81 | """
82 | self.approved = True
83 | self.creator = creator
84 | self.save()
85 |
--------------------------------------------------------------------------------
/example/users/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Generated by Django 1.11.22 on 2019-07-12 16:01
3 | from __future__ import unicode_literals
4 |
5 | import django.contrib.auth.models
6 | from django.db import migrations, models
7 | import django.utils.timezone
8 |
9 |
10 | class Migration(migrations.Migration):
11 |
12 | initial = True
13 |
14 | dependencies = [
15 | ("auth", "0008_alter_user_username_max_length"),
16 | ]
17 |
18 | operations = [
19 | migrations.CreateModel(
20 | name="User",
21 | fields=[
22 | (
23 | "id",
24 | models.AutoField(
25 | auto_created=True,
26 | primary_key=True,
27 | serialize=False,
28 | verbose_name="ID",
29 | ),
30 | ),
31 | ("password", models.CharField(max_length=128, verbose_name="password")),
32 | (
33 | "last_login",
34 | models.DateTimeField(
35 | blank=True, null=True, verbose_name="last login"
36 | ),
37 | ),
38 | (
39 | "is_superuser",
40 | models.BooleanField(
41 | default=False,
42 | help_text="Designates that this user has all permissions without explicitly assigning them.",
43 | verbose_name="superuser status",
44 | ),
45 | ),
46 | ("username", models.CharField(max_length=100)),
47 | ("first_name", models.CharField(max_length=50)),
48 | ("last_name", models.CharField(max_length=50)),
49 | ("email", models.EmailField(max_length=254, unique=True)),
50 | ("greeting_message", models.TextField()),
51 | ("is_staff", models.BooleanField(default=False)),
52 | ("is_active", models.BooleanField(default=True)),
53 | (
54 | "date_joined",
55 | models.DateTimeField(default=django.utils.timezone.now),
56 | ),
57 | (
58 | "groups",
59 | models.ManyToManyField(
60 | blank=True,
61 | help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.",
62 | related_name="user_set",
63 | related_query_name="user",
64 | to="auth.Group",
65 | verbose_name="groups",
66 | ),
67 | ),
68 | (
69 | "user_permissions",
70 | models.ManyToManyField(
71 | blank=True,
72 | help_text="Specific permissions for this user.",
73 | related_name="user_set",
74 | related_query_name="user",
75 | to="auth.Permission",
76 | verbose_name="user permissions",
77 | ),
78 | ),
79 | ],
80 | options={"abstract": False,},
81 | managers=[("objects", django.contrib.auth.models.UserManager()),],
82 | ),
83 | ]
84 |
--------------------------------------------------------------------------------
/authority/decorators.py:
--------------------------------------------------------------------------------
1 | import inspect
2 | from django.http import HttpResponseRedirect
3 | from django.utils.http import urlquote
4 | from django.utils.functional import wraps
5 | from django.db.models import Model
6 | from django.apps import apps
7 | from django.shortcuts import get_object_or_404
8 | from django.conf import settings
9 | from django.contrib.auth import REDIRECT_FIELD_NAME
10 |
11 | from authority.utils import get_check
12 | from authority.views import permission_denied
13 |
14 |
15 | try:
16 | basestring
17 | except NameError:
18 | basestring = str
19 |
20 |
21 | def permission_required(perm, *lookup_variables, **kwargs):
22 | """
23 | Decorator for views that checks whether a user has a particular permission
24 | enabled, redirecting to the log-in page if necessary.
25 | """
26 | login_url = kwargs.pop("login_url", settings.LOGIN_URL)
27 | redirect_field_name = kwargs.pop("redirect_field_name", REDIRECT_FIELD_NAME)
28 | redirect_to_login = kwargs.pop("redirect_to_login", True)
29 |
30 | def decorate(view_func):
31 | def decorated(request, *args, **kwargs):
32 | if request.user.is_authenticated:
33 | params = []
34 | for lookup_variable in lookup_variables:
35 | if isinstance(lookup_variable, basestring):
36 | value = kwargs.get(lookup_variable, None)
37 | if value is None:
38 | continue
39 | params.append(value)
40 | elif isinstance(lookup_variable, (tuple, list)):
41 | model, lookup, varname = lookup_variable
42 | value = kwargs.get(varname, None)
43 | if value is None:
44 | continue
45 | if isinstance(model, basestring):
46 | model_class = apps.get_model(*model.split("."))
47 | else:
48 | model_class = model
49 | if model_class is None:
50 | raise ValueError(
51 | "The given argument '%s' is not a valid model." % model
52 | )
53 | if inspect.isclass(model_class) and not issubclass(
54 | model_class, Model
55 | ):
56 | raise ValueError(
57 | "The argument %s needs to be a model." % model
58 | )
59 | obj = get_object_or_404(model_class, **{lookup: value})
60 | params.append(obj)
61 | check = get_check(request.user, perm)
62 | granted = False
63 | if check is not None:
64 | granted = check(*params)
65 | if granted or request.user.has_perm(perm):
66 | return view_func(request, *args, **kwargs)
67 | if redirect_to_login:
68 | path = urlquote(request.get_full_path())
69 | tup = login_url, redirect_field_name, path
70 | return HttpResponseRedirect("%s?%s=%s" % tup)
71 | return permission_denied(request)
72 |
73 | return wraps(view_func)(decorated)
74 |
75 | return decorate
76 |
77 |
78 | def permission_required_or_403(perm, *args, **kwargs):
79 | """
80 | Decorator that wraps the permission_required decorator and returns a
81 | permission denied (403) page instead of redirecting to the login URL.
82 | """
83 | kwargs["redirect_to_login"] = False
84 | return permission_required(perm, *args, **kwargs)
85 |
--------------------------------------------------------------------------------
/example/templates/flatpages/default.html:
--------------------------------------------------------------------------------
1 | {% load permissions %}
3 |
4 |
5 | {{ flatpage.title }}
6 |
7 |
8 | Title: {{ flatpage.title }}
9 | Content: {{ flatpage.content }}
10 |
11 | Examples
12 |
13 | The permissions granted for this flatpage:
14 |
15 | {% get_permissions flatpage for request.user %}
16 | get_permissions flatpage for request.user
17 | {% for perm in permissions %}
18 | {{ perm.user }}: {{ perm }} {% permission_delete_link perm %}
19 | {% endfor %}
20 |
21 |
22 | {% get_permissions flatpage for request.user as "request_user_permissions" %}
23 | get_permissions flatpage for request.user as "request_user_permissions"
24 | {% for perm in request_user_permissions %}
25 | {{ perm.user }}: {{ perm }} {% permission_delete_link perm %}
26 | {% endfor %}
27 |
28 |
29 | {% get_permissions flatpage as "all_permissions" %}
30 | get_permissions flatpage as "all_permissions"
31 | {% for perm in all_permissions %}
32 | {{ perm.user }}: {{ perm }} {% permission_delete_link perm %}
33 | {% endfor %}
34 |
35 |
36 |
37 |
38 | The permissions requested for this flatpage:
39 |
40 | {% get_permission_requests flatpage as "all_perm_requests" %}
41 | get_permission_requests flatpage as "all_perm_requests"
42 | {% for perm_request in all_perm_requests %}
43 | {{ perm_request.user }}: {{ perm_request }} {% permission_request_approve_link perm_request %}{% permission_request_delete_link perm_request %}
44 | {% endfor %}
45 |
46 |
47 |
48 |
49 | Permission form for adding a specific permission "top_secret"
50 | {% permission_form for flatpage using "flatpage_permission.top_secret" %}
51 |
52 |
53 | Permission form with a list of options queried from the authority
54 | {% permission_form for flatpage %}
55 |
56 |
57 | Add permission for this flatpage here:
58 | {% add_url_for_obj flatpage %}
59 |
60 |
61 | Request a kind of access:
62 | {% permission_request_form for flatpage %}
63 |
64 |
65 | Permission request form for adding a specific permission "add_flatpage"
66 | {% permission_request_form for flatpage using "flatpage_permission.add_flatpage" %}
67 |
68 |
69 | Request permission for this flatpage here:
70 | {% request_url_for_obj flatpage %}
71 |
72 |
73 | Detailed tests
74 |
75 | Can I change this flatpage?
76 | ifhasperm "flatpage_permission.change_flatpage" request.user:
77 |
78 | {% ifhasperm "flatpage_permission.change_flatpage" request.user %}
79 | Yes, you are allowed.
80 | {% else %}
81 | Nope, sorry.
82 | {% endifhasperm %}
83 |
84 |
85 | Can I access this top secret flat page?
86 | ifhasperm "flatpage_permission.top_secret" request.user flatpage:
87 |
88 | {% ifhasperm "flatpage_permission.top_secret" request.user flatpage %}
89 | Yes, you are of course allowed to view flatpage '{{ flatpage }}', aren't you?
90 | {% else %}
91 | Nope, sorry. Wait, how can you read this then?
92 | {% endifhasperm %}
93 |
94 |
95 | Again, can I really access this top secret flat page?
96 | get_permission "flatpage_permission.top_secret" for request.user and flatpage as "secret_agent":
97 | {% get_permission "flatpage_permission.top_secret" for request.user and flatpage as "secret_agent" %}
98 |
99 | {% if secret_agent %}
100 | Yes {{ request.user }}, you are a secret agent
101 | {% else %}
102 | Nope, only a programmer, sorry
103 | {% endif %}
104 |
105 |
106 |
107 |
108 |
--------------------------------------------------------------------------------
/docs/create_basic_permission.txt:
--------------------------------------------------------------------------------
1 | .. _create-basic-permission:
2 |
3 | =========================
4 | Create a basic permission
5 | =========================
6 |
7 | .. index::
8 | single: autodiscover
9 | single: permissions.py
10 | single: BasePermission
11 |
12 | Where to store permissions?
13 | ===========================
14 |
15 | First of all: All following permission classes should be placed in a file
16 | called ``permissions.py`` in your application. For the *why* please have a
17 | look on `How permissions are discovered`_.
18 |
19 | Basic permissions
20 | =================
21 |
22 | Let's start with an example::
23 |
24 | import authority
25 | from authority import permissions
26 | from django.contrib.flatpages.models import FlatPage
27 |
28 | class FlatpagePermission(permissions.BasePermission):
29 | label = 'flatpage_permission'
30 |
31 | authority.register(FlatPage, FlatpagePermission)
32 |
33 | Let's have a look at the code above. First of, if you want to create a new
34 | permission you have to subclass it from the BasePermission class::
35 |
36 | from authority import permissions
37 | class FlatpagePermission(permissions.BasePermission):
38 | # ...
39 |
40 | Next, you need to name this permission using the ``label`` attribute::
41 |
42 | class FlatpagePermission(permissions.BasePermission):
43 | label = 'flatpage_permission'
44 |
45 | And finally you need to register the permission with the pool of all other
46 | permissions::
47 |
48 | authority.register(FlatPage, FlatpagePermission)
49 |
50 | The syntax of this is simple::
51 |
52 | authority.register(, )
53 |
54 | While this is not much code, you already wrapped Django's basic permissions
55 | (add_flatpage, change_flatpage, delete_flatpage) for the model ``FlatPage``
56 | and you are ready to use it within your templates or code:
57 |
58 | .. note:: See `Django's basic permissions`_ how Django creates this permissions for you.
59 |
60 | .. _Django's basic permissions: http://docs.djangoproject.com/en/dev/topics/auth/#permissions
61 |
62 |
63 | Example permission checks
64 | =========================
65 |
66 | This section shows you how to check for Django's basic permissions with
67 | django-authority.
68 |
69 | In your python code
70 | -------------------
71 | ::
72 |
73 | def my_view(request):
74 | check = FlatPagePermission(request.user)
75 | if check.change_flatpage():
76 | print "Yay, you can change a flatpage!"
77 |
78 | Using the view decorator
79 | ------------------------
80 | ::
81 |
82 | from authority.decorators import permission_required_or_403
83 |
84 | @permission_required_or_403('flatpage_permission.change_flatpage')
85 | def my_view(request):
86 | # ...
87 |
88 | See :ref:`check-decorator` how the decorator works in detail.
89 |
90 | In your templates
91 | -----------------
92 | ::
93 |
94 | {% ifhasperm "flatpage_permission.change_flatpage" request.user %}
95 | Yay, you can change a flatpage!
96 | {% else %}
97 | Nope, sorry. You aren't allowed to change a flatpage.
98 | {% endifhasperm %}
99 |
100 | See :ref:`check-templates` how the templatetag works in detail.
101 |
102 | How permissions are discovered
103 | ==============================
104 |
105 | On first runtime of your Django project ``authority.autodiscover()`` will
106 | load all ``permissions.py`` files that are in your ``settings.INSTALLED_APPS``
107 | applications. See :ref:`configuration` how to set up ``autodiscover``.
108 |
109 | .. image:: .static/authority-permission-py.png
110 | :align: left
111 |
112 | We encourage you to place your permission classes in a file called
113 | ``permissions.py`` inside your application directories. This will not only
114 | keep your application files clean, but it will also load every permission
115 | class at runtime when used with ``authority.autodiscover()``.
116 |
117 | If you really want, you can place these permission-classes in other files
118 | that are loaded at runtime. ``__init__.py`` or ``models.py`` are such files.
119 |
--------------------------------------------------------------------------------
/authority/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import models, migrations
5 | import datetime
6 | from django.conf import settings
7 |
8 |
9 | class Migration(migrations.Migration):
10 |
11 | dependencies = [
12 | ("auth", "0001_initial"),
13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
14 | ("contenttypes", "0001_initial"),
15 | ]
16 |
17 | operations = [
18 | migrations.CreateModel(
19 | name="Permission",
20 | fields=[
21 | (
22 | "id",
23 | models.AutoField(
24 | verbose_name="ID",
25 | serialize=False,
26 | auto_created=True,
27 | primary_key=True,
28 | ),
29 | ),
30 | ("codename", models.CharField(max_length=100, verbose_name="codename")),
31 | ("object_id", models.PositiveIntegerField()),
32 | (
33 | "approved",
34 | models.BooleanField(
35 | default=False,
36 | help_text="Designates whether the permission has been approved and treated as active. Unselect this instead of deleting permissions.",
37 | verbose_name="approved",
38 | ),
39 | ),
40 | (
41 | "date_requested",
42 | models.DateTimeField(
43 | default=datetime.datetime.now, verbose_name="date requested"
44 | ),
45 | ),
46 | (
47 | "date_approved",
48 | models.DateTimeField(
49 | null=True, verbose_name="date approved", blank=True
50 | ),
51 | ),
52 | (
53 | "content_type",
54 | models.ForeignKey(
55 | related_name="row_permissions",
56 | to="contenttypes.ContentType",
57 | on_delete=models.CASCADE,
58 | ),
59 | ),
60 | (
61 | "creator",
62 | models.ForeignKey(
63 | related_name="created_permissions",
64 | blank=True,
65 | to=settings.AUTH_USER_MODEL,
66 | null=True,
67 | on_delete=models.CASCADE,
68 | ),
69 | ),
70 | (
71 | "group",
72 | models.ForeignKey(
73 | blank=True, to="auth.Group", null=True, on_delete=models.CASCADE
74 | ),
75 | ),
76 | (
77 | "user",
78 | models.ForeignKey(
79 | related_name="granted_permissions",
80 | blank=True,
81 | to=settings.AUTH_USER_MODEL,
82 | null=True,
83 | on_delete=models.CASCADE,
84 | ),
85 | ),
86 | ],
87 | options={
88 | "verbose_name": "permission",
89 | "verbose_name_plural": "permissions",
90 | "permissions": (
91 | ("change_foreign_permissions", "Can change foreign permissions"),
92 | ("delete_foreign_permissions", "Can delete foreign permissions"),
93 | ("approve_permission_requests", "Can approve permission requests"),
94 | ),
95 | },
96 | bases=(models.Model,),
97 | ),
98 | migrations.AlterUniqueTogether(
99 | name="permission",
100 | unique_together=set(
101 | [("codename", "object_id", "content_type", "user", "group")]
102 | ),
103 | ),
104 | ]
105 |
--------------------------------------------------------------------------------
/docs/check_decorator.txt:
--------------------------------------------------------------------------------
1 | .. _check-decorator:
2 |
3 | =====================================
4 | Check permissions using the decorator
5 | =====================================
6 |
7 | .. index::
8 | single: permission_required
9 | single: permission_required_or_403
10 |
11 | .. note:: A decorator is not the ultimate painkiller, if you need to deal with
12 | complex permission handling, take a look at :ref:`check-python`.
13 |
14 | The decorator syntax
15 | ====================
16 |
17 | Lets start with an example permission::
18 |
19 | class FlatpagePermission(permissions.BasePermission):
20 | label = 'flatpage_permission'
21 | checks = ('can_do_foo',)
22 |
23 | def can_do_foo(self):
24 | # ...
25 |
26 | authority.register(Campaign, FlatpagePermission)
27 |
28 | A decorator for such a simple view would look like::
29 |
30 | from authority.decorators import permission_required
31 |
32 | @permission_required('flatpage_permission.can_do_foo')
33 | def my_view(request):
34 | # ...
35 |
36 | The decorator automatically takes the user object from the view's arguments
37 | and calls ``can_do_foo``. If this function returns ``True``, the view gets
38 | called, otherwise the user will be redirected to the login page.
39 |
40 | Passing arguments to the permission
41 | -----------------------------------
42 |
43 | You can pass any arguments to the permission function. Assumed our permission
44 | function looks like this::
45 |
46 | def can_do_foo(self, view_arg1, view_arg2=None):
47 | # ...
48 |
49 | Our decorator can *grab* the arguments from the view and passes it to the
50 | permission function. Just take the arguments from the view and place them as
51 | a string on the decorator::
52 |
53 | @permission_required('flatpage_permission.can_do_foo', 'arg1', 'arg2')
54 | def my_view(required, arg1, arg2):
55 | # ...
56 |
57 | What happens under the hood?::
58 |
59 | # Assumed the view gets called like this
60 | my_view(request, 'bla', 'blubb')
61 |
62 | # At the end, the decorator would been called like this
63 | can_do_foo('bla', 'blubb')
64 |
65 | Passing queryset lookups to the permission
66 | ------------------------------------------
67 |
68 | You can pass queryset lookups instead of an argument. This might look a bit
69 | strange first, but it can save you a ton of code. Instead of passing a simple
70 | string to the permission function, declare a tuple of the syntax::
71 |
72 | (, '', 'view_arg')
73 | # .. or ..
74 | ('.', '', 'view_arg')
75 |
76 | Here is an example::
77 |
78 | # permission.py
79 | def can_do_foo(self, flatpage_instance=None):
80 | # ...
81 |
82 | # views.py
83 | from django.contrib.flatpages.models import Flatpage
84 | @permission_required('flatpage_permission.can_do_foo', (Flatpage, 'url__iexact', 'url'))
85 | def flatpage(required, url):
86 | # ...
87 |
88 | What happens under the hood? It's nearly the same as the *simple* decorator
89 | would do, except that the argument is fetched with a ``get_object_or_404``
90 | statement. So this is the same::
91 |
92 | (Flatpage, 'url__iexact', 'url')
93 | get_object_or_404(Flatpage, 'url__iexact'='/about/')
94 |
95 | .. note:: For all available field lookups, please refer to the Django documentation:
96 | `Field lookups`_
97 |
98 | .. _Field lookups: http://docs.djangoproject.com/en/dev/ref/models/querysets/#id7
99 |
100 | Contributed decorators
101 | ======================
102 |
103 | django-authority contributes two decorators, the syntax of both is the same as
104 | described above:
105 |
106 | * permission_required
107 | * permission_required_or_403
108 |
109 | In a nutshell, ``permission_required_or_403`` does the same as ``permission_required``
110 | except it returns a Http403 Response instead of redirecting to the login page.
111 |
112 | Just like Django's ``500.html`` and ``404.html`` you are able to override the
113 | template used in the permission denied page. Simply create a ``403.html``
114 | template in your template directory. It will get the path of the denied page
115 | passed as the context variable ``request_path``.
116 |
--------------------------------------------------------------------------------
/docs/create_per_object_permission.txt:
--------------------------------------------------------------------------------
1 | .. _create-per-object-permission:
2 |
3 | ==============================
4 | Create a per-object permission
5 | ==============================
6 |
7 | django-authority provides a super simple but nifty feature called *per-object
8 | permission*. A description would be::
9 |
10 | Attach a to an object
11 | Attach a to an user
12 |
13 | If the user has and the object has then do-something,
14 | otherwise do-something-else.
15 |
16 | This might sound strange but let's have a closer look on this pattern.
17 | In terms of users and flatpages a visual example would be:
18 |
19 | .. image:: .static/authority-object-1to1.png
20 |
21 | *The user is allowed to review the flatpage "Events".*
22 |
23 | You are not limited to a 1:1 relation, you can add this ``codename`` to
24 | multiple objects:
25 |
26 | .. image:: .static/authority-object-1toN.png
27 |
28 | *The user is allowed to review the flatpages "Events" and "Contact".*
29 |
30 | And you can do this with any objects in any direction:
31 |
32 | .. image:: .static/authority-object-NtoN.png
33 |
34 | *The user is allowed to review the flatpages "Events" and "Contact". Another
35 | user is allowed to publish the flatpage "Events".*
36 |
37 | Create per-object permissions
38 | =============================
39 |
40 | Creating per-object permissions is super simple. See this piece of permission
41 | class code::
42 |
43 | class FlatPagePermission(BasePermission):
44 | label = 'flatpage_permission'
45 | checks = ('review',)
46 |
47 | authority.register(FlatPage, FlatPagePermission)
48 |
49 | This permission class is similar to the one we already created in
50 | :ref:`create-basic-permission` but we added the line::
51 |
52 | checks = ('review',)
53 |
54 | This tells the permission class that it has a permission check (or ``codename``)
55 | ``review``. Under the hood this check gets translated to ``review_flatpage``
56 | (``review_``).
57 |
58 | .. important:: Be sure that you have understand that we have not written any
59 | line of code yet. We just added the ``codename`` to the checks attribute.
60 |
61 | Attach per-object permissions to objects
62 | ========================================
63 |
64 | Please see :ref:`handling-admin` for this.
65 |
66 | Check per-object permissions
67 | ============================
68 |
69 | As we noted above, we have not written any permission comparing code yet. This
70 | is your work. In theory the permission lookup for per-object permissions is::
71 |
72 | if has and has :
73 | return True
74 | else:
75 | return False
76 |
77 | .. important::
78 |
79 | The syntax is similiar to the permission checks we've already
80 | seen in :ref:`create-basic-permission` for the basic permissions but now
81 | we have to pass each function a model instance we want to check!
82 |
83 | In your python code
84 | -------------------
85 | ::
86 |
87 | from myapp.permissions import FlatPagePermission
88 | def my_view(request):
89 | check = FlatPagePermission(request.user)
90 | flatpage_object = Flatpage.objects.get(url='/homepage/')
91 | if check.review_flatpage(flatpage_object):
92 | print "Yay, you can change *this* flatpage!"
93 |
94 | Using the view decorator
95 | ------------------------
96 | ::
97 |
98 | from django.contrib.auth import Flatpage
99 | from authority.decorators import permission_required_or_403
100 |
101 | @permission_required_or_403('flatpage_permission.review_flatpage',
102 | (Flatpage, 'url__iexact', 'url')) # The flatpage_object
103 | def my_view(request, url):
104 | # ...
105 |
106 | See :ref:`check-decorator` how the decorator works in detail.
107 |
108 | In your templates
109 | -----------------
110 | ::
111 |
112 | {% ifhasperm "flatpage_permission.review_flatpage" request.user flatpage_object %}
113 | Yay, you can change *this* flatpage!
114 | {% else %}
115 | Nope, sorry. You aren't allowed to change *this* flatpage.
116 | {% endifhasperm %}
117 |
118 | See :ref:`check-templates` how the template tag works in detail.
119 |
--------------------------------------------------------------------------------
/docs/create_custom_permission.txt:
--------------------------------------------------------------------------------
1 | .. _create-custom-permisson:
2 |
3 | ==========================
4 | Create a custom permission
5 | ==========================
6 |
7 | django-authority allows you to define powerful custom permission. Let's start
8 | again with an example code::
9 |
10 | import authority
11 | from authority import permissions
12 | from django.contrib.flatpages.models import Flatpage
13 |
14 | class FlatpagePermission(permissions.BasePermission):
15 | label = 'flatpage_permission'
16 |
17 | authority.register(Flatpage, FlatpagePermission)
18 |
19 | A custom permission is a simple method of the permission class::
20 |
21 | import authority
22 | from authority import permissions
23 | from django.contrib.flatpages.models import Flatpage
24 |
25 | class FlatpagePermission(permissions.BasePermission):
26 | label = 'flatpage_permission'
27 | checks = ('my_custom_check',)
28 |
29 | def my_custom_check(self, flatpage):
30 | if(flatpage.url == '/about/'):
31 | return True
32 | return False
33 |
34 | authority.register(Flatpage, FlatpagePermission)
35 |
36 | Note that we first added the name of your custom permission to the ``checks``
37 | attribute, like in :ref:`create-per-object-permission`::
38 |
39 | checks = ('my_custom_check',)
40 |
41 | The permission itself is a simple function that accepts an arbitrary number of
42 | arguments. A permission class should always return a boolean whether the
43 | permission is True or False::
44 |
45 | def my_custom_check(self, flatpage):
46 | if flatpage.url == '/about/':
47 | return True
48 | return False
49 |
50 | .. warning:: Although it's possible to return other values than ``True``, for
51 | example an object which also evluates to True, we highly advise to only
52 | return booleans.
53 |
54 | Custom permissions are not necessary related to a model, you can define simpler
55 | permissions too. For example, return True if it's between 10 and 12 o'clock::
56 |
57 | def datetime_check(self):
58 | hour = int(datetime.datetime.now().strftime("%H"))
59 | if hour >= 10 and hour <= 12:
60 | return True
61 | return False
62 |
63 | But most often you want to combine such permissions checks. The next example
64 | would allow an user to have permission to edit a flatpage only between
65 | 8 and 12 o'clock in the morning::
66 |
67 | def morning_flatpage_check(self, flatpage):
68 | hour = int(datetime.datetime.now().strftime("%H"))
69 | if hour >= 8 and hour <= 12 and flatpage.url == '/about/':
70 | return True
71 | return False
72 |
73 | Check custom permissions
74 | ========================
75 |
76 | The permission check is similar to :ref:`create-basic-permission` and
77 | :ref:`create-per-object-permission`.
78 |
79 | .. warning:: Although *per-object* permissions are translated to
80 | ``_`` this is not the case for custom permissions!
81 | A custom permission ``my_custom_check`` remains ``my_custom_check``.
82 |
83 | In your python code
84 | -------------------
85 | ::
86 |
87 | from myapp.permissions import FlatPagePermission
88 | def my_view(request):
89 | check = FlatPagePermission(request.user)
90 | flatpage_object = Flatpage.objects.get(url='/homepage/')
91 | if check.my_custom_check(flatpage=flatpage_object):
92 | print "Yay, you can change *this* flatpage!"
93 |
94 | Using the view decorator
95 | ------------------------
96 | ::
97 |
98 | from django.contrib.auth import Flatpage
99 | from authority.decorators import permission_required_or_403
100 |
101 | @permission_required_or_403('flatpage_permission.my_custom_check',
102 | (Flatpage, 'url__iexact', 'url')) # The flatpage_object
103 | def my_view(request, url):
104 | # ...
105 |
106 | See :ref:`check-decorator` how the decorator works in detail.
107 |
108 | In your templates
109 | -----------------
110 | ::
111 |
112 | {% ifhasperm "flatpage_permission.my_custom_check" request.user flatpage_object %}
113 | Yay, you can change *this* flatpage!
114 | {% else %}
115 | Nope, sorry. You aren't allowed to change *this* flatpage.
116 | {% endifhasperm %}
117 |
118 | See :ref:`check-templates` how the templatetag works in detail.
119 |
--------------------------------------------------------------------------------
/authority/forms.py:
--------------------------------------------------------------------------------
1 | from django import forms
2 | from django.utils.translation import ugettext_lazy as _
3 | from django.contrib.auth import get_user_model
4 | from django.contrib.auth.models import Group
5 | from django.contrib.contenttypes.models import ContentType
6 | from django.utils.safestring import mark_safe
7 |
8 | from authority import permissions
9 | from authority.utils import get_choices_for
10 | from authority.models import Permission
11 |
12 | User = get_user_model()
13 |
14 |
15 | class BasePermissionForm(forms.ModelForm):
16 | codename = forms.CharField(label=_("Permission"))
17 |
18 | class Meta:
19 | model = Permission
20 | exclude = []
21 |
22 | def __init__(self, perm=None, obj=None, approved=False, *args, **kwargs):
23 | self.perm = perm
24 | self.obj = obj
25 | self.approved = approved
26 | if obj and perm:
27 | self.base_fields["codename"].widget = forms.HiddenInput()
28 | elif obj and (not perm or not approved):
29 | perms = get_choices_for(self.obj)
30 | self.base_fields["codename"].widget = forms.Select(choices=perms)
31 | super(BasePermissionForm, self).__init__(*args, **kwargs)
32 |
33 | def save(self, request, commit=True, *args, **kwargs):
34 | self.instance.creator = request.user
35 | self.instance.content_type = ContentType.objects.get_for_model(self.obj)
36 | self.instance.object_id = self.obj.id
37 | self.instance.codename = self.perm
38 | self.instance.approved = self.approved
39 | return super(BasePermissionForm, self).save(commit)
40 |
41 |
42 | class UserPermissionForm(BasePermissionForm):
43 | user = forms.CharField(label=_("User"))
44 |
45 | class Meta(BasePermissionForm.Meta):
46 | fields = ("user",)
47 |
48 | def __init__(self, *args, **kwargs):
49 | if not kwargs.get("approved", False):
50 | self.base_fields["user"].widget = forms.HiddenInput()
51 | super(UserPermissionForm, self).__init__(*args, **kwargs)
52 |
53 | def clean_user(self):
54 | username = self.cleaned_data["user"]
55 | try:
56 | user = User.objects.get(username__iexact=username)
57 | except User.DoesNotExist:
58 | raise forms.ValidationError(
59 | mark_safe(_("A user with that username does not exist."))
60 | )
61 | check = permissions.BasePermission(user=user)
62 | error_msg = None
63 | if user.is_superuser:
64 | error_msg = _(
65 | "The user %(user)s do not need to request "
66 | "access to any permission as it is a super user."
67 | )
68 | elif check.has_perm(self.perm, self.obj):
69 | error_msg = _(
70 | "The user %(user)s already has the permission "
71 | "'%(perm)s' for %(object_name)s '%(obj)s'"
72 | )
73 | elif check.requested_perm(self.perm, self.obj):
74 | error_msg = _(
75 | "The user %(user)s already requested the permission"
76 | " '%(perm)s' for %(object_name)s '%(obj)s'"
77 | )
78 | if error_msg:
79 | error_msg = error_msg % {
80 | "object_name": self.obj._meta.object_name.lower(),
81 | "perm": self.perm,
82 | "obj": self.obj,
83 | "user": user,
84 | }
85 | raise forms.ValidationError(mark_safe(error_msg))
86 | return user
87 |
88 |
89 | class GroupPermissionForm(BasePermissionForm):
90 | group = forms.CharField(label=_("Group"))
91 |
92 | class Meta(BasePermissionForm.Meta):
93 | fields = ("group",)
94 |
95 | def clean_group(self):
96 | groupname = self.cleaned_data["group"]
97 | try:
98 | group = Group.objects.get(name__iexact=groupname)
99 | except Group.DoesNotExist:
100 | raise forms.ValidationError(
101 | mark_safe(_("A group with that name does not exist."))
102 | )
103 | check = permissions.BasePermission(group=group)
104 | if check.has_perm(self.perm, self.obj):
105 | raise forms.ValidationError(
106 | mark_safe(
107 | _(
108 | "This group already has the permission '%(perm)s' "
109 | "for %(object_name)s '%(obj)s'"
110 | )
111 | % {
112 | "perm": self.perm,
113 | "object_name": self.obj._meta.object_name.lower(),
114 | "obj": self.obj,
115 | }
116 | )
117 | )
118 | return group
119 |
--------------------------------------------------------------------------------
/authority/views.py:
--------------------------------------------------------------------------------
1 | from django.shortcuts import render, get_object_or_404
2 | from django.http import HttpResponseRedirect, HttpResponseForbidden
3 | from django.apps import apps
4 | from django.utils.translation import ugettext as _
5 | from django.template import loader
6 | from django.contrib.auth.decorators import login_required
7 |
8 | from authority.models import Permission
9 | from authority.forms import UserPermissionForm
10 | from authority.templatetags.permissions import url_for_obj
11 |
12 |
13 | def get_next(request, obj=None):
14 | next = request.REQUEST.get("next")
15 | if not next:
16 | if obj and hasattr(obj, "get_absolute_url"):
17 | next = obj.get_absolute_url()
18 | else:
19 | next = "/"
20 | return next
21 |
22 |
23 | @login_required
24 | def add_permission(
25 | request,
26 | app_label,
27 | module_name,
28 | pk,
29 | approved=False,
30 | template_name="authority/permission_form.html",
31 | extra_context=None,
32 | form_class=UserPermissionForm,
33 | ):
34 | codename = request.POST.get("codename", None)
35 | try:
36 | model = apps.get_model(app_label, module_name)
37 | except LookupError:
38 | return permission_denied(request)
39 | obj = get_object_or_404(model, pk=pk)
40 | next = get_next(request, obj)
41 | if approved:
42 | if not request.user.has_perm("authority.add_permission"):
43 | return HttpResponseRedirect(
44 | url_for_obj("authority-add-permission-request", obj)
45 | )
46 | view_name = "authority-add-permission"
47 | else:
48 | view_name = "authority-add-permission-request"
49 | if request.method == "POST":
50 | if codename is None:
51 | return HttpResponseForbidden(next)
52 | form = form_class(
53 | data=request.POST,
54 | obj=obj,
55 | approved=approved,
56 | perm=codename,
57 | initial=dict(codename=codename),
58 | )
59 | if not approved:
60 | # Limit permission request to current user
61 | form.data["user"] = request.user
62 | if form.is_valid():
63 | form.save(request)
64 | request.user.message_set.create(
65 | message=_("You added a permission request.")
66 | )
67 | return HttpResponseRedirect(next)
68 | else:
69 | form = form_class(
70 | obj=obj, approved=approved, perm=codename, initial=dict(codename=codename)
71 | )
72 | context = {
73 | "form": form,
74 | "form_url": url_for_obj(view_name, obj),
75 | "next": next,
76 | "perm": codename,
77 | "approved": approved,
78 | }
79 | if extra_context:
80 | context.update(extra_context)
81 | return render(request, template_name, context)
82 |
83 |
84 | @login_required
85 | def approve_permission_request(request, permission_pk):
86 | requested_permission = get_object_or_404(Permission, pk=permission_pk)
87 | if request.user.has_perm("authority.approve_permission_requests"):
88 | requested_permission.approve(request.user)
89 | request.user.message_set.create(
90 | message=_("You approved the permission request.")
91 | )
92 | next = get_next(request, requested_permission)
93 | return HttpResponseRedirect(next)
94 |
95 |
96 | @login_required
97 | def delete_permission(request, permission_pk, approved):
98 | permission = get_object_or_404(Permission, pk=permission_pk, approved=approved)
99 | if (
100 | request.user.has_perm("authority.delete_foreign_permissions")
101 | or request.user == permission.creator
102 | ):
103 | permission.delete()
104 | if approved:
105 | msg = _("You removed the permission.")
106 | else:
107 | msg = _("You removed the permission request.")
108 | request.user.message_set.create(message=msg)
109 | next = get_next(request)
110 | return HttpResponseRedirect(next)
111 |
112 |
113 | def permission_denied(request, template_name=None, extra_context=None):
114 | """
115 | Default 403 handler.
116 |
117 | Templates: `403.html`
118 | Context:
119 | request_path
120 | The path of the requested URL (e.g., '/app/pages/bad_page/')
121 | """
122 | if template_name is None:
123 | template_name = ("403.html", "authority/403.html")
124 | context = {
125 | "request_path": request.path,
126 | }
127 | if extra_context:
128 | context.update(extra_context)
129 | return HttpResponseForbidden(
130 | loader.render_to_string(
131 | template_name=template_name, context=context, request=request,
132 | )
133 | )
134 |
--------------------------------------------------------------------------------
/docs/.theme/nature/static/nature.css_t:
--------------------------------------------------------------------------------
1 | /**
2 | * Sphinx stylesheet -- default theme
3 | * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4 | */
5 |
6 | @import url("basic.css");
7 |
8 | div.documentwrapper {
9 | float: left;
10 | width: 100%;
11 | }
12 |
13 | div.bodywrapper {
14 | margin: 0 0 0 230px;
15 | }
16 |
17 | /* -- page layout ----------------------------------------------------------- */
18 |
19 | body {
20 | font-family: Arial, sans-serif;
21 | font-size: 100%;
22 | background-color: #111;
23 | color: #555;
24 | margin: 0;
25 | padding: 0;
26 | }
27 |
28 | hr{
29 | border: 1px solid #B1B4B6;
30 | }
31 |
32 | div.document {
33 | background-color: #eee;
34 | }
35 |
36 | div.body {
37 | background-color: #ffffff;
38 | color: #3E4349;
39 | padding: 0 30px 30px 30px;
40 | font-size: 0.8em;
41 | }
42 |
43 | div.footer {
44 | color: #555;
45 | width: 100%;
46 | padding: 13px 0;
47 | text-align: center;
48 | font-size: 75%;
49 | }
50 |
51 | div.footer a {
52 | color: #444;
53 | text-decoration: underline;
54 | }
55 |
56 | div.related {
57 | background-color: #6BA81E;
58 | line-height: 32px;
59 | color: #fff;
60 | text-shadow: 0px 1px 0 #444;
61 | font-size: 0.80em;
62 | }
63 |
64 | div.related a {
65 | color: #E2F3CC;
66 | }
67 |
68 | div.sphinxsidebar {
69 | font-size: 0.75em;
70 | line-height: 1.5em;
71 | }
72 |
73 | div.sphinxsidebarwrapper{
74 | padding: 20px 0;
75 | }
76 |
77 | div.sphinxsidebar h3,
78 | div.sphinxsidebar h4 {
79 | font-family: Arial, sans-serif;
80 | color: #222;
81 | font-size: 1.2em;
82 | font-weight: normal;
83 | margin: 0;
84 | padding: 5px 10px;
85 | background-color: #ddd;
86 | text-shadow: 1px 1px 0 white
87 | }
88 |
89 | div.sphinxsidebar h4{
90 | font-size: 1.1em;
91 | }
92 |
93 | div.sphinxsidebar h3 a {
94 | color: #444;
95 | }
96 |
97 |
98 | div.sphinxsidebar p {
99 | color: #888;
100 | padding: 5px 20px;
101 | }
102 |
103 | div.sphinxsidebar p.topless {
104 | }
105 |
106 | div.sphinxsidebar ul {
107 | margin: 10px 20px;
108 | padding: 0;
109 | color: #000;
110 | }
111 |
112 | div.sphinxsidebar a {
113 | color: #444;
114 | }
115 |
116 | div.sphinxsidebar input {
117 | border: 1px solid #ccc;
118 | font-family: sans-serif;
119 | font-size: 1em;
120 | }
121 |
122 | div.sphinxsidebar input[type=text]{
123 | margin-left: 20px;
124 | }
125 |
126 | /* -- body styles ----------------------------------------------------------- */
127 |
128 | a {
129 | color: #005B81;
130 | text-decoration: none;
131 | }
132 |
133 | a:hover {
134 | color: #E32E00;
135 | text-decoration: underline;
136 | }
137 |
138 | div.body h1,
139 | div.body h2,
140 | div.body h3,
141 | div.body h4,
142 | div.body h5,
143 | div.body h6 {
144 | font-family: Arial, sans-serif;
145 | background-color: #BED4EB;
146 | font-weight: normal;
147 | color: #212224;
148 | margin: 30px 0px 10px 0px;
149 | padding: 5px 0 5px 10px;
150 | text-shadow: 0px 1px 0 white
151 | }
152 |
153 | div.body h1 { border-top: 20px solid white; margin-top: 0; font-size: 200%; }
154 | div.body h2 { font-size: 150%; background-color: #C8D5E3; }
155 | div.body h3 { font-size: 120%; background-color: #D8DEE3; }
156 | div.body h4 { font-size: 110%; background-color: #D8DEE3; }
157 | div.body h5 { font-size: 100%; background-color: #D8DEE3; }
158 | div.body h6 { font-size: 100%; background-color: #D8DEE3; }
159 |
160 | a.headerlink {
161 | color: #c60f0f;
162 | font-size: 0.8em;
163 | padding: 0 4px 0 4px;
164 | text-decoration: none;
165 | }
166 |
167 | a.headerlink:hover {
168 | background-color: #c60f0f;
169 | color: white;
170 | }
171 |
172 | div.body p, div.body dd, div.body li {
173 | line-height: 1.5em;
174 | }
175 |
176 | div.admonition p.admonition-title + p {
177 | display: inline;
178 | }
179 |
180 | div.highlight{
181 | background-color: white;
182 | }
183 |
184 | div.note {
185 | background-color: #eee;
186 | border: 1px solid #ccc;
187 | }
188 |
189 | div.seealso {
190 | background-color: #ffc;
191 | border: 1px solid #ff6;
192 | }
193 |
194 | div.topic {
195 | background-color: #eee;
196 | }
197 |
198 | div.warning {
199 | background-color: #ffe4e4;
200 | border: 1px solid #f66;
201 | }
202 |
203 | p.admonition-title {
204 | display: inline;
205 | }
206 |
207 | p.admonition-title:after {
208 | content: ":";
209 | }
210 |
211 | pre {
212 | padding: 10px;
213 | background-color: White;
214 | color: #222;
215 | line-height: 1.2em;
216 | border: 1px solid #C6C9CB;
217 | font-size: 1.2em;
218 | margin: 1.5em 0 1.5em 0;
219 | -webkit-box-shadow: 1px 1px 1px #d8d8d8;
220 | -moz-box-shadow: 1px 1px 1px #d8d8d8;
221 | }
222 |
223 | tt {
224 | background-color: #ecf0f3;
225 | color: #222;
226 | padding: 1px 2px;
227 | font-size: 1.2em;
228 | font-family: monospace;
229 | }
--------------------------------------------------------------------------------
/authority/sites.py:
--------------------------------------------------------------------------------
1 | from inspect import getmembers, ismethod
2 | from django.apps import apps
3 | from django.db import models
4 | from django.db.models.base import ModelBase
5 | from django.utils.translation import ugettext_lazy as _
6 | from django.core.exceptions import ImproperlyConfigured
7 |
8 | from authority.permissions import BasePermission
9 |
10 |
11 | class AlreadyRegistered(Exception):
12 | pass
13 |
14 |
15 | class NotRegistered(Exception):
16 | pass
17 |
18 |
19 | class PermissionSite(object):
20 | """
21 | A dictionary that contains permission instances and their labels.
22 | """
23 |
24 | _registry = {}
25 | _choices = {}
26 |
27 | def get_permission_by_label(self, label):
28 | for perm_cls in self._registry.values():
29 | if perm_cls.label == label:
30 | return perm_cls
31 | return None
32 |
33 | def get_permissions_by_model(self, model):
34 | return [perm for perm in self._registry.values() if perm.model == model]
35 |
36 | def get_check(self, user, label):
37 | perm_label, check_name = label.split(".")
38 | perm_cls = self.get_permission_by_label(perm_label)
39 | if perm_cls is None:
40 | return None
41 | perm_instance = perm_cls(user)
42 | return getattr(perm_instance, check_name, None)
43 |
44 | def get_labels(self):
45 | return [perm.label for perm in self._registry.values()]
46 |
47 | def get_choices_for(self, obj, default=models.BLANK_CHOICE_DASH):
48 | model_cls = obj
49 | if not isinstance(obj, ModelBase):
50 | model_cls = obj.__class__
51 | if model_cls in self._choices:
52 | return self._choices[model_cls]
53 | choices = [] + default
54 | for perm in self.get_permissions_by_model(model_cls):
55 | for name, check in getmembers(perm, ismethod):
56 | if name in perm.checks:
57 | signature = "%s.%s" % (perm.label, name)
58 | label = getattr(check, "short_description", signature)
59 | choices.append((signature, label))
60 | self._choices[model_cls] = choices
61 | return choices
62 |
63 | def register(self, model_or_iterable, permission_class=None, **options):
64 | if not permission_class:
65 | permission_class = BasePermission
66 |
67 | if isinstance(model_or_iterable, ModelBase):
68 | model_or_iterable = [model_or_iterable]
69 |
70 | if permission_class.label in self.get_labels():
71 | raise ImproperlyConfigured(
72 | "The name of %s conflicts with %s"
73 | % (
74 | permission_class,
75 | self.get_permission_by_label(permission_class.label),
76 | )
77 | )
78 |
79 | for model in model_or_iterable:
80 | if model in self._registry:
81 | raise AlreadyRegistered(
82 | "The model %s is already registered" % model.__name__
83 | )
84 | if options:
85 | options["__module__"] = __name__
86 | permission_class = type(
87 | "%sPermission" % model.__name__, (permission_class,), options
88 | )
89 |
90 | permission_class.model = model
91 | self.setup(model, permission_class)
92 | self._registry[model] = permission_class
93 |
94 | def unregister(self, model_or_iterable):
95 | if isinstance(model_or_iterable, ModelBase):
96 | model_or_iterable = [model_or_iterable]
97 | for model in model_or_iterable:
98 | if model not in self._registry:
99 | raise NotRegistered("The model %s is not registered" % model.__name__)
100 | del self._registry[model]
101 |
102 | def setup(self, model, permission):
103 | for check_name in permission.checks:
104 | check_func = getattr(permission, check_name, None)
105 | if check_func is not None:
106 | func = self.create_check(check_name, check_func)
107 | func.__name__ = check_name
108 | func.short_description = getattr(
109 | check_func,
110 | "short_description",
111 | _("%(object_name)s permission '%(check)s'")
112 | % {"object_name": model._meta.object_name, "check": check_name},
113 | )
114 | setattr(permission, check_name, func)
115 | else:
116 | permission.generic_checks.append(check_name)
117 | for check_name in permission.generic_checks:
118 | func = self.create_check(check_name, generic=True)
119 | object_name = model._meta.object_name
120 | func_name = "%s_%s" % (check_name, object_name.lower())
121 | func.short_description = _("Can %(check)s this %(object_name)s") % {
122 | "object_name": model._meta.object_name.lower(),
123 | "check": check_name,
124 | }
125 | func.check_name = check_name
126 | if func_name not in permission.checks:
127 | permission.checks = list(permission.checks) + [func_name]
128 | setattr(permission, func_name, func)
129 | setattr(model, "permissions", PermissionDescriptor())
130 |
131 | def create_check(self, check_name, check_func=None, generic=False):
132 | def check(self, *args, **kwargs):
133 | granted = self.can(check_name, generic, *args, **kwargs)
134 | if check_func and not granted:
135 | return check_func(self, *args, **kwargs)
136 | return granted
137 |
138 | return check
139 |
140 |
141 | class PermissionDescriptor(object):
142 | def get_content_type(self, obj=None):
143 | ContentType = apps.get_model("contenttypes", "contenttype")
144 | if obj:
145 | return ContentType.objects.get_for_model(obj)
146 | else:
147 | raise Exception(
148 | "Invalid arguments given to PermissionDescriptor.get_content_type"
149 | )
150 |
151 | def __get__(self, instance, owner):
152 | if instance is None:
153 | return self
154 | ct = self.get_content_type(instance)
155 | return ct.row_permissions.all()
156 |
157 |
158 | site = PermissionSite()
159 | get_check = site.get_check
160 | get_choices_for = site.get_choices_for
161 | register = site.register
162 | unregister = site.unregister
163 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | ================
2 | django-authority
3 | ================
4 |
5 | .. image:: https://jazzband.co/static/img/badge.svg
6 | :target: https://jazzband.co/
7 | :alt: Jazzband
8 |
9 | .. image:: https://github.com/jazzband/django-authority/workflows/Test/badge.svg
10 | :target: https://github.com/jazzband/django-authority/actions
11 | :alt: GitHub Actions
12 |
13 | .. image:: https://codecov.io/gh/jazzband/django-authority/branch/master/graph/badge.svg
14 | :target: https://codecov.io/gh/jazzband/django-authority
15 |
16 | This is a Django app for per-object-permissions that includes a bunch of
17 | helpers to create custom permission checks.
18 |
19 | The main website for django-authority is
20 | `django-authority.readthedocs.org`_. You can also install the
21 | `in-development version`_ of django-authority with
22 | ``pip install django-authority==dev`` or ``easy_install django-authority==dev``.
23 |
24 | .. _`django-authority.readthedocs.org`: https://django-authority.readthedocs.io/
25 | .. _in-development version: https://github.com/jazzband/django-authority/archive/master.zip#egg=django-authority-dev
26 |
27 | Example
28 | =======
29 |
30 | To get the example project running do:
31 |
32 | - Bootstrap the environment by running in a virtualenv::
33 |
34 | pip install Django
35 | pip install -e .
36 |
37 | - Sync the database::
38 |
39 | python example/manage.py migrate
40 |
41 | - Run the development server and visit the admin at http://127.0.0.1:8000/admin/::
42 |
43 | python example/manage.py runserver
44 |
45 | Now create a flatage and open it to see some of the templatetags in action.
46 | Don't hesitate to use the admin to edit the permission objects.
47 |
48 | Please use https://github.com/jazzband/django-authority/issues/ for issues and bug reports.
49 |
50 | Documentation
51 | =============
52 |
53 | The documenation is currently in development. You can create a nice looking
54 | html version using the setup.py::
55 |
56 | python setup.py build_sphinx
57 |
58 | Changelog:
59 | ==========
60 |
61 | 0.15 (unreleased):
62 | ------------------
63 |
64 | * Moved CI to GitHub Actions.
65 | * Add Django 3.0 and 3.1 support.
66 | * Add Python 3.6 and 3.8 support.
67 |
68 | 0.14 (2020-02-07):
69 | ------------------
70 |
71 | * Add Django 2.2 support
72 | * Add Python 3.7 support
73 | * Various fixes around the test harness.
74 | * Use Django's own method of auto-loading permissions modules.
75 | * Fix Django admin incompatibility regarding a method removed years ago.
76 | * Removed unused compatibility code.
77 | * Fix BasePermission.assign for group permissions.
78 |
79 | 0.13.1 (2018-01-28):
80 | --------------------
81 |
82 | * Minor fixes to the documentation and versioning.
83 |
84 | 0.13 (2018-01-28):
85 | ------------------
86 |
87 | * Added support for Django 1.11
88 | * Drop Support for Python 3.3
89 | * Fixed a bug with template loader
90 |
91 | 0.12 (2017-01-10):
92 | ------------------
93 |
94 | * Added support for Django 1.10
95 |
96 | 0.11 (2016-07-17):
97 | ------------------
98 |
99 | * Added Migration in order to support Django 1.8
100 |
101 | * Dropped Support for Django 1.7 and lower
102 |
103 | * Remove SQL Migration Files
104 |
105 | * Documentation Updates
106 |
107 | * Fix linter issues
108 |
109 | 0.10 (2015-12-14):
110 | ------------------
111 |
112 | * Fixed a bug with BasePermissionForm and django 1.8
113 |
114 | 0.9 (2015-11-11):
115 | -----------------
116 |
117 | * Added support for Django 1.7 and 1.8
118 |
119 | * Dropped support for Django 1.3
120 |
121 | 0.8 (2013-12-20):
122 | -----------------
123 |
124 | * Added support for Django 1.6
125 |
126 | 0.7 (2013-07-03):
127 | -----------------
128 |
129 | * No longer doing dependent sub-queries. It will be faster to do two small
130 | queries instead of one with a dependent sub-query in the general case.
131 |
132 | 0.6 (2013-06-13):
133 | -----------------
134 |
135 | * Added support for custom user models (Django 1.5 only).
136 |
137 | 0.5 (2013-03-18):
138 | -----------------
139 |
140 | * It is now possible to minimize the number of queries when using
141 | django-authority by caching the results of the Permission query. This can be
142 | done by adding ``AUTHORITY_USE_SMART_CACHE = True`` to your settings.py
143 | * Confirmed support (via travis ci) for all combinations of Python 2.6,
144 | Python2.7 and Django 1.3, Django 1.4, Django 1.5. Added Python 3.3 support
145 | for Django 1.5
146 |
147 |
148 | 0.4 (2010-01-15):
149 | -----------------
150 |
151 | * Fixed an issue with the UserPermissionForm not being able to override the
152 | widget of the user field.
153 |
154 | * Added ability to override form class in ``add_permission`` view.
155 |
156 | * Added easy way to assign permissions via a permission instance, e.g.:
157 |
158 | .. code-block:: python
159 |
160 | from django.contrib.auth.models import User
161 | from mysite.articles.permissions import ArticlePermission
162 |
163 | bob = User.objects.get(username='bob')
164 | article_permission = ArticlePermission(bob)
165 | article_permission.assign(content_object=article)
166 |
167 |
168 | 0.3 (2009-07-28):
169 | -----------------
170 |
171 | * This version adds multiple fields to the Permission model and is
172 | therefore a **backwards incompatible** update.
173 |
174 | This was required to add a feature that allows users to request,
175 | withdraw, deny and approve permissions. Request and approval date
176 | are now saved, as well as an ``approved`` property. An admin action has
177 | been added for bulk approval.
178 |
179 | To migrate your existing data you can use the SQL files included in
180 | the source (`migrations/`_), currently available for MySQL, Postgres
181 | and SQLite.
182 |
183 | * The templatetags have also been refactored to be easier to customize
184 | which required a change in the template tag signature:
185 |
186 | Old:
187 |
188 | .. code-block:: html+django
189 |
190 | {% permission_form flatpage %}
191 | {% permission_form flatpage "flatpage_permission.top_secret" %}
192 | {% permission_form OBJ PERMISSION_LABEL.CHECK_NAME %}
193 |
194 | New:
195 |
196 | .. code-block:: html+django
197 |
198 | {% permission_form for flatpage %}
199 | {% permission_form for flatpage using "flatpage_permission.top_secret" %}
200 | {% permission_form for OBJ using PERMISSION_LABEL.CHECK_NAME [with TEMPLATE] %}
201 |
202 | New templatetags:
203 |
204 | * ``permission_request_form``
205 | * ``get_permission_request``
206 | * ``get_permission_requests``
207 | * ``permission_request_approve_link``
208 | * ``permission_request_delete_link``
209 | * ``request_url_for_obj``
210 |
211 | * The ``add_permission`` view is now accessible with GET requests and
212 | allows to request permissions, but also add them (only for users with
213 | the 'authority.add_permission' Django permission).
214 |
215 | .. _`migrations/`: https://github.com/jazzbands/django-authority/tree/master/migrations
216 |
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # django-authority documentation build configuration file, created by
4 | # sphinx-quickstart on Thu Jul 9 10:52:07 2009.
5 | #
6 | # This file is execfile()d with the current directory set to its containing dir.
7 | #
8 | # Note that not all possible configuration values are present in this
9 | # autogenerated file.
10 | #
11 | # All configuration values have a default; values that are commented out
12 | # serve to show the default.
13 |
14 | from pkg_resources import get_distribution
15 |
16 | # If extensions (or modules to document with autodoc) are in another directory,
17 | # add these directories to sys.path here. If the directory is relative to the
18 | # documentation root, use os.path.abspath to make it absolute, like shown here.
19 | # sys.path.append(os.path.abspath('.'))
20 | # sys.path.append(os.path.join(os.path.dirname(__file__), '../src/'))
21 |
22 | # -- General configuration -----------------------------------------------------
23 |
24 | # Add any Sphinx extension module names here, as strings. They can be extensions
25 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
26 | extensions = []
27 |
28 | # Add any paths that contain templates here, relative to this directory.
29 | # templates_path = ['.templates']
30 |
31 | # The suffix of source filenames.
32 | source_suffix = ".txt"
33 |
34 | # The encoding of source files.
35 | # source_encoding = 'utf-8'
36 |
37 | # The master toctree document.
38 | master_doc = "index"
39 |
40 | # General information about the project.
41 | project = u"django-authority"
42 | copyright = u"2009-2020, Jannis Leidel"
43 |
44 | # The version info for the project you're documenting, acts as replacement for
45 | # |version| and |release|, also used in various other places throughout the
46 | # built documents.
47 | #
48 | # The full version, including alpha/beta/rc tags.
49 | release = get_distribution("django-authority").version
50 | # The short X.Y version.
51 | version = ".".join(release.split(".")[:2])
52 |
53 | # The language for content autogenerated by Sphinx. Refer to documentation
54 | # for a list of supported languages.
55 | # language = None
56 |
57 | # There are two options for replacing |today|: either, you set today to some
58 | # non-false value, then it is used:
59 | # today = ''
60 | # Else, today_fmt is used as the format for a strftime call.
61 | # today_fmt = '%B %d, %Y'
62 |
63 | # List of documents that shouldn't be included in the build.
64 | # unused_docs = []
65 |
66 | # List of directories, relative to source directory, that shouldn't be searched
67 | # for source files.
68 | exclude_trees = ["build"]
69 |
70 | # The reST default role (used for this markup: `text`) to use for all documents.
71 | # default_role = None
72 |
73 | # If true, '()' will be appended to :func: etc. cross-reference text.
74 | # add_function_parentheses = True
75 |
76 | # If true, the current module name will be prepended to all description
77 | # unit titles (such as .. function::).
78 | # add_module_names = True
79 |
80 | # If true, sectionauthor and moduleauthor directives will be shown in the
81 | # output. They are ignored by default.
82 | # show_authors = False
83 |
84 | # The name of the Pygments (syntax highlighting) style to use.
85 | pygments_style = "sphinx"
86 |
87 | # A list of ignored prefixes for module index sorting.
88 | # modindex_common_prefix = []
89 |
90 |
91 | # -- Options for HTML output ---------------------------------------------------
92 |
93 | # The theme to use for HTML and HTML Help pages. Major themes that come with
94 | # Sphinx are currently 'default' and 'sphinxdoc'.
95 | html_theme = "nature"
96 |
97 | # Theme options are theme-specific and customize the look and feel of a theme
98 | # further. For a list of options available for each theme, see the
99 | # documentation.
100 | # html_theme_options = {}
101 |
102 | # Add any paths that contain custom themes here, relative to this directory.
103 | html_theme_path = [".theme"]
104 |
105 | # The name for this set of Sphinx documents. If None, it defaults to
106 | # " v documentation".
107 | # html_title = None
108 |
109 | # A shorter title for the navigation bar. Default is the same as html_title.
110 | # html_short_title = None
111 |
112 | # The name of an image file (relative to this directory) to place at the top
113 | # of the sidebar.
114 | html_logo = ".static/logo.png"
115 |
116 | # The name of an image file (within the static path) to use as favicon of the
117 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
118 | # pixels large.
119 | html_favicon = "favicon.png"
120 |
121 | # Add any paths that contain custom static files (such as style sheets) here,
122 | # relative to this directory. They are copied after the builtin static files,
123 | # so a file named "default.css" will overwrite the builtin "default.css".
124 | html_static_path = [".static"]
125 |
126 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
127 | # using the given strftime format.
128 | # html_last_updated_fmt = '%b %d, %Y'
129 |
130 | # If true, SmartyPants will be used to convert quotes and dashes to
131 | # typographically correct entities.
132 | html_use_smartypants = True
133 |
134 | # Custom sidebar templates, maps document names to template names.
135 | # html_sidebars = {}
136 |
137 | # Additional templates that should be rendered to pages, maps page names to
138 | # template names.
139 | # html_additional_pages = {}
140 |
141 | # If false, no module index is generated.
142 | html_use_modindex = True
143 |
144 | # If false, no index is generated.
145 | html_use_index = True
146 |
147 | # If true, the index is split into individual pages for each letter.
148 | # html_split_index = False
149 |
150 | # If true, links to the reST sources are added to the pages.
151 | html_show_sourcelink = False
152 |
153 | # If true, an OpenSearch description file will be output, and all pages will
154 | # contain a tag referring to it. The value of this option must be the
155 | # base URL from which the finished HTML is served.
156 | # html_use_opensearch = ''
157 |
158 | # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml").
159 | # html_file_suffix = ''
160 |
161 | # Output file base name for HTML help builder.
162 | htmlhelp_basename = "django-authoritydoc"
163 |
164 |
165 | # -- Options for LaTeX output --------------------------------------------------
166 |
167 | # The paper size ('letter' or 'a4').
168 | # latex_paper_size = 'letter'
169 |
170 | # The font size ('10pt', '11pt' or '12pt').
171 | # latex_font_size = '10pt'
172 |
173 | # Grouping the document tree into LaTeX files. List of tuples
174 | # (source start file, target name, title, author, documentclass [howto/manual]).
175 | latex_documents = [
176 | (
177 | "index",
178 | "django-authority.tex",
179 | u"django-authority Documentation",
180 | u"The django-authority team",
181 | "manual",
182 | ),
183 | ]
184 |
185 | # The name of an image file (relative to this directory) to place at the top of
186 | # the title page.
187 | # latex_logo = None
188 |
189 | # For "manual" documents, if this is true, then toplevel headings are parts,
190 | # not chapters.
191 | # latex_use_parts = False
192 |
193 | # Additional stuff for the LaTeX preamble.
194 | # latex_preamble = ''
195 |
196 | # Documents to append as an appendix to all manuals.
197 | # latex_appendices = []
198 |
199 | # If false, no module index is generated.
200 | # latex_use_modindex = True
201 |
--------------------------------------------------------------------------------
/authority/admin.py:
--------------------------------------------------------------------------------
1 | from django import forms
2 | from django.http import HttpResponseRedirect
3 | from django.utils.translation import ugettext, ungettext, ugettext_lazy as _
4 | from django.shortcuts import render
5 | from django.utils.safestring import mark_safe
6 | from django.forms.formsets import all_valid
7 | from django.contrib import admin
8 | from django.contrib.admin import actions, helpers
9 | from django.contrib.contenttypes.admin import GenericTabularInline
10 | from django.contrib.contenttypes.models import ContentType
11 | from django.core.exceptions import PermissionDenied
12 |
13 | try:
14 | from django.utils.encoding import force_text
15 | except ImportError:
16 | from django.utils.encoding import force_unicode as force_text
17 |
18 | from authority.models import Permission
19 | from authority.widgets import GenericForeignKeyRawIdWidget
20 | from authority.utils import get_choices_for
21 |
22 |
23 | class PermissionInline(GenericTabularInline):
24 | model = Permission
25 | raw_id_fields = ("user", "group", "creator")
26 | extra = 1
27 |
28 | def formfield_for_dbfield(self, db_field, **kwargs):
29 | if db_field.name == "codename":
30 | perm_choices = get_choices_for(self.parent_model)
31 | kwargs["label"] = _("permission")
32 | kwargs["widget"] = forms.Select(choices=perm_choices)
33 | return super(PermissionInline, self).formfield_for_dbfield(db_field, **kwargs)
34 |
35 |
36 | class ActionPermissionInline(PermissionInline):
37 | raw_id_fields = ()
38 | template = "admin/edit_inline/action_tabular.html"
39 |
40 |
41 | class ActionErrorList(forms.utils.ErrorList):
42 | def __init__(self, inline_formsets):
43 | super(ActionErrorList, self).__init__()
44 | for inline_formset in inline_formsets:
45 | self.extend(inline_formset.non_form_errors())
46 | for errors_in_inline_form in inline_formset.errors:
47 | self.extend(errors_in_inline_form.values())
48 |
49 |
50 | def edit_permissions(modeladmin, request, queryset):
51 | opts = modeladmin.model._meta
52 | app_label = opts.app_label
53 |
54 | # Check that the user has the permission to edit permissions
55 | if not (
56 | request.user.is_superuser
57 | or request.user.has_perm("authority.change_permission")
58 | or request.user.has_perm("authority.change_foreign_permissions")
59 | ):
60 | raise PermissionDenied
61 |
62 | inline = ActionPermissionInline(queryset.model, modeladmin.admin_site)
63 | formsets = []
64 | for obj in queryset:
65 | prefixes = {}
66 | FormSet = inline.get_formset(request, obj)
67 | prefix = "%s-%s" % (FormSet.get_default_prefix(), obj.pk)
68 | prefixes[prefix] = prefixes.get(prefix, 0) + 1
69 | if prefixes[prefix] != 1:
70 | prefix = "%s-%s" % (prefix, prefixes[prefix])
71 | if request.POST.get("post"):
72 | formset = FormSet(
73 | data=request.POST, files=request.FILES, instance=obj, prefix=prefix
74 | )
75 | else:
76 | formset = FormSet(instance=obj, prefix=prefix)
77 | formsets.append(formset)
78 |
79 | media = modeladmin.media
80 | inline_admin_formsets = []
81 | for formset in formsets:
82 | fieldsets = list(inline.get_fieldsets(request))
83 | inline_admin_formset = helpers.InlineAdminFormSet(inline, formset, fieldsets)
84 | inline_admin_formsets.append(inline_admin_formset)
85 | media = media + inline_admin_formset.media
86 |
87 | if request.POST.get("post"):
88 | if all_valid(formsets):
89 | for formset in formsets:
90 | formset.save()
91 | else:
92 | modeladmin.message_user(
93 | request,
94 | "; ".join(
95 | err.as_text() for formset in formsets for err in formset.errors
96 | ),
97 | )
98 | # redirect to full request path to make sure we keep filter
99 | return HttpResponseRedirect(request.get_full_path())
100 |
101 | context = {
102 | "errors": ActionErrorList(formsets),
103 | "title": ugettext("Permissions for %s") % force_text(opts.verbose_name_plural),
104 | "inline_admin_formsets": inline_admin_formsets,
105 | "app_label": app_label,
106 | "change": True,
107 | "form_url": mark_safe(""),
108 | "opts": opts,
109 | "target_opts": queryset.model._meta,
110 | "content_type_id": ContentType.objects.get_for_model(queryset.model).id,
111 | "save_as": False,
112 | "save_on_top": False,
113 | "is_popup": False,
114 | "media": mark_safe(media),
115 | "show_delete": False,
116 | "action_checkbox_name": helpers.ACTION_CHECKBOX_NAME,
117 | "queryset": queryset,
118 | "object_name": force_text(opts.verbose_name),
119 | }
120 | template_name = getattr(
121 | modeladmin,
122 | "permission_change_form_template",
123 | [
124 | "admin/%s/%s/permission_change_form.html"
125 | % (app_label, opts.object_name.lower()),
126 | "admin/%s/permission_change_form.html" % app_label,
127 | "admin/permission_change_form.html",
128 | ],
129 | )
130 | return render(request, template_name, context)
131 |
132 |
133 | edit_permissions.short_description = _(
134 | "Edit permissions for selected %(verbose_name_plural)s"
135 | )
136 |
137 |
138 | class PermissionAdmin(admin.ModelAdmin):
139 | list_display = ("codename", "content_type", "user", "group", "approved")
140 | list_filter = ("approved", "content_type")
141 | search_fields = ("user__username", "group__name", "codename")
142 | raw_id_fields = ("user", "group", "creator")
143 | generic_fields = ("content_object",)
144 | actions = ["approve_permissions"]
145 | fieldsets = (
146 | (None, {"fields": ("codename", ("content_type", "object_id"))}),
147 | (_("Permitted"), {"fields": ("approved", "user", "group")}),
148 | (_("Creation"), {"fields": ("creator", "date_requested", "date_approved")}),
149 | )
150 |
151 | def formfield_for_dbfield(self, db_field, **kwargs):
152 | # For generic foreign keys marked as generic_fields we use a special widget
153 | names = [
154 | f.fk_field
155 | for f in self.model._meta.virtual_fields
156 | if f.name in self.generic_fields
157 | ]
158 | if db_field.name in names:
159 | for gfk in self.model._meta.virtual_fields:
160 | if gfk.fk_field == db_field.name:
161 | kwargs["widget"] = GenericForeignKeyRawIdWidget(
162 | gfk.ct_field, self.admin_site._registry.keys()
163 | )
164 | break
165 | return super(PermissionAdmin, self).formfield_for_dbfield(db_field, **kwargs)
166 |
167 | def queryset(self, request):
168 | user = request.user
169 | if user.is_superuser or user.has_perm("permissions.change_foreign_permissions"):
170 | return super(PermissionAdmin, self).queryset(request)
171 | return super(PermissionAdmin, self).queryset(request).filter(creator=user)
172 |
173 | def approve_permissions(self, request, queryset):
174 | for permission in queryset:
175 | permission.approve(request.user)
176 | message = ungettext(
177 | "%(count)d permission successfully approved.",
178 | "%(count)d permissions successfully approved.",
179 | len(queryset),
180 | )
181 | self.message_user(request, message % {"count": len(queryset)})
182 |
183 | approve_permissions.short_description = _("Approve selected permissions")
184 |
185 |
186 | admin.site.register(Permission, PermissionAdmin)
187 |
188 | if actions:
189 | admin.site.add_action(edit_permissions)
190 |
--------------------------------------------------------------------------------
/authority/permissions.py:
--------------------------------------------------------------------------------
1 | from django.conf import settings
2 | from django.contrib.auth.models import Permission as DjangoPermission
3 | from django.contrib.contenttypes.models import ContentType
4 | from django.db.models import Q
5 | from django.db.models.base import Model, ModelBase
6 | from django.template.defaultfilters import slugify
7 |
8 | from authority.exceptions import NotAModel, UnsavedModelInstance
9 | from authority.models import Permission
10 |
11 |
12 | class PermissionMetaclass(type):
13 | """
14 | Used to generate the default set of permission checks "add", "change" and
15 | "delete".
16 | """
17 |
18 | def __new__(cls, name, bases, attrs):
19 | new_class = super(PermissionMetaclass, cls).__new__(cls, name, bases, attrs)
20 | if not new_class.label:
21 | new_class.label = "%s_permission" % new_class.__name__.lower()
22 | new_class.label = slugify(new_class.label)
23 | if new_class.checks is None:
24 | new_class.checks = []
25 | # force check names to be lower case
26 | new_class.checks = [check.lower() for check in new_class.checks]
27 | return new_class
28 |
29 |
30 | class BasePermission(object):
31 | """
32 | Base Permission class to be used to define app permissions.
33 | """
34 |
35 | __metaclass__ = PermissionMetaclass
36 |
37 | checks = ()
38 | label = None
39 | generic_checks = ["add", "browse", "change", "delete"]
40 |
41 | def __init__(self, user=None, group=None, *args, **kwargs):
42 | self.user = user
43 | self.group = group
44 | super(BasePermission, self).__init__(*args, **kwargs)
45 |
46 | def _get_user_cached_perms(self):
47 | """
48 | Set up both the user and group caches.
49 | """
50 | if not self.user:
51 | return {}, {}
52 | group_pks = set(self.user.groups.values_list("pk", flat=True,))
53 | perms = Permission.objects.filter(
54 | Q(user__pk=self.user.pk) | Q(group__pk__in=group_pks),
55 | )
56 | user_permissions = {}
57 | group_permissions = {}
58 | for perm in perms:
59 | if perm.user_id == self.user.pk:
60 | user_permissions[
61 | (
62 | perm.object_id,
63 | perm.content_type_id,
64 | perm.codename,
65 | perm.approved,
66 | )
67 | ] = True
68 | # If the user has the permission do for something, but perm.user !=
69 | # self.user then by definition that permission came from the
70 | # group.
71 | else:
72 | group_permissions[
73 | (
74 | perm.object_id,
75 | perm.content_type_id,
76 | perm.codename,
77 | perm.approved,
78 | )
79 | ] = True
80 | return user_permissions, group_permissions
81 |
82 | def _get_group_cached_perms(self):
83 | """
84 | Set group cache.
85 | """
86 | if not self.group:
87 | return {}
88 | perms = Permission.objects.filter(group=self.group,)
89 | group_permissions = {}
90 | for perm in perms:
91 | group_permissions[
92 | (perm.object_id, perm.content_type_id, perm.codename, perm.approved,)
93 | ] = True
94 | return group_permissions
95 |
96 | def _prime_user_perm_caches(self):
97 | """
98 | Prime both the user and group caches and put them on the ``self.user``.
99 | In addition add a cache filled flag on ``self.user``.
100 | """
101 | perm_cache, group_perm_cache = self._get_user_cached_perms()
102 | self.user._authority_perm_cache = perm_cache
103 | self.user._authority_group_perm_cache = group_perm_cache
104 | self.user._authority_perm_cache_filled = True
105 |
106 | def _prime_group_perm_caches(self):
107 | """
108 | Prime the group cache and put them on the ``self.group``.
109 | In addition add a cache filled flag on ``self.group``.
110 | """
111 | perm_cache = self._get_group_cached_perms()
112 | self.group._authority_perm_cache = perm_cache
113 | self.group._authority_perm_cache_filled = True
114 |
115 | @property
116 | def _user_perm_cache(self):
117 | """
118 | cached_permissions will generate the cache in a lazy fashion.
119 | """
120 | # Check to see if the cache has been primed.
121 | if not self.user:
122 | return {}
123 | cache_filled = getattr(self.user, "_authority_perm_cache_filled", False,)
124 | if cache_filled:
125 | # Don't really like the name for this, but this matches how Django
126 | # does it.
127 | return self.user._authority_perm_cache
128 |
129 | # Prime the cache.
130 | self._prime_user_perm_caches()
131 | return self.user._authority_perm_cache
132 |
133 | @property
134 | def _group_perm_cache(self):
135 | """
136 | cached_permissions will generate the cache in a lazy fashion.
137 | """
138 | # Check to see if the cache has been primed.
139 | if not self.group:
140 | return {}
141 | cache_filled = getattr(self.group, "_authority_perm_cache_filled", False,)
142 | if cache_filled:
143 | # Don't really like the name for this, but this matches how Django
144 | # does it.
145 | return self.group._authority_perm_cache
146 |
147 | # Prime the cache.
148 | self._prime_group_perm_caches()
149 | return self.group._authority_perm_cache
150 |
151 | @property
152 | def _user_group_perm_cache(self):
153 | """
154 | cached_permissions will generate the cache in a lazy fashion.
155 | """
156 | # Check to see if the cache has been primed.
157 | if not self.user:
158 | return {}
159 | cache_filled = getattr(self.user, "_authority_perm_cache_filled", False,)
160 | if cache_filled:
161 | return self.user._authority_group_perm_cache
162 |
163 | # Prime the cache.
164 | self._prime_user_perm_caches()
165 | return self.user._authority_group_perm_cache
166 |
167 | def invalidate_permissions_cache(self):
168 | """
169 | In the event that the Permission table is changed during the use of a
170 | permission the Permission cache will need to be invalidated and
171 | regenerated. By calling this method the invalidation will occur, and
172 | the next time the cached_permissions is used the cache will be
173 | re-primed.
174 | """
175 | if self.user:
176 | self.user._authority_perm_cache_filled = False
177 | if self.group:
178 | self.group._authority_perm_cache_filled = False
179 |
180 | @property
181 | def use_smart_cache(self):
182 | use_smart_cache = getattr(settings, "AUTHORITY_USE_SMART_CACHE", True)
183 | return (self.user or self.group) and use_smart_cache
184 |
185 | def has_user_perms(self, perm, obj, approved, check_groups=True):
186 | if not self.user:
187 | return False
188 | if self.user.is_superuser:
189 | return True
190 | if not self.user.is_active:
191 | return False
192 |
193 | if self.use_smart_cache:
194 | content_type_pk = Permission.objects.get_content_type(obj).pk
195 |
196 | def _user_has_perms(cached_perms):
197 | # Check to see if the permission is in the cache.
198 | return cached_perms.get((obj.pk, content_type_pk, perm, approved,))
199 |
200 | # Check to see if the permission is in the cache.
201 | if _user_has_perms(self._user_perm_cache):
202 | return True
203 |
204 | # Optionally check group permissions
205 | if check_groups:
206 | return _user_has_perms(self._user_group_perm_cache)
207 | return False
208 |
209 | # Actually hit the DB, no smart cache used.
210 | return (
211 | Permission.objects.user_permissions(
212 | self.user, perm, obj, approved, check_groups,
213 | )
214 | .filter(object_id=obj.pk,)
215 | .exists()
216 | )
217 |
218 | def has_group_perms(self, perm, obj, approved):
219 | """
220 | Check if group has the permission for the given object
221 | """
222 | if not self.group:
223 | return False
224 |
225 | if self.use_smart_cache:
226 | content_type_pk = Permission.objects.get_content_type(obj).pk
227 |
228 | def _group_has_perms(cached_perms):
229 | # Check to see if the permission is in the cache.
230 | return cached_perms.get((obj.pk, content_type_pk, perm, approved,))
231 |
232 | # Check to see if the permission is in the cache.
233 | return _group_has_perms(self._group_perm_cache)
234 |
235 | # Actually hit the DB, no smart cache used.
236 | return (
237 | Permission.objects.group_permissions(self.group, perm, obj, approved,)
238 | .filter(object_id=obj.pk,)
239 | .exists()
240 | )
241 |
242 | def has_perm(self, perm, obj, check_groups=True, approved=True):
243 | """
244 | Check if user has the permission for the given object
245 | """
246 | if self.user:
247 | if self.has_user_perms(perm, obj, approved, check_groups):
248 | return True
249 | if self.group:
250 | return self.has_group_perms(perm, obj, approved)
251 | return False
252 |
253 | def requested_perm(self, perm, obj, check_groups=True):
254 | """
255 | Check if user requested a permission for the given object
256 | """
257 | return self.has_perm(perm, obj, check_groups, False)
258 |
259 | def can(self, check, generic=False, *args, **kwargs):
260 | if not args:
261 | args = [self.model]
262 | perms = False
263 | for obj in args:
264 | # skip this obj if it's not a model class or instance
265 | if not isinstance(obj, (ModelBase, Model)):
266 | continue
267 | # first check Django's permission system
268 | if self.user:
269 | perm = self.get_django_codename(check, obj, generic)
270 | perms = perms or self.user.has_perm(perm)
271 | perm = self.get_codename(check, obj, generic)
272 | # then check authority's per object permissions
273 | if not isinstance(obj, ModelBase) and isinstance(obj, self.model):
274 | # only check the authority if obj is not a model class
275 | perms = perms or self.has_perm(perm, obj)
276 | return perms
277 |
278 | def get_django_codename(
279 | self, check, model_or_instance, generic=False, without_left=False
280 | ):
281 | if without_left:
282 | perm = check
283 | else:
284 | perm = "%s.%s" % (model_or_instance._meta.app_label, check.lower())
285 | if generic:
286 | perm = "%s_%s" % (perm, model_or_instance._meta.object_name.lower(),)
287 | return perm
288 |
289 | def get_codename(self, check, model_or_instance, generic=False):
290 | perm = "%s.%s" % (self.label, check.lower())
291 | if generic:
292 | perm = "%s_%s" % (perm, model_or_instance._meta.object_name.lower(),)
293 | return perm
294 |
295 | def assign(self, check=None, content_object=None, generic=False):
296 | """
297 | Assign a permission to a user.
298 |
299 | To assign permission for all checks: let check=None.
300 | To assign permission for all objects: let content_object=None.
301 |
302 | If generic is True then "check" will be suffixed with _modelname.
303 | """
304 | result = []
305 |
306 | if not content_object:
307 | content_objects = (self.model,)
308 | elif not isinstance(content_object, (list, tuple)):
309 | content_objects = (content_object,)
310 | else:
311 | content_objects = content_object
312 |
313 | if not check:
314 | checks = self.generic_checks + getattr(self, "checks", [])
315 | elif not isinstance(check, (list, tuple)):
316 | checks = (check,)
317 | else:
318 | checks = check
319 |
320 | for content_object in content_objects:
321 | # raise an exception before adding any permission
322 | # i think Django does not rollback by default
323 | if not isinstance(content_object, (Model, ModelBase)):
324 | raise NotAModel(content_object)
325 | elif isinstance(content_object, Model) and not content_object.pk:
326 | raise UnsavedModelInstance(content_object)
327 |
328 | content_type = ContentType.objects.get_for_model(content_object)
329 |
330 | for check in checks:
331 | if isinstance(content_object, Model):
332 | # make an authority per object permission
333 | codename = self.get_codename(check, content_object, generic,)
334 | try:
335 | perm = Permission.objects.get(
336 | user=self.user,
337 | group=self.group,
338 | codename=codename,
339 | approved=True,
340 | content_type=content_type,
341 | object_id=content_object.pk,
342 | )
343 | except Permission.DoesNotExist:
344 | perm = Permission.objects.create(
345 | user=self.user,
346 | group=self.group,
347 | content_object=content_object,
348 | codename=codename,
349 | approved=True,
350 | )
351 |
352 | result.append(perm)
353 |
354 | elif isinstance(content_object, ModelBase):
355 | # make a Django permission
356 | codename = self.get_django_codename(
357 | check, content_object, generic, without_left=True,
358 | )
359 | try:
360 | perm = DjangoPermission.objects.get(codename=codename)
361 | except DjangoPermission.DoesNotExist:
362 | name = check
363 | if "_" in name:
364 | name = name[0 : name.find("_")]
365 | perm = DjangoPermission(
366 | name=name, codename=codename, content_type=content_type,
367 | )
368 | perm.save()
369 | self.user.user_permissions.add(perm)
370 | result.append(perm)
371 |
372 | return result
373 |
--------------------------------------------------------------------------------
/authority/tests.py:
--------------------------------------------------------------------------------
1 | from django.conf import settings
2 | from django.contrib.auth import get_user_model
3 | from django.contrib.auth.models import Group, Permission as DjangoPermission
4 | from django.contrib.contenttypes.models import ContentType
5 | from django.core.exceptions import MultipleObjectsReturned
6 | from django.db.models import Q
7 | from django.test import TestCase
8 | from django.urls import reverse
9 |
10 | import authority
11 | from authority import permissions
12 | from authority.models import Permission
13 | from authority.exceptions import NotAModel, UnsavedModelInstance
14 |
15 | # Load the form
16 | from authority.forms import UserPermissionForm # noqa
17 |
18 |
19 | User = get_user_model()
20 | FIXTURES = ["tests_custom.json"]
21 | QUERY = Q(email="jezdez@github.com")
22 |
23 |
24 | class UserPermission(permissions.BasePermission):
25 | checks = ("browse",)
26 | label = "user_permission"
27 |
28 |
29 | authority.utils.register(User, UserPermission)
30 |
31 |
32 | class GroupPermission(permissions.BasePermission):
33 | checks = ("browse",)
34 | label = "group_permission"
35 |
36 |
37 | authority.utils.register(Group, GroupPermission)
38 |
39 |
40 | class DjangoPermissionChecksTestCase(TestCase):
41 | """
42 | Django permission objects have certain methods that are always present,
43 | test those here.
44 |
45 | self.user will be given:
46 | - django permission add_user (test_add)
47 | - authority to delete_user which is him (test_delete)
48 |
49 | This permissions are given in the test case and not in the fixture, for
50 | later reference.
51 | """
52 |
53 | fixtures = FIXTURES
54 |
55 | def setUp(self):
56 | self.user = User.objects.get(QUERY)
57 | self.check = UserPermission(self.user)
58 |
59 | def test_no_permission(self):
60 | self.assertFalse(self.check.add_user())
61 | self.assertFalse(self.check.delete_user())
62 | self.assertFalse(self.check.delete_user(self.user))
63 |
64 | def test_add(self):
65 | # setup
66 | perm = DjangoPermission.objects.get(codename="add_user")
67 | self.user.user_permissions.add(perm)
68 |
69 | # test
70 | self.assertTrue(self.check.add_user())
71 |
72 | def test_delete(self):
73 | perm = Permission(
74 | user=self.user,
75 | content_object=self.user,
76 | codename="user_permission.delete_user",
77 | approved=True,
78 | )
79 | perm.save()
80 |
81 | # test
82 | self.assertFalse(self.check.delete_user())
83 | self.assertTrue(self.check.delete_user(self.user))
84 |
85 |
86 | class AssignBehaviourTest(TestCase):
87 | """
88 | self.user will be given:
89 | - permission add_user (test_add),
90 | - permission delete_user for him (test_delete),
91 | - all existing codenames permissions: a/b/c/d (test_all),
92 | """
93 |
94 | fixtures = FIXTURES
95 |
96 | def setUp(self):
97 | self.user = User.objects.get(QUERY)
98 | self.group1, _ = Group.objects.get_or_create(name="Test Group 1")
99 | self.group2, _ = Group.objects.get_or_create(name="Test Group 2")
100 | self.group3, _ = Group.objects.get_or_create(name="Test Group 2")
101 | self.check = UserPermission(self.user)
102 |
103 | def test_add(self):
104 | result = self.check.assign(check="add_user")
105 |
106 | self.assertTrue(isinstance(result[0], DjangoPermission))
107 | self.assertTrue(self.check.add_user())
108 |
109 | def test_assign_to_group(self):
110 | result = UserPermission(group=self.group1).assign(
111 | check="delete_user", content_object=self.user
112 | )
113 |
114 | self.assertIsInstance(result, list)
115 | self.assertIsInstance(result[0], Permission)
116 | self.assertTrue(UserPermission(group=self.group1).delete_user(self.user))
117 |
118 | def test_assign_to_group_does_not_overwrite_other_group_permission(self):
119 | UserPermission(group=self.group1).assign(
120 | check="delete_user", content_object=self.user
121 | )
122 | UserPermission(group=self.group2).assign(
123 | check="delete_user", content_object=self.user
124 | )
125 | self.assertTrue(UserPermission(group=self.group2).delete_user(self.user))
126 | self.assertTrue(UserPermission(group=self.group1).delete_user(self.user))
127 |
128 | def test_assign_to_group_does_not_fail_when_two_group_perms_exist(self):
129 | for group in self.group1, self.group2:
130 | perm = Permission(
131 | group=group,
132 | content_object=self.user,
133 | codename="user_permission.delete_user",
134 | approved=True,
135 | )
136 | perm.save()
137 |
138 | try:
139 | UserPermission(group=self.group3).assign(
140 | check="delete_user", content_object=self.user
141 | )
142 | except MultipleObjectsReturned:
143 | self.fail("assign() should not have raised this exception")
144 |
145 | def test_delete(self):
146 | result = self.check.assign(content_object=self.user, check="delete_user",)
147 |
148 | self.assertTrue(isinstance(result[0], Permission))
149 | self.assertFalse(self.check.delete_user())
150 | self.assertTrue(self.check.delete_user(self.user))
151 |
152 | def test_all(self):
153 | result = self.check.assign(content_object=self.user)
154 | self.assertTrue(isinstance(result, list))
155 | self.assertTrue(self.check.browse_user(self.user))
156 | self.assertTrue(self.check.delete_user(self.user))
157 | self.assertTrue(self.check.add_user(self.user))
158 | self.assertTrue(self.check.change_user(self.user))
159 |
160 |
161 | class GenericAssignBehaviourTest(TestCase):
162 | """
163 | self.user will be given:
164 | - permission add (test_add),
165 | - permission delete for him (test_delete),
166 | """
167 |
168 | fixtures = FIXTURES
169 |
170 | def setUp(self):
171 | self.user = User.objects.get(QUERY)
172 | self.check = UserPermission(self.user)
173 |
174 | def test_add(self):
175 | result = self.check.assign(check="add", generic=True)
176 |
177 | self.assertTrue(isinstance(result[0], DjangoPermission))
178 | self.assertTrue(self.check.add_user())
179 |
180 | def test_delete(self):
181 | result = self.check.assign(
182 | content_object=self.user, check="delete", generic=True,
183 | )
184 |
185 | self.assertTrue(isinstance(result[0], Permission))
186 | self.assertFalse(self.check.delete_user())
187 | self.assertTrue(self.check.delete_user(self.user))
188 |
189 |
190 | class AssignExceptionsTest(TestCase):
191 | """
192 | Tests that exceptions are thrown if assign() was called with inconsistent
193 | arguments.
194 | """
195 |
196 | fixtures = FIXTURES
197 |
198 | def setUp(self):
199 | self.user = User.objects.get(QUERY)
200 | self.check = UserPermission(self.user)
201 |
202 | def test_unsaved_model(self):
203 | try:
204 | self.check.assign(content_object=User())
205 | except UnsavedModelInstance:
206 | return True
207 | self.fail()
208 |
209 | def test_not_model_content_object(self):
210 | try:
211 | self.check.assign(content_object="fail")
212 | except NotAModel:
213 | return True
214 | self.fail()
215 |
216 |
217 | class SmartCachingTestCase(TestCase):
218 | """
219 | The base test case for all tests that have to do with smart caching.
220 | """
221 |
222 | fixtures = FIXTURES
223 |
224 | def setUp(self):
225 | # Create a user.
226 | self.user = User.objects.get(QUERY)
227 |
228 | # Create a group.
229 | self.group = Group.objects.create()
230 | self.group.user_set.add(self.user)
231 |
232 | # Make the checks
233 | self.user_check = UserPermission(user=self.user)
234 | self.group_check = GroupPermission(group=self.group)
235 |
236 | # Ensure we are using the smart cache.
237 | settings.AUTHORITY_USE_SMART_CACHE = True
238 |
239 | def tearDown(self):
240 | ContentType.objects.clear_cache()
241 |
242 | def _old_user_permission_check(self):
243 | # This is what the old, pre-cache system would check to see if a user
244 | # had a given permission.
245 | return Permission.objects.user_permissions(
246 | self.user, "foo", self.user, approved=True, check_groups=True,
247 | )
248 |
249 | def _old_group_permission_check(self):
250 | # This is what the old, pre-cache system would check to see if a user
251 | # had a given permission.
252 | return Permission.objects.group_permissions(
253 | self.group, "foo", self.group, approved=True,
254 | )
255 |
256 |
257 | class PerformanceTest(SmartCachingTestCase):
258 | """
259 | Tests that permission are actually cached and that the number of queries
260 | stays constant.
261 | """
262 |
263 | def test_has_user_perms(self):
264 | # Show that when calling has_user_perms multiple times no additional
265 | # queries are done.
266 |
267 | # Make sure the has_user_perms check does not get short-circuited.
268 | assert not self.user.is_superuser
269 | assert self.user.is_active
270 |
271 | # Regardless of how many times has_user_perms is called, the number of
272 | # queries is the same.
273 | # Content type and permissions (2 queries)
274 | with self.assertNumQueries(3):
275 | for _ in range(5):
276 | # Need to assert it so the query actually gets executed.
277 | assert not self.user_check.has_user_perms(
278 | "foo", self.user, True, False,
279 | )
280 |
281 | def test_group_has_perms(self):
282 | with self.assertNumQueries(2):
283 | for _ in range(5):
284 | assert not self.group_check.has_group_perms("foo", self.group, True,)
285 |
286 | def test_has_user_perms_check_group(self):
287 | # Regardless of the number groups permissions, it should only take one
288 | # query to check both users and groups.
289 | # Content type and permissions (2 queries)
290 | with self.assertNumQueries(3):
291 | self.user_check.has_user_perms(
292 | "foo", self.user, approved=True, check_groups=True,
293 | )
294 |
295 | def test_invalidate_user_permissions_cache(self):
296 | # Show that calling invalidate_permissions_cache will cause extra
297 | # queries.
298 | # For each time invalidate_permissions_cache gets called, you
299 | # will need to do one query to get content type and one to get
300 | # the permissions.
301 | with self.assertNumQueries(6):
302 | for _ in range(5):
303 | assert not self.user_check.has_user_perms(
304 | "foo", self.user, True, False,
305 | )
306 |
307 | # Invalidate the cache to show that a query will be generated when
308 | # checking perms again.
309 | self.user_check.invalidate_permissions_cache()
310 | ContentType.objects.clear_cache()
311 |
312 | # One query to re generate the cache.
313 | for _ in range(5):
314 | assert not self.user_check.has_user_perms(
315 | "foo", self.user, True, False,
316 | )
317 |
318 | def test_invalidate_group_permissions_cache(self):
319 | # Show that calling invalidate_permissions_cache will cause extra
320 | # queries.
321 | # For each time invalidate_permissions_cache gets called, you
322 | # will need to do one query to get content type and one to get
323 | with self.assertNumQueries(4):
324 | for _ in range(5):
325 | assert not self.group_check.has_group_perms("foo", self.group, True,)
326 |
327 | # Invalidate the cache to show that a query will be generated when
328 | # checking perms again.
329 | self.group_check.invalidate_permissions_cache()
330 | ContentType.objects.clear_cache()
331 |
332 | # One query to re generate the cache.
333 | for _ in range(5):
334 | assert not self.group_check.has_group_perms("foo", self.group, True,)
335 |
336 | def test_has_user_perms_check_group_multiple(self):
337 | # Create a permission with just a group.
338 | Permission.objects.create(
339 | content_type=Permission.objects.get_content_type(User),
340 | object_id=self.user.pk,
341 | codename="foo",
342 | group=self.group,
343 | approved=True,
344 | )
345 | # By creating the Permission objects the Content type cache
346 | # gets created.
347 |
348 | # Check the number of queries.
349 | with self.assertNumQueries(2):
350 | assert self.user_check.has_user_perms("foo", self.user, True, True)
351 |
352 | # Create a second group.
353 | new_group = Group.objects.create(name="new_group")
354 | new_group.user_set.add(self.user)
355 |
356 | # Create a permission object for it.
357 | Permission.objects.create(
358 | content_type=Permission.objects.get_content_type(User),
359 | object_id=self.user.pk,
360 | codename="foo",
361 | group=new_group,
362 | approved=True,
363 | )
364 |
365 | self.user_check.invalidate_permissions_cache()
366 |
367 | # Make sure it is the same number of queries.
368 | with self.assertNumQueries(2):
369 | assert self.user_check.has_user_perms("foo", self.user, True, True)
370 |
371 |
372 | class GroupPermissionCacheTestCase(SmartCachingTestCase):
373 | """
374 | Tests that peg expected behaviour
375 | """
376 |
377 | def test_has_user_perms_with_groups(self):
378 | perms = self._old_user_permission_check()
379 | self.assertEqual([], list(perms))
380 |
381 | # Use the new cached user perms to show that the user does not have the
382 | # perms.
383 | can_foo_with_group = self.user_check.has_user_perms(
384 | "foo", self.user, approved=True, check_groups=True,
385 | )
386 | self.assertFalse(can_foo_with_group)
387 |
388 | # Create a permission with just that group.
389 | perm = Permission.objects.create(
390 | content_type=Permission.objects.get_content_type(User),
391 | object_id=self.user.pk,
392 | codename="foo",
393 | group=self.group,
394 | approved=True,
395 | )
396 |
397 | # Old permission check
398 | perms = self._old_user_permission_check()
399 | self.assertEqual([perm], list(perms))
400 |
401 | # Invalidate the cache.
402 | self.user_check.invalidate_permissions_cache()
403 | can_foo_with_group = self.user_check.has_user_perms(
404 | "foo", self.user, approved=True, check_groups=True,
405 | )
406 | self.assertTrue(can_foo_with_group)
407 |
408 | def test_has_group_perms_no_user(self):
409 | # Make sure calling has_user_perms on a permission that does not have a
410 | # user does not throw any errors.
411 | can_foo_with_group = self.group_check.has_group_perms(
412 | "foo", self.user, approved=True,
413 | )
414 | self.assertFalse(can_foo_with_group)
415 |
416 | perms = self._old_group_permission_check()
417 | self.assertEqual([], list(perms))
418 |
419 | # Create a permission with just that group.
420 | perm = Permission.objects.create(
421 | content_type=Permission.objects.get_content_type(Group),
422 | object_id=self.group.pk,
423 | codename="foo",
424 | group=self.group,
425 | approved=True,
426 | )
427 |
428 | # Old permission check
429 | perms = self._old_group_permission_check()
430 | self.assertEqual([perm], list(perms))
431 |
432 | # Invalidate the cache.
433 | self.group_check.invalidate_permissions_cache()
434 |
435 | can_foo_with_group = self.group_check.has_group_perms(
436 | "foo", self.group, approved=True,
437 | )
438 | self.assertTrue(can_foo_with_group)
439 |
440 |
441 | class AddPermissionTestCase(TestCase):
442 | def test_add_permission_permission_denied_is_403(self):
443 | user = get_user_model().objects.create(username="foo", email="foo@example.com",)
444 | user.set_password("pw")
445 | user.save()
446 |
447 | assert self.client.login(username="foo@example.com", password="pw")
448 | url = reverse(
449 | "authority-add-permission-request",
450 | kwargs={"app_label": "foo", "module_name": "Bar", "pk": 1,},
451 | )
452 | r = self.client.get(url)
453 | self.assertEqual(r.status_code, 403)
454 |
--------------------------------------------------------------------------------
/authority/templatetags/permissions.py:
--------------------------------------------------------------------------------
1 | from django import template
2 | from django.contrib.auth import get_user_model
3 | from django.core.exceptions import ImproperlyConfigured
4 | from django.urls import reverse
5 | from django.contrib.auth.models import AnonymousUser
6 |
7 | from authority.utils import get_check
8 | from authority import permissions
9 | from authority.models import Permission
10 | from authority.forms import UserPermissionForm
11 |
12 |
13 | User = get_user_model()
14 | register = template.Library()
15 |
16 |
17 | @register.simple_tag
18 | def url_for_obj(view_name, obj):
19 | return reverse(
20 | view_name,
21 | kwargs={
22 | "app_label": obj._meta.app_label,
23 | "module_name": obj._meta.module_name,
24 | "pk": obj.pk,
25 | },
26 | )
27 |
28 |
29 | @register.simple_tag
30 | def add_url_for_obj(obj):
31 | return url_for_obj("authority-add-permission", obj)
32 |
33 |
34 | @register.simple_tag
35 | def request_url_for_obj(obj):
36 | return url_for_obj("authority-add-permission-request", obj)
37 |
38 |
39 | class ResolverNode(template.Node):
40 | """
41 | A small wrapper that adds a convenient resolve method.
42 | """
43 |
44 | def resolve(self, var, context):
45 | """Resolves a variable out of context if it's not in quotes"""
46 | if var is None:
47 | return var
48 | if var[0] in ('"', "'") and var[-1] == var[0]:
49 | return var[1:-1]
50 | else:
51 | return template.Variable(var).resolve(context)
52 |
53 | @classmethod
54 | def next_bit_for(cls, bits, key, if_none=None):
55 | try:
56 | return bits[bits.index(key) + 1]
57 | except ValueError:
58 | return if_none
59 |
60 |
61 | class PermissionComparisonNode(ResolverNode):
62 | """
63 | Implements a node to provide an "if user/group has permission on object"
64 | """
65 |
66 | @classmethod
67 | def handle_token(cls, parser, token):
68 | bits = token.contents.split()
69 | if 5 < len(bits) < 3:
70 | raise template.TemplateSyntaxError(
71 | "'%s' tag takes three, " "four or five arguments" % bits[0]
72 | )
73 | end_tag = "endifhasperm"
74 | nodelist_true = parser.parse(("else", end_tag))
75 | token = parser.next_token()
76 | if token.contents == "else": # there is an 'else' clause in the tag
77 | nodelist_false = parser.parse((end_tag,))
78 | parser.delete_first_token()
79 | else:
80 | nodelist_false = template.NodeList()
81 | if len(bits) == 3: # this tag requires at most 2 objects . None is given
82 | objs = (None, None)
83 | elif len(bits) == 4: # one is given
84 | objs = (bits[3], None)
85 | else: # two are given
86 | objs = (bits[3], bits[4])
87 | return cls(bits[2], bits[1], nodelist_true, nodelist_false, *objs)
88 |
89 | def __init__(self, user, perm, nodelist_true, nodelist_false, *objs):
90 | self.user = user
91 | self.objs = objs
92 | self.perm = perm
93 | self.nodelist_true = nodelist_true
94 | self.nodelist_false = nodelist_false
95 |
96 | def render(self, context):
97 | try:
98 | user = self.resolve(self.user, context)
99 | perm = self.resolve(self.perm, context)
100 | if self.objs:
101 | objs = []
102 | for obj in self.objs:
103 | if obj is not None:
104 | objs.append(self.resolve(obj, context))
105 | else:
106 | objs = None
107 | check = get_check(user, perm)
108 | if check is not None:
109 | if check(*objs):
110 | # return True if check was successful
111 | return self.nodelist_true.render(context)
112 | # If the app couldn't be found
113 | except (ImproperlyConfigured, ImportError):
114 | return ""
115 | # If either variable fails to resolve, return nothing.
116 | except template.VariableDoesNotExist:
117 | return ""
118 | # If the types don't permit comparison, return nothing.
119 | except (TypeError, AttributeError):
120 | return ""
121 | return self.nodelist_false.render(context)
122 |
123 |
124 | @register.tag
125 | def ifhasperm(parser, token):
126 | """
127 | This function provides functionality for the 'ifhasperm' template tag
128 |
129 | Syntax::
130 |
131 | {% ifhasperm PERMISSION_LABEL.CHECK_NAME USER *OBJS %}
132 | lalala
133 | {% else %}
134 | meh
135 | {% endifhasperm %}
136 |
137 | {% ifhasperm "poll_permission.change_poll" request.user %}
138 | lalala
139 | {% else %}
140 | meh
141 | {% endifhasperm %}
142 |
143 | """
144 | return PermissionComparisonNode.handle_token(parser, token)
145 |
146 |
147 | class PermissionFormNode(ResolverNode):
148 | @classmethod
149 | def handle_token(cls, parser, token, approved):
150 | bits = token.contents.split()
151 | kwargs = {
152 | "obj": cls.next_bit_for(bits, "for"),
153 | "perm": cls.next_bit_for(bits, "using", None),
154 | "template_name": cls.next_bit_for(bits, "with", ""),
155 | "approved": approved,
156 | }
157 | return cls(**kwargs)
158 |
159 | def __init__(self, obj, perm=None, approved=False, template_name=None):
160 | self.obj = obj
161 | self.perm = perm
162 | self.approved = approved
163 | self.template_name = template_name
164 |
165 | def render(self, context):
166 | obj = self.resolve(self.obj, context)
167 | perm = self.resolve(self.perm, context)
168 | if self.template_name:
169 | template_name = [
170 | self.resolve(o, context) for o in self.template_name.split(",")
171 | ]
172 | else:
173 | template_name = "authority/permission_form.html"
174 | request = context["request"]
175 | extra_context = {}
176 | if self.approved:
177 | if request.user.is_authenticated and request.user.has_perm(
178 | "authority.add_permission"
179 | ):
180 | extra_context = {
181 | "form_url": url_for_obj("authority-add-permission", obj),
182 | "next": request.build_absolute_uri(),
183 | "approved": self.approved,
184 | "form": UserPermissionForm(
185 | perm, obj, approved=self.approved, initial=dict(codename=perm)
186 | ),
187 | }
188 | else:
189 | if request.user.is_authenticated and not request.user.is_superuser:
190 | extra_context = {
191 | "form_url": url_for_obj("authority-add-permission-request", obj),
192 | "next": request.build_absolute_uri(),
193 | "approved": self.approved,
194 | "form": UserPermissionForm(
195 | perm,
196 | obj,
197 | approved=self.approved,
198 | initial=dict(codename=perm, user=request.user.username),
199 | ),
200 | }
201 | return template.loader.render_to_string(template_name, extra_context, request)
202 |
203 |
204 | @register.tag
205 | def permission_form(parser, token):
206 | """
207 | Renders an "add permissions" form for the given object. If no object
208 | is given it will render a select box to choose from.
209 |
210 | Syntax::
211 |
212 | {% permission_form for OBJ using PERMISSION_LABEL.CHECK_NAME [with TEMPLATE] %}
213 | {% permission_form for lesson using "lesson_permission.add_lesson" %}
214 |
215 | """
216 | return PermissionFormNode.handle_token(parser, token, approved=True)
217 |
218 |
219 | @register.tag
220 | def permission_request_form(parser, token):
221 | """
222 | Renders an "add permissions" form for the given object. If no object
223 | is given it will render a select box to choose from.
224 |
225 | Syntax::
226 |
227 | {% permission_request_form for OBJ and PERMISSION_LABEL.CHECK_NAME [with TEMPLATE] %}
228 | {% permission_request_form for lesson using "lesson_permission.add_lesson"
229 | with "authority/permission_request_form.html" %}
230 |
231 | """
232 | return PermissionFormNode.handle_token(parser, token, approved=False)
233 |
234 |
235 | class PermissionsForObjectNode(ResolverNode):
236 | @classmethod
237 | def handle_token(cls, parser, token, approved, name):
238 | bits = token.contents.split()
239 | tag_name = bits[0]
240 | kwargs = {
241 | "obj": cls.next_bit_for(bits, tag_name),
242 | "user": cls.next_bit_for(bits, "for"),
243 | "var_name": cls.next_bit_for(bits, "as", name),
244 | "approved": approved,
245 | }
246 | return cls(**kwargs)
247 |
248 | def __init__(self, obj, user, var_name, approved, perm=None):
249 | self.obj = obj
250 | self.user = user
251 | self.perm = perm
252 | self.var_name = var_name
253 | self.approved = approved
254 |
255 | def render(self, context):
256 | obj = self.resolve(self.obj, context)
257 | var_name = self.resolve(self.var_name, context)
258 | user = self.resolve(self.user, context)
259 | perms = []
260 | if not isinstance(user, AnonymousUser):
261 | perms = Permission.objects.for_object(obj, self.approved)
262 | if isinstance(user, User):
263 | perms = perms.filter(user=user)
264 | context[var_name] = perms
265 | return ""
266 |
267 |
268 | @register.tag
269 | def get_permissions(parser, token):
270 | """
271 | Retrieves all permissions associated with the given obj and user
272 | and assigns the result to a context variable.
273 |
274 | Syntax::
275 |
276 | {% get_permissions obj %}
277 | {% for perm in permissions %}
278 | {{ perm }}
279 | {% endfor %}
280 |
281 | {% get_permissions obj as "my_permissions" %}
282 | {% get_permissions obj for request.user as "my_permissions" %}
283 |
284 | """
285 | return PermissionsForObjectNode.handle_token(
286 | parser, token, approved=True, name='"permissions"'
287 | )
288 |
289 |
290 | @register.tag
291 | def get_permission_requests(parser, token):
292 | """
293 | Retrieves all permissions requests associated with the given obj and user
294 | and assigns the result to a context variable.
295 |
296 | Syntax::
297 |
298 | {% get_permission_requests obj %}
299 | {% for perm in permissions %}
300 | {{ perm }}
301 | {% endfor %}
302 |
303 | {% get_permission_requests obj as "my_permissions" %}
304 | {% get_permission_requests obj for request.user as "my_permissions" %}
305 |
306 | """
307 | return PermissionsForObjectNode.handle_token(
308 | parser, token, approved=False, name='"permission_requests"'
309 | )
310 |
311 |
312 | class PermissionForObjectNode(ResolverNode):
313 | @classmethod
314 | def handle_token(cls, parser, token, approved, name):
315 | bits = token.contents.split()
316 | tag_name = bits[0]
317 | kwargs = {
318 | "perm": cls.next_bit_for(bits, tag_name),
319 | "user": cls.next_bit_for(bits, "for"),
320 | "objs": cls.next_bit_for(bits, "and"),
321 | "var_name": cls.next_bit_for(bits, "as", name),
322 | "approved": approved,
323 | }
324 | return cls(**kwargs)
325 |
326 | def __init__(self, perm, user, objs, approved, var_name):
327 | self.perm = perm
328 | self.user = user
329 | self.objs = objs
330 | self.var_name = var_name
331 | self.approved = approved
332 |
333 | def render(self, context):
334 | objs = [self.resolve(obj, context) for obj in self.objs.split(",")]
335 | var_name = self.resolve(self.var_name, context)
336 | perm = self.resolve(self.perm, context)
337 | user = self.resolve(self.user, context)
338 | granted = False
339 | if not isinstance(user, AnonymousUser):
340 | if self.approved:
341 | check = get_check(user, perm)
342 | if check is not None:
343 | granted = check(*objs)
344 | else:
345 | check = permissions.BasePermission(user=user)
346 | for obj in objs:
347 | granted = check.requested_perm(perm, obj)
348 | if granted:
349 | break
350 | context[var_name] = granted
351 | return ""
352 |
353 |
354 | @register.tag
355 | def get_permission(parser, token):
356 | """
357 | Performs a permission check with the given signature, user and objects
358 | and assigns the result to a context variable.
359 |
360 | Syntax::
361 |
362 | {% get_permission PERMISSION_LABEL.CHECK_NAME for USER and *OBJS [as VARNAME] %}
363 |
364 | {% get_permission "poll_permission.change_poll"
365 | for request.user and poll as "is_allowed" %}
366 | {% get_permission "poll_permission.change_poll"
367 | for request.user and poll,second_poll as "is_allowed" %}
368 |
369 | {% if is_allowed %}
370 | I've got ze power to change ze pollllllzzz. Muahahaa.
371 | {% else %}
372 | Meh. No power for meeeee.
373 | {% endif %}
374 |
375 | """
376 | return PermissionForObjectNode.handle_token(
377 | parser, token, approved=True, name='"permission"'
378 | )
379 |
380 |
381 | @register.tag
382 | def get_permission_request(parser, token):
383 | """
384 | Performs a permission request check with the given signature, user and objects
385 | and assigns the result to a context variable.
386 |
387 | Syntax::
388 |
389 | {% get_permission_request PERMISSION_LABEL.CHECK_NAME for USER and *OBJS [as VARNAME] %}
390 |
391 | {% get_permission_request "poll_permission.change_poll"
392 | for request.user and poll as "asked_for_permissio" %}
393 | {% get_permission_request "poll_permission.change_poll"
394 | for request.user and poll,second_poll as "asked_for_permissio" %}
395 |
396 | {% if asked_for_permissio %}
397 | Dude, you already asked for permission!
398 | {% else %}
399 | Oh, please fill out this 20 page form and sign here.
400 | {% endif %}
401 |
402 | """
403 | return PermissionForObjectNode.handle_token(
404 | parser, token, approved=False, name='"permission_request"'
405 | )
406 |
407 |
408 | def base_link(context, perm, view_name):
409 | return {
410 | "next": context["request"].build_absolute_uri(),
411 | "url": reverse(view_name, kwargs={"permission_pk": perm.pk}),
412 | }
413 |
414 |
415 | @register.inclusion_tag("authority/permission_delete_link.html", takes_context=True)
416 | def permission_delete_link(context, perm):
417 | """
418 | Renders a html link to the delete view of the given permission. Returns
419 | no content if the request-user has no permission to delete foreign
420 | permissions.
421 | """
422 | user = context["request"].user
423 | if user.is_authenticated:
424 | if (
425 | user.has_perm("authority.delete_foreign_permissions")
426 | or user.pk == perm.creator.pk
427 | ):
428 | return base_link(context, perm, "authority-delete-permission")
429 | return {"url": None}
430 |
431 |
432 | @register.inclusion_tag(
433 | "authority/permission_request_delete_link.html", takes_context=True
434 | )
435 | def permission_request_delete_link(context, perm):
436 | """
437 | Renders a html link to the delete view of the given permission request.
438 | Returns no content if the request-user has no permission to delete foreign
439 | permissions.
440 | """
441 | user = context["request"].user
442 | if user.is_authenticated:
443 | link_kwargs = base_link(context, perm, "authority-delete-permission-request")
444 | if user.has_perm("authority.delete_permission"):
445 | link_kwargs["is_requestor"] = False
446 | return link_kwargs
447 | if not perm.approved and perm.user == user:
448 | link_kwargs["is_requestor"] = True
449 | return link_kwargs
450 | return {"url": None}
451 |
452 |
453 | @register.inclusion_tag(
454 | "authority/permission_request_approve_link.html", takes_context=True
455 | )
456 | def permission_request_approve_link(context, perm):
457 | """
458 | Renders a html link to the approve view of the given permission request.
459 | Returns no content if the request-user has no permission to delete foreign
460 | permissions.
461 | """
462 | user = context["request"].user
463 | if user.is_authenticated:
464 | if user.has_perm("authority.approve_permission_requests"):
465 | return base_link(context, perm, "authority-approve-permission-request")
466 | return {"url": None}
467 |
--------------------------------------------------------------------------------