├── 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 |
5 | {{ form.as_p }} 6 |

7 | 8 |

9 |
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 | [![Jazzband](https://jazzband.co/static/img/jazzband.svg)](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 |
4 | 5 | {{ form.as_p }} 6 |

7 | {% if approved %} 8 | 9 | {% else %} 10 | 11 | {% endif %} 12 |

13 |
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 %}
25 |
26 | {% csrf_token %} 27 |
28 | {% if is_popup %}{% endif %} 29 | {% if save_on_top %}{% submit_row %}{% endif %} 30 | {% if errors %} 31 |

32 | {% blocktrans count errors|length as counter %}Please correct the error below.{% plural %}Please correct the errors below.{% endblocktrans %} 33 |

34 |
    {% for error in adminform.form.non_field_errors %}
  • {{ error }}
  • {% endfor %}
35 | {% endif %} 36 | 37 | {% for fieldset in adminform %} 38 | {% include "admin/includes/fieldset.html" %} 39 | {% endfor %} 40 | 41 | {% for inline_admin_formset in inline_admin_formsets %} 42 | {% include inline_admin_formset.opts.template %} 43 | {% endfor %} 44 | 45 | {% block after_related_objects %}{% endblock %} 46 | 47 |
48 | 49 |
50 | 51 |
52 | {% for obj in queryset %} 53 | 54 | {% endfor %} 55 | 56 | 57 |
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 |
3 | 65 | 66 |
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 | '%s' 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 |
  1. 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 |
  2. 37 | 38 |
  3. 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 |
  4. 48 | 49 |
  5. Permission form for adding a specific permission "top_secret" 50 | {% permission_form for flatpage using "flatpage_permission.top_secret" %} 51 |
  6. 52 | 53 |
  7. Permission form with a list of options queried from the authority 54 | {% permission_form for flatpage %} 55 |
  8. 56 | 57 |
  9. Add permission for this flatpage here: 58 | {% add_url_for_obj flatpage %} 59 |
  10. 60 | 61 |
  11. Request a kind of access: 62 | {% permission_request_form for flatpage %} 63 |
  12. 64 | 65 |
  13. Permission request form for adding a specific permission "add_flatpage" 66 | {% permission_request_form for flatpage using "flatpage_permission.add_flatpage" %} 67 |
  14. 68 | 69 |
  15. Request permission for this flatpage here: 70 | {% request_url_for_obj flatpage %} 71 |
  16. 72 | 73 |
  17. 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 |
  18. 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 | --------------------------------------------------------------------------------