├── permproject ├── __init__.py ├── wsgi.py ├── urls.py ├── requirements │ └── reqs.txt └── settings.py ├── testapp1 ├── __init__.py ├── tests │ ├── __init__.py │ ├── test_utils.py │ ├── test_role.py │ ├── test_models.py │ ├── test_permission_mixin.py │ ├── test_rolemanager.py │ ├── test_shortcuts.py │ └── test_mixins.py ├── migrations │ ├── __init__.py │ └── 0001_initial.py ├── apps.py ├── admin.py ├── other_roles.py ├── roles.py └── models.py ├── testapp2 ├── __init__.py ├── migrations │ ├── __init__.py │ └── 0001_initial.py ├── apps.py ├── admin.py ├── models.py └── roles.py ├── improved_permissions ├── migrations │ ├── __init__.py │ ├── 0001_initial.py │ └── 0002_auto_20180227_2108.py ├── templatetags │ ├── __init__.py │ └── roletags.py ├── __init__.py ├── admin.py ├── apps.py ├── exceptions.py ├── shortcuts.py ├── checkers.py ├── models.py ├── mixins.py ├── getters.py ├── assignments.py ├── roles.py └── utils.py ├── MANIFEST.in ├── setup.cfg ├── docs ├── source │ ├── api.rst │ ├── mixins.rst │ ├── help.rst │ ├── setup.rst │ ├── index.rst │ ├── shortcuts.rst │ ├── start.rst │ ├── conf.py │ ├── inheritance.rst │ └── role.rst └── Makefile ├── setup.sh ├── .gitignore ├── test.sh ├── .prospector.yaml ├── .coveragerc ├── manage.py ├── LICENSE ├── .travis.yml ├── setup.py ├── README.md └── .pylintrc /permproject/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testapp1/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testapp2/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testapp1/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testapp1/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testapp2/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /improved_permissions/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /improved_permissions/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.md 3 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | -------------------------------------------------------------------------------- /docs/source/api.rst: -------------------------------------------------------------------------------- 1 | API Reference 2 | ============= 3 | 4 | TODO 5 | -------------------------------------------------------------------------------- /testapp1/apps.py: -------------------------------------------------------------------------------- 1 | """ testapp1 app configs """ 2 | from django.apps import AppConfig 3 | 4 | 5 | class Testapp1Config(AppConfig): 6 | name = 'testapp1' 7 | -------------------------------------------------------------------------------- /testapp2/apps.py: -------------------------------------------------------------------------------- 1 | """ testapp2 app configs """ 2 | from django.apps import AppConfig 3 | 4 | 5 | class Testapp2Config(AppConfig): 6 | name = 'testapp2' 7 | -------------------------------------------------------------------------------- /testapp2/admin.py: -------------------------------------------------------------------------------- 1 | """ testapp2 admin configs """ 2 | from django.contrib import admin 3 | 4 | from testapp2.models import Library 5 | 6 | admin.site.register(Library) 7 | -------------------------------------------------------------------------------- /improved_permissions/__init__.py: -------------------------------------------------------------------------------- 1 | """ permissions init module """ 2 | default_app_config = 'improved_permissions.apps.ImprovedPermissionsConfig' # pylint: disable=invalid-name 3 | -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | pip install -r permproject/requirements/reqs.txt 2 | rm -f db.sqlite3 3 | 4 | ./manage.py makemigrations 5 | ./manage.py migrate 6 | 7 | ./manage.py runserver 0.0.0.0:8000 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | 3 | # Python Compiled Files 4 | *.pye 5 | *.pyc 6 | 7 | # Database 8 | *.sqlite3 9 | 10 | # Tests 11 | .coverage 12 | pylint-report.txt 13 | cover 14 | 15 | # Sphinx 16 | docs/build 17 | -------------------------------------------------------------------------------- /testapp2/models.py: -------------------------------------------------------------------------------- 1 | """ testapp2 models """ 2 | from django.db import models 3 | 4 | from improved_permissions.mixins import RoleMixin 5 | 6 | 7 | class Library(RoleMixin, models.Model): 8 | title = models.CharField(max_length=256) 9 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | rm -rf cover 4 | isort -rc --skip .venv --skip migrations . 5 | pylint --load-plugins=pylint_django **/*.py 6 | coverage erase 7 | coverage run manage.py test --failfast 8 | coverage report 9 | coverage html 10 | -------------------------------------------------------------------------------- /testapp1/admin.py: -------------------------------------------------------------------------------- 1 | """ testapp1 admin configs """ 2 | from django.contrib import admin 3 | 4 | from testapp1.models import Book, Chapter, Paragraph 5 | 6 | admin.site.register(Book) 7 | admin.site.register(Chapter) 8 | admin.site.register(Paragraph) 9 | -------------------------------------------------------------------------------- /testapp1/other_roles.py: -------------------------------------------------------------------------------- 1 | """ testapp1 another role module """ 2 | from improved_permissions.roles import ALL_MODELS, Role 3 | 4 | 5 | class AnotherRole(Role): 6 | verbose_name = "Another Role" 7 | models = ALL_MODELS 8 | deny = [] 9 | inherit_deny = [] 10 | -------------------------------------------------------------------------------- /.prospector.yaml: -------------------------------------------------------------------------------- 1 | strictness: veryhigh 2 | 3 | ignore-paths: 4 | - migrations 5 | 6 | uses: 7 | - django 8 | 9 | pep8: 10 | full: true 11 | options: 12 | max-line-length: 120 13 | 14 | pylint: 15 | full: true 16 | 17 | mccabe: 18 | disable: 19 | - MC0001 20 | -------------------------------------------------------------------------------- /improved_permissions/admin.py: -------------------------------------------------------------------------------- 1 | """ permissions admin configs """ 2 | from django.contrib import admin 3 | from django.contrib.auth.models import Permission 4 | 5 | from improved_permissions.models import RolePermission, UserRole 6 | 7 | admin.site.register(Permission) 8 | admin.site.register(RolePermission) 9 | admin.site.register(UserRole) 10 | -------------------------------------------------------------------------------- /docs/source/mixins.rst: -------------------------------------------------------------------------------- 1 | Mixins 2 | ====== 3 | 4 | We've implemented three mixins to make it easier to use the shortcuts in your project. All mixins are located in ``improved_permisions.mixins``. 5 | 6 | RoleMixin 7 | ^^^^^^^^^ 8 | 9 | Mixin for objects. 10 | 11 | UserRoleMixin 12 | ^^^^^^^^^^^^^ 13 | 14 | Mixin for users. 15 | 16 | PermissionMixin 17 | ^^^^^^^^^^^^^^^ 18 | 19 | Mixin for views. 20 | -------------------------------------------------------------------------------- /improved_permissions/apps.py: -------------------------------------------------------------------------------- 1 | """ permissions configs """ 2 | from django.apps import AppConfig 3 | 4 | from improved_permissions.utils import autodiscover, register_cleanup 5 | 6 | 7 | class ImprovedPermissionsConfig(AppConfig): 8 | name = 'improved_permissions' 9 | verbose_name = 'Django Improved Permissions' 10 | 11 | def ready(self): 12 | register_cleanup() 13 | autodiscover() 14 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch=True 3 | source=. 4 | omit = 5 | */.venv/* 6 | */migrations/* 7 | permproject/settings/* 8 | permproject/wsgi.py 9 | */apps.py 10 | setup.py 11 | 12 | [report] 13 | ignore_errors=True 14 | skip_covered=True 15 | exclude_lines= 16 | pragma: no cover 17 | raise AssertionError 18 | raise NotImplementedError 19 | if __name__ == .__main__.: 20 | 21 | [html] 22 | directory=cover 23 | -------------------------------------------------------------------------------- /improved_permissions/exceptions.py: -------------------------------------------------------------------------------- 1 | """permissions exceptions""" 2 | 3 | 4 | class ImproperlyConfigured(Exception): 5 | pass 6 | 7 | 8 | class ParentNotFound(Exception): 9 | pass 10 | 11 | 12 | class RoleNotFound(Exception): 13 | pass 14 | 15 | 16 | class InvalidRoleAssignment(Exception): 17 | pass 18 | 19 | 20 | class InvalidPermissionAssignment(Exception): 21 | pass 22 | 23 | 24 | class NotAllowed(Exception): 25 | pass 26 | -------------------------------------------------------------------------------- /testapp2/roles.py: -------------------------------------------------------------------------------- 1 | """ testapp2 roles """ 2 | from improved_permissions import roles 3 | from testapp2.models import Library 4 | 5 | 6 | class LibraryOwner(roles.Role): 7 | verbose_name = 'Biblioterário' 8 | models = [Library] 9 | unique = True 10 | deny = [] 11 | inherit = True 12 | inherit_deny = ['testapp1.review'] 13 | 14 | 15 | class LibraryWorker(roles.Role): 16 | verbose_name = 'Worker' 17 | models = [Library] 18 | allow = [] 19 | -------------------------------------------------------------------------------- /permproject/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for permproject project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.0/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "permproject.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /docs/source/help.rst: -------------------------------------------------------------------------------- 1 | Help 2 | ==== 3 | 4 | So do I. 5 | 6 | Need further help? 7 | ****************** 8 | 9 | oi. 10 | 11 | Contributing 12 | ************ 13 | 14 | Feel free to create new issues if you have suggestions or find some bugs. 15 | 16 | Commercial Support 17 | ****************** 18 | 19 | This project is used in products of SSYS clients. 20 | 21 | We are always looking for exciting work, so if you need any commercial support, feel free to get in touch: contato@ssys.com.br 22 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "permproject.settings") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError as exc: 10 | raise ImportError( 11 | "Couldn't import Django. Are you sure it's installed and " 12 | "available on your PYTHONPATH environment variable? Did you " 13 | "forget to activate a virtual environment?" 14 | ) from exc 15 | execute_from_command_line(sys.argv) 16 | -------------------------------------------------------------------------------- /testapp2/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.1 on 2018-02-27 21:08 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Library', 16 | fields=[ 17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('title', models.CharField(max_length=256)), 19 | ], 20 | options={ 21 | 'abstract': False, 22 | }, 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = DjangoImprovedPermissions 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /testapp1/roles.py: -------------------------------------------------------------------------------- 1 | """ testapp1 roles """ 2 | from improved_permissions.roles import ALL_MODELS, Role 3 | from testapp1.models import Book, Chapter, MyUser, Paragraph 4 | 5 | 6 | class Author(Role): 7 | verbose_name = 'Author' 8 | models = [Book, Chapter, Paragraph] 9 | deny = ['testapp1.review'] 10 | 11 | 12 | class Reviewer(Role): 13 | verbose_name = 'Reviewer' 14 | models = [Book] 15 | allow = ['testapp1.review'] 16 | inherit = True 17 | inherit_allow = ['testapp1.review'] 18 | 19 | 20 | class Advisor(Role): 21 | verbose_name = "Advisor" 22 | models = [MyUser] 23 | unique = True 24 | deny = [] 25 | 26 | 27 | class Coordenator(Role): 28 | verbose_name = "Coordenator" 29 | models = ALL_MODELS 30 | inherit_deny = ['testapp1.change_user'] 31 | -------------------------------------------------------------------------------- /permproject/urls.py: -------------------------------------------------------------------------------- 1 | """permproject URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/2.0/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.contrib import admin 17 | from django.urls import path 18 | 19 | urlpatterns = [ 20 | path('admin/', admin.site.urls), 21 | ] 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 S-SYS Sistemas e Soluções Tecnológicas 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3.6 3 | git: 4 | depth: false 5 | install: 6 | - pip install -r permproject/requirements/reqs.txt 7 | script: 8 | - pylint --load-plugins=pylint_django **/*.py 9 | - coverage run manage.py test 10 | after_success: 11 | - coverage report 12 | - coveralls 13 | deploy: 14 | provider: pypi 15 | user: ssys 16 | password: 17 | secure: MD/NIxEIoVXZEXQ/b/xiYQgZuMSoKDgX9sDYBVFNcOiShEv6NklVIzv4sMYkaF6vYWsO+gdgFKZeyYvk4OPy87zjJmyztXiiXjpsTHT3+Z5ZG/YzBd6XKzhh3Xk8mgiUbYEAWzNBqM42BdjrJ0lH3HXFyZtW1XvtLjLcCzT2by+EGdOI8BxBA0ZEAQo7kCRfk3OXAFwAnl/hYy115VxTck7whbwU9fDniro5JT9IiIEO8KmaSN8lKbvM58VR1H/+fTLiryd2H+OiGCKLCmsp0KkPgyQ0Dcat3o4WlsDvaMWxvQbE+ycXkoT/EElVNsB6iMj2fZIlzNt2tDaHMQ6bB9Re/hvxTTDlPgVbONsiG4ZGVwdDF9W1gqiEaRnwUqVtExMWsyxkQkhhs57Pv03weLADkt6cHmHmAkqs1bhBMBmdg3UA/h1hODT9FiE+5Ry76AFele0iG0ILz316URPgEjwuFsSi4kMbnpVhK3nBFBjCiQZSdK6ibWjtqCe4l2IoWtLNV/64epGk5wG+hamNn9md2eE7tiqmiy2+0wAEzO4fyWq4n1YZCWj3cvCgcVFpOwJXKx4cNCAUeiCk1uuuceyJ+y+dxo38kMKtAVerSI2sitJd3n6pF1pK0mTAr4iMz+DtZ1D09IGGBUCrlur3eBE56pYPg++2/3jOugoW/f8= 18 | on: 19 | branch: master 20 | tags: true 21 | -------------------------------------------------------------------------------- /permproject/requirements/reqs.txt: -------------------------------------------------------------------------------- 1 | alabaster==0.7.10 2 | argh==0.26.2 3 | astroid==1.6.1 4 | Babel==2.5.3 5 | certifi==2018.1.18 6 | chardet==3.0.4 7 | coverage==4.5.1 8 | coveralls==1.3.0 9 | Django==2.0.1 10 | docopt==0.6.2 11 | docutils==0.14 12 | dodgy==0.1.9 13 | flake8==3.5.0 14 | flake8-polyfill==1.0.2 15 | idna==2.6 16 | imagesize==1.0.0 17 | isort==4.3.3 18 | Jinja2==2.10 19 | lazy-object-proxy==1.3.1 20 | livereload==2.5.1 21 | MarkupSafe==1.0 22 | mccabe==0.6.1 23 | packaging==17.1 24 | pathtools==0.1.2 25 | pep8-naming==0.5.0 26 | port-for==0.3.1 27 | prospector==0.12.7 28 | pycodestyle==2.0.0 29 | pydocstyle==2.1.1 30 | pyflakes==1.6.0 31 | Pygments==2.2.0 32 | pylint==1.8.2 33 | pylint-celery==0.3 34 | pylint-common==0.2.5 35 | pylint-django==0.9.0 36 | pylint-flask==0.5 37 | pylint-plugin-utils==0.2.6 38 | pyparsing==2.2.0 39 | pytz==2018.3 40 | PyYAML==3.12 41 | requests==2.18.4 42 | requirements-detector==0.5.2 43 | setoptconf==0.2.0 44 | six==1.11.0 45 | snowballstemmer==1.2.1 46 | Sphinx==1.7.1 47 | sphinx-autobuild==0.7.1 48 | sphinx-rtd-theme==0.2.4 49 | sphinxcontrib-websupport==1.0.1 50 | tornado==5.0 51 | urllib3==1.22 52 | watchdog==0.8.3 53 | wrapt==1.10.11 54 | -------------------------------------------------------------------------------- /docs/source/setup.rst: -------------------------------------------------------------------------------- 1 | Setup 2 | ===== 3 | 4 | Installation 5 | ************ 6 | We are in PyPI. Just use the following command within your development environment: :: 7 | 8 | pip install django-improved-permissions 9 | 10 | 11 | Configuration 12 | ************* 13 | 14 | We use some apps that are already present in Django: ``auth`` and ``contenttypes``. Probably they are already declared, but just make sure so we don't have any issues later. :: 15 | 16 | # settings.py 17 | 18 | INSTALLED_APPS = ( 19 | ... 20 | 'django.contrib.auth', 21 | 'django.contrib.contenttypes', 22 | ... 23 | ) 24 | 25 | Now, you need to add our app inside your Django project. To do this, add ``improved_permissions`` into your ``INSTALLED_APPS``:: 26 | 27 | # settings.py 28 | 29 | INSTALLED_APPS = ( 30 | ... 31 | 'improved_permissions', 32 | ... 33 | ) 34 | 35 | .. note:: We are almost there! We use some tables in the database to store the permissions, so you must run ``./manage.py migrate improved_permissions`` in order to migrate all models needed. 36 | 37 | Yeah, all set to start! Let's go to the next page to get a quick view of how everything works. 38 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ improved_permissions setup configs """ 2 | import os 3 | 4 | from setuptools import find_packages, setup 5 | 6 | with open(os.path.join(os.path.dirname(__file__), 'README.md')) as readme: 7 | README = readme.read() 8 | 9 | # allow setup.py to be run from any path 10 | os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir))) 11 | 12 | setup( 13 | name='django-improved-permissions', 14 | version='0.2.2', 15 | packages=[ 16 | 'improved_permissions', 17 | 'improved_permissions.migrations', 18 | 'improved_permissions.templatetags', 19 | ], 20 | include_package_data=True, 21 | license='MIT License', 22 | description='A Django app to handle all kinds of permissions and roles.', 23 | long_description=README, 24 | url='https://github.com/s-sys/django-improved-permissions', 25 | author='S-SYS Sistemas e Soluções Tecnológicas', 26 | author_email='contato@ssys.com.br', 27 | classifiers=[ 28 | 'Environment :: Web Environment', 29 | 'Framework :: Django', 30 | 'Framework :: Django :: 2.0', 31 | 'Intended Audience :: Developers', 32 | 'Programming Language :: Python', 33 | 'Programming Language :: Python :: 3.6' 34 | ], 35 | ) 36 | -------------------------------------------------------------------------------- /improved_permissions/templatetags/roletags.py: -------------------------------------------------------------------------------- 1 | """ permissions templatetags """ 2 | from django import template 3 | 4 | from improved_permissions.exceptions import NotAllowed 5 | from improved_permissions.shortcuts import get_role as alias_get_role 6 | from improved_permissions.shortcuts import has_permission 7 | 8 | register = template.Library() 9 | 10 | 11 | @register.simple_tag 12 | def has_perm(user, permission, obj=None, persistent=None): 13 | """Adapts has_permission shortcut into a templatetag.""" 14 | any_object = False 15 | 16 | # Checking the any_object kwarg. 17 | if obj and isinstance(obj, str) and obj == 'any': 18 | any_object = True 19 | obj = None 20 | 21 | # Checking the persistent kwarg. 22 | if persistent and isinstance(persistent, str): 23 | if persistent == 'persistent': 24 | persistent = True 25 | elif persistent == 'non-persistent': 26 | persistent = False 27 | else: 28 | raise NotAllowed( 29 | "Use 'persistent' or 'non-persistent' on Persistent Mode." 30 | ) 31 | else: 32 | persistent = None 33 | 34 | # Return the default behavior of the shortcut. 35 | return has_permission(user, permission, obj, any_object, persistent) 36 | 37 | 38 | @register.filter 39 | def get_role(user, obj=None): 40 | return alias_get_role(user, obj).get_verbose_name() 41 | -------------------------------------------------------------------------------- /testapp1/tests/test_utils.py: -------------------------------------------------------------------------------- 1 | """ utils tests """ 2 | from django.test import TestCase 3 | 4 | from improved_permissions.exceptions import (ImproperlyConfigured, 5 | ParentNotFound, RoleNotFound) 6 | from improved_permissions.utils import (autodiscover, get_model, get_parents, 7 | get_roleclass, is_unique_together, 8 | string_to_permission) 9 | from testapp1.models import MyUser 10 | 11 | 12 | class FakeModel1(object): 13 | class RoleOptions: 14 | pass 15 | 16 | 17 | class FakeModel2(object): 18 | class RoleOptions: 19 | permission_parents = ['hello'] 20 | 21 | 22 | class FakeModel3(object): 23 | class RoleOptions: 24 | unique_together = 'not a bool value' 25 | 26 | 27 | class UtilsTest(TestCase): 28 | """ utils class tests """ 29 | 30 | def test_exceptions(self): 31 | """ test all exceptions on utils module """ 32 | with self.assertRaises(RoleNotFound): 33 | get_roleclass(12345) 34 | 35 | with self.assertRaises(RoleNotFound): 36 | get_roleclass('I am not a role.') 37 | 38 | self.assertEqual(get_model('I am not a model.'), None) 39 | 40 | self.assertEqual(get_parents(FakeModel1), []) 41 | 42 | with self.assertRaises(ParentNotFound): 43 | get_parents(FakeModel2) 44 | 45 | with self.assertRaises(ImproperlyConfigured): 46 | is_unique_together(FakeModel3) 47 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to Django Improved Permissions! 2 | ======================================= 3 | 4 | .. image:: https://coveralls.io/repos/github/s-sys/django-improved-permissions/badge.svg?branch=master 5 | :target: https://coveralls.io/github/s-sys/django-improved-permissions?branch=master 6 | :alt: Coverage 7 | 8 | .. image:: https://travis-ci.org/s-sys/django-improved-permissions.svg?branch=master 9 | :target: https://travis-ci.org/s-sys/django-improved-permissions 10 | :alt: Build Status 11 | 12 | .. image:: https://readthedocs.org/projects/django-improved-permissions/badge/?version=latest 13 | :target: http://django-improved-permissions.readthedocs.io/en/latest/?badge=latest 14 | :alt: Documentation Status 15 | 16 | .. image:: https://badge.fury.io/py/django-improved-permissions.svg 17 | :target: https://badge.fury.io/py/django-improved-permissions 18 | :alt: PyPI version 19 | 20 | Django Improved Permissions (DIP) is a django application made to make django's default permission system more robust. Here are some highlights: 21 | 22 | * Object-level Permissions 23 | 24 | * Role Assignment 25 | 26 | * Permissions Inheritance 27 | 28 | * Cache 29 | 30 | * Customizable Permissions per User Instance 31 | 32 | 33 | .. toctree:: 34 | :maxdepth: 2 35 | :caption: Contents: 36 | 37 | setup 38 | start 39 | role 40 | shortcuts 41 | mixins 42 | inheritance 43 | api 44 | help 45 | 46 | 47 | Indices and tables 48 | ================== 49 | 50 | * :ref:`genindex` 51 | * :ref:`modindex` 52 | * :ref:`search` 53 | -------------------------------------------------------------------------------- /improved_permissions/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.1 on 2018-02-27 21:08 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ('auth', '0009_alter_user_last_name_max_length'), 13 | ('contenttypes', '0002_remove_content_type_name'), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='RolePermission', 19 | fields=[ 20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('access', models.BooleanField(choices=[(True, 'Allow'), (False, 'Deny')], default=True)), 22 | ], 23 | ), 24 | migrations.CreateModel( 25 | name='UserRole', 26 | fields=[ 27 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 28 | ('role_class', models.CharField(max_length=256)), 29 | ('object_id', models.PositiveIntegerField(null=True)), 30 | ('content_type', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')), 31 | ('permissions', models.ManyToManyField(related_name='roles', through='improved_permissions.RolePermission', to='auth.Permission', verbose_name='Permissões')), 32 | ], 33 | options={ 34 | 'verbose_name_plural': 'Role Instances', 35 | 'verbose_name': 'Role Instance', 36 | }, 37 | ), 38 | ] 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Django Improved Permissions 2 | 3 | [![Coverage Status](https://coveralls.io/repos/github/s-sys/django-improved-permissions/badge.svg?branch=master)](https://coveralls.io/github/s-sys/django-improved-permissions?branch=master) 4 | [![Build Status](https://travis-ci.org/s-sys/django-improved-permissions.svg?branch=master)](https://travis-ci.org/s-sys/django-improved-permissions) 5 | [![Documentation Status](https://readthedocs.org/projects/django-improved-permissions/badge/?version=latest)](http://django-improved-permissions.readthedocs.io/en/latest/?badge=latest) 6 | [![PyPI version](https://badge.fury.io/py/django-improved-permissions.svg)](https://badge.fury.io/py/django-improved-permissions) 7 | 8 | --- 9 | 10 | ## Warning 11 | This repository is archived and there will be no future maintenance. However, it is very stable and very useful for small django projects. 12 | 13 | --- 14 | 15 | Django Improved Permissions (DIP) is a django application made to make django's default permission system more robust. Here are some highlights: 16 | 17 | * Object-level Permissions 18 | * Role Assignment 19 | * Permissions Inheritance 20 | * Cache 21 | * Customizable Permissions per User Instance 22 | 23 | ## Documentation 24 | 25 | The full documentation is available in our page in Read the Docs. 26 | 27 | http://django-improved-permissions.readthedocs.io/en/latest/ 28 | 29 | ## Contributing 30 | 31 | Feel free to create new issues if you have suggestions or find some bugs. 32 | 33 | ## Commercial Support 34 | 35 | This project is used in products of SSYS clients. 36 | 37 | We are always looking for exciting work, so if you need any commercial support, feel free to get in touch: contato@ssys.com.br 38 | -------------------------------------------------------------------------------- /improved_permissions/migrations/0002_auto_20180227_2108.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.1 on 2018-02-27 21:08 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | ('improved_permissions', '0001_initial'), 14 | ('auth', '0009_alter_user_last_name_max_length'), 15 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 16 | ] 17 | 18 | operations = [ 19 | migrations.AddField( 20 | model_name='userrole', 21 | name='user', 22 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='roles', to=settings.AUTH_USER_MODEL, verbose_name='Usuário'), 23 | ), 24 | migrations.AddField( 25 | model_name='rolepermission', 26 | name='permission', 27 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='accesses', to='auth.Permission'), 28 | ), 29 | migrations.AddField( 30 | model_name='rolepermission', 31 | name='role', 32 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='accesses', to='improved_permissions.UserRole'), 33 | ), 34 | migrations.AlterUniqueTogether( 35 | name='userrole', 36 | unique_together={('user', 'role_class', 'content_type', 'object_id')}, 37 | ), 38 | migrations.AlterUniqueTogether( 39 | name='rolepermission', 40 | unique_together={('role', 'permission')}, 41 | ), 42 | ] 43 | -------------------------------------------------------------------------------- /improved_permissions/shortcuts.py: -------------------------------------------------------------------------------- 1 | """ permissions shortcuts """ 2 | from improved_permissions import assignments, checkers, getters 3 | 4 | 5 | def get_user(role_class=None, obj=None): 6 | return getters.get_user(role_class, obj) 7 | 8 | 9 | def get_users(role_class=None, obj=None): 10 | return getters.get_users(role_class, obj) 11 | 12 | 13 | def get_objects(user, role_class=None, model=None): 14 | return getters.get_objects(user, role_class, model) 15 | 16 | 17 | def get_role(user, obj=None): 18 | return getters.get_role(user, obj) 19 | 20 | 21 | def get_roles(user, obj=None): 22 | return getters.get_roles(user, obj) 23 | 24 | 25 | def has_role(user, role_class=None, obj=None): 26 | return checkers.has_role(user, role_class, obj) 27 | 28 | 29 | def has_permission(user, permission, obj=None, any_object=False, persistent=None): 30 | return checkers.has_permission(user, permission, obj, any_object, persistent) 31 | 32 | 33 | def assign_role(user, role_class, obj=None): 34 | assignments.assign_role(user, role_class, obj) 35 | 36 | 37 | def assign_roles(users_list, role_class, obj=None): 38 | assignments.assign_roles(users_list, role_class, obj) 39 | 40 | 41 | def remove_role(user, role_class=None, obj=None): 42 | assignments.remove_role(user, role_class, obj) 43 | 44 | 45 | def remove_roles(users_list, role_class=None, obj=None): 46 | assignments.remove_roles(users_list, role_class, obj) 47 | 48 | 49 | def remove_all(role_class=None, obj=None): 50 | assignments.remove_all(role_class, obj) 51 | 52 | 53 | def assign_permission(user, role_class, permission, access, obj=None): 54 | assignments.assign_permission(user, role_class, permission, access, obj) 55 | -------------------------------------------------------------------------------- /testapp1/tests/test_role.py: -------------------------------------------------------------------------------- 1 | """ RoleManager tests """ 2 | from django.apps import apps 3 | from django.test import TestCase 4 | 5 | from improved_permissions.exceptions import ImproperlyConfigured, NotAllowed 6 | from improved_permissions.roles import ALL_MODELS, Role, RoleManager 7 | from testapp1 import roles 8 | 9 | 10 | class Advisor(Role): 11 | verbose_name = "Advisor" 12 | models = ALL_MODELS 13 | unique = True 14 | deny = [] 15 | 16 | 17 | class RoleTest(TestCase): 18 | """ Role class tests """ 19 | 20 | def setUp(self): 21 | RoleManager.cleanup() 22 | RoleManager.register_role(roles.Advisor) 23 | RoleManager.register_role(roles.Coordenator) 24 | 25 | def test_incorrect_implementations(self): 26 | """ test for exceptions """ 27 | 28 | # Trying to instantiate. 29 | with self.assertRaises(ImproperlyConfigured): 30 | Role() 31 | 32 | # Trying to use the methods on the abstract class. 33 | with self.assertRaises(ImproperlyConfigured): 34 | Role.get_class_name() 35 | 36 | # Trying to register another class but using 37 | # the same name. 38 | with self.assertRaises(ImproperlyConfigured): 39 | RoleManager.register_role(Advisor) 40 | 41 | # Trying to access the inherit mode of a 42 | # role class using inherit=False. 43 | with self.assertRaises(NotAllowed): 44 | roles.Advisor.get_inherit_mode() 45 | 46 | # Check if the role class using ALL_MODELS 47 | # actually get all models by your method. 48 | models_list = roles.Coordenator.get_models() 49 | self.assertEqual(models_list, apps.get_models()) 50 | -------------------------------------------------------------------------------- /docs/source/shortcuts.rst: -------------------------------------------------------------------------------- 1 | Shortcuts 2 | ========= 3 | 4 | These functions are the heart of this app. Everything you need to do in your project is implemented in the ``shortcuts`` module. 5 | 6 | .. note:: Do not rush your project using the shortcuts directly. We have an easiest way to use these shorcuts using **mixins** in your models. Click here to check it out. 7 | 8 | 9 | Checkers 10 | ^^^^^^^^ 11 | 12 | .. function:: has_role(user, role_class, obj=None) 13 | 14 | Returns True if the user has the role to the object. 15 | 16 | .. function:: has_permission(user, permission, obj=None) 17 | 18 | Returns True if the user has the permission. 19 | 20 | Assigning and Revoking 21 | ^^^^^^^^^^^^^^^^^^^^^^ 22 | 23 | .. function:: assign_role(user, role_class, obj=None) 24 | 25 | Assign the role to the user. 26 | 27 | .. function:: assign_roles(users_list, role_class, obj=None) 28 | 29 | Assign the role to all users in the list. 30 | 31 | .. function:: remove_role(user, role_class, obj=None) 32 | 33 | Remove the role and your permissions of the object from the user. 34 | 35 | .. function:: remove_roles(users_list=None, role_class, obj=None) 36 | 37 | Remove the role and your permissions of the object from all users in the list. 38 | 39 | 40 | Getters 41 | ^^^^^^^ 42 | 43 | .. function:: get_role(user, obj=None) 44 | 45 | Get the unique role class of the user related to the object. 46 | 47 | .. function:: get_roles(user, obj=None) 48 | 49 | Get all role classes of the user related to the object. 50 | 51 | .. function:: get_user(role_class=None, obj=None) 52 | 53 | Get the unique user instance according to the object. 54 | 55 | .. function:: get_user(role_class=None, obj=None) 56 | 57 | Get the unique user instance according to the object. 58 | 59 | .. function:: get_users(role_class=None, obj=None) 60 | 61 | Get all users instances according to the object. 62 | 63 | .. function:: get_objects(user, role_class=None, model=None) 64 | 65 | Get all objects related to the user. 66 | -------------------------------------------------------------------------------- /testapp1/models.py: -------------------------------------------------------------------------------- 1 | """ testapp1 models """ 2 | from django.contrib.auth.models import AbstractUser 3 | from django.db import models 4 | 5 | from improved_permissions.mixins import RoleMixin, UserRoleMixin 6 | from testapp2.models import Library 7 | 8 | 9 | class MyUser(UserRoleMixin, AbstractUser): 10 | """ MyUser test model """ 11 | class Meta: 12 | default_permissions = () 13 | permissions = ( 14 | ('add_user', 'Add New User'), 15 | ('change_user', 'Change User'), 16 | ('delete_user', 'Delete User'), 17 | ) 18 | 19 | 20 | class Book(RoleMixin, models.Model): 21 | """ Book test model """ 22 | title = models.CharField(max_length=256) 23 | library = models.ForeignKey(Library, on_delete=models.PROTECT) 24 | 25 | class Meta: 26 | permissions = [('view_book', 'View Book'), ('review', 'Review'),] 27 | 28 | class RoleOptions: 29 | permission_parents = ['library'] 30 | 31 | 32 | class Chapter(RoleMixin, models.Model): 33 | """ Chapter test model """ 34 | title = models.CharField(max_length=256) 35 | book = models.ForeignKey(Book, on_delete=models.CASCADE, related_name='chapters') 36 | cited_by = models.ForeignKey(Book, blank=True, null=True, on_delete=models.PROTECT, related_name='citations') 37 | 38 | class Meta: 39 | permissions = [('view_chapter', 'View Chapter'),] 40 | 41 | class RoleOptions: 42 | permission_parents = ['book', 'cited_by'] 43 | 44 | 45 | class Paragraph(RoleMixin, models.Model): 46 | chapter = models.ForeignKey(Chapter, on_delete=models.CASCADE) 47 | content = models.TextField() 48 | 49 | class Meta: 50 | permissions = [('view_paragraph', 'View Paragraph'),] 51 | 52 | class RoleOptions: 53 | permission_parents = ['chapter'] 54 | 55 | 56 | class UniqueTogether(RoleMixin, models.Model): 57 | content = models.TextField() 58 | 59 | class Meta: 60 | default_permissions = () 61 | permissions = [('nothing', 'Nothing!'),] 62 | 63 | class RoleOptions: 64 | unique_together = True 65 | -------------------------------------------------------------------------------- /testapp1/tests/test_models.py: -------------------------------------------------------------------------------- 1 | """ models tests """ 2 | from django.core.exceptions import ValidationError 3 | from django.test import TestCase 4 | 5 | from improved_permissions.models import UserRole 6 | from improved_permissions.roles import Role, RoleManager 7 | from testapp1.models import MyUser 8 | from testapp1.roles import Advisor, Coordenator 9 | 10 | 11 | class ModelsTest(TestCase): 12 | """ test methods in the model module """ 13 | 14 | def setUp(self): 15 | RoleManager.cleanup() 16 | RoleManager.register_role(Advisor) 17 | RoleManager.register_role(Coordenator) 18 | 19 | self.john = MyUser.objects.create(username='john') 20 | self.bob = MyUser.objects.create(username='bob') 21 | 22 | def test_output(self): 23 | """ check if the str method works fine """ 24 | self.john.assign_role(Advisor, self.bob) 25 | obj = UserRole.objects.get(user=self.john) 26 | self.assertEqual(str(obj), 'john is Advisor of bob') 27 | 28 | self.bob.assign_role(Coordenator) 29 | obj = UserRole.objects.get(user=self.bob) 30 | self.assertEqual(str(obj), 'bob is Coordenator') 31 | 32 | def test_role_property(self): 33 | """ check if the role property works fine """ 34 | self.john.assign_role(Advisor, self.bob) 35 | obj = UserRole.objects.get(user=self.john) 36 | self.assertEqual(obj.role, Advisor) 37 | self.assertEqual(obj.get_verbose_name(), 'Advisor') 38 | 39 | def test_edit_incorrectly(self): 40 | """ 41 | check if its possible to put 42 | wrong role class into UserRole 43 | """ 44 | self.john.assign_role(Advisor, self.bob) 45 | 46 | with self.assertRaises(ValidationError): 47 | obj = UserRole.objects.get(user=self.john) 48 | obj.role_class = 'this class doesnt exist.' 49 | obj.save() 50 | 51 | def test_signal(self): 52 | """ 53 | Test if the role instance are properly removed 54 | once the object is deleted. 55 | """ 56 | self.john.assign_role(Advisor, self.bob) 57 | 58 | # Check if the UserRole instance was created. 59 | ur_count = UserRole.objects.filter(user=self.john).count() 60 | self.assertEqual(ur_count, 1) 61 | self.assertTrue(self.john.has_permission('testapp1.change_user', self.bob)) 62 | 63 | # Removing the object attached to the UserRole. 64 | self.bob.delete() 65 | 66 | # Check if the UserRole instance has removed by the signal. 67 | ur_count = UserRole.objects.filter(user=self.john).count() 68 | self.assertEqual(ur_count, 0) 69 | self.assertFalse(self.john.has_permission('testapp1.change_user', self.bob)) 70 | -------------------------------------------------------------------------------- /docs/source/start.rst: -------------------------------------------------------------------------------- 1 | Quick Start 2 | =========== 3 | 4 | The entire DIP permissions system works based on roles. In other words, if you want to have permissions between a user and a certain object, you need to define a role for this relationship. 5 | 6 | Creating your first Role class 7 | ****************************** 8 | 9 | First, supose that you have the following model in your ``models.py``: :: 10 | 11 | # myapp/models.py 12 | 13 | from django.db import models 14 | 15 | class Book(models.Model): 16 | title = models.CharField(max_length=256) 17 | content = models.TextField(max_length=1000) 18 | 19 | class Meta: 20 | permissions = ( 21 | ('read_book', 'Can Read Book'), 22 | ('review_book', 'Can Review Book') 23 | ) 24 | 25 | .. note:: Notice that the permission statements inside models is exactly like the Django ``auth`` system. 26 | 27 | Now, create a new file inside of any app of your project named ``roles.py`` and implement as follows: :: 28 | 29 | # myapp/roles.py 30 | 31 | from improved_permissions.roles import Role 32 | from myapp.models import Book 33 | 34 | class Author(Role): 35 | verbose_name = "Author" 36 | models = [Book] 37 | deny = ['myapp.review_book'] 38 | 39 | class Reviewer(Role): 40 | verbose_name = "Reviewer" 41 | models = [Book] 42 | allow = ['myapp.review_book'] 43 | 44 | Ready! You can now use the DIP functions to assign, remove, and check permissions. 45 | 46 | Every time your project starts, we use an ``autodiscover`` in order to validate and register your Role classes automatically. So, don't worry about to do anything else. 47 | 48 | Using the first shortcuts 49 | ************************* 50 | 51 | Once you implement the role classes, you are ready to use our shortcuts. For example, let's create a ``Book`` object and an ``Author`` role for a user: :: 52 | 53 | from django.contrib.auth.models import User 54 | from improved_permissions.shortcuts import assign_role, has_permission, has_role 55 | 56 | from myapp.models import Book 57 | from myapp.roles import Author, Reviewer 58 | 59 | 60 | john = User.objects.get(pk=1) 61 | book = Book.objects.create(title='Nice Book', content='Such content.') 62 | 63 | has_role(john, Author, book) 64 | >>> False 65 | has_permission(john, 'myapp.read_book', book) 66 | >>> False 67 | 68 | assign_role(john, Author, book) 69 | 70 | has_role(john, Author, book) 71 | >>> True 72 | has_permission(john, 'myapp.read_book', book) 73 | >>> True 74 | has_permission(john, 'myapp.review_book', book) 75 | >>> False 76 | 77 | You just met the shortcuts ``assign_role``, ``has_role`` and ``has_permission``. If you don't get how they work, no problem. First, let's understand all about implementing ``Role`` classes in the next section. 78 | -------------------------------------------------------------------------------- /improved_permissions/checkers.py: -------------------------------------------------------------------------------- 1 | """checkers functions""" 2 | from django.contrib.contenttypes.models import ContentType 3 | 4 | from improved_permissions.exceptions import NotAllowed 5 | from improved_permissions.models import UserRole 6 | from improved_permissions.utils import (check_my_model, get_config, 7 | get_from_cache, get_parents, 8 | get_roleclass, inherit_check, 9 | string_to_permission) 10 | 11 | 12 | def has_role(user, role_class=None, obj=None): 13 | """ 14 | Check if the "user" has any role 15 | attached to him. 16 | 17 | If "role_class" is provided, only 18 | this role class will be counted. 19 | 20 | If "obj" is provided, the search is 21 | refined to look only at that object. 22 | """ 23 | 24 | query = UserRole.objects.filter(user=user) 25 | role = None 26 | 27 | if role_class: 28 | # Filtering by role class. 29 | role = get_roleclass(role_class) 30 | query = query.filter(role_class=role.get_class_name(), user=user) 31 | 32 | if obj: 33 | # Filtering by object. 34 | ct_obj = ContentType.objects.get_for_model(obj) 35 | query.filter(content_type=ct_obj.id, object_id=obj.id) 36 | 37 | # Check if object belongs 38 | # to the role class. 39 | check_my_model(role, obj) 40 | 41 | return query.count() > 0 42 | 43 | 44 | def has_permission(user, permission, obj=None, any_object=False, persistent=None): 45 | """ 46 | Return True if the "user" has the "permission". 47 | """ 48 | perm_obj = string_to_permission(permission) 49 | 50 | # Checking the 'any_object' bypass kwarg. 51 | if any_object and obj: 52 | raise NotAllowed( 53 | "You cannot provide an object and use any_object=True at same time." 54 | ) 55 | 56 | # Checking the 'persistent' bypass kwarg. 57 | if not isinstance(persistent, bool): 58 | persistent = get_config('PERSISTENT', False) 59 | 60 | stack = list() 61 | stack.append(obj) 62 | while stack: 63 | # Getting the permissions list of the first 64 | # role class based on their role ranking. 65 | current_obj = stack.pop(0) 66 | result_tuple = get_from_cache(user, current_obj, any_object) 67 | 68 | if result_tuple: 69 | # Checking now for database results. 70 | result = None 71 | for perm_tuple in result_tuple[1]: 72 | if perm_tuple[0] == perm_obj.id: 73 | result = perm_tuple[1] 74 | break 75 | 76 | # If nothing was found, check for inherit results. 77 | if result is None: 78 | result = inherit_check(result_tuple[0], permission) 79 | 80 | # We got a result. 81 | # Now checking for persistent mode. 82 | if result or not persistent: 83 | return result 84 | 85 | # Try to look even further 86 | # for possible parent fields. 87 | parents_list = get_parents(current_obj) 88 | for parent in parents_list: 89 | stack.append(parent) 90 | 91 | # Force another iteration in case of any permission was 92 | # found using object-related roles. Using 'None', we 93 | # check now for ALL_MODELS roles. 94 | if obj and not stack: 95 | stack.append(None) 96 | obj = None 97 | 98 | # If all fails and the user does not have 99 | # a role class with "ALL_MODELS", we finally 100 | # deny the permission. 101 | return False 102 | -------------------------------------------------------------------------------- /testapp1/tests/test_permission_mixin.py: -------------------------------------------------------------------------------- 1 | """ permission mixin tests """ 2 | from django.core.exceptions import ImproperlyConfigured, PermissionDenied 3 | from django.test import TestCase 4 | 5 | from improved_permissions.mixins import PermissionMixin 6 | from improved_permissions.roles import RoleManager 7 | from testapp1.models import MyUser 8 | from testapp1.roles import Advisor 9 | 10 | 11 | class Request(object): 12 | user = None 13 | 14 | 15 | class Dispatch(object): 16 | def dispatch(self, request, *args, **kwargs): 17 | return 'Protected View' 18 | 19 | 20 | class RequestView(PermissionMixin, Dispatch): 21 | def __init__(self, user=None, string=None, obj=None): 22 | if user: 23 | self.request = Request() 24 | self.request.user = user 25 | if string: 26 | self.permission_string = string 27 | if obj: 28 | self.permission_object = obj 29 | 30 | 31 | class ObjectView(PermissionMixin): 32 | def __init__(self, obj): 33 | self.object = obj 34 | 35 | 36 | class GetObjectView(PermissionMixin): 37 | def __init__(self, obj): 38 | self.something = obj 39 | 40 | def get_object(self): 41 | return self.something 42 | 43 | 44 | class PermissionMixinTest(TestCase): 45 | """ test if the permissionmixin works fine """ 46 | 47 | def setUp(self): 48 | RoleManager.cleanup() 49 | RoleManager.register_role(Advisor) 50 | 51 | self.john = MyUser.objects.create(username='john') 52 | self.bob = MyUser.objects.create(username='bob') 53 | 54 | def test_set_string(self): 55 | """ ... """ 56 | view = RequestView(string='Hello!') 57 | perm_string = view.get_permission_string() 58 | self.assertEqual(perm_string, 'Hello!') 59 | 60 | with self.assertRaises(ImproperlyConfigured): 61 | view.get_permission_object() 62 | 63 | view.permission_object = self.bob 64 | perm_obj = view.get_permission_object() 65 | self.assertEqual(perm_obj, self.bob) 66 | 67 | def test_set_object(self): 68 | """ ... """ 69 | view = RequestView(obj=self.john) 70 | perm_obj = view.get_permission_object() 71 | self.assertEqual(perm_obj, self.john) 72 | 73 | with self.assertRaises(ImproperlyConfigured): 74 | view.get_permission_string() 75 | 76 | view.permission_string = 'Hello!' 77 | perm_string = view.get_permission_string() 78 | self.assertEqual(perm_string, 'Hello!') 79 | 80 | # Getting via attribute self.object 81 | view = ObjectView(self.john) 82 | perm_obj = view.get_permission_object() 83 | self.assertEqual(perm_obj, self.john) 84 | 85 | # Getting via method get_object() 86 | view = GetObjectView(self.john) 87 | perm_obj = view.get_permission_object() 88 | self.assertEqual(perm_obj, self.john) 89 | 90 | def test_despatch(self): 91 | """ test if dispatch calls check_permission """ 92 | self.bob.assign_role(Advisor, self.john) 93 | view = RequestView(user=self.bob, obj=self.john) 94 | view.permission_string = 'testapp1.add_user' 95 | self.assertEqual(view.dispatch(None), 'Protected View') 96 | 97 | view.permission_string = 'testapp1.review' 98 | with self.assertRaises(PermissionDenied): 99 | view.dispatch(None) 100 | 101 | def test_anyobject_despatch(self): 102 | """ test if dispatch calls check_permission using any_object """ 103 | self.bob.assign_role(Advisor, self.john) 104 | view = RequestView(user=self.bob) 105 | view.permission_string = 'testapp1.add_user' 106 | view.permission_any_object = True 107 | self.assertEqual(view.dispatch(None), 'Protected View') 108 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | extension-pkg-whitelist= 3 | ignore=CVS 4 | ignore-patterns= 5 | #init-hook= 6 | jobs=4 7 | load-plugins= 8 | persistent=yes 9 | #rcfile= 10 | unsafe-load-any-extension=no 11 | 12 | [MESSAGES CONTROL] 13 | confidence= 14 | disable=bare-except,no-member,locally-disabled,too-few-public-methods,too-many-ancestors,keyword-arg-before-vararg 15 | enable= 16 | 17 | [REPORTS] 18 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 19 | msg-template={path}:{line}: [{msg_id}({symbol}), {obj}] {msg} 20 | output-format=text 21 | reports=no 22 | score=no 23 | 24 | [REFACTORING] 25 | max-nested-blocks=5 26 | 27 | [FORMAT] 28 | expected-line-ending-format= 29 | ignore-long-lines=^\s*(# )??$ 30 | indent-after-paren=4 31 | indent-string=' ' 32 | max-line-length=120 33 | max-module-lines=300 34 | no-space-check= 35 | single-line-class-stmt=no 36 | single-line-if-stmt=no 37 | 38 | [VARIABLES] 39 | additional-builtins= 40 | allow-global-unused-variables=yes 41 | callbacks=cb_,_cb 42 | dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ 43 | ignored-argument-names=_.*|^ignored_|^unused_ 44 | init-import=yes 45 | redefining-builtins-modules=six.moves,future.builtins 46 | 47 | [MISCELLANEOUS] 48 | notes= 49 | 50 | [SPELLING] 51 | spelling-dict= 52 | spelling-ignore-words= 53 | spelling-private-dict-file= 54 | spelling-store-unknown-words=no 55 | 56 | [BASIC] 57 | argument-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ 58 | argument-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ 59 | attr-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ 60 | attr-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ 61 | bad-names=foo,bar,baz,toto,tutu,tata 62 | class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ 63 | class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ 64 | class-name-hint=[A-Z_][a-zA-Z0-9]+$ 65 | class-rgx=[A-Z_][a-zA-Z0-9]+$ 66 | const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ 67 | const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ 68 | docstring-min-length=20 69 | function-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ 70 | function-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ 71 | good-names=i,j,k,ex,Run,_,urlpatterns,app_name,register,application 72 | include-naming-hint=no 73 | inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ 74 | inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ 75 | method-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ 76 | method-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ 77 | module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 78 | module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 79 | name-group= 80 | no-docstring-rgx=^_ 81 | property-classes=abc.abstractproperty 82 | variable-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ 83 | variable-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$ 84 | 85 | [SIMILARITIES] 86 | ignore-comments=no 87 | ignore-docstrings=no 88 | ignore-imports=no 89 | min-similarity-lines=2 90 | 91 | [LOGGING] 92 | logging-modules=logging 93 | 94 | [TYPECHECK] 95 | contextmanager-decorators=contextlib.contextmanager 96 | generated-members= 97 | ignore-mixin-members=yes 98 | ignore-on-opaque-inference=yes 99 | ignored-classes=optparse.Values,thread._local,_thread._local 100 | ignored-modules= 101 | missing-member-hint=yes 102 | missing-member-hint-distance=1 103 | missing-member-max-choices=1 104 | 105 | [DESIGN] 106 | max-args=5 107 | max-attributes=7 108 | max-bool-expr=5 109 | max-branches=12 110 | max-locals=15 111 | max-parents=7 112 | max-public-methods=30 113 | max-returns=6 114 | max-statements=50 115 | min-public-methods=1 116 | 117 | [CLASSES] 118 | defining-attr-methods=__init__,__new__,setUp 119 | exclude-protected=_asdict,_fields,_replace,_source,_make 120 | valid-classmethod-first-arg=cls 121 | valid-metaclass-classmethod-first-arg=mcs 122 | 123 | [IMPORTS] 124 | allow-wildcard-with-all=no 125 | analyse-fallback-blocks=yes 126 | deprecated-modules=optparse,tkinter.tix 127 | ext-import-graph= 128 | import-graph= 129 | int-import-graph= 130 | known-standard-library= 131 | known-third-party=enchant 132 | 133 | [EXCEPTIONS] 134 | overgeneral-exceptions=Exception 135 | -------------------------------------------------------------------------------- /permproject/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for permproject project. 3 | 4 | Generated by 'django-admin startproject' using Django 2.0.1. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.0/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/2.0/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/2.0/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = 'os$v$gnassq0l#2c=@+4m2)prwi&99(o*ixdsba!nu_%6%%#3h' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = ['*'] 29 | 30 | 31 | # Application definition 32 | 33 | DJANGO_APPS = [ 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | ] 41 | 42 | EXTERNAL_APPS = [] 43 | 44 | INTERNAL_APPS = [ 45 | 'improved_permissions', 46 | 'testapp1', 47 | 'testapp2' 48 | ] 49 | 50 | INSTALLED_APPS = DJANGO_APPS + EXTERNAL_APPS + INTERNAL_APPS 51 | 52 | MIDDLEWARE = [ 53 | 'django.middleware.security.SecurityMiddleware', 54 | 'django.contrib.sessions.middleware.SessionMiddleware', 55 | 'django.middleware.common.CommonMiddleware', 56 | 'django.middleware.csrf.CsrfViewMiddleware', 57 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 58 | 'django.contrib.messages.middleware.MessageMiddleware', 59 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 60 | ] 61 | 62 | ROOT_URLCONF = 'permproject.urls' 63 | 64 | TEMPLATES = [ 65 | { 66 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 67 | 'DIRS': [], 68 | 'APP_DIRS': True, 69 | 'OPTIONS': { 70 | 'context_processors': [ 71 | 'django.template.context_processors.debug', 72 | 'django.template.context_processors.request', 73 | 'django.contrib.auth.context_processors.auth', 74 | 'django.contrib.messages.context_processors.messages', 75 | ], 76 | }, 77 | }, 78 | ] 79 | 80 | WSGI_APPLICATION = 'permproject.wsgi.application' 81 | 82 | 83 | # Database 84 | # https://docs.djangoproject.com/en/2.0/ref/settings/#databases 85 | 86 | DATABASES = { 87 | 'default': { 88 | 'ENGINE': 'django.db.backends.sqlite3', 89 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 90 | } 91 | } 92 | 93 | 94 | # Password validation 95 | # https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators 96 | 97 | AUTH_PASSWORD_VALIDATORS = [ 98 | { 99 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 100 | }, 101 | { 102 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 103 | }, 104 | { 105 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 106 | }, 107 | { 108 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 109 | }, 110 | ] 111 | 112 | 113 | # Internationalization 114 | # https://docs.djangoproject.com/en/2.0/topics/i18n/ 115 | 116 | LANGUAGE_CODE = 'en-us' 117 | 118 | TIME_ZONE = 'UTC' 119 | 120 | USE_I18N = True 121 | 122 | USE_L10N = True 123 | 124 | USE_TZ = True 125 | 126 | 127 | # Static files (CSS, JavaScript, Images) 128 | # https://docs.djangoproject.com/en/2.0/howto/static-files/ 129 | 130 | STATIC_URL = '/static/' 131 | 132 | AUTH_USER_MODEL = 'testapp1.MyUser' 133 | 134 | IMPROVED_PERMISSIONS_SETTINGS = { 135 | 'CACHE_PREFIX_KEY': 'dip', 136 | 'PERSISTENT': False, 137 | 'CACHE': 'default', 138 | 'MODULE': 'roles', 139 | } 140 | -------------------------------------------------------------------------------- /testapp1/tests/test_rolemanager.py: -------------------------------------------------------------------------------- 1 | """ RoleManager tests """ 2 | from django.test import TestCase 3 | 4 | from improved_permissions.exceptions import (ImproperlyConfigured, 5 | InvalidRoleAssignment) 6 | from improved_permissions.roles import ALL_MODELS, Role, RoleManager 7 | from improved_permissions.utils import autodiscover, get_config 8 | from testapp1.models import MyUser 9 | from testapp1.other_roles import AnotherRole 10 | from testapp1.roles import Advisor 11 | 12 | 13 | class NoVerboseNameRole(Role): 14 | pass 15 | 16 | 17 | class NoModelRole1(Role): 18 | verbose_name = "Role" 19 | 20 | 21 | class NoModelRole2(Role): 22 | verbose_name = "Role" 23 | models = None 24 | 25 | 26 | class NoModelRole3(Role): 27 | verbose_name = "Role" 28 | models = [1, 2] 29 | 30 | 31 | class NoAllowDenyRole(Role): 32 | verbose_name = "Role" 33 | models = [MyUser] 34 | 35 | 36 | class WrongDenyRole(Role): 37 | verbose_name = "Role" 38 | models = ['testapp1.MyUser'] 39 | deny = 'I am not a list' 40 | 41 | 42 | class RoleManagerTest(TestCase): 43 | """ RoleManager class tests """ 44 | 45 | def setUp(self): 46 | RoleManager.cleanup() 47 | 48 | def test_another_module(self): 49 | """ test if the config dictionary works fine """ 50 | 51 | # Testing module name. 52 | new_ipc = {'MODULE': 'other_roles'} 53 | with self.settings(IMPROVED_PERMISSIONS_SETTINGS=new_ipc): 54 | autodiscover() 55 | roles_list = RoleManager.get_roles() 56 | self.assertEqual(roles_list, [AnotherRole]) 57 | 58 | # Testing the case if the dictionary does not exists. 59 | with self.settings(IMPROVED_PERMISSIONS_SETTINGS=None): 60 | self.assertEqual(get_config('CACHE', 'new_default'), 'new_default') 61 | self.assertEqual(get_config('MODULE', 'new_roles'), 'new_roles') 62 | 63 | # Test the default cache prefix key 64 | self.assertEqual(get_config('CACHE_PREFIX_KEY', 'prefix'), 'dip') 65 | 66 | # Test the new cache prefix key via settings 67 | new_ipc = {'CACHE_PREFIX_KEY': 'django-improved-permissions-'} 68 | with self.settings(IMPROVED_PERMISSIONS_SETTINGS=new_ipc): 69 | self.assertEqual(get_config('CACHE_PREFIX_KEY', 'prefix'), 'django-improved-permissions-') 70 | 71 | def test_incorrect_implementations(self): 72 | """ test all exceptions on validate method """ 73 | 74 | # Trying to instantiate RoleManager. 75 | with self.assertRaises(ImproperlyConfigured): 76 | RoleManager() 77 | 78 | # Trying to register a random object. 79 | with self.assertRaises(ImproperlyConfigured): 80 | RoleManager.register_role(object) 81 | 82 | # Role class without "verbose_name". 83 | with self.assertRaises(ImproperlyConfigured): 84 | RoleManager.register_role(NoVerboseNameRole) 85 | 86 | # Role class without "models". 87 | with self.assertRaises(ImproperlyConfigured): 88 | RoleManager.register_role(NoModelRole1) 89 | 90 | # Role class with "models" as no list. 91 | with self.assertRaises(ImproperlyConfigured): 92 | RoleManager.register_role(NoModelRole2) 93 | 94 | # Role class with "models" as list of random things. 95 | with self.assertRaises(ImproperlyConfigured): 96 | RoleManager.register_role(NoModelRole3) 97 | 98 | # Role class without any deny or allow. 99 | with self.assertRaises(ImproperlyConfigured): 100 | RoleManager.register_role(NoAllowDenyRole) 101 | 102 | # Role class with "deny" defined incorrectly. 103 | with self.assertRaises(ImproperlyConfigured): 104 | RoleManager.register_role(WrongDenyRole) 105 | 106 | # Registering the role classes correctly. 107 | RoleManager.register_role(Advisor) 108 | 109 | # Trying to register Advisor again. 110 | with self.assertRaises(ImproperlyConfigured): 111 | RoleManager.register_role(Advisor) 112 | 113 | # Checking list. 114 | self.assertEqual(RoleManager.get_roles(), [Advisor]) 115 | -------------------------------------------------------------------------------- /improved_permissions/models.py: -------------------------------------------------------------------------------- 1 | """ permissions models """ 2 | from django.conf import settings 3 | from django.contrib.auth.models import Permission 4 | from django.contrib.contenttypes.fields import GenericForeignKey 5 | from django.contrib.contenttypes.models import ContentType 6 | from django.core.exceptions import ValidationError 7 | from django.db import models 8 | 9 | from improved_permissions.exceptions import RoleNotFound 10 | from improved_permissions.roles import ALL_MODELS, ALLOW_MODE 11 | from improved_permissions.utils import (get_permissions_list, get_roleclass, 12 | permission_to_string) 13 | 14 | 15 | class UserRole(models.Model): 16 | """ 17 | UserRole 18 | 19 | This model represents the relationship between 20 | a user instance of the project with any other 21 | Django model, according to the rules defined 22 | in the Role class. 23 | """ 24 | user = models.ForeignKey( 25 | settings.AUTH_USER_MODEL, 26 | on_delete=models.CASCADE, 27 | related_name='roles', 28 | verbose_name='Usuário' 29 | ) 30 | 31 | permissions = models.ManyToManyField( 32 | Permission, 33 | through='RolePermission', 34 | related_name='roles', 35 | verbose_name='Permissões' 36 | ) 37 | 38 | role_class = models.CharField(max_length=256) 39 | 40 | content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE, null=True) 41 | object_id = models.PositiveIntegerField(null=True) 42 | obj = GenericForeignKey() 43 | 44 | class Meta: 45 | verbose_name = 'Role Instance' 46 | verbose_name_plural = 'Role Instances' 47 | unique_together = ('user', 'role_class', 'content_type', 'object_id') 48 | 49 | def __str__(self): 50 | role = get_roleclass(self.role_class) 51 | output = '{user} is {role}'.format(user=self.user, role=role.get_verbose_name()) 52 | if self.obj: 53 | output += ' of {obj}'.format(obj=self.obj) 54 | return output 55 | 56 | @property 57 | def role(self): 58 | return get_roleclass(self.role_class) 59 | 60 | def get_verbose_name(self): 61 | return self.role.get_verbose_name() 62 | 63 | def clean(self): 64 | try: 65 | get_roleclass(self.role_class) 66 | except RoleNotFound: 67 | raise ValidationError( 68 | {'role_class': 'This string representation does not exist as a Role class.'} 69 | ) 70 | 71 | def save(self, *args, **kwargs): # pylint: disable=arguments-differ,unused-argument 72 | self.clean() 73 | super().save() 74 | 75 | # non-object roles does not have specific 76 | # permissions auto created. 77 | if self.role.models == ALL_MODELS: 78 | return 79 | 80 | all_perms = get_permissions_list(self.role.get_models()) 81 | 82 | # Filtering the permissions based 83 | # on "allow" or "deny". 84 | role_instances = list() 85 | for perm in all_perms: 86 | access = None 87 | perm_s = permission_to_string(perm) 88 | if self.role.get_mode() == ALLOW_MODE: 89 | # [Allow Mode] 90 | # Put the access as "True" only for 91 | # the permissions in allow list. 92 | access = True if perm_s in self.role.allow else False 93 | else: 94 | # [Deny Mode] 95 | # Put the access as "False" only for 96 | # the permissions in deny list. 97 | access = False if perm_s in self.role.deny else True 98 | role_instances.append(RolePermission(role=self, permission=perm, access=access)) 99 | 100 | RolePermission.objects.bulk_create(role_instances) 101 | 102 | 103 | class RolePermission(models.Model): 104 | """ 105 | RolePermission 106 | 107 | This model has the function of performing 108 | the m2m relation between the Permission 109 | and the UserRole instances. It is possible 110 | that different instances of the same UserRole 111 | may have access to different permissions. 112 | """ 113 | PERMISSION_CHOICES = ( 114 | (True, 'Allow'), 115 | (False, 'Deny') 116 | ) 117 | 118 | access = models.BooleanField(choices=PERMISSION_CHOICES, default=True) 119 | 120 | role = models.ForeignKey( 121 | UserRole, 122 | on_delete=models.CASCADE, 123 | related_name='accesses' 124 | ) 125 | 126 | permission = models.ForeignKey( 127 | Permission, 128 | on_delete=models.CASCADE, 129 | related_name='accesses' 130 | ) 131 | 132 | class Meta: 133 | unique_together = ('role', 'permission') 134 | -------------------------------------------------------------------------------- /improved_permissions/mixins.py: -------------------------------------------------------------------------------- 1 | """ permissions mixins """ 2 | from django.contrib.contenttypes.fields import GenericRelation 3 | from django.core.exceptions import ImproperlyConfigured, PermissionDenied 4 | from django.db import models 5 | 6 | from improved_permissions import shortcuts 7 | from improved_permissions.models import UserRole 8 | 9 | 10 | class UserRoleMixin(models.Model): 11 | """ 12 | UserRoleMixin 13 | 14 | This mixin is a helper to be attached 15 | in the User model in order to use the 16 | most of the methods in the shortcuts 17 | module. 18 | """ 19 | class Meta: 20 | abstract = True 21 | 22 | def has_role(self, role_class=None, obj=None): 23 | return shortcuts.has_role(self, role_class, obj) 24 | 25 | def get_role(self, obj=None): 26 | return shortcuts.get_role(self, obj) 27 | 28 | def get_roles(self, obj=None): 29 | return shortcuts.get_roles(self, obj) 30 | 31 | def get_objects(self, role_class=None, model=None): 32 | return shortcuts.get_objects(self, role_class, model) 33 | 34 | def assign_role(self, role_class, obj=None): 35 | return shortcuts.assign_role(self, role_class, obj) 36 | 37 | def remove_role(self, role_class=None, obj=None): 38 | return shortcuts.remove_role(self, role_class, obj) 39 | 40 | def has_permission(self, permission, obj=None, any_object=False, persistent=None): 41 | return shortcuts.has_permission(self, permission, obj, any_object, persistent) 42 | 43 | 44 | class RoleMixin(models.Model): 45 | """ 46 | RoleMixin 47 | 48 | This mixin is a helper to be attached 49 | in any model that heavily use the methods 50 | in the shortcuts module. 51 | 52 | All shortcuts become methods of the model 53 | omitting the "obj" argument, using itself 54 | to fill it. 55 | """ 56 | 57 | roles = GenericRelation(UserRole) 58 | 59 | class Meta: 60 | abstract = True 61 | 62 | def get_user(self, role_class=None): 63 | return shortcuts.get_user(role_class, self) 64 | 65 | def get_users(self, role_class=None): 66 | return shortcuts.get_users(role_class, self) 67 | 68 | def has_role(self, user, role_class=None): 69 | return shortcuts.has_role(user, role_class, self) 70 | 71 | def get_role(self, user): 72 | return shortcuts.get_role(user, self) 73 | 74 | def get_roles(self, user): 75 | return shortcuts.get_roles(user, self) 76 | 77 | def assign_role(self, user, role_class): 78 | return shortcuts.assign_role(user, role_class, self) 79 | 80 | def assign_roles(self, users_list, role_class): 81 | return shortcuts.assign_roles(users_list, role_class, self) 82 | 83 | def remove_role(self, user, role_class=None): 84 | return shortcuts.remove_role(user, role_class, self) 85 | 86 | def remove_roles(self, users_list, role_class=None): 87 | return shortcuts.remove_roles(users_list, role_class, self) 88 | 89 | def remove_all(self, role_class=None): 90 | return shortcuts.remove_all(role_class, self) 91 | 92 | 93 | class PermissionMixin(object): 94 | """ 95 | PermissionMixin 96 | 97 | This mixin helps the class-based views 98 | to secure them based in permissions. 99 | """ 100 | permission_string = "" 101 | permission_any_object = False 102 | permission_persistent = None 103 | 104 | def get_permission_string(self): 105 | if self.permission_string != "": 106 | return self.permission_string 107 | 108 | raise ImproperlyConfigured( 109 | "Provide a 'permission_string' attribute." 110 | ) 111 | 112 | def get_permission_object(self): 113 | if self.permission_any_object: 114 | return None 115 | 116 | elif hasattr(self, 'permission_object'): 117 | return self.permission_object 118 | 119 | elif hasattr(self, 'object') and self.object is not None: 120 | return self.object 121 | 122 | elif hasattr(self, 'get_object'): 123 | return self.get_object() 124 | 125 | raise ImproperlyConfigured( 126 | "Provide a 'permission_object' attribute or implement " 127 | "a 'get_permission_object' method." 128 | ) 129 | 130 | def check_permission(self): 131 | return shortcuts.has_permission( 132 | self.request.user, 133 | self.get_permission_string(), 134 | self.get_permission_object(), 135 | self.permission_any_object, 136 | self.permission_persistent, 137 | ) 138 | 139 | def dispatch(self, request, *args, **kwargs): 140 | if not self.check_permission(): 141 | raise PermissionDenied 142 | return super().dispatch(request, *args, **kwargs) 143 | -------------------------------------------------------------------------------- /improved_permissions/getters.py: -------------------------------------------------------------------------------- 1 | """getters functions""" 2 | from django.contrib.auth import get_user_model 3 | from django.contrib.contenttypes.models import ContentType 4 | 5 | from improved_permissions.exceptions import NotAllowed 6 | from improved_permissions.models import UserRole 7 | from improved_permissions.utils import check_my_model, get_roleclass 8 | 9 | 10 | def get_user(role_class=None, obj=None): 11 | """ 12 | Get the User instance attached to the object. 13 | Only one UserRole must exists and this relation 14 | must be unique=True. 15 | 16 | Returns None if there is no user attached 17 | to the object. 18 | """ 19 | query = UserRole.objects.select_related('user').all() 20 | role = None 21 | 22 | if role_class: 23 | # All users who have "role_class" attached to any object. 24 | role = get_roleclass(role_class) 25 | query = query.filter(role_class=role.get_class_name()) 26 | 27 | if obj: 28 | # All users who have any role attached to the object. 29 | ct_obj = ContentType.objects.get_for_model(obj) 30 | query = query.filter(content_type=ct_obj.id, object_id=obj.id) 31 | 32 | # Check if object belongs 33 | # to the role class. 34 | check_my_model(role, obj) 35 | 36 | # Looking for a role class using unique=True 37 | selected = list() 38 | for ur_obj in query: 39 | role = get_roleclass(ur_obj.role_class) 40 | if role.unique is True: 41 | selected.append(ur_obj.user) 42 | 43 | users_set = set(selected) 44 | if len(users_set) > 1: 45 | raise NotAllowed( 46 | 'Multiple unique roles was found using ' 47 | 'the function get_user. Use get_users ' 48 | 'instead.' 49 | ) 50 | if len(users_set) == 1: 51 | return selected[0] 52 | return None 53 | 54 | 55 | def get_users(role_class=None, obj=None): 56 | """ 57 | If "role_class" and "obj" is provided, 58 | returns a QuerySet of users who has 59 | this role class attached to the 60 | object. 61 | 62 | If only "role_class" is provided, returns 63 | a QuerySet of users who has this role 64 | class attached to any object. 65 | 66 | If neither "role_class" or "obj" are provided, 67 | returns all users of the project. 68 | """ 69 | role = None 70 | kwargs = {} 71 | 72 | if role_class: 73 | # All users who have "role_class" attached to any object. 74 | role = get_roleclass(role_class) 75 | kwargs['roles__role_class'] = role.get_class_name() 76 | 77 | if obj: 78 | # All users who have any role attached to the object. 79 | ct_obj = ContentType.objects.get_for_model(obj) 80 | kwargs['roles__content_type'] = ct_obj.id 81 | kwargs['roles__object_id'] = obj.id 82 | 83 | # Check if object belongs 84 | # to the role class. 85 | check_my_model(role, obj) 86 | 87 | # Return as a distinct QuerySet. 88 | return get_user_model().objects.filter(**kwargs).distinct() 89 | 90 | def get_objects(user, role_class=None, model=None): 91 | """ 92 | Return the list of objects attached 93 | to a given user. 94 | 95 | If "role_class" is provided, only the objects 96 | which as registered in that role class will 97 | be returned. 98 | 99 | If "model" is provided, only the objects 100 | of that model will be returned. 101 | """ 102 | query = UserRole.objects.filter(user=user) 103 | role = None 104 | 105 | if role_class: 106 | # Filtering by role class. 107 | role = get_roleclass(role_class) 108 | query = query.filter(role_class=role.get_class_name()) 109 | 110 | if model: 111 | # Filtering by model. 112 | ct_obj = ContentType.objects.get_for_model(model) 113 | query = query.filter(content_type=ct_obj.id) 114 | 115 | # Check if object belongs 116 | # to the role class. 117 | check_my_model(role, model) 118 | 119 | # TODO 120 | result = set(ur_obj.obj for ur_obj in query) 121 | # TODO 122 | return list(result) 123 | 124 | 125 | def get_role(user, obj=None): 126 | """ 127 | Proxy method to be used when you sure that 128 | will have only one role class attached. 129 | """ 130 | return get_roles(user, obj)[0] 131 | 132 | 133 | def get_roles(user, obj=None): 134 | """ 135 | Return a list of role classes 136 | that is attached to "user". 137 | 138 | If "obj" is provided, the object 139 | must be attached as well. 140 | """ 141 | query = UserRole.objects.filter(user=user) 142 | if obj: 143 | ct_obj = ContentType.objects.get_for_model(obj) 144 | query = query.filter(content_type=ct_obj.id, object_id=obj.id) 145 | 146 | # Transform the string representations 147 | # into role classes and return as list. 148 | return [get_roleclass(ur_obj.role_class) for ur_obj in query] 149 | -------------------------------------------------------------------------------- /testapp1/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.1 on 2018-03-23 15:27 2 | 3 | import django.contrib.auth.models 4 | import django.contrib.auth.validators 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | import django.utils.timezone 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | initial = True 13 | 14 | dependencies = [ 15 | ('testapp2', '0001_initial'), 16 | ('auth', '0009_alter_user_last_name_max_length'), 17 | ] 18 | 19 | operations = [ 20 | migrations.CreateModel( 21 | name='MyUser', 22 | fields=[ 23 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 24 | ('password', models.CharField(max_length=128, verbose_name='password')), 25 | ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), 26 | ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), 27 | ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), 28 | ('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')), 29 | ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), 30 | ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), 31 | ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), 32 | ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), 33 | ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), 34 | ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')), 35 | ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')), 36 | ], 37 | options={ 38 | 'permissions': (('add_user', 'Add New User'), ('change_user', 'Change User'), ('delete_user', 'Delete User')), 39 | 'default_permissions': (), 40 | }, 41 | managers=[ 42 | ('objects', django.contrib.auth.models.UserManager()), 43 | ], 44 | ), 45 | migrations.CreateModel( 46 | name='Book', 47 | fields=[ 48 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 49 | ('title', models.CharField(max_length=256)), 50 | ('library', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='testapp2.Library')), 51 | ], 52 | options={ 53 | 'permissions': [('view_book', 'View Book'), ('review', 'Review')], 54 | }, 55 | ), 56 | migrations.CreateModel( 57 | name='Chapter', 58 | fields=[ 59 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 60 | ('title', models.CharField(max_length=256)), 61 | ('book', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='chapters', to='testapp1.Book')), 62 | ('cited_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='citations', to='testapp1.Book')), 63 | ], 64 | options={ 65 | 'permissions': [('view_chapter', 'View Chapter')], 66 | }, 67 | ), 68 | migrations.CreateModel( 69 | name='Paragraph', 70 | fields=[ 71 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 72 | ('content', models.TextField()), 73 | ('chapter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='testapp1.Chapter')), 74 | ], 75 | options={ 76 | 'permissions': [('view_paragraph', 'View Paragraph')], 77 | }, 78 | ), 79 | migrations.CreateModel( 80 | name='UniqueTogether', 81 | fields=[ 82 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 83 | ('content', models.TextField()), 84 | ], 85 | options={ 86 | 'permissions': [('nothing', 'Nothing!')], 87 | 'default_permissions': (), 88 | }, 89 | ), 90 | ] 91 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Configuration file for the Sphinx documentation builder. 4 | # 5 | # This file does only contain a selection of the most common options. For a 6 | # full list see the documentation: 7 | # http://www.sphinx-doc.org/en/stable/config 8 | 9 | # -- Path setup -------------------------------------------------------------- 10 | 11 | # If extensions (or modules to document with autodoc) are in another directory, 12 | # add these directories to sys.path here. If the directory is relative to the 13 | # documentation root, use os.path.abspath to make it absolute, like shown here. 14 | # 15 | import os 16 | 17 | on_rtd = os.environ.get('READTHEDOCS') == 'True' 18 | 19 | # -- Project information ----------------------------------------------------- 20 | 21 | project = 'Django Improved Permissions' 22 | copyright = '2018, S-SYS' 23 | author = 'Gabriel de Biasi' 24 | 25 | # The short X.Y version 26 | version = '' 27 | # The full version, including alpha/beta/rc tags 28 | release = '' 29 | 30 | 31 | # -- General configuration --------------------------------------------------- 32 | 33 | # If your documentation needs a minimal Sphinx version, state it here. 34 | # 35 | # needs_sphinx = '1.0' 36 | 37 | # Add any Sphinx extension module names here, as strings. They can be 38 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 39 | # ones. 40 | extensions = [ 41 | 'sphinx.ext.autodoc', 42 | 'sphinx.ext.coverage', 43 | ] 44 | 45 | # Add any paths that contain templates here, relative to this directory. 46 | templates_path = ['_templates'] 47 | 48 | # The suffix(es) of source filenames. 49 | # You can specify multiple suffix as a list of string: 50 | # 51 | # source_suffix = ['.rst', '.md'] 52 | source_suffix = '.rst' 53 | 54 | # The master toctree document. 55 | master_doc = 'index' 56 | 57 | # The language for content autogenerated by Sphinx. Refer to documentation 58 | # for a list of supported languages. 59 | # 60 | # This is also used if you do content translation via gettext catalogs. 61 | # Usually you set "language" from the command line for these cases. 62 | language = None 63 | 64 | # List of patterns, relative to source directory, that match files and 65 | # directories to ignore when looking for source files. 66 | # This pattern also affects html_static_path and html_extra_path . 67 | exclude_patterns = [] 68 | 69 | # The name of the Pygments (syntax highlighting) style to use. 70 | pygments_style = 'sphinx' 71 | 72 | 73 | # -- Options for HTML output ------------------------------------------------- 74 | 75 | # The theme to use for HTML and HTML Help pages. See the documentation for 76 | # a list of builtin themes. 77 | # 78 | if on_rtd: 79 | html_theme = 'default' 80 | else: 81 | import sphinx_rtd_theme 82 | html_theme = "sphinx_rtd_theme" 83 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 84 | 85 | # Theme options are theme-specific and customize the look and feel of a theme 86 | # further. For a list of options available for each theme, see the 87 | # documentation. 88 | # 89 | # html_theme_options = {} 90 | 91 | # Add any paths that contain custom static files (such as style sheets) here, 92 | # relative to this directory. They are copied after the builtin static files, 93 | # so a file named "default.css" will overwrite the builtin "default.css". 94 | html_static_path = ['_static'] 95 | 96 | # Custom sidebar templates, must be a dictionary that maps document names 97 | # to template names. 98 | # 99 | # The default sidebars (for documents that don't match any pattern) are 100 | # defined by theme itself. Builtin themes are using these templates by 101 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 102 | # 'searchbox.html']``. 103 | # 104 | # html_sidebars = {} 105 | 106 | 107 | # -- Options for HTMLHelp output --------------------------------------------- 108 | 109 | # Output file base name for HTML help builder. 110 | htmlhelp_basename = 'DjangoImprovedPermissionsdoc' 111 | 112 | 113 | # -- Options for LaTeX output ------------------------------------------------ 114 | 115 | latex_elements = { 116 | # The paper size ('letterpaper' or 'a4paper'). 117 | # 118 | # 'papersize': 'letterpaper', 119 | 120 | # The font size ('10pt', '11pt' or '12pt'). 121 | # 122 | # 'pointsize': '10pt', 123 | 124 | # Additional stuff for the LaTeX preamble. 125 | # 126 | # 'preamble': '', 127 | 128 | # Latex figure (float) alignment 129 | # 130 | # 'figure_align': 'htbp', 131 | } 132 | 133 | # Grouping the document tree into LaTeX files. List of tuples 134 | # (source start file, target name, title, 135 | # author, documentclass [howto, manual, or own class]). 136 | latex_documents = [ 137 | (master_doc, 'DjangoImprovedPermissions.tex', 'Django Improved Permissions Documentation', 138 | 'Gabriel de Biasi', 'manual'), 139 | ] 140 | 141 | 142 | # -- Options for manual page output ------------------------------------------ 143 | 144 | # One entry per manual page. List of tuples 145 | # (source start file, name, description, authors, manual section). 146 | man_pages = [ 147 | (master_doc, 'djangoimprovedpermissions', 'Django Improved Permissions Documentation', 148 | [author], 1) 149 | ] 150 | 151 | 152 | # -- Options for Texinfo output ---------------------------------------------- 153 | 154 | # Grouping the document tree into Texinfo files. List of tuples 155 | # (source start file, target name, title, author, 156 | # dir menu entry, description, category) 157 | texinfo_documents = [ 158 | (master_doc, 'DjangoImprovedPermissions', 'Django Improved Permissions Documentation', 159 | author, 'DjangoImprovedPermissions', 'One line description of project.', 160 | 'Miscellaneous'), 161 | ] 162 | 163 | 164 | # -- Extension configuration ------------------------------------------------- 165 | -------------------------------------------------------------------------------- /improved_permissions/assignments.py: -------------------------------------------------------------------------------- 1 | """assignments functions""" 2 | from django.contrib.contenttypes.models import ContentType 3 | 4 | from improved_permissions.exceptions import (InvalidPermissionAssignment, 5 | InvalidRoleAssignment) 6 | from improved_permissions.getters import get_roles, get_users 7 | from improved_permissions.models import RolePermission, UserRole 8 | from improved_permissions.roles import ALL_MODELS 9 | from improved_permissions.utils import (check_my_model, delete_from_cache, 10 | get_roleclass, is_unique_together, 11 | string_to_permission) 12 | 13 | 14 | def assign_role(user, role_class, obj=None): 15 | """ 16 | Proxy method to be used for one 17 | User instance. 18 | """ 19 | assign_roles([user], role_class, obj) 20 | 21 | 22 | def assign_roles(users_list, role_class, obj=None): 23 | """ 24 | Create a RolePermission object in the database 25 | referencing the followling role_class to the 26 | user. 27 | """ 28 | users_set = set(users_list) 29 | role = get_roleclass(role_class) 30 | name = role.get_verbose_name() 31 | 32 | # Check if object belongs 33 | # to the role class. 34 | check_my_model(role, obj) 35 | 36 | # If no object is provided but the role needs specific models. 37 | if not obj and role.models != ALL_MODELS: 38 | raise InvalidRoleAssignment( 39 | 'The role "%s" must be assigned with a object.' % name 40 | ) 41 | 42 | # If a object is provided but the role does not needs a object. 43 | if obj and role.models == ALL_MODELS: 44 | raise InvalidRoleAssignment( 45 | 'The role "%s" must not be assigned with a object.' % name 46 | ) 47 | 48 | # Check if the model accepts multiple roles 49 | # attached using the same User instance. 50 | if obj and is_unique_together(obj): 51 | for user in users_set: 52 | has_user = get_roles(user=user, obj=obj) 53 | if has_user: 54 | raise InvalidRoleAssignment( 55 | 'The user "%s" already has a role attached ' 56 | 'to the object "%s".' % (user, obj) 57 | ) 58 | 59 | if role.unique is True: 60 | # If the role is marked as unique but multiple users are provided. 61 | if len(users_list) > 1: 62 | raise InvalidRoleAssignment( 63 | 'Multiple users were provided using "%s", ' 64 | 'but it is marked as unique.' % name 65 | ) 66 | 67 | # If the role is marked as unique but already has an user attached. 68 | has_user = get_users(role_class=role, obj=obj) 69 | if has_user: 70 | raise InvalidRoleAssignment( 71 | 'The object "%s" already has a "%s" attached ' 72 | 'and it is marked as unique.' % (obj, name) 73 | ) 74 | 75 | for user in users_set: 76 | ur_instance = UserRole(role_class=role.get_class_name(), user=user) 77 | if obj: 78 | ur_instance.obj = obj 79 | ur_instance.save() 80 | 81 | # Cleaning the cache system. 82 | delete_from_cache(user, obj) 83 | 84 | 85 | def assign_permission(user, role_class, permission, access, obj=None): 86 | """ 87 | Assign a specific permission value 88 | to a given UserRole instance. 89 | The values used in this method overrides 90 | any configuration of "allow/deny" or 91 | "inherit_allow/inherit_deny". 92 | """ 93 | role = get_roleclass(role_class) 94 | perm = string_to_permission(permission) 95 | query = UserRole.objects.filter(user=user, role_class=role.get_class_name()) 96 | if obj: 97 | ct_obj = ContentType.objects.get_for_model(obj) 98 | query = query.filter(content_type=ct_obj.id, object_id=obj.id) 99 | 100 | if not query: 101 | raise InvalidPermissionAssignment('No Role instance was affected.') 102 | 103 | for role_obj in query: 104 | perm_obj, created = RolePermission.objects.get_or_create( # pylint: disable=W0612 105 | role=role_obj, 106 | permission=perm 107 | ) 108 | perm_obj.access = bool(access) 109 | perm_obj.save() 110 | 111 | # Cleaning the cache system. 112 | delete_from_cache(user, role_obj.obj) 113 | 114 | 115 | def remove_role(user, role_class=None, obj=None): 116 | """ 117 | Proxy method to be used for one 118 | User instance. 119 | """ 120 | remove_roles([user], role_class, obj) 121 | 122 | 123 | def remove_roles(users_list, role_class=None, obj=None): 124 | """ 125 | Delete all RolePermission objects in the database 126 | referencing the followling role_class to the 127 | user. 128 | If "obj" is provided, only the instances refencing 129 | this object will be deleted. 130 | """ 131 | query = UserRole.objects.filter(user__in=users_list) 132 | role = None 133 | 134 | if role_class: 135 | # Filtering by role class. 136 | role = get_roleclass(role_class) 137 | query = query.filter(role_class=role.get_class_name()) 138 | 139 | if obj: 140 | # Filtering by object. 141 | ct_obj = ContentType.objects.get_for_model(obj) 142 | query = query.filter(content_type=ct_obj.id, object_id=obj.id) 143 | 144 | # Check if object belongs 145 | # to the role class. 146 | check_my_model(role, obj) 147 | 148 | # Cleaning the cache system. 149 | for user in users_list: 150 | delete_from_cache(user, obj) 151 | 152 | # Cleaning the database. 153 | query.delete() 154 | 155 | 156 | def remove_all(role_class=None, obj=None): 157 | """ 158 | Remove all roles of the project. 159 | 160 | If "role_class" is provided, 161 | only the roles of "role_class" 162 | will be affected. 163 | 164 | If "obj" is provided, only 165 | users for that object will 166 | lose the role. 167 | """ 168 | query = UserRole.objects.all() 169 | role = None 170 | 171 | if role_class: 172 | # Filtering by role class. 173 | role = get_roleclass(role_class) 174 | query = UserRole.objects.filter(role_class=role.get_class_name()) 175 | 176 | if obj: 177 | # Filtering by object. 178 | ct_obj = ContentType.objects.get_for_model(obj) 179 | query = query.filter(content_type=ct_obj.id, object_id=obj.id) 180 | 181 | # Check if object belongs 182 | # to the role class. 183 | check_my_model(role, obj) 184 | 185 | # Cleaning the cache system. 186 | for role_obj in query: 187 | delete_from_cache(role_obj.user, role_obj.obj) 188 | 189 | # Cleaning the database. 190 | role_obj.delete() 191 | -------------------------------------------------------------------------------- /docs/source/inheritance.rst: -------------------------------------------------------------------------------- 1 | Permissions Inheritance 2 | ======================= 3 | 4 | The DIP allows you to implement inheritance permissions for your objects. For example, a librarian does't need to have explicit permissions to all their books in his library as long as the books make it clear that the library is an object in which it "belongs" to. 5 | 6 | Class RoleOptions 7 | ^^^^^^^^^^^^^^^^^ 8 | 9 | The class ``RoleOptions`` works just like the ``Meta`` class in the Django models, helping us to define some attributes related to that specific model. This class has the following attributes: 10 | 11 | ====================== =================== =========== 12 | Attribute Type Description 13 | ====================== =================== =========== 14 | ``permission_parents`` ``list`` of ``str`` List of ``ForeignKey`` or ``GenericForeignKey`` fields on the model to be considered as *parent* of the model. 15 | ``unique_together`` ``bool`` If ``True``, this model only allows one assignment to any User instance. 16 | ====================== =================== =========== 17 | 18 | Working with the inheritance 19 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 20 | 21 | Let's go back to that first example, the model ``Book``. We are going to implement another model named ``Library`` and create a ``ForeignKey`` field in ``Book`` to create a relationship between them. So, our ``models.py`` will be something like that: :: 22 | 23 | # myapp/models.py 24 | 25 | from django.db import models 26 | 27 | class Library(models.Model): 28 | name = models.CharField(max_length=256) 29 | 30 | 31 | class Book(models.Model): 32 | title = models.CharField(max_length=256) 33 | content = models.TextField(max_length=1000) 34 | my_library = models.ForeignKey(Library) 35 | 36 | class Meta: 37 | permissions = ( 38 | ('read_book', 'Can Read Book'), 39 | ('review_book', 'Can Review Book') 40 | ) 41 | 42 | We need to say to DIP that the ``my_library`` represents a parent of the ``Book`` model. In other words, any roles related to the ``Library`` model with ``inherit=True`` will be elected to search for more permissions. 43 | 44 | The way to do this is implementing another inner class in the model, the class ``RoleOptions`` and defining the list ``permission_parents``: :: 45 | 46 | 47 | # myapp/models.py 48 | 49 | from django.db import models 50 | from improved_permissions.mixins import RoleMixin 51 | 52 | 53 | class Library(models.Model, RoleMixin): 54 | name = models.CharField(max_length=256) 55 | 56 | 57 | class Book(models.Model, RoleMixin): 58 | title = models.CharField(max_length=256) 59 | content = models.TextField(max_length=1000) 60 | my_library = models.ForeignKey(Library) 61 | 62 | class Meta: 63 | permissions = ( 64 | ('read_book', 'Can Read Book'), 65 | ('review_book', 'Can Review Book') 66 | ) 67 | 68 | class RoleOptions: 69 | permission_parents = ['my_library'] 70 | 71 | Let's create a new role in order to represent the ``Library`` instances. :: 72 | 73 | 74 | # myapp/roles.py 75 | 76 | from improved_permissions.roles import Role 77 | from myapp.models import Library 78 | 79 | class LibraryManager(Role): 80 | verbose_name = 'Library Manager' 81 | models = [Library] 82 | allow = [] 83 | inherit = True 84 | inherit_allow = ['myapp.read_book'] 85 | 86 | After that, the field ``my_library`` already represents a parent model of the ``Book``. Now, let's go to the terminal to make some tests: :: 87 | 88 | # Django Shell 89 | 90 | from django.contrib.auth.models import User 91 | from improved_permissions.shortcuts import assign_role, has_permission 92 | from myapp.models import Book, Library 93 | from myapp.roles import LibraryManager 94 | 95 | john = User.objects.get(pk=1) 96 | 97 | library = Library.objects.create(name='Important Library') 98 | book = Book.objects.create(title='New Book', content='Much content', my_library=library) 99 | 100 | # John has nothing :( 101 | has_permission(john, 'myapp.read_book', book) 102 | >>> False 103 | 104 | # John receives an role attached to "library". 105 | assign_role(john, LibraryManager, library) 106 | 107 | # Now, we got True by permission inheritance. 108 | has_permission(john, 'myapp.read_book', book) 109 | >>> True 110 | 111 | 112 | Unique roles to a given object 113 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 114 | 115 | There is a scenario where a model has several roles related to it, but a single user must be assigned to only one of them. In order to allow this behavior, we have the boolean attribute called ``unique_together``. 116 | 117 | Let's say that one user must not be the ``Author`` and the ``Reviewer`` of a given ``Book`` instance at same time. Let's see on the terminal: :: 118 | 119 | 120 | # Django Shell 121 | 122 | from django.contrib.auth.models import User 123 | from improved_permissions.shortcuts import assign_role, has_permission 124 | from myapp.models import Book 125 | from myapp.roles import Author, Reviewer 126 | 127 | john = User.objects.get(pk=1) 128 | book = Book.objects.create(title='New Book', content='Much content', my_library=library) 129 | 130 | # John is the Author. 131 | assign_role(john, Author, book) 132 | 133 | # And also the Reviewer. 134 | assign_role(john, Reviewer, book) 135 | 136 | # We cannot allow that :( 137 | has_permission(john, 'myapp.read_book', book) 138 | >>> True 139 | has_permission(john, 'myapp.review_book', book) 140 | >>> True 141 | 142 | Now, let's change the class ``RoleOptions`` inside of ``Book``: :: 143 | 144 | 145 | # myapp/models.py 146 | 147 | from django.db import models 148 | from improved_permissions.mixins import RoleMixin 149 | 150 | class Book(models.Model, RoleMixin): 151 | title = models.CharField(max_length=256) 152 | content = models.TextField(max_length=1000) 153 | my_library = models.ForeignKey(Library) 154 | 155 | class Meta: 156 | permissions = ( 157 | ('read_book', 'Can Read Book'), 158 | ('review_book', 'Can Review Book') 159 | ) 160 | 161 | class RoleOptions: 162 | permission_parents = ['my_library'] 163 | 164 | # new feature here! 165 | # -------------------- 166 | unique_together = True 167 | # -------------------- 168 | 169 | Going back to the terminal to see the result: :: 170 | 171 | 172 | # Django Shell 173 | 174 | from django.contrib.auth.models import User 175 | from improved_permissions.shortcuts import assign_role, has_permission 176 | from myapp.models import Book 177 | from myapp.roles import Author, Reviewer 178 | 179 | john = User.objects.get(pk=1) 180 | book = Book.objects.create(title='New Book', content='Much content', my_library=library) 181 | 182 | # John is the Author. 183 | assign_role(john, Author, book) 184 | 185 | # Can be the Reviewer now? 186 | assign_role(john, Reviewer, book) 187 | >>> InvalidRoleAssignment: 'The user "john" already has a role attached to the object "book".' 188 | 189 | Yeah! Now we are safe. 190 | -------------------------------------------------------------------------------- /docs/source/role.rst: -------------------------------------------------------------------------------- 1 | Role Class 2 | ========== 3 | 4 | Role classes must be implemented by inheriting the ``Role`` class present in ``improved_permissions.roles``. 5 | 6 | Whenever you start your project, they are automatically validated and can already be used. If something is wrong, ``RoleManager`` will raise an exception with a message explaining the cause of the error. 7 | 8 | .. note:: Only Role classes inside modules called ``roles.py`` are automatically validated. See here to change this behavior. 9 | 10 | 11 | Required attributes 12 | ******************* 13 | 14 | The ``Role`` class has some attributes that are required to be properly registered by our ``RoleManager``. The description of these attributes is in the following table: 15 | 16 | ===================== ========================== === 17 | Attribute Type Description 18 | ===================== ========================== === 19 | ``verbose_name`` ``str`` Used to print some information about the role. 20 | ``models`` ``list`` or ``ALL_MODELS`` Defines which models this role can be attached. 21 | ``allow`` or ``deny`` ``list`` Defines which permissions should be allowed or denied. You must define only one of them. 22 | ===================== ========================== === 23 | 24 | Optional Attributes 25 | ******************* 26 | 27 | The ``Role`` class also has other attributes, which are considered as optional. When they are not declared, we assign default values for these arguments. 28 | 29 | ===================================== ======== ========= === 30 | Attribute Type Default Description 31 | ===================================== ======== ========= === 32 | ``unique`` ``bool`` ``False`` Only one User instance is allowed to be attached to a given object using this role. 33 | ``ranking`` ``int`` ``0`` Used in order to solve permissions conflit. More about this in the examples. 34 | ``inherit`` ``bool`` ``False`` Allows this role to inherit permissions from its child models. Read about this feature here. 35 | ``inherit_allow`` or ``inherit_deny`` ``list`` ``[]`` Specifies which inherit permissions should be allowed or denied. You must define only one of them. 36 | ===================================== ======== ========= === 37 | 38 | Unique Roles 39 | ^^^^^^^^^^^^ 40 | 41 | You will probably arrive in a case where an object can only have one User instance assigned to a particular role. But, it is as easy as learning python to do this using DIP. For example: :: 42 | 43 | class CarOwner(Role): 44 | verbose_name = 'Owner of a Car' 45 | models = [Car] 46 | deny = [] 47 | 48 | Now, let's test this on the terminal: :: 49 | 50 | my_car = Car.objects.create(name='My New Car') 51 | 52 | # Giving a new car to john! 53 | john = User.objects.get(pk=1) 54 | assign_role(john, CarOwner, my_car) 55 | 56 | # That's right. 57 | has_role(john, CarOwner, my_car) 58 | >>> True 59 | 60 | # Oh, no... 61 | bob = User.objects.get(pk=2) 62 | assign_role(bob, CarOwner, my_car) 63 | 64 | # And now? 65 | has_role(bob, CarOwner, my_car) 66 | >>> True # Noooooo :( 67 | 68 | To fix this, let's change the implementation of the role class and add the ``unique`` attribute. :: 69 | 70 | class CarOwner(Role): 71 | verbose_name = 'Owner of a Car' 72 | models = [Car] 73 | deny = [] 74 | unique = True 75 | 76 | Let's test this again: :: 77 | 78 | my_car = Car.objects.create(name='My New Car') 79 | 80 | # Giving a new car to john! 81 | john = User.objects.get(pk=1) 82 | assign_role(john, CarOwner, my_car) 83 | 84 | # That's right. 85 | has_role(john, CarOwner, my_car) 86 | >>> True 87 | 88 | # And now we are protected :D 89 | bob = User.objects.get(pk=2) 90 | assign_role(bob, CarOwner, my_car) 91 | >>> InvalidRoleAssignment: 'The object "Car" already has a "CarOwner" attached and it is marked as unique.' 92 | 93 | Role Ranking 94 | ^^^^^^^^^^^^ 95 | 96 | There are several cases that can lead your project to have permissions conflicts. We have a basic scenario to show you how this happens and how you can use role ranking to solve it. For example: :: 97 | 98 | class Teacher(Role): 99 | verbose_name = 'Teacher' 100 | models = [User] 101 | deny = ['user.update_user'] 102 | 103 | class Advisor(Role): 104 | verbose_name = 'Advisor' 105 | models = [User] 106 | deny = [] 107 | 108 | Note that these roles have conflicting permissions if both are assigned to the same User instance. To solve this conflict problem, you can assign an integer value to ``ranking``, present in the Role class. This value will be used to sort the permissions to be used by the DIP. 109 | 110 | In other words, the lower the ``ranking`` value, more important this role is. So, let's work using ranking now: :: 111 | 112 | class Teacher(Role): 113 | verbose_name = 'Teacher' 114 | models = [User] 115 | deny = ['user.update_user'] 116 | ranking = 1 117 | 118 | class Advisor(Role): 119 | verbose_name = 'Teacher' 120 | models = [User] 121 | deny = [] 122 | ranking = 0 123 | 124 | Now let's test this on the terminal: :: 125 | 126 | john = User.objects.get(pk=1) 127 | bob = User.objects.get(pk=2) 128 | 129 | assign_role(john, Advisor, bob) 130 | assign_role(john, Teacher, bob) 131 | 132 | # Now has_permission returns True using 133 | # the role "Advisor" by Role Ranking. 134 | has_permission(john, 'user.update_user', bob) 135 | >>> True 136 | 137 | Role Classes using ALL_MODELS 138 | ***************************** 139 | 140 | If you need a role that manages any model of your project, you can define the ``models`` attribute using ``ALL_MODELS``. These classes are ``inherit=True`` by default because they don't have their own permissions, only inherited permissions. For example: :: 141 | 142 | # myapp/roles.py 143 | 144 | from improved_permissions.roles import ALL_MODELS, Role 145 | 146 | class SuperUser(Role): 147 | verbose_name = 'Super Man Role' 148 | models = ALL_MODELS 149 | deny = [] 150 | inherit_deny = [] 151 | 152 | Because this class is not attached to a specific model, you can use the shortcuts without defining objects. For example: :: 153 | 154 | from myapp.models import Book 155 | from myapp.roles import SuperUser 156 | 157 | john = User.objects.get(pk=1) 158 | book = Book.objects.create(title='Nice Book', content='Such content.') 159 | 160 | # You shouldn't pass an object during assignment. 161 | assign_role(john, SuperUser) 162 | 163 | # This line will raise an InvalidRoleAssignment exception 164 | assign_role(john, SuperUser, book) 165 | 166 | # You can check with and without an object. 167 | has_permission(john, 'myapp.read_book') 168 | >>> True 169 | has_permission(john, 'myapp.read_book', book) 170 | >>> True 171 | 172 | Public Methods 173 | ************** 174 | 175 | The role classes have some class methods that you can call if you need them. 176 | 177 | .. function:: get_verbose_name(): str 178 | 179 | Returns the ``verbose_name`` attribute. Example: :: 180 | 181 | from myapp.roles import Author, Reviewer 182 | 183 | Author.get_verbose_name() 184 | >>> 'Author' 185 | Reviewer.get_verbose_name() 186 | >>> 'Reviewer' 187 | 188 | .. function:: is_my_model(model): bool 189 | 190 | Checks if the role can be attached to the argument ``model``. The argument can be either the model class or an instance. Example: :: 191 | 192 | from myapp.models import Book 193 | from myapp.roles import Author 194 | 195 | Author.is_my_model('some data') 196 | >>> False 197 | Author.is_my_model(Book) 198 | >>> True 199 | my_book = Book.objects.create(title='Nice Book', content='Nice content.') 200 | Author.is_my_model(my_book) 201 | >>> True 202 | 203 | .. function:: get_models(): list 204 | 205 | Returns a list of all model classes which this role can be attached. If the ``models`` attribute was defined using ``ALL_MODELS``, this method will return a list of all valid models of the project. For example: :: 206 | 207 | from myapp.models import Book 208 | from myapp.roles import Author, SuperUser 209 | 210 | Author.get_models() 211 | >>> [Book] 212 | SuperUser.get_models() 213 | >>> [Book, User, Permission, ContentType, ...] # all models known by Django 214 | 215 | In the next section, we describe all existing shortcuts in this app. 216 | -------------------------------------------------------------------------------- /improved_permissions/roles.py: -------------------------------------------------------------------------------- 1 | """ definition of role and rolemanager """ 2 | from django.apps import apps 3 | 4 | from improved_permissions.exceptions import ImproperlyConfigured, NotAllowed 5 | from improved_permissions.utils import get_model 6 | 7 | ALLOW_MODE = 0 8 | DENY_MODE = 1 9 | 10 | ALL_MODELS = -1 11 | 12 | 13 | class RoleManager(object): 14 | """ 15 | RoleManager 16 | 17 | This class holds the list 18 | of all Role classes validated 19 | and in use by the project. 20 | 21 | """ 22 | __ROLE_CLASSES_LIST = list() 23 | 24 | def __new__(cls, *args, **kwargs): # pylint: disable=unused-argument 25 | raise ImproperlyConfigured('RoleManager must not be instantiated.') 26 | 27 | @classmethod 28 | def register_role(cls, new_class): 29 | """ 30 | Validate and register a new Role 31 | class in the Manager to be used 32 | in the project. 33 | """ 34 | from improved_permissions.utils import is_role 35 | 36 | # Check if is actually a role class. 37 | if not is_role(new_class): 38 | raise ImproperlyConfigured( 39 | '"%s" is not a class inherited ' 40 | 'from Role.' % str(new_class) 41 | ) 42 | 43 | # Looking for name conflits or if this 44 | # class was already registered before. 45 | new_name = new_class.get_class_name() 46 | for current_class in cls.__ROLE_CLASSES_LIST: 47 | current_name = current_class.get_class_name() 48 | if current_class == new_class: 49 | raise ImproperlyConfigured( 50 | '"%s" was already registered as ' 51 | 'a valid Role class.' % new_name 52 | ) 53 | 54 | elif current_name == new_name: 55 | raise ImproperlyConfigured( 56 | '"Another role was already defined using ' 57 | '"%s". Choose another name for this Role ' 58 | 'class.' % current_name 59 | ) 60 | 61 | cls.__validate(new_class) 62 | cls.__ROLE_CLASSES_LIST.append(new_class) 63 | 64 | @classmethod 65 | def get_roles(cls): 66 | """ 67 | Return the list of 68 | all registered Role 69 | classes. 70 | """ 71 | return list(cls.__ROLE_CLASSES_LIST) 72 | 73 | @classmethod 74 | def cleanup(cls): 75 | """ 76 | Flush the current list of all 77 | Roles registered in the project. 78 | """ 79 | cls.__ROLE_CLASSES_LIST = list() 80 | 81 | @classmethod 82 | def __validate(cls, new_class): # pylint: disable=too-many-branches 83 | """ 84 | Check if all attributes needed 85 | was properly defined in the 86 | new Role class. 87 | """ 88 | name = new_class.get_class_name() 89 | 90 | # Check for "verbose_name" definition. 91 | if not hasattr(new_class, 'verbose_name'): 92 | raise ImproperlyConfigured( 93 | 'Provide a "verbose_name" definition ' 94 | 'to the Role class "%s".' % name 95 | ) 96 | 97 | # Check if the atribute "models" is 98 | # defined correctly. 99 | cls.__validate_models(new_class) 100 | 101 | # Role classes with "models" = ALLMODELS 102 | # does not use allow/deny. In this case, 103 | # all permissions must be specified in 104 | # "inherit_allow" and "inherit_deny". 105 | if new_class.models != ALL_MODELS: 106 | new_class.MODE = cls.__validate_allow_deny(new_class, 'allow', 'deny') 107 | 108 | # Ensuring that "inherit" exists. 109 | # Default: False 110 | if not hasattr(new_class, 'inherit') or not isinstance(new_class.inherit, bool): 111 | new_class.inherit = False 112 | 113 | if new_class.inherit is True: 114 | new_class.INHERIT_MODE = cls.__validate_allow_deny(new_class, 'inherit_allow', 'inherit_deny') 115 | 116 | # Ensuring that "unique" exists. 117 | # Default: False 118 | if not hasattr(new_class, 'unique') or not isinstance(new_class.unique, bool): 119 | new_class.unique = False 120 | 121 | # Ensuring that "ranking" exists. 122 | # Default: 0 123 | if not hasattr(new_class, 'ranking') or not isinstance(new_class.ranking, int): 124 | new_class.ranking = 0 125 | 126 | @classmethod 127 | def __validate_models(cls, new_class): 128 | """ 129 | Check if the attribute "models" is a valid list 130 | of Django models or the constant ALL_MODELS. 131 | """ 132 | name = new_class.get_verbose_name() 133 | 134 | models_isvalid = True 135 | if hasattr(new_class, 'models'): 136 | if isinstance(new_class.models, list): 137 | # Check for every item in the "models" list. 138 | valid_list = list() 139 | for model in new_class.models: 140 | # Get the model class or "app_label.model". 141 | model_class = get_model(model) 142 | if model_class: 143 | valid_list.append(model_class) 144 | else: 145 | models_isvalid = False 146 | break 147 | new_class.models = valid_list 148 | elif new_class.models == ALL_MODELS: 149 | # Role classes with ALL_MODELS autoimplies inherit=True. 150 | new_class.inherit = True 151 | new_class.unique = False 152 | new_class.MODE = DENY_MODE 153 | new_class.allow = [] 154 | new_class.deny = [] 155 | else: 156 | models_isvalid = False 157 | else: 158 | models_isvalid = False 159 | 160 | if not models_isvalid: 161 | raise ImproperlyConfigured( 162 | 'Provide a list of Models classes via definition ' 163 | 'of "models" to the Role class "%s".' % name 164 | ) 165 | 166 | @classmethod 167 | def __validate_allow_deny(cls, new_class, allow_field, deny_field): 168 | """ 169 | This method validates the set attributes "allow/inherit_allow" 170 | and "deny/inherit_deny", checking if their values are a valid 171 | list of permissions in string representation. 172 | """ 173 | name = new_class.get_verbose_name() 174 | 175 | # Checking for "allow" and "deny" fields 176 | c_allow = hasattr(new_class, allow_field) 177 | c_deny = hasattr(new_class, deny_field) 178 | 179 | # XOR operation. 180 | if c_allow and c_deny or not c_allow and not c_deny: 181 | raise ImproperlyConfigured( 182 | 'Provide either "%s" or "%s" when inherit=True' 183 | ' or models=ALL_MODELS for the Role "%s".' 184 | '' % (allow_field, deny_field, name) 185 | ) 186 | 187 | if c_allow and isinstance(getattr(new_class, allow_field), list): 188 | result = ALLOW_MODE 189 | 190 | elif c_deny and isinstance(getattr(new_class, deny_field), list): 191 | result = DENY_MODE 192 | else: 193 | raise ImproperlyConfigured( 194 | '"%s" or "%s" must to be a list in the Role ' 195 | '"%s".' % (allow_field, deny_field, name) 196 | ) 197 | 198 | # Return the mode. 199 | return result 200 | 201 | 202 | class Role(object): 203 | """ 204 | Role 205 | 206 | This abstract class is used 207 | to manage all requests regarding 208 | role permissions. 209 | 210 | """ 211 | 212 | def __new__(cls, *args, **kwargs): # pylint: disable=unused-argument 213 | raise ImproperlyConfigured('Role classes must not be instantiated.') 214 | 215 | @classmethod 216 | def __protect(cls): 217 | if cls == Role: 218 | raise ImproperlyConfigured('The role class itself must not be used.') 219 | 220 | @classmethod 221 | def get_class_name(cls): 222 | cls.__protect() 223 | return str(cls.__name__.lower()) 224 | 225 | @classmethod 226 | def get_verbose_name(cls): 227 | cls.__protect() 228 | return str(cls.verbose_name) 229 | 230 | @classmethod 231 | def get_mode(cls): 232 | cls.__protect() 233 | return cls.MODE 234 | 235 | @classmethod 236 | def get_inherit_mode(cls): 237 | cls.__protect() 238 | if cls.inherit is True: 239 | return cls.INHERIT_MODE 240 | raise NotAllowed( 241 | 'The role "%s" is not marked as unique.' % cls.get_verbose_name() 242 | ) 243 | 244 | @classmethod 245 | def get_models(cls): 246 | cls.__protect() 247 | if cls.models == ALL_MODELS: 248 | return list(apps.get_models()) # All models known by Django. 249 | return list(cls.models) 250 | 251 | @classmethod 252 | def is_my_model(cls, model): 253 | cls.__protect() 254 | return model._meta.model in cls.get_models() # pylint: disable=protected-access 255 | -------------------------------------------------------------------------------- /testapp1/tests/test_shortcuts.py: -------------------------------------------------------------------------------- 1 | """ permissions tests """ 2 | from django.test import TestCase 3 | 4 | from improved_permissions.exceptions import (InvalidPermissionAssignment, 5 | InvalidRoleAssignment, NotAllowed) 6 | from improved_permissions.roles import ALL_MODELS, Role, RoleManager 7 | from improved_permissions.shortcuts import (assign_permission, assign_role, 8 | assign_roles, get_user, get_users, 9 | has_permission, has_role, 10 | remove_all, remove_role, 11 | remove_roles) 12 | from improved_permissions.templatetags.roletags import get_role as tg_get_role 13 | from improved_permissions.templatetags.roletags import has_perm as tg_has_perm 14 | from testapp1.models import Chapter, MyUser, UniqueTogether 15 | 16 | 17 | class Advisor(Role): 18 | verbose_name = "Advisor" 19 | models = [MyUser] 20 | unique = True 21 | deny = [] 22 | 23 | 24 | class Teacher(Role): 25 | verbose_name = "Teacher" 26 | models = [MyUser] 27 | deny = ['testapp1.delete_user'] 28 | ranking = 1 29 | 30 | 31 | class Secretary(Role): 32 | verbose_name = "Secretary" 33 | models = [MyUser, UniqueTogether] 34 | allow = ['testapp1.delete_user'] 35 | 36 | 37 | class SubCoordenator(Role): 38 | verbose_name = "Sub Coordenator" 39 | models = ALL_MODELS 40 | inherit_allow = ['testapp1.change_user'] 41 | 42 | 43 | class Coordenator(Role): 44 | verbose_name = "Coordenator" 45 | models = ALL_MODELS 46 | inherit_deny = ['testapp1.change_user'] 47 | 48 | 49 | class UniqueOwner(Role): 50 | verbose_name = "Owner of Unique" 51 | models = [UniqueTogether] 52 | deny = [] 53 | 54 | 55 | class ShortcutsTest(TestCase): 56 | """ test all functions in shortcuts """ 57 | 58 | def setUp(self): 59 | RoleManager.cleanup() 60 | RoleManager.register_role(Advisor) 61 | RoleManager.register_role(Teacher) 62 | RoleManager.register_role(Secretary) 63 | RoleManager.register_role(SubCoordenator) 64 | RoleManager.register_role(Coordenator) 65 | RoleManager.register_role(UniqueOwner) 66 | 67 | self.john = MyUser.objects.create(username="john") 68 | self.bob = MyUser.objects.create(username="bob") 69 | self.mike = MyUser.objects.create(username="mike") 70 | self.julie = MyUser.objects.create(username="julie") 71 | self.unique = UniqueTogether.objects.create(content='Some data') 72 | 73 | def test_assign_roles(self): 74 | """ test if the assignment methods work fine """ 75 | assign_roles([self.john, self.mike], Teacher, self.bob) 76 | 77 | users_list = get_users(Teacher, self.bob) 78 | self.assertEqual(list(users_list), [self.john, self.mike]) 79 | 80 | with self.assertRaises(NotAllowed): 81 | assign_role(self.john, Teacher, Chapter) 82 | 83 | def test_assign_roles_allmodels(self): 84 | """ test if the roles using ALL_MODELS work fine """ 85 | assign_role(self.john, Coordenator) 86 | 87 | # Trying to assign a non-object role using a object 88 | with self.assertRaises(InvalidRoleAssignment): 89 | assign_role(self.john, Coordenator, self.bob) 90 | 91 | # Trying to assign a object role without object 92 | with self.assertRaises(InvalidRoleAssignment): 93 | assign_role(self.john, Advisor) 94 | 95 | users_list = get_users(Coordenator) 96 | self.assertEqual(list(users_list), [self.john]) 97 | 98 | def test_assign_roles_unique(self): 99 | """ test if 'unique' attribute works fine """ 100 | assign_role(self.john, Advisor, self.bob) 101 | assign_role(self.john, Advisor, self.julie) 102 | 103 | # Get the single user that has the role attached. 104 | self.assertEqual(get_user(Advisor), self.john) 105 | 106 | assign_role(self.mike, Advisor, self.john) 107 | 108 | # Now, multiple users have Advisor. 109 | # So the method should not be used. 110 | with self.assertRaises(NotAllowed): 111 | get_user(Advisor) 112 | 113 | # Trying to assign the multiple roles using a Role with unique=True 114 | with self.assertRaises(InvalidRoleAssignment): 115 | assign_roles([self.john, self.mike], Advisor, self.julie) 116 | 117 | # Trying to add the role again using a Role with unique=True 118 | with self.assertRaises(InvalidRoleAssignment): 119 | assign_role(self.mike, Advisor, self.bob) 120 | 121 | # Trying to add the role again using a Role with unique=True 122 | with self.assertRaises(InvalidRoleAssignment): 123 | assign_role(self.mike, Advisor, self.julie) 124 | 125 | users_list = get_users(Advisor) 126 | self.assertEqual(list(users_list), [self.john, self.mike]) 127 | 128 | def test_unique_together(self): 129 | """ test if models marked as unique_together works fine """ 130 | 131 | # Nothing attached to the object. 132 | users_list = self.unique.get_users() 133 | self.assertEqual(list(users_list), []) 134 | 135 | # Confirm that the object was attached to the user. 136 | self.mike.assign_role(UniqueOwner, self.unique) 137 | self.assertTrue(self.mike.has_role(UniqueOwner, self.unique)) 138 | 139 | # Try to attach another role to the object 140 | # but using the same user instance. 141 | with self.assertRaises(InvalidRoleAssignment): 142 | self.mike.assign_role(Secretary, self.unique) 143 | 144 | # Remove the old role instance and try again 145 | # to attach the new role. 146 | self.mike.remove_role(UniqueOwner, self.unique) 147 | self.mike.assign_role(Secretary, self.unique) 148 | self.assertTrue(self.mike.has_role(Secretary, self.unique)) 149 | 150 | def test_has_role(self): 151 | """ test if has_role method works fine """ 152 | assign_role(self.john, Coordenator) 153 | assign_role(self.john, Advisor, self.bob) 154 | 155 | self.assertTrue(has_role(self.john, Coordenator)) 156 | self.assertTrue(has_role(self.john, Advisor, self.bob)) 157 | 158 | with self.assertRaises(NotAllowed): 159 | has_role(self.john, Advisor, self.unique) 160 | 161 | def test_has_permission(self): 162 | """ test if the has_permission method works fine """ 163 | assign_role(self.john, Teacher, self.bob) 164 | self.assertTrue(has_permission(self.john, 'testapp1.add_user', self.bob)) 165 | self.assertFalse(has_permission(self.mike, 'testapp1.delete_user', self.bob)) 166 | 167 | assign_role(self.mike, Secretary, self.bob) 168 | self.assertTrue(has_permission(self.mike, 'testapp1.delete_user', self.bob)) 169 | self.assertFalse(has_permission(self.mike, 'testapp1.add_user', self.bob)) 170 | 171 | assign_role(self.bob, SubCoordenator) 172 | self.assertTrue(has_permission(self.bob, 'testapp1.change_user')) 173 | self.assertTrue(has_permission(self.bob, 'testapp1.change_user', self.julie)) 174 | self.assertFalse(has_permission(self.bob, 'testapp1.add_user', self.julie)) 175 | 176 | assign_role(self.julie, Coordenator) 177 | self.assertTrue(has_permission(self.julie, 'testapp1.add_user')) 178 | self.assertTrue(has_permission(self.julie, 'testapp1.add_user', self.bob)) 179 | self.assertFalse(has_permission(self.julie, 'testapp1.change_user', self.bob)) 180 | 181 | # john has both Advisor and Teacher now, 182 | # but Advisor has better ranking. 183 | assign_role(self.john, Advisor, self.bob) 184 | self.assertTrue(has_permission(self.john, 'testapp1.delete_user', self.bob)) 185 | 186 | def test_template_tags(self): 187 | """ test if template tags work fine """ 188 | assign_role(self.julie, Coordenator) 189 | 190 | self.assertTrue(tg_has_perm(self.julie, 'testapp1.add_user', self.bob)) 191 | self.assertFalse(tg_has_perm(self.julie, 'testapp1.change_user', self.bob)) 192 | 193 | # Check for non-object related role. 194 | self.assertEqual(tg_get_role(self.julie), 'Coordenator') 195 | 196 | # Check of object related role. 197 | assign_role(self.julie, Secretary, self.bob) 198 | self.assertEqual(tg_get_role(self.julie, self.bob), 'Secretary') 199 | 200 | def test_remove_roles(self): 201 | """ test if the remove_roles method works fine """ 202 | assign_role(self.john, Teacher, self.bob) 203 | remove_role(self.john, Teacher, self.bob) 204 | 205 | users_list = get_users(Teacher) 206 | self.assertEqual(list(users_list), []) 207 | 208 | assign_roles([self.john, self.mike], Teacher, self.bob) 209 | remove_roles([self.john, self.mike], Teacher) 210 | 211 | users_list = get_users(Teacher) 212 | self.assertEqual(list(users_list), []) 213 | 214 | assign_role(self.julie, Coordenator) 215 | self.assertTrue(has_permission(self.julie, 'testapp1.add_user')) 216 | remove_role(self.julie) 217 | self.assertFalse(has_permission(self.julie, 'testapp1.add_user')) 218 | 219 | # Remove all roles from the project. 220 | assign_roles([self.john, self.mike], Teacher, self.bob) 221 | remove_all(Coordenator) 222 | remove_all(Teacher) 223 | self.assertEqual(list(get_users(Coordenator)), []) 224 | self.assertEqual(list(get_users(Teacher)), []) 225 | 226 | def test_assign_permissions(self): 227 | """ test if the permissions assignment works fine """ 228 | 229 | # Assign the role and try to use a permission denied by default. 230 | assign_role(self.john, Teacher, self.bob) 231 | assign_role(self.john, Teacher, self.julie) 232 | self.assertFalse(has_permission(self.john, 'testapp1.delete_user', self.bob)) 233 | self.assertFalse(has_permission(self.john, 'testapp1.delete_user', self.julie)) 234 | 235 | # Explicitly assign the permission using access=True and the object. 236 | assign_permission(self.john, Teacher, 'testapp1.delete_user', True, self.bob) 237 | 238 | # Result: Only the specified object was affected. 239 | self.assertTrue(has_permission(self.john, 'testapp1.delete_user', self.bob)) 240 | self.assertFalse(has_permission(self.john, 'testapp1.delete_user', self.julie)) 241 | 242 | # Assign the role and try to use a permission allowed by default 243 | assign_role(self.mike, Teacher, self.bob) 244 | assign_role(self.mike, Teacher, self.julie) 245 | self.assertTrue(has_permission(self.mike, 'testapp1.add_user', self.bob)) 246 | self.assertTrue(has_permission(self.mike, 'testapp1.add_user', self.julie)) 247 | 248 | # Explicitly assign the permission using access=False but without an object. 249 | assign_permission(self.mike, Teacher, 'testapp1.add_user', False) 250 | 251 | # Result: All the user's role instances were affected 252 | self.assertFalse(has_permission(self.mike, 'testapp1.add_user', self.bob)) 253 | self.assertFalse(has_permission(self.mike, 'testapp1.add_user', self.julie)) 254 | 255 | # Trying to assign a wrong permission. 256 | with self.assertRaises(InvalidPermissionAssignment): 257 | assign_permission(self.mike, Secretary, 'testapp1.add_user', access=False) 258 | 259 | # Expliciting a permission to a role using ALL_MODELS. 260 | assign_role(self.julie, SubCoordenator) 261 | self.assertFalse(has_permission(self.julie, 'testapp1.delete_user')) 262 | 263 | assign_permission(self.julie, SubCoordenator, 'testapp1.delete_user', access=True) 264 | self.assertTrue(has_permission(self.julie, 'testapp1.delete_user')) 265 | 266 | # Other permissions are still False. 267 | self.assertFalse(has_permission(self.julie, 'testapp1.add_user')) 268 | 269 | remove_role(self.julie, SubCoordenator) 270 | self.assertFalse(has_permission(self.julie, 'testapp1.delete_user')) 271 | -------------------------------------------------------------------------------- /testapp1/tests/test_mixins.py: -------------------------------------------------------------------------------- 1 | """ mixins tests """ 2 | from django.test import TestCase 3 | 4 | from improved_permissions.exceptions import NotAllowed 5 | from improved_permissions.models import RolePermission 6 | from improved_permissions.roles import ALL_MODELS, Role, RoleManager 7 | from improved_permissions.shortcuts import get_users 8 | from improved_permissions.templatetags.roletags import has_perm as tg_has_perm 9 | from improved_permissions.utils import dip_cache 10 | from testapp1.models import Book, Chapter, MyUser, Paragraph 11 | from testapp1.roles import Advisor, Author, Coordenator, Reviewer 12 | from testapp2.models import Library 13 | from testapp2.roles import LibraryOwner, LibraryWorker 14 | 15 | 16 | class MixinsTest(TestCase): 17 | """ mixins test class """ 18 | 19 | def setUp(self): 20 | self.john = MyUser.objects.create(username='john') 21 | self.bob = MyUser.objects.create(username='bob') 22 | self.mike = MyUser.objects.create(username='mike') 23 | 24 | self.library = Library.objects.create(title='Cool Library') 25 | self.book = Book.objects.create(title='Very Nice Book 1', library=self.library) 26 | self.chapter = Chapter.objects.create(title='Very Nice Chapter 1', book=self.book) 27 | self.paragraph = Paragraph.objects.create(content='Such Text 1', chapter=self.chapter) 28 | 29 | self.another_book = Book.objects.create(title='Very Nice Book 2', library=self.library) 30 | 31 | RoleManager.cleanup() 32 | RoleManager.register_role(Author) 33 | RoleManager.register_role(Advisor) 34 | RoleManager.register_role(Coordenator) 35 | RoleManager.register_role(Reviewer) 36 | RoleManager.register_role(LibraryOwner) 37 | RoleManager.register_role(LibraryWorker) 38 | dip_cache().clear() 39 | 40 | def test_inherit_permission(self): 41 | """ test if the inherit works fine """ 42 | 43 | self.library.assign_role(self.john, LibraryOwner) 44 | self.book.assign_role(self.bob, Author) 45 | 46 | self.assertTrue(self.bob.has_role(Author)) 47 | self.assertTrue(self.john.has_role(LibraryOwner)) 48 | 49 | # Author role assigned to the book, but the chapter 50 | # and the paragraph are children of book. 51 | self.assertTrue(self.bob.has_permission('testapp1.add_book', self.book)) 52 | self.assertTrue(self.bob.has_permission('testapp1.add_chapter', self.chapter)) 53 | self.assertTrue(self.bob.has_permission('testapp1.add_paragraph', self.paragraph)) 54 | 55 | # Another books are denied. 56 | self.assertFalse(self.bob.has_permission('testapp1.add_book', self.another_book)) 57 | 58 | 59 | # As the role Author denies 'review', the 60 | # children will deny as well. 61 | self.assertFalse(self.bob.has_permission('testapp1.review', self.book)) 62 | self.assertFalse(self.bob.has_permission('testapp1.review', self.chapter)) 63 | self.assertFalse(self.bob.has_permission('testapp1.review', self.paragraph)) 64 | 65 | # Reviewer role assigned to the book, but the chapter 66 | # and the paragraph are children of book. 67 | self.book.assign_role(self.mike, Reviewer) 68 | self.assertTrue(self.mike.has_permission('testapp1.review', self.book)) 69 | self.assertTrue(self.mike.has_permission('testapp1.review', self.chapter)) 70 | self.assertTrue(self.mike.has_permission('testapp1.review', self.paragraph)) 71 | 72 | # Another books are denied. 73 | self.assertFalse(self.mike.has_permission('testapp1.review', self.another_book)) 74 | 75 | # As the role Reviewer only allows 'review', the 76 | # children will deny as well. 77 | self.assertFalse(self.mike.has_permission('testapp1.add_book', self.book)) 78 | 79 | # Check if the library owner inherits the permissions from all of them. 80 | self.assertTrue(self.john.has_permission('testapp1.change_book', self.book)) 81 | self.assertTrue(self.john.has_permission('testapp1.change_book', self.chapter)) 82 | self.assertTrue(self.john.has_permission('testapp1.change_book', self.paragraph)) 83 | 84 | self.book.remove_role(self.bob, Author) 85 | self.library.remove_roles([self.john], LibraryOwner) 86 | 87 | def test_anyobject_permissions(self): 88 | """ test the has_permission function using any_object=True """ 89 | self.john.assign_role(LibraryOwner, self.library) 90 | self.assertTrue(self.john.has_permission('testapp1.change_book', self.book)) 91 | self.assertTrue(tg_has_perm(self.john, 'testapp1.change_book', self.book)) 92 | 93 | # Without an object, we got a False. 94 | self.assertFalse(self.john.has_permission('testapp1.change_book')) 95 | self.assertFalse(tg_has_perm(self.john, 'testapp1.change_book')) 96 | 97 | # Using any_object=True we are allowing to bypass the instance check. 98 | self.assertTrue(self.john.has_permission('testapp1.change_book', any_object=True)) 99 | self.assertTrue(tg_has_perm(self.john, 'testapp1.change_book', 'any')) 100 | 101 | # Provide an object and use any_object=True 102 | # at same time is not allowed. 103 | with self.assertRaises(NotAllowed): 104 | self.john.has_permission('testapp1.change_book', self.book, any_object=True) 105 | 106 | def test_persistent_permission(self): 107 | """ test permission behavior over persistent mode """ 108 | self.john.assign_role(Coordenator) 109 | self.john.assign_role(Author, self.book) 110 | self.assertFalse(self.john.has_permission('testapp1.review', self.book)) 111 | self.assertFalse(tg_has_perm(self.john, 'testapp1.review', self.book)) 112 | self.assertFalse(tg_has_perm(self.john, 'testapp1.review', self.book, 'non-persistent')) 113 | 114 | # Using the templatetag option incorrectly. 115 | with self.assertRaises(NotAllowed): 116 | tg_has_perm(self.john, 'testapp1.review', self.book, 'i am not a option.') 117 | 118 | # Test persistent mode using the 'persistent' bypass kwarg. 119 | self.assertTrue(self.john.has_permission('testapp1.review', self.book, persistent=True)) 120 | self.assertTrue(tg_has_perm(self.john, 'testapp1.review', self.book, 'persistent')) 121 | 122 | new_settings = {'PERSISTENT': True} 123 | with self.settings(IMPROVED_PERMISSIONS_SETTINGS=new_settings): 124 | # Test persistent mode using default settings. 125 | self.assertTrue(self.john.has_permission('testapp1.review', self.book)) 126 | 127 | # Checking if the bypass still has preference. 128 | self.assertTrue(tg_has_perm(self.john, 'testapp1.review', self.book, 'persistent')) 129 | self.assertFalse(tg_has_perm(self.john, 'testapp1.review', self.book, 'non-persistent')) 130 | 131 | # Testing using a role with inherit=False in order to ignore it aswell. 132 | self.john.assign_role(LibraryWorker, self.library) 133 | self.assertTrue(self.john.has_permission('testapp1.review', self.book)) 134 | 135 | def test_get_users(self): 136 | """ test if the get_user method works fine """ 137 | 138 | # There is no user for the "library" yet. 139 | self.assertEqual(self.library.get_user(), None) 140 | 141 | self.book.assign_roles([self.bob], Author) 142 | self.john.assign_role(Advisor, self.bob) 143 | self.library.assign_role(self.mike, LibraryOwner) 144 | 145 | # Get single user instance. 146 | result = self.library.get_user() 147 | self.assertEqual(result, self.mike) 148 | 149 | # Get single user instance and role class. 150 | result = self.library.get_user(LibraryOwner) 151 | self.assertEqual(result, self.mike) 152 | 153 | # Try to get a user from a object with 154 | # Role class unique=False 155 | self.assertEqual(self.book.get_user(), None) 156 | 157 | # Get all users with who is Author of "book". 158 | self.book.assign_role(self.john, Author) 159 | result = self.book.get_users(Author) 160 | self.assertEqual(list(result), [self.bob, self.john]) 161 | 162 | # Trying to use the reverse GerericRelation. 163 | reverse = list(self.book.roles.values_list('user', flat=True)) 164 | self.assertEqual(reverse, [self.bob.id, self.john.id]) 165 | 166 | # Get all users with any role to "library". 167 | result = self.library.get_users() 168 | self.assertEqual(list(result), [self.mike]) 169 | 170 | # Try to get users list from a object 171 | # using a wrong Role class. 172 | with self.assertRaises(NotAllowed): 173 | self.library.get_users(Advisor) 174 | 175 | # Get all users with any role and any object. 176 | # As the result is a QuerySet, we use order_by() 177 | result = get_users().order_by('-username') 178 | self.assertEqual(list(result), [self.mike, self.john, self.bob]) 179 | 180 | self.john.remove_role(Advisor) 181 | self.assertFalse(self.john.has_role(Advisor, self.bob)) 182 | 183 | def test_get_role(self): 184 | """ test if the get_role and get_roles methods work fine """ 185 | self.book.assign_role(self.mike, Author) 186 | 187 | self.assertTrue(self.book.has_role(self.mike)) 188 | self.assertTrue(self.book.has_role(self.mike, Author)) 189 | 190 | # Check for single role class. 191 | self.assertEqual(self.mike.get_role(), Author) 192 | self.assertEqual(self.mike.get_role(self.book), Author) 193 | self.assertEqual(self.book.get_role(self.mike), Author) 194 | 195 | self.mike.assign_role(Reviewer, self.book) 196 | 197 | # Check for multiple role class. 198 | self.assertEqual(self.mike.get_roles(), [Author, Reviewer]) 199 | self.assertEqual(self.mike.get_roles(self.book), [Author, Reviewer]) 200 | self.assertEqual(self.book.get_roles(self.mike), [Author, Reviewer]) 201 | 202 | def test_remove_all(self): 203 | """ test if the remove_all shortcut works fine """ 204 | self.book.assign_role(self.john, Author) 205 | self.book.assign_role(self.mike, Reviewer) 206 | 207 | self.assertEqual(list(self.book.get_users()), [self.john, self.mike]) 208 | self.assertTrue(self.john.has_permission('testapp1.add_book', self.book)) 209 | self.assertTrue(self.mike.has_permission('testapp1.review', self.book)) 210 | 211 | # Remove all Authors from "book". 212 | self.book.remove_all(Author) 213 | self.assertEqual(list(self.book.get_users()), [self.mike]) 214 | self.assertFalse(self.john.has_permission('testapp1.add_book', self.book)) 215 | 216 | # Remove all Reviewers from "book". 217 | self.book.remove_all(Reviewer) 218 | self.assertEqual(list(self.book.get_users()), []) 219 | self.assertFalse(self.mike.has_permission('testapp1.review', self.book)) 220 | 221 | # Assigning the roles again. 222 | self.book.assign_role(self.john, Author) 223 | self.book.assign_role(self.mike, Reviewer) 224 | 225 | self.assertEqual(list(self.book.get_users()), [self.john, self.mike]) 226 | self.assertTrue(self.john.has_permission('testapp1.add_book', self.book)) 227 | self.assertTrue(self.mike.has_permission('testapp1.review', self.book)) 228 | 229 | # Remove any user of any role from "book". 230 | self.book.remove_all() 231 | self.assertEqual(list(self.book.get_users()), []) 232 | self.assertFalse(self.john.has_permission('testapp1.add_book', self.book)) 233 | self.assertFalse(self.mike.has_permission('testapp1.review', self.book)) 234 | 235 | def test_get_objects(self): 236 | """ test if the get_objects method works fine """ 237 | self.book.assign_role(self.bob, Author) 238 | self.john.assign_role(Advisor, self.bob) 239 | self.library.assign_role(self.john, LibraryOwner) 240 | 241 | # Get all objects where john is "Advisor". 242 | result = self.john.get_objects(Advisor) 243 | self.assertEqual(result, [self.bob]) 244 | 245 | # Get all objects of john for any Role. 246 | result = self.john.get_objects() 247 | self.assertEqual(result, [self.library, self.bob]) 248 | 249 | # Get all objects of john but only of User model. 250 | result = self.john.get_objects(model=MyUser) 251 | self.assertEqual(result, [self.bob]) 252 | -------------------------------------------------------------------------------- /improved_permissions/utils.py: -------------------------------------------------------------------------------- 1 | """ permissions utils """ 2 | # pylint: disable=too-many-lines 3 | import inspect 4 | 5 | from improved_permissions.exceptions import (ImproperlyConfigured, NotAllowed, 6 | ParentNotFound, RoleNotFound) 7 | 8 | CACHE_KEY_PREFIX = 'dip' 9 | 10 | 11 | def is_role(role_class): 12 | """ 13 | Check if the argument is a valid Role class. 14 | This method DOES NOT check if the class is registered in RoleManager. 15 | """ 16 | from improved_permissions.roles import Role 17 | return inspect.isclass(role_class) and issubclass(role_class, Role) and role_class != Role 18 | 19 | 20 | def get_config(key, default): 21 | """ 22 | Get the dictionary "IMPROVED_PERMISSIONS_SETTINGS" 23 | from the settings module. 24 | Return "default" if "key" is not present in 25 | the dictionary. 26 | """ 27 | from django.conf import settings 28 | 29 | config_dict = getattr(settings, 'IMPROVED_PERMISSIONS_SETTINGS', None) 30 | if config_dict: 31 | if key in config_dict: 32 | return config_dict[key] 33 | return default 34 | 35 | 36 | def dip_cache(): 37 | """ 38 | Proxy method used to get the cache 39 | object belonging to the DIP. 40 | """ 41 | from django.core.cache import caches 42 | return caches[get_config('CACHE', 'default')] 43 | 44 | 45 | def autodiscover(): 46 | """ 47 | Find for Role class implementations 48 | on all apps in order to auto-register. 49 | """ 50 | from importlib import import_module 51 | from django.conf import settings 52 | from improved_permissions.roles import RoleManager 53 | 54 | try: 55 | # Check if the Permission model 56 | # is ready to be used. 57 | from django.contrib.auth.models import Permission 58 | Permission.objects.count() 59 | except: # pragma: no cover 60 | return 61 | 62 | # Remove all references about previous 63 | # role classes and erase all cache. 64 | RoleManager.cleanup() 65 | dip_cache().clear() 66 | 67 | # Looking for Role classes in "roles.py". 68 | module = get_config('MODULE', 'roles') 69 | for app in settings.INSTALLED_APPS: 70 | try: 71 | mod = import_module('%s.%s' % (app, module)) 72 | rc_list = [obj[1] for obj in inspect.getmembers(mod, is_role)] 73 | for roleclass in rc_list: 74 | RoleManager.register_role(roleclass) 75 | except ImportError: 76 | pass 77 | 78 | # Clear the cache again after 79 | # all registrations. 80 | dip_cache().clear() 81 | 82 | 83 | def get_roleclass(role_class): 84 | """ 85 | Get the role class signature 86 | by string or by itself. 87 | """ 88 | from improved_permissions.roles import RoleManager 89 | roles_list = RoleManager.get_roles() 90 | 91 | if isinstance(role_class, str): 92 | # Trying to get the role class 93 | # via string representation. 94 | for role in roles_list: 95 | if role.get_class_name() == role_class: 96 | return role 97 | 98 | elif role_class in roles_list: 99 | # Already a Role class. 100 | return role_class 101 | 102 | raise RoleNotFound( 103 | "'%s' is not a registered role class." % role_class 104 | ) 105 | 106 | 107 | def get_model(model): 108 | """ 109 | Transforms a string representation 110 | into a valid Django Model class. 111 | """ 112 | from django.apps import apps 113 | from django.db.models import Model 114 | 115 | result = None 116 | if inspect.isclass(model) and issubclass(model, Model): 117 | result = model 118 | elif isinstance(model, str): 119 | app_label, modelname = model.split('.') 120 | try: 121 | result = apps.get_model(app_label, modelname) 122 | except LookupError: 123 | pass 124 | return result 125 | 126 | 127 | def string_to_permission(perm): 128 | """ 129 | Transforms a string representation 130 | into a Permission instance. 131 | """ 132 | from django.contrib.auth.models import Permission 133 | 134 | # Checking if the Permission instance 135 | # exists in the cache system. 136 | prefix = get_config('CACHE_PREFIX_KEY', CACHE_KEY_PREFIX) 137 | key = '{}-permission-{}'.format(prefix, perm) 138 | perm_obj = dip_cache().get(key) 139 | 140 | # If not, creates the query to 141 | # get the Permission instance 142 | # and store into the cache. 143 | if perm_obj is None: 144 | app, codename = perm.split('.') 145 | perm_obj = (Permission.objects 146 | .select_related('content_type') 147 | .filter(content_type__app_label=app, codename=codename) 148 | .get()) 149 | dip_cache().set(key, perm_obj) 150 | 151 | return perm_obj 152 | 153 | 154 | def permission_to_string(perm): 155 | """ 156 | Transforms a Permission instance 157 | into a string representation. 158 | """ 159 | app_label = perm.content_type.app_label 160 | codename = perm.codename 161 | return '%s.%s' % (app_label, codename) 162 | 163 | 164 | def get_permissions_list(models_list): 165 | """ 166 | Given a list of Model instances or a Model 167 | classes, return all Permissions related to it. 168 | """ 169 | from django.contrib.auth.models import Permission 170 | from django.contrib.contenttypes.models import ContentType 171 | 172 | ct_list = ContentType.objects.get_for_models(*models_list) 173 | ct_ids = [ct.id for cls, ct in ct_list.items()] 174 | 175 | return list(Permission.objects.filter(content_type_id__in=ct_ids)) 176 | 177 | 178 | def get_parents(model): 179 | """ 180 | Return the list of instances refered 181 | as "parents" of a given model instance. 182 | """ 183 | result = list() 184 | options = getattr(model, 'RoleOptions', None) 185 | if options: 186 | parents_list = getattr(options, 'permission_parents', None) 187 | if parents_list: 188 | for parent in parents_list: 189 | field = getattr(model, parent, False) 190 | if field is False: 191 | # Field does not exist. 192 | raise ParentNotFound('The field "%s" was not found in the ' 193 | 'model "%s".' % (parent, str(model))) 194 | elif field is not None: 195 | # Only getting non-null parents. 196 | result.append(field) 197 | return result 198 | 199 | 200 | def is_unique_together(model): 201 | """ 202 | Return True if the model does not 203 | accept multiple roles attached to 204 | it using the user instance. 205 | """ 206 | options = getattr(model, 'RoleOptions', None) 207 | if options: 208 | unique = getattr(options, 'unique_together', None) 209 | if unique: 210 | if isinstance(unique, bool): 211 | return unique 212 | raise ImproperlyConfigured('The field "unique_together" of "%s" must ' 213 | 'be a bool value.' % (str(model))) 214 | return False 215 | 216 | 217 | def inherit_check(role_s, permission): 218 | """ 219 | Check if the role class has the following 220 | permission in inherit mode. 221 | """ 222 | from improved_permissions.roles import ALLOW_MODE 223 | 224 | role = get_roleclass(role_s) 225 | if role.inherit is True: 226 | if role.get_inherit_mode() == ALLOW_MODE: 227 | return True if permission in role.inherit_allow else False 228 | return False if permission in role.inherit_deny else True 229 | return False 230 | 231 | 232 | def cleanup_handler(sender, instance, **kwargs): # pylint: disable=unused-argument 233 | """ 234 | This function is attached to the post_delete 235 | signal of all models of Django. Used to remove 236 | useless role instances and permissions. 237 | """ 238 | from django.contrib.contenttypes.models import ContentType 239 | from improved_permissions.models import UserRole 240 | 241 | ct_obj = ContentType.objects.get_for_model(instance) 242 | ur_list = UserRole.objects.filter(content_type=ct_obj.id, object_id=instance.id) 243 | 244 | for ur_obj in ur_list: 245 | # Cleaning the cache system. 246 | delete_from_cache(ur_obj.user, instance) 247 | ur_obj.delete() 248 | 249 | 250 | def register_cleanup(): 251 | """ 252 | Register the function "cleanup_handler" 253 | to all models in the project. 254 | """ 255 | from django.apps import apps 256 | from django.db.models.signals import post_delete 257 | from improved_permissions.models import UserRole, RolePermission 258 | 259 | ignore = [UserRole, RolePermission] 260 | for model in apps.get_models(): 261 | if model not in ignore and hasattr(model, 'id'): 262 | post_delete.connect(cleanup_handler, sender=model, dispatch_uid=str(model)) 263 | 264 | 265 | def check_my_model(role, obj): 266 | """ 267 | if both are provided, check if obj 268 | (instance or model class) belongs 269 | to the role class. 270 | """ 271 | if role and obj and not role.is_my_model(obj): 272 | model_name = obj._meta.model # pylint: disable=protected-access 273 | raise NotAllowed('The model "%s" does not belong to the Role "%s"' 274 | '.' % (model_name, role.get_verbose_name())) 275 | 276 | 277 | def generate_cache_key(user, obj, any_object): 278 | """ 279 | Generate a md5 digest based on the 280 | string representation of the user 281 | and the object passed via arguments. 282 | """ 283 | from hashlib import md5 284 | 285 | key = md5() 286 | str_key = str(user.__class__) + str(user) + str(user.id) 287 | if obj: 288 | str_key += str(obj.__class__) + str(obj) + str(obj.id) 289 | elif any_object: 290 | str_key += 'any' 291 | 292 | key.update(str_key.encode('utf-8')) 293 | prefix = get_config('CACHE_PREFIX_KEY', CACHE_KEY_PREFIX) 294 | return '{}-userrole-{}'.format(prefix, key.hexdigest()) 295 | 296 | 297 | def delete_from_cache(user, obj): 298 | """ 299 | Delete all permissions data from 300 | the cache about the user and the 301 | object passed via arguments. 302 | """ 303 | key = generate_cache_key(user, obj, any_object=False) 304 | dip_cache().delete(key) 305 | 306 | key = generate_cache_key(user, obj=None, any_object=True) 307 | dip_cache().delete(key) 308 | 309 | 310 | def get_from_cache(user, obj, any_object): 311 | """ 312 | Get all permissions data about 313 | the user and the object passed 314 | via arguments e store it in 315 | the Django cache system. 316 | """ 317 | from django.contrib.contenttypes.models import ContentType 318 | from improved_permissions.models import UserRole 319 | 320 | # Key preparation. 321 | key = generate_cache_key(user, obj, any_object) 322 | 323 | # Check for the cached data. 324 | data = dip_cache().get(key) 325 | if data is None: 326 | query = UserRole.objects.prefetch_related('accesses').filter(user=user) 327 | 328 | # Filtering by object. 329 | if obj: 330 | ct_obj = ContentType.objects.get_for_model(obj) 331 | query = query.filter(content_type=ct_obj.id).filter(object_id=obj.id) 332 | elif not any_object: 333 | query = query.filter(content_type__isnull=True).filter(object_id__isnull=True) 334 | 335 | # Getting only the required values. 336 | query = query.values_list( 337 | 'role_class', 338 | 'accesses__permission', 339 | 'accesses__access' 340 | ) 341 | 342 | # Transform the query result into 343 | # a dictionary. 344 | data = dict() 345 | for item in query: 346 | perms_list = data.get(item[0], []) 347 | if item[0] and item[1]: 348 | perms_list.append((item[1], item[2])) 349 | data[item[0]] = perms_list 350 | 351 | # Ordering the tuple by their Role Ranking values. 352 | data = sorted(data.items(), key=lambda role: get_roleclass(role[0]).ranking) 353 | 354 | # Now, we get only the data from the 355 | # first role class found. 356 | data = data[0] if data else () 357 | 358 | # Set the data to the cache. 359 | dip_cache().set(key, data) 360 | 361 | return data 362 | --------------------------------------------------------------------------------