Commit 0ac0d872 authored by Michael Fladischer's avatar Michael Fladischer

Update upstream source from tag 'upstream/2.0.0'

Update to upstream version '2.0.0'
with Debian dir ec233b942a9abef4b05f50f8542789191c9913df
parents 296f3bf2 7a2f6d38
[report]
exclude_lines =
pragma: no cover
omit =
*/python?.?/*
*/site-packages/nose/*
*/rules/compat/*
*/rules/apps.py
# ---------------------------------------------------------------------------
# Python support matrix per Django version
#
# 2.6 1.5 1.6
# 2.7 1.5 1.6 1.7 1.8 1.9 1.10 1.11
# 3.3 1.5 1.6 1.7 1.8
# 3.4 1.7 1.8 1.9 1.10 1.11 2.0
# 3.5 1.8 1.9 1.10 1.11 2.0
# 3.6 1.10 1.11 2.0
# ---------------------------------------------------------------------------
sudo: false sudo: false
language: python language: python
python:
- "2.7"
- "3.4"
- "3.5"
- "3.6"
- "pypy"
- "pypy3"
matrix: matrix:
include: include:
- { python: 2.6, env: TOXENV=py26-django15 } - {python: "3.6", env: TOXENV=packaging}
- { python: 2.6, env: TOXENV=py26-django16 }
- { python: 2.7, env: TOXENV=py27-django15 }
- { python: 2.7, env: TOXENV=py27-django16 }
- { python: 2.7, env: TOXENV=py27-django17 }
- { python: 2.7, env: TOXENV=py27-django18 }
- { python: 2.7, env: TOXENV=py27-django19 }
- { python: 2.7, env: TOXENV=py27-django110 }
- { python: 2.7, env: TOXENV=py27-django111 }
- { python: 3.3, env: TOXENV=py33-django15 }
- { python: 3.3, env: TOXENV=py33-django16 }
- { python: 3.3, env: TOXENV=py33-django17 }
- { python: 3.3, env: TOXENV=py33-django18 }
- { python: 3.4, env: TOXENV=py34-django17 }
- { python: 3.4, env: TOXENV=py34-django18 }
- { python: 3.4, env: TOXENV=py34-django19 }
- { python: 3.4, env: TOXENV=py34-django110 }
- { python: 3.4, env: TOXENV=py34-django20 }
- { python: 3.4, env: TOXENV=py34-django111 }
- { python: 3.5, env: TOXENV=py35-django18 }
- { python: 3.5, env: TOXENV=py35-django19 }
- { python: 3.5, env: TOXENV=py35-django110 }
- { python: 3.5, env: TOXENV=py35-django111 }
- { python: 3.5, env: TOXENV=py35-django20 }
- { python: 3.6, env: TOXENV=py36-django110 }
- { python: 3.6, env: TOXENV=py36-django111 }
- { python: 3.6, env: TOXENV=py36-django20 }
install: install:
- pip install tox - pip install tox tox-travis tox-venv
- pip install coveralls
script: tox script: tox
after_success: after_success:
- coveralls - coveralls
Changelog Changelog
========= =========
## v2.0.0 - 2018/07/22
- Removed support for Python 2.6 and 3.3
- Removed support for Django versions before 1.11
- Removed ``SkipPredicate`` exception and ``skip`` method of ``Predicate``
- Removed ``replace_rule`` and related APIs
- Added ``set_rule`` and related APIs to safely replace a rule without having
to ensure one already exists
- Added compatibility with Django v2.1
- Re-introduced support for PyPy and PyPy 3
- Changed Python and Django supported versions policy to exclude end-of-life
versions. Support for EOL'd versions will be dropped in minor version
updates of ``rules`` from now on.
## v1.4.0 - 2018/07/21
- Fixed masking AttributeErrors raised from CBV get_object
- Fixed compatibility with `inspect` in newer Python 3 versions
- Added ability to replace rules and permissions
## v1.3.0 - 2017/12/13 ## v1.3.0 - 2017/12/13
- Added support for Django 2.0 - Added support for Django 2.0
......
...@@ -43,6 +43,7 @@ Table of Contents ...@@ -43,6 +43,7 @@ Table of Contents
================= =================
- `Requirements`_ - `Requirements`_
- `Upgrading from 1.x`_
- `How to install`_ - `How to install`_
- `Configuring Django`_ - `Configuring Django`_
...@@ -76,8 +77,34 @@ Table of Contents ...@@ -76,8 +77,34 @@ Table of Contents
Requirements Requirements
============ ============
``rules`` requires Python 2.6/3.3 or newer. It can optionally integrate with ``rules`` requires Python 2.7/3.4 or newer. It can optionally integrate with
Django, in which case requires Django 1.5 or newer. Django, in which case requires Django 1.11 or newer.
*Note*: At any given moment in time, ``rules`` will maintain support for all
currently supported Django versions, while dropping support for those versions
that reached end-of-life in minor releases. See the `Supported Versions`_
section on Django Project website for the current state and timeline.
.. _Supported Versions: https://www.djangoproject.com/download/#supported-versions
Upgrading from 1.x
==================
* Support Python 2.6 and 3.3, and Django versions before 1.11 has been
dropped.
* The ``SkipPredicate`` exception and ``skip()`` method of ``Predicate``,
that were used to signify that a predicate should be skipped, have been
removed. You may return ``None`` from your predicate to achieve this.
* The APIs to replace a rule's predicate have been renamed and their
behaviour changed. ``replace_rule`` and ``replace_perm`` functions and
``replace_rule`` method of ``RuleSet`` have been renamed to ``set_rule``,
``set_perm`` and ``RuleSet.set_perm`` respectively. The old behaviour was
to raise a ``KeyError`` if a rule by the given name did not exist. Since
version 2.0 this has changed and you can safely use ``set_*`` to set a
rule's predicate without having to ensure the rule exists first.
How to install How to install
...@@ -267,12 +294,7 @@ We can now update our ``can_edit_book`` rule: ...@@ -267,12 +294,7 @@ We can now update our ``can_edit_book`` rule:
.. code:: python .. code:: python
>>> rules.add_rule('can_edit_book', is_book_author_or_editor) >>> rules.set_rule('can_edit_book', is_book_author_or_editor)
Traceback (most recent call last):
...
KeyError: A rule with name `can_edit_book` already exists
>>> rules.remove_rule('can_edit_book')
>>> rules.add_rule('can_edit_book', is_book_author_or_editor)
>>> rules.test_rule('can_edit_book', adrian, guidetodjango) >>> rules.test_rule('can_edit_book', adrian, guidetodjango)
True True
>>> rules.test_rule('can_delete_book', adrian, guidetodjango) >>> rules.test_rule('can_delete_book', adrian, guidetodjango)
...@@ -425,11 +447,9 @@ For more information on the decorator and helper function, refer to the ...@@ -425,11 +447,9 @@ For more information on the decorator and helper function, refer to the
Using the class-based view mixin Using the class-based view mixin
++++++++++++++++++++++++++++++++ ++++++++++++++++++++++++++++++++
Django 1.9 introduced a new set of access mixins that you can use in your Django includes a set of access mixins that you can use in your class-based
class-based views to enforce authorization. ``rules`` extends this framework views to enforce authorization. ``rules`` extends this framework to provide
to provide a mixin for object-level permissions, ``PermissionRequiredMixin``. object-level permissions via a mixin, ``PermissionRequiredMixin``.
Note that ``rules`` will seamlessly fall back to importing its own copy of
Django's access mixins module for versions of Django prior to 1.9.
The following example will automatically test for permission against the The following example will automatically test for permission against the
instance returned by the view's ``get_object`` method: instance returned by the view's ``get_object`` method:
...@@ -490,11 +510,14 @@ set to also use ``rules`` to authorize any add/change/delete actions in the ...@@ -490,11 +510,14 @@ set to also use ``rules`` to authorize any add/change/delete actions in the
Admin. The Admin asks for *four* different permissions, depending on action: Admin. The Admin asks for *four* different permissions, depending on action:
- ``<app_label>.add_<modelname>`` - ``<app_label>.add_<modelname>``
- ``<app_label>.view_<modelname>``
- ``<app_label>.change_<modelname>`` - ``<app_label>.change_<modelname>``
- ``<app_label>.delete_<modelname>`` - ``<app_label>.delete_<modelname>``
- ``<app_label>`` - ``<app_label>``
The first three are obvious. The fourth is the required permission for an app *Note:* view permission is new in Django v2.1.
The first four are obvious. The fifth is the required permission for an app
to be displayed in the Admin's "dashboard". Here's some rules for our to be displayed in the Admin's "dashboard". Here's some rules for our
imaginary ``books`` app as an example: imaginary ``books`` app as an example:
...@@ -502,6 +525,7 @@ imaginary ``books`` app as an example: ...@@ -502,6 +525,7 @@ imaginary ``books`` app as an example:
>>> rules.add_perm('books', rules.always_allow) >>> rules.add_perm('books', rules.always_allow)
>>> rules.add_perm('books.add_book', is_staff) >>> rules.add_perm('books.add_book', is_staff)
>>> rules.add_perm('books.view_book', is_staff | has_secret_access_code)
>>> rules.add_perm('books.change_book', is_staff) >>> rules.add_perm('books.change_book', is_staff)
>>> rules.add_perm('books.delete_book', is_staff) >>> rules.add_perm('books.delete_book', is_staff)
...@@ -513,11 +537,10 @@ If you'd like to tell Django whether a user has permissions on a specific ...@@ -513,11 +537,10 @@ If you'd like to tell Django whether a user has permissions on a specific
object, you'd have to override the following methods of a model's object, you'd have to override the following methods of a model's
``ModelAdmin``: ``ModelAdmin``:
- ``has_view_permission(user, obj=None)``
- ``has_change_permission(user, obj=None)`` - ``has_change_permission(user, obj=None)``
- ``has_delete_permission(user, obj=None)`` - ``has_delete_permission(user, obj=None)``
**Note:** There's also ``has_add_permission(user)`` but is not relevant here.
``rules`` comes with a custom ``ModelAdmin`` subclass, ``rules`` comes with a custom ``ModelAdmin`` subclass,
``rules.contrib.admin.ObjectPermissionsModelAdmin``, that overrides these ``rules.contrib.admin.ObjectPermissionsModelAdmin``, that overrides these
methods to pass on the edited model instance to the authorization backends, methods to pass on the edited model instance to the authorization backends,
...@@ -544,6 +567,11 @@ Now this allows you to specify permissions like this: ...@@ -544,6 +567,11 @@ Now this allows you to specify permissions like this:
>>> rules.add_perm('books.change_book', is_book_author_or_editor) >>> rules.add_perm('books.change_book', is_book_author_or_editor)
>>> rules.add_perm('books.delete_book', is_book_author) >>> rules.add_perm('books.delete_book', is_book_author)
To preserve backwards compatibility, Django will ask for either *view* or
*change* permission. For maximum flexibility, ``rules`` behaves subtly
different: ``rules`` will ask for the change permission if and only if no rule
exists for the view permission.
Advanced features Advanced features
================= =================
...@@ -651,10 +679,6 @@ You may skip evaluation by returning ``None`` from your predicate: ...@@ -651,10 +679,6 @@ You may skip evaluation by returning ``None`` from your predicate:
Returning ``None`` signifies that the predicate need not be evaluated, thus Returning ``None`` signifies that the predicate need not be evaluated, thus
leaving the predicate result up to that point unchanged. leaving the predicate result up to that point unchanged.
**Note:** This is new in version 1.1.0. It was possible to skip predicates in
older versions by calling the predicate's ``skip()`` method, but this has been
deprecated and support will be completely removed in a future version.
Logging predicate evaluation Logging predicate evaluation
---------------------------- ----------------------------
...@@ -707,9 +731,9 @@ On the other hand, because importing predicates from all over the place in ...@@ -707,9 +731,9 @@ On the other hand, because importing predicates from all over the place in
order to define rules can lead to circular imports and broken hearts, it's order to define rules can lead to circular imports and broken hearts, it's
best to further split predicates and rules in different modules. best to further split predicates and rules in different modules.
If using Django 1.7 and later, ``rules`` may optionally be configured to ``rules`` may optionally be configured to autodiscover ``rules.py`` modules in
autodiscover ``rules.py`` modules in your apps and import them at startup. To your apps and import them at startup. To have ``rules`` do so, just edit your
have ``rules`` do so, just edit your ``INSTALLED_APPS`` setting: ``INSTALLED_APPS`` setting:
.. code:: python .. code:: python
...@@ -719,8 +743,8 @@ have ``rules`` do so, just edit your ``INSTALLED_APPS`` setting: ...@@ -719,8 +743,8 @@ have ``rules`` do so, just edit your ``INSTALLED_APPS`` setting:
) )
**Note:** On Python 2, you must also add the following to the top of your **Note:** On Python 2, you must also add the following to the top of your
``rules.py`` file, or you'll get import errors trying to import ``rules.py`` file, or you'll get import errors trying to import ``rules``
``django-rules`` itself: itself:
.. code:: python .. code:: python
...@@ -730,7 +754,8 @@ have ``rules`` do so, just edit your ``INSTALLED_APPS`` setting: ...@@ -730,7 +754,8 @@ have ``rules`` do so, just edit your ``INSTALLED_APPS`` setting:
API Reference API Reference
============= =============
Everything is accessible from the root ``rules`` module. The core APIs are accessible from the root ``rules`` module. Django-specific
functionality for the Admin and views is available from ``rules.contrib``.
Class ``rules.Predicate`` Class ``rules.Predicate``
...@@ -795,6 +820,9 @@ Instance methods ...@@ -795,6 +820,9 @@ Instance methods
Adds a predicate to the rule set, assigning it to the given rule name. Adds a predicate to the rule set, assigning it to the given rule name.
Raises ``KeyError`` if another rule with that name already exists. Raises ``KeyError`` if another rule with that name already exists.
``set_rule(name, predicate)``
Set the rule with the given name, regardless if one already exists.
``remove_rule(name)`` ``remove_rule(name)``
Remove the rule with the given name. Raises ``KeyError`` if a rule with Remove the rule with the given name. Raises ``KeyError`` if a rule with
that name does not exist. that name does not exist.
...@@ -883,6 +911,10 @@ Managing the shared rule set ...@@ -883,6 +911,10 @@ Managing the shared rule set
``add_rule(name, predicate)`` ``add_rule(name, predicate)``
Adds a rule to the shared rule set. See ``RuleSet.add_rule``. Adds a rule to the shared rule set. See ``RuleSet.add_rule``.
``set_rule(name, predicate)``
Set the rule with the given name from the shared rule set. See
``RuleSet.set_rule``.
``remove_rule(name)`` ``remove_rule(name)``
Remove a rule from the shared rule set. See ``RuleSet.remove_rule``. Remove a rule from the shared rule set. See ``RuleSet.remove_rule``.
...@@ -900,6 +932,9 @@ Managing the permissions rule set ...@@ -900,6 +932,9 @@ Managing the permissions rule set
``add_perm(name, predicate)`` ``add_perm(name, predicate)``
Adds a rule to the permissions rule set. See ``RuleSet.add_rule``. Adds a rule to the permissions rule set. See ``RuleSet.add_rule``.
``set_perm(name, predicate)``
Replace a rule from the permissions rule set. See ``RuleSet.set_rule``.
``remove_perm(name)`` ``remove_perm(name)``
Remove a rule from the permissions rule set. See ``RuleSet.remove_rule``. Remove a rule from the permissions rule set. See ``RuleSet.remove_rule``.
......
from .rulesets import RuleSet, add_rule, remove_rule, rule_exists, test_rule from .rulesets import RuleSet, add_rule, set_rule, remove_rule, rule_exists, test_rule
from .permissions import add_perm, remove_perm, perm_exists, has_perm from .permissions import add_perm, set_perm, remove_perm, perm_exists, has_perm
from .predicates import (Predicate, predicate, always_true, always_false, from .predicates import (Predicate, predicate, always_true, always_false,
always_allow, always_deny, is_authenticated, always_allow, always_deny, is_authenticated,
is_superuser, is_staff, is_active, is_group_member) is_superuser, is_staff, is_active, is_group_member)
VERSION = (1, 3, 0, 'final', 1) VERSION = (2, 0, 0, 'final', 1)
default_app_config = 'rules.apps.RulesConfig' default_app_config = 'rules.apps.RulesConfig'
"""
This is a copy of django.contrib.auth.mixins module, shipped with django-rules
for compatibility with Django versions before 1.9. Used under permission by
the Django Software Foundation.
Copyright (c) Django Software Foundation and individual contributors.
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. Neither the name of Django nor the names of its contributors may be used
to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
from django.conf import settings
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.contrib.auth.views import redirect_to_login
from django.core.exceptions import ImproperlyConfigured, PermissionDenied
from django.utils import six
from django.utils.encoding import force_text
class AccessMixin(object):
"""
Abstract CBV mixin that gives access mixins the same customizable
functionality.
"""
login_url = None
permission_denied_message = ''
raise_exception = False
redirect_field_name = REDIRECT_FIELD_NAME
def get_login_url(self):
"""
Override this method to override the login_url attribute.
"""
login_url = self.login_url or settings.LOGIN_URL
if not login_url:
raise ImproperlyConfigured(
'{0} is missing the login_url attribute. Define {0}.login_url, settings.LOGIN_URL, or override '
'{0}.get_login_url().'.format(self.__class__.__name__)
)
return force_text(login_url)
def get_permission_denied_message(self):
"""
Override this method to override the permission_denied_message attribute.
"""
return self.permission_denied_message
def get_redirect_field_name(self):
"""
Override this method to override the redirect_field_name attribute.
"""
return self.redirect_field_name
def handle_no_permission(self):
if self.raise_exception:
raise PermissionDenied(self.get_permission_denied_message())
return redirect_to_login(self.request.get_full_path(), self.get_login_url(), self.get_redirect_field_name())
class LoginRequiredMixin(AccessMixin):
"""
CBV mixin which verifies that the current user is authenticated.
"""
def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated():
return self.handle_no_permission()
return super(LoginRequiredMixin, self).dispatch(request, *args, **kwargs)
class PermissionRequiredMixin(AccessMixin):
"""
CBV mixin which verifies that the current user has all specified
permissions.
"""
permission_required = None
def get_permission_required(self):
"""
Override this method to override the permission_required attribute.
Must return an iterable.
"""
if self.permission_required is None:
raise ImproperlyConfigured(
'{0} is missing the permission_required attribute. Define {0}.permission_required, or override '
'{0}.get_permission_required().'.format(self.__class__.__name__)
)
if isinstance(self.permission_required, six.string_types):
perms = (self.permission_required, )
else:
perms = self.permission_required
return perms
def has_permission(self):
"""
Override this method to customize the way permissions are checked.
"""
perms = self.get_permission_required()
return self.request.user.has_perms(perms)
def dispatch(self, request, *args, **kwargs):
if not self.has_permission():
return self.handle_no_permission()
return super(PermissionRequiredMixin, self).dispatch(request, *args, **kwargs)
class UserPassesTestMixin(AccessMixin):
"""
CBV Mixin that allows you to define a test function which must return True
if the current user can access the view.
"""
def test_func(self):
raise NotImplementedError(
'{0} is missing the implementation of the test_func() method.'.format(self.__class__.__name__)
)
def get_test_func(self):
"""
Override this method to use a different test_func method.
"""
return self.test_func
def dispatch(self, request, *args, **kwargs):
user_test_result = self.get_test_func()()
if not user_test_result:
return self.handle_no_permission()
return super(UserPassesTestMixin, self).dispatch(request, *args, **kwargs)
from __future__ import absolute_import
try:
from inspect import getfullargspec
except ImportError:
# Python 2 compatibility
from inspect import getargspec as getfullargspec
from inspect import ismethod, isfunction
from django.contrib import admin from django.contrib import admin
from django.contrib.auth import get_permission_codename
try: from ..permissions import perm_exists
from django.contrib.auth import get_permission_codename
except ImportError: # pragma: no cover
# Django < 1.6
def get_permission_codename(action, opts):
return '%s_%s' % (action, opts.object_name.lower())
class ObjectPermissionsModelAdminMixin(object): class ObjectPermissionsModelAdminMixin(object):
def has_view_permission(self, request, obj=None):
opts = self.opts
codename = get_permission_codename('view', opts)
perm = '%s.%s' % (opts.app_label, codename)
if perm_exists(perm):
return request.user.has_perm(perm, obj)
else:
return self.has_change_permission(request, obj)
def has_change_permission(self, request, obj=None): def has_change_permission(self, request, obj=None):
opts = self.opts opts = self.opts
codename = get_permission_codename('change', opts) codename = get_permission_codename('change', opts)
......
...@@ -2,6 +2,7 @@ from functools import wraps ...@@ -2,6 +2,7 @@ from functools import wraps
from django.conf import settings from django.conf import settings
from django.contrib.auth import REDIRECT_FIELD_NAME from django.contrib.auth import REDIRECT_FIELD_NAME
from django.contrib.auth import mixins
from django.contrib.auth.views import redirect_to_login from django.contrib.auth.views import redirect_to_login
from django.core.exceptions import PermissionDenied, ImproperlyConfigured, FieldError from django.core.exceptions import PermissionDenied, ImproperlyConfigured, FieldError
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
...@@ -9,12 +10,6 @@ from django.utils import six ...@@ -9,12 +10,6 @@ from django.utils import six
from django.utils.decorators import available_attrs from django.utils.decorators import available_attrs
from django.utils.encoding import force_text from django.utils.encoding import force_text
try:
from django.contrib.auth import mixins
except ImportError: # pragma: no cover
# Django < 1.9
from ..compat import access_mixins as mixins
# These are made available for convenience, as well as for use in Django # These are made available for convenience, as well as for use in Django
# versions before 1.9. For usage help see Django's docs for 1.9 or later. # versions before 1.9. For usage help see Django's docs for 1.9 or later.
...@@ -39,10 +34,10 @@ class PermissionRequiredMixin(mixins.PermissionRequiredMixin): ...@@ -39,10 +34,10 @@ class PermissionRequiredMixin(mixins.PermissionRequiredMixin):
``SingleObjectMixin``. Returns None if there's no ``get_object`` ``SingleObjectMixin``. Returns None if there's no ``get_object``
method. method.
""" """
try: if hasattr(self, 'get_object') and callable(self.get_object):
# Requires SingleObjectMixin or equivalent ``get_object`` method # Requires SingleObjectMixin or equivalent ``get_object`` method
return self.get_object() return self.get_object()
except AttributeError: # pragma: no cover else: # pragma: no cover
return None return None
def has_permission(self): def has_permission(self):
...@@ -124,7 +119,7 @@ def permission_required(perm, fn=None, login_url=None, raise_exception=False, re ...@@ -124,7 +119,7 @@ def permission_required(perm, fn=None, login_url=None, raise_exception=False, re
# Get the object to check permissions against # Get the object to check permissions against
if callable(fn): if callable(fn):
obj = fn(request, *args, **kwargs) obj = fn(request, *args, **kwargs)
else: else: # pragma: no cover
obj = fn obj = fn
# Get the user # Get the user
......
...@@ -8,6 +8,10 @@ def add_perm(name, pred): ...@@ -8,6 +8,10 @@ def add_perm(name, pred):
permissions.add_rule(name, pred) permissions.add_rule(name, pred)
def set_perm(name, pred):
permissions.set_rule(name, pred)
def remove_perm(name): def remove_perm(name):
permissions.remove_rule(name) permissions.remove_rule(name)
...@@ -21,7 +25,7 @@ def has_perm(name, *args, **kwargs): ...@@ -21,7 +25,7 @@ def has_perm(name, *args, **kwargs):
class ObjectPermissionBackend(object): class ObjectPermissionBackend(object):
def authenticate(self, username, password): def authenticate(self, *args, **kwargs):
return None return None
def has_perm(self, user, perm, *args, **kwargs): def has_perm(self, user, perm, *args, **kwargs):
......
import inspect
import logging import logging
import operator import operator
import threading import threading
from functools import partial, update_wrapper from functools import partial, update_wrapper
from warnings import warn
from .compat import inspect
logger = logging.getLogger('rules') logger = logging.getLogger('rules')
class SkipPredicate(Exception): def assert_has_kwonlydefaults(fn, msg):
""" argspec = inspect.getfullargspec(fn)
Use to reject usage of a predicate. if hasattr(argspec, 'kwonlyargs'):
""" if not argspec.kwonlyargs:
def __init__(self, *args, **kwargs): return
warn('Skipping predicates by raising the SkipPredicate exception ' if not argspec.kwonlydefaults or len(argspec.kwonlyargs) > len(argspec.kwonlydefaults.keys()):
'has been deprecated. Return `None` from your predicate instead.', raise TypeError(msg)
DeprecationWarning)
super(SkipPredicate, self).__init__(*args, **kwargs)
class Context(dict): class Context(dict):
...@@ -53,26 +51,29 @@ class Predicate(object): ...@@ -53,26 +51,29 @@ class Predicate(object):
# - fn(obj=None) # - fn(obj=None)
# - fn() # - fn()
assert callable(fn), 'The given predicate is not callable.' assert callable(fn), 'The given predicate is not callable.'
innerfn = fn
if isinstance(fn, Predicate): if isinstance(fn, Predicate):
fn, num_args, var_args, name = fn.fn, fn.num_args, fn.var_args, name or fn.name fn, num_args, var_args, name = fn.fn, fn.num_args, fn.var_args, name or fn.name
innerfn = fn
elif isinstance(fn, partial): elif isinstance(fn, partial):
argspec = inspect.getargspec(fn.func) innerfn = fn.func
argspec = inspect.getfullargspec(innerfn)
var_args = argspec.varargs is not None var_args = argspec.varargs is not None
num_args = len(argspec.args) - len(fn.args) num_args = len(argspec.args) - len(fn.args)
if inspect.ismethod(fn.func): if inspect.ismethod(innerfn):