Giter VIP home page Giter VIP logo

django-rules's Introduction

rules

rules is a tiny but powerful app providing object-level permissions to Django, without requiring a database. At its core, it is a generic framework for building rule-based systems, similar to decision trees. It can also be used as a standalone library in other contexts and frameworks.

image

image

image

image

image

image

Features

rules has got you covered. rules is:

  • Documented, tested, reliable and easy to use.
  • Versatile. Decorate callables to build complex graphs of predicates. Predicates can be any type of callable -- simple functions, lambdas, methods, callable class objects, partial functions, decorated functions, anything really.
  • A good Django citizen. Seamless integration with Django views, templates and the Admin for testing for object-level permissions.
  • Efficient and smart. No need to mess around with a database to figure out whether John really wrote that book.
  • Simple. Dive in the code. You'll need 10 minutes to figure out how it works.
  • Powerful. rules comes complete with advanced features, such as invocation context and storage for arbitrary data, skipping evaluation of predicates under specific conditions, logging of evaluated predicates and more!

Table of Contents

Requirements

rules requires Python 3.8 or newer. The last version to support Python 2.7 is rules 2.2. It can optionally integrate with Django, in which case requires Django 3.2 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.

Upgrading from 2.x

The are no significant changes between rules 2.x and 3.x except dropping support for Python 2, so before upgrading to 3.x you just need to make sure you're running a supported Python 3 version.

Upgrading from 1.x

  • Support for 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

Using pip:

$ pip install rules

Manually:

$ git clone https://github.com/dfunckt/django-rules.git
$ cd django-rules
$ python setup.py install

Run tests with:

$ ./runtests.sh

You may also want to read Best practices for general advice on how to use rules.

Configuring Django

Add rules to INSTALLED_APPS:

INSTALLED_APPS = (
    # ...
    'rules',
)

Add the authentication backend:

AUTHENTICATION_BACKENDS = (
    'rules.permissions.ObjectPermissionBackend',
    'django.contrib.auth.backends.ModelBackend',
)

Using Rules

rules is based on the idea that you maintain a dict-like object that maps string keys used as identifiers of some kind, to callables, called predicates. This dict-like object is actually an instance of RuleSet and the predicates are instances of Predicate.

Creating predicates

Let's ignore rule sets for a moment and go ahead and define a predicate. The easiest way is with the @predicate decorator:

>>> @rules.predicate
>>> def is_book_author(user, book):
...     return book.author == user
...
>>> is_book_author
<Predicate:is_book_author object at 0x10eeaa490>

This predicate will return True if the book's author is the given user, False otherwise.

Predicates can be created from any callable that accepts anything from zero to two positional arguments:

  • fn(obj, target)
  • fn(obj)
  • fn()

This is their generic form. If seen from the perspective of authorization in Django, the equivalent signatures are:

  • fn(user, obj)
  • fn(user)
  • fn()

Predicates can do pretty much anything with the given arguments, but must always return True if the condition they check is true, False otherwise. rules comes with several predefined predicates that you may read about later on in API Reference, that are mostly useful when dealing with authorization in Django.

Dynamic predicates

If needed predicates can be created dynamically depending on parameters:

import rules


def role_is(role_id):
    @rules.predicate
    def user_has_role(user):
        return user.role.id == role_id

    return user_has_role


rules.add_perm("reports.view_report_abc", role_is(12))
rules.add_perm("reports.view_report_xyz", role_is(13))

Setting up rules

Let's pretend that we want to let authors edit or delete their books, but not books written by other authors. So, essentially, what determines whether an author can edit or can delete a given book is whether they are its author.

In rules, such requirements are modelled as rules. A rule is a map of a unique identifier (eg. "can edit") to a predicate. Rules are grouped together into a rule set. rules has two predefined rule sets:

  • A default rule set storing shared rules.
  • Another rule set storing rules that serve as permissions in a Django context.

So, let's define our first couple of rules, adding them to the shared rule set. We can use the is_book_author predicate we defined earlier:

>>> rules.add_rule('can_edit_book', is_book_author)
>>> rules.add_rule('can_delete_book', is_book_author)

Assuming we've got some data, we can now test our rules:

>>> from django.contrib.auth.models import User
>>> from books.models import Book
>>> guidetodjango = Book.objects.get(isbn='978-1-4302-1936-1')
>>> guidetodjango.author
<User: adrian>
>>> adrian = User.objects.get(username='adrian')
>>> rules.test_rule('can_edit_book', adrian, guidetodjango)
True
>>> rules.test_rule('can_delete_book', adrian, guidetodjango)
True

Nice... but not awesome.

Combining predicates

Predicates by themselves are not so useful -- not more useful than any other function would be. Predicates, however, can be combined using binary operators to create more complex ones. Predicates support the following operators:

  • P1 & P2: Returns a new predicate that returns True if both predicates return True, otherwise False. If P1 returns False, P2 will not be evaluated.
  • P1 | P2: Returns a new predicate that returns True if any of the predicates returns True, otherwise False. If P1 returns True, P2 will not be evaluated.
  • P1 ^ P2: Returns a new predicate that returns True if one of the predicates returns True and the other returns False, otherwise False.
  • ~P: Returns a new predicate that returns the negated result of the original predicate.

Suppose the requirement for allowing a user to edit a given book was for them to be either the book's author, or a member of the "editors" group. Allowing users to delete a book should still be determined by whether the user is the book's author.

With rules that's easy to implement. We'd have to define another predicate, that would return True if the given user is a member of the "editors" group, False otherwise. The built-in is_group_member factory will come in handy:

>>> is_editor = rules.is_group_member('editors')
>>> is_editor
<Predicate:is_group_member:editors object at 0x10eee1350>

We could combine it with the is_book_author predicate to create a new one that checks for either condition:

>>> is_book_author_or_editor = is_book_author | is_editor
>>> is_book_author_or_editor
<Predicate:(is_book_author | is_group_member:editors) object at 0x10eee1390>

We can now update our can_edit_book rule:

>>> rules.set_rule('can_edit_book', is_book_author_or_editor)
>>> rules.test_rule('can_edit_book', adrian, guidetodjango)
True
>>> rules.test_rule('can_delete_book', adrian, guidetodjango)
True

Let's see what happens with another user:

>>> martin = User.objects.get(username='martin')
>>> list(martin.groups.values_list('name', flat=True))
['editors']
>>> rules.test_rule('can_edit_book', martin, guidetodjango)
True
>>> rules.test_rule('can_delete_book', martin, guidetodjango)
False

Awesome.

So far, we've only used the underlying, generic framework for defining and testing rules. This layer is not at all specific to Django; it may be used in any context. There's actually no import of anything Django-related in the whole app (except in the rules.templatetags module). rules however can integrate tightly with Django to provide authorization.

Using Rules with Django

rules is able to provide object-level permissions in Django. It comes with an authorization backend and a couple template tags for use in your templates.

Permissions

In rules, permissions are a specialised type of rules. You still define rules by creating and combining predicates. These rules however, must be added to a permissions-specific rule set that comes with rules so that they can be picked up by the rules authorization backend.

Creating permissions

The convention for naming permissions in Django is app_label.action_object, and we like to adhere to that. Let's add rules for the books.change_book and books.delete_book permissions:

>>> rules.add_perm('books.change_book', is_book_author | is_editor)
>>> rules.add_perm('books.delete_book', is_book_author)

See the difference in the API? add_perm adds to a permissions-specific rule set, whereas add_rule adds to a default shared rule set. It's important to know however, that these two rule sets are separate, meaning that adding a rule in one does not make it available to the other.

Checking for permission

Let's go ahead and check whether adrian has change permission to the guidetodjango book:

>>> adrian.has_perm('books.change_book', guidetodjango)
False

When you call the User.has_perm method, Django asks each backend in settings.AUTHENTICATION_BACKENDS whether a user has the given permission for the object. When queried for object permissions, Django's default authentication backend always returns False. rules comes with an authorization backend, that is able to provide object-level permissions by looking into the permissions-specific rule set.

Let's add the rules authorization backend in settings:

AUTHENTICATION_BACKENDS = (
    'rules.permissions.ObjectPermissionBackend',
    'django.contrib.auth.backends.ModelBackend',
)

Now, checking again gives adrian the required permissions:

>>> adrian.has_perm('books.change_book', guidetodjango)
True
>>> adrian.has_perm('books.delete_book', guidetodjango)
True
>>> martin.has_perm('books.change_book', guidetodjango)
True
>>> martin.has_perm('books.delete_book', guidetodjango)
False

NOTE: Calling has_perm on a superuser will ALWAYS return True.

Permissions in models

NOTE: The features described in this section work on Python 3+ only.

It is common to have a set of permissions for a model, like what Django offers with its default model permissions (such as add, change etc.). When using rules as the permission checking backend, you can declare object-level permissions for any model in a similar way, using a new Meta option.

First, you need to switch your model's base and metaclass to the slightly extended versions provided in rules.contrib.models. There are several classes and mixins you can use, depending on whether you're already using a custom base and/or metaclass for your models or not. The extensions are very slim and don't affect the models' behavior in any way other than making it register permissions.

  • If you're using the stock django.db.models.Model as base for your models, simply switch over to RulesModel and you're good to go.
  • If you already have a custom base class adding common functionality to your models, add RulesModelMixin to the classes it inherits from and set RulesModelBase as its metaclass, like so:

    from django.db.models import Model
    from rules.contrib.models import RulesModelBase, RulesModelMixin
    
    class MyModel(RulesModelMixin, Model, metaclass=RulesModelBase):
        ...
  • If you're using a custom metaclass for your models, you'll already know how to make it inherit from RulesModelBaseMixin yourself.

Then, create your models like so, assuming you're using RulesModel as base directly:

import rules
from rules.contrib.models import RulesModel

class Book(RulesModel):
    class Meta:
        rules_permissions = {
            "add": rules.is_staff,
            "read": rules.is_authenticated,
        }

This would be equivalent to the following calls:

rules.add_perm("app_label.add_book", rules.is_staff)
rules.add_perm("app_label.read_book", rules.is_authenticated)

There are methods in RulesModelMixin that you can overwrite in order to customize how a model's permissions are registered. See the documented source code for details if you need this.

Of special interest is the get_perm classmethod of RulesModelMixin, which can be used to convert a permission type to the corresponding full permission name. If you need to query for some type of permission on a given model programmatically, this is handy:

if user.has_perm(Book.get_perm("read")):
    ...

Permissions in views

rules comes with a set of view decorators to help you enforce authorization in your views.

Using the function-based view decorator

For function-based views you can use the permission_required decorator:

from django.shortcuts import get_object_or_404
from rules.contrib.views import permission_required
from posts.models import Post

def get_post_by_pk(request, post_id):
    return get_object_or_404(Post, pk=post_id)

@permission_required('posts.change_post', fn=get_post_by_pk)
def post_update(request, post_id):
    # ...

Usage is straight-forward, but there's one thing in the example above that stands out and this is the get_post_by_pk function. This function, given the current request and all arguments passed to the view, is responsible for fetching and returning the object to check permissions against -- i.e. the Post instance with PK equal to the given post_id in the example. This specific use-case is quite common so, to save you some typing, rules comes with a generic helper function that you can use to do this declaratively. The example below is equivalent to the one above:

from rules.contrib.views import permission_required, objectgetter
from posts.models import Post

@permission_required('posts.change_post', fn=objectgetter(Post, 'post_id'))
def post_update(request, post_id):
    # ...

For more information on the decorator and helper function, refer to the rules.contrib.views module.

Using the class-based view mixin

Django includes a set of access mixins that you can use in your class-based views to enforce authorization. rules extends this framework to provide object-level permissions via a mixin, PermissionRequiredMixin.

The following example will automatically test for permission against the instance returned by the view's get_object method:

from django.views.generic.edit import UpdateView
from rules.contrib.views import PermissionRequiredMixin
from posts.models import Post

class PostUpdate(PermissionRequiredMixin, UpdateView):
    model = Post
    permission_required = 'posts.change_post'

You can customise the object either by overriding get_object or get_permission_object.

For more information refer to the Django documentation and the rules.contrib.views module.

Checking permission automatically based on view type

If you use the mechanisms provided by rules.contrib.models to register permissions for your models as described in Permissions in models, there's another convenient mixin for class-based views available for you.

rules.contrib.views.AutoPermissionRequiredMixin can recognize the type of view it's used with and check for the corresponding permission automatically.

This example view would, without any further configuration, automatically check for the "posts.change_post" permission, given that the app label is "posts":

from django.views.generic import UpdateView
from rules.contrib.views import AutoPermissionRequiredMixin
from posts.models import Post

class UpdatePostView(AutoPermissionRequiredMixin, UpdateView):
    model = Post

By default, the generic CRUD views from django.views.generic are mapped to the native Django permission types (add, change, delete and view). However, the pre-defined mappings can be extended, changed or replaced altogether when subclassing AutoPermissionRequiredMixin. See the fully documented source code for details on how to do that properly.

Permissions and rules in templates

rules comes with two template tags to allow you to test for rules and permissions in templates.

Add rules to your INSTALLED_APPS:

INSTALLED_APPS = (
    # ...
    'rules',
)

Then, in your template:

{% load rules %}

{% has_perm 'books.change_book' author book as can_edit_book %}
{% if can_edit_book %}
    ...
{% endif %}

{% test_rule 'has_super_feature' user as has_super_feature %}
{% if has_super_feature %}
    ...
{% endif %}

Permissions in the Admin

If you've setup rules to be used with permissions in Django, you're almost 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:

  • <app_label>.add_<modelname>
  • <app_label>.view_<modelname>
  • <app_label>.change_<modelname>
  • <app_label>.delete_<modelname>
  • <app_label>

Note: view permission is new in Django v2.1 and should not be added in versions before that.

The first four are obvious. The fifth is the required permission for an app to be displayed in the Admin's "dashboard". Overriding it does not restrict access to the add, change or delete views. Here's some rules for our imaginary books app as an example:

>>> rules.add_perm('books', rules.always_allow)
>>> 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.delete_book', is_staff)

Django Admin does not support object-permissions, in the sense that it will never ask for permission to perform an action on an object, only whether a user is allowed to act on (any) instances of a model.

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 ModelAdmin:

  • has_view_permission(user, obj=None)
  • has_change_permission(user, obj=None)
  • has_delete_permission(user, obj=None)

rules comes with a custom ModelAdmin subclass, rules.contrib.admin.ObjectPermissionsModelAdmin, that overrides these methods to pass on the edited model instance to the authorization backends, thus enabling permissions per object in the Admin:

# books/admin.py
from django.contrib import admin
from rules.contrib.admin import ObjectPermissionsModelAdmin
from .models import Book

class BookAdmin(ObjectPermissionsModelAdmin):
    pass

admin.site.register(Book, BookAdmin)

Now this allows you to specify permissions like this:

>>> rules.add_perm('books', rules.always_allow)
>>> rules.add_perm('books.add_book', has_author_profile)
>>> rules.add_perm('books.change_book', is_book_author_or_editor)
>>> 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.

Permissions in Django Rest Framework

Similar to rules.contrib.views.AutoPermissionRequiredMixin, there is a rules.contrib.rest_framework.AutoPermissionViewSetMixin for viewsets in Django Rest Framework. The difference is that it doesn't derive permission from the type of view but from the API action (create, retrieve etc.) that's tried to be performed. Of course, it also requires you to declare your models as described in Permissions in models.

Here is a possible ModelViewSet for the Post model with fully automated CRUD permission checking:

from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
from rules.contrib.rest_framework import AutoPermissionViewSetMixin
from posts.models import Post

class PostSerializer(ModelSerializer):
    class Meta:
        model = Post
        fields = "__all__"

class PostViewSet(AutoPermissionViewSetMixin, ModelViewSet):
    queryset = Post.objects.all()
    serializer_class = PostSerializer

By default, the CRUD actions of ModelViewSet are mapped to the native Django permission types (add, change, delete and view). The list action has no permission checking enabled. However, the pre-defined mappings can be extended, changed or replaced altogether when using (or subclassing) AutoPermissionViewSetMixin. Custom API actions defined via the @action decorator may then be mapped as well. See the fully documented source code for details on how to properly customize the default behavior.

Advanced features

Custom rule sets

You may create as many rule sets as you need:

>>> features = rules.RuleSet()

And manipulate them by adding, removing, querying and testing rules:

>>> features.rule_exists('has_super_feature')
False
>>> is_special_user = rules.is_group_member('special')
>>> features.add_rule('has_super_feature', is_special_user)
>>> 'has_super_feature' in features
True
>>> features['has_super_feature']
<Predicate:is_group_member:special object at 0x10eeaa500>
>>> features.test_rule('has_super_feature', adrian)
True
>>> features.remove_rule('has_super_feature')

Note however that custom rule sets are not available in Django templates --you need to provide integration yourself.

Invocation context

A new context is created as a result of invoking Predicate.test() and is only valid for the duration of the invocation. A context is a simple dict that you can use to store arbitrary data, (eg. caching computed values, setting flags, etc.), that can be used by predicates later on in the chain. Inside a predicate function it can be used like so:

>>> @predicate
... def mypred(a, b):
...     value = compute_expensive_value(a)
...     mypred.context['value'] = value
...     return True

Other predicates can later use stored values:

>>> @predicate
... def myotherpred(a, b):
...     value = myotherpred.context.get('value')
...     if value is not None:
...         return do_something_with_value(value)
...     else:
...         return do_something_without_value()

Predicate.context provides a single args attribute that contains the arguments as given to test() at the beginning of the invocation.

Binding "self"

In a predicate's function body, you can refer to the predicate instance itself by its name, eg. is_book_author. Passing bind=True as a keyword argument to the predicate decorator will let you refer to the predicate with self, which is more convenient. Binding self is just syntactic sugar. As a matter of fact, the following two are equivalent:

>>> @predicate
... def is_book_author(user, book):
...     if is_book_author.context.args:
...         return user == book.author
...     return False

>>> @predicate(bind=True)
... def is_book_author(self, user, book):
...     if self.context.args:
...         return user == book.author
...     return False

Skipping predicates

You may skip evaluation by returning None from your predicate:

>>> @predicate(bind=True)
... def is_book_author(self, user, book):
...     if len(self.context.args) > 1:
...         return user == book.author
...     else:
...         return None

Returning None signifies that the predicate need not be evaluated, thus leaving the predicate result up to that point unchanged.

Logging predicate evaluation

rules can optionally be configured to log debug information as rules are evaluated to help with debugging your predicates. Messages are sent at the DEBUG level to the 'rules' logger. The following dictConfig configures a console logger (place this in your project's settings.py if you're using rules with Django):

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'handlers': {
        'console': {
            'level': 'DEBUG',
            'class': 'logging.StreamHandler',
        },
    },
    'loggers': {
        'rules': {
            'handlers': ['console'],
            'level': 'DEBUG',
            'propagate': True,
        },
    },
}

When this logger is active each individual predicate will have a log message printed when it is evaluated.

Best practices

Before you can test for rules, these rules must be registered with a rule set, and for this to happen the modules containing your rule definitions must be imported.

For complex projects with several predicates and rules, it may not be practical to define all your predicates and rules inside one module. It might be best to split them among any sub-components of your project. In a Django context, these sub-components could be the apps for your project.

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 best to further split predicates and rules in different modules.

rules may optionally be configured to autodiscover rules.py modules in your apps and import them at startup. To have rules do so, just edit your INSTALLED_APPS setting:

INSTALLED_APPS = (
    # replace 'rules' with:
    'rules.apps.AutodiscoverRulesConfig',
)

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 itself:

from __future__ import absolute_import

API Reference

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

You create Predicate instances by passing in a callable:

>>> def is_book_author(user, book):
...     return book.author == user
...
>>> pred = Predicate(is_book_author)
>>> pred
<Predicate:is_book_author object at 0x10eeaa490>

You may optionally provide a different name for the predicate that is used when inspecting it:

>>> pred = Predicate(is_book_author, name='another_name')
>>> pred
<Predicate:another_name object at 0x10eeaa490>

Also, you may optionally provide bind=True in order to be able to access the predicate instance with self:

>>> def is_book_author(self, user, book):
...     if self.context.args:
...         return user == book.author
...     return False
...
>>> pred = Predicate(is_book_author, bind=True)
>>> pred
<Predicate:is_book_author object at 0x10eeaa490>

Instance methods

test(obj=None, target=None)

Returns the result of calling the passed in callable with zero, one or two positional arguments, depending on how many it accepts.

Class rules.RuleSet

RuleSet extends Python's built-in dict type. Therefore, you may create and use a rule set any way you'd use a dict.

Instance methods

add_rule(name, predicate)

Adds a predicate to the rule set, assigning it to the given rule name. 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 the rule with the given name. Raises KeyError if a rule with that name does not exist.

rule_exists(name)

Returns True if a rule with the given name exists, False otherwise.

test_rule(name, obj=None, target=None)

Returns the result of calling predicate.test(obj, target) where predicate is the predicate for the rule with the given name. Returns False if a rule with the given name does not exist.

Decorators

@predicate

Decorator that creates a predicate out of any callable:

>>> @predicate
... def is_book_author(user, book):
...     return book.author == user
...
>>> is_book_author
<Predicate:is_book_author object at 0x10eeaa490>

Customising the predicate name:

>>> @predicate(name='another_name')
... def is_book_author(user, book):
...     return book.author == user
...
>>> is_book_author
<Predicate:another_name object at 0x10eeaa490>

Binding self:

>>> @predicate(bind=True)
... def is_book_author(self, user, book):
...     if 'user_has_special_flag' in self.context:
...         return self.context['user_has_special_flag']
...     return book.author == user

Predefined predicates

always_allow(), always_true()

Always returns True.

always_deny(), always_false()

Always returns False.

is_authenticated(user)

Returns the result of calling user.is_authenticated(). Returns False if the given user does not have an is_authenticated method.

is_superuser(user)

Returns the result of calling user.is_superuser. Returns False if the given user does not have an is_superuser property.

is_staff(user)

Returns the result of calling user.is_staff. Returns False if the given user does not have an is_staff property.

is_active(user)

Returns the result of calling user.is_active. Returns False if the given user does not have an is_active property.

is_group_member(*groups)

Factory that creates a new predicate that returns True if the given user is a member of all the given groups, False otherwise.

Shortcuts

Managing the shared rule set

add_rule(name, predicate)

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 a rule from the shared rule set. See RuleSet.remove_rule.

rule_exists(name)

Returns whether a rule exists in the shared rule set. See RuleSet.rule_exists.

test_rule(name, obj=None, target=None)

Tests the rule with the given name. See RuleSet.test_rule.

Managing the permissions rule set

add_perm(name, predicate)

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 a rule from the permissions rule set. See RuleSet.remove_rule.

perm_exists(name)

Returns whether a rule exists in the permissions rule set. See RuleSet.rule_exists.

has_perm(name, user=None, obj=None)

Tests the rule with the given name. See RuleSet.test_rule.

Licence

django-rules is distributed under the MIT licence.

Copyright (c) 2014 Akis Kesoglou

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:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

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.

django-rules's People

Contributors

bradenmacdonald avatar bulv1ne avatar cclauss avatar danlamanna avatar dfunckt avatar dyve avatar eviltwin avatar federicobond avatar ffx01 avatar georgek avatar hashlash avatar jacobh avatar jerebenitez avatar mlsen avatar orf avatar pjsier avatar real-gecko avatar rixx avatar sergioisidoro avatar slafs avatar smcoll avatar thedrow avatar ticosax avatar uri-rodberg avatar zainab-amir avatar zerolab avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

django-rules's Issues

Making rules work with CreateView

Not sure this is really an issue, but good for your documentation:
By default I have found rules don't work with CreateView because there is no object instance yet, so it throws a PK error. However, I have defined rules as such that will accept just the user object and will return the appropriate True/False. I really needed some security rules on the CreateView, so I started messing with the get_object function (unused for CreateView) and I made this work by adding the following function 'get_object' to the CreateView class:

class CustomCreateView(PermissionRequiredMixin, CreateView): """ other stuff """ def get_object(self): pass

Now I'm trying to get this working with ListView, which is proving to be a bit more difficult...

Autodiscover, how?

So I've read in the documentation that you can have rules autodiscover rule.py files in your applications. That is great, but the documentation doesn't clarify is both rules and rules.apps.AutodiscoverRulesConfig should be in INSTALLED_APPS.

  • If I add only rules, then rules.py files are not discovered
  • If I add both, then I get "ImproperlyConfigured: Application labels aren't unique, duplicates: rules"
  • If I only add rules.apps.AutodiscoverRulesConfig then it says that it cannot import predicate from rules in from rules import predicate.

Is this intended behavior?

Optimize rule evaluation

Assume a rule such as:

my_rule = first_condition | second_condition

I would assume that second_condition would not be evaluated if first_condition evaluated to True. It appears that django-rules doesn't doesn't apply this optimization.

Is there a way to handle this with the library? If not, what would be involved in adding it? (This is particularly valuable for my use-case, as I am abusing the predicates to mutate data when the predicates are called.)

Thanks for a great library!

Is it time for a new release ?

Hi @dfunckt,
I'm a happy user of the master branch for several month now.
I think django-rules is stable and mature feature wised to be released and pushed to pypi.

Let me know if you need help.

Permission Required Decorator always return True

(sorry for my english)

Information:
Django 1.9

Settings:
AUTHENTICATION_BACKENDS = (

'django.contrib.auth.backends.ModelBackend',
'rules.permissions.ObjectPermissionBackend',

)

INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rules.apps.AutodiscoverRulesConfig',
'debug_toolbar',

 Autre

]

rules.py files with the predicate and permission(add_perm)

views.py with import rules.py

when I use permission_required, it always return True, when I test the function(predicate) in the views for see the result with print, it's good but permission_required don't work.

ex:

If I have rules.py:
@rules.predicate
def is_author(user, book):
return book.author == user

rules.add_perm('edit_book', is_author)

if I used @permission_required('edit_book', fn=objectgetter(Book, 'id') it doesn't work but if I use in the views: is_author(object_user, object_book) it's work.

rules.compat not in the setup packages

pip install git+https://github.com/dfunckt/django-rules.git@master won't install the compat package.
As a result, doing something like from rules.contrib.views import LoginRequiredMixin will fail.

Enhancement - Human readable name for rules/permissions

Hi, I've been a fan of this lib for a long time, so now I'm implementing it on a bigger scale than usual in a project and ran into an enhancement.

The context is that we want to connect some custom rules/permissions with roles, and build an UI around this. We like the format of appname:permission_name to name a permission, but this is not very readable for the end-user assigning permissions to roles. Similar to Django's permission system, we'd like to be able to provide a human readable name to the permission.

API wise, I suggest extending the API from

rules.add_perm('appname:permission_name', some_predicate)

to

rules.add_perm('appname:permission_name', some_predicate, verbose_name=_("A translatable string") )

Looking at the source code, this would make the RuleSet a bit more complex object than a simple dict, a single rule within a RuleSet would probably need to be an object itself where the verbose_name is also kept.

Thoughts? Comments? I'm willing to contribute on this with a PR!

Updating permissions does not work

I couldn't find any solution online to this issue so I'm hoping you could help me out. So here's the scenario. I first created the permission below weeks ago. Back then, I wanted to display the list of companies only to admin users:
add_perm('mining.list_company', is_admin_level)

The above code worked perfectly. However, couple of days ago, we have made some changes and we want the list of companies to be available to all users. So I have this now:
add_perm('mining.list_company', is_authenticated)

However, when I go to the company list page using a regular user, I still get a permission denied error even though I have already changed the permission. I went into a little bit of digging and it looks like the add_perm method gets executed only ONCE, that's why the is_authenticated does not get recognized. I was able to confirm this because when I tried adding pdb debugging inside the predicate itself, it does not get executed and just proceeds to permission denied error.

Am I missing anything here? It seems I'm the only one having this issue. Below are the necessary codes:

predicates.py

from __future__ import unicode_literals, absolute_import

from rules import predicate


@predicate()
def is_authenticated(user):
    return user.is_authenticated()

@predicate()
def is_admin_level(user):
    return user.is_admin_level

rules.py

from __future__ import unicode_literals, absolute_import
from rules import add_perm
from .predicates import is_authenticated

add_perm('mining.list_company', is_authenticated)

views.py

class CompanyList(LoginRequiredMixin, PermissionMixin, ListView):
    logger = logging.getLogger(__name__)
    context_object_name = 'companies'
    permission_required = 'mining.list_company'
    template_name = 'mining/company/list.html'
    paginate_by = 10

Again, everything is working perfectly fine before. The error appeared when I changed is_admin_level to is_authenticated. I would really appreciate it if you could point me to the right direction here. Thanks in advance!

Does django-rules work with django rest framework viewsets?

Hi,

I am trying to integrate rules with DRF's viewsets but I keep getting an AttributeError: 'OrgViewSet' object has no attribute 'request'

Here is my viewset code:

class OrgViewSet(PermissionRequiredMixin,
                 mixins.ListModelMixin,
                 viewsets.GenericViewSet):
    permission_required = CHANGE_ORG
    queryset = Org.objects.all()
    serializer_class = OrgSerializer

Here is the stack trace

Internal Server Error: /orgs/
Traceback (most recent call last):
  File "/Users/dre/code/treebeard/venv/lib/python3.6/site-packages/django/core/handlers/exception.py", line 41, in inner
    response = get_response(request)
  File "/Users/dre/code/treebeard/venv/lib/python3.6/site-packages/django/core/handlers/base.py", line 187, in _get_response
    response = self.process_exception_by_middleware(e, request)
  File "/Users/dre/code/treebeard/venv/lib/python3.6/site-packages/django/core/handlers/base.py", line 185, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/Users/dre/code/treebeard/venv/lib/python3.6/site-packages/django/views/decorators/csrf.py", line 58, in wrapped_view
    return view_func(*args, **kwargs)
  File "/Users/dre/code/treebeard/venv/lib/python3.6/site-packages/rest_framework/viewsets.py", line 86, in view
    return self.dispatch(request, *args, **kwargs)
  File "/Users/dre/code/treebeard/venv/lib/python3.6/site-packages/django/contrib/auth/mixins.py", line 90, in dispatch
    if not self.has_permission():
  File "/Users/dre/code/treebeard/venv/lib/python3.6/site-packages/rules/contrib/views.py", line 51, in has_permission
    return self.request.user.has_perms(perms, obj)
AttributeError: 'OrgViewSet' object has no attribute 'request'

Any help would be greatly appreciated.

rules.contrib.views.objectgetter() does not work with default view argument values

When using the permission_required decorator as follows:
@permission_required('perm_name', fn=objectgetter(model_name, view_arg)),
it is not possible to leverage default argument values as specified in the view declaration.

If for example, I declare my view as follows:
def edit_circle(request, cid=0):
...

When the view is called without the 'cid' argument, the following exception is raised:
ImproperlyConfigured: Argument cid is not available. Given arguments: []

Admin Integration

Very noob in Python/Django:

Reading the docs, I get the basic idea of applying django-rules in a django template.

However, I feel completely overwhelmed when it comes to applying it in django-admin.

Could you please provide a hint?

Autodiscovering does not work

Hello!
I am using django 1.7 and trying to enable autodiscovering of rules.py files as it shown in the README file:

INSTALLED_APPS = (
    # ...
    'rules',
    'rules.apps.AutodiscoverRulesConfig',
)

But I am getting the following error:

Traceback (most recent call last):
  File "./manage.py", line 10, in <module>
    execute_from_command_line(sys.argv)
  File "/usr/lib/python3.4/site-packages/django/core/management/__init__.py", line 385, in execute_from_command_line
    utility.execute()
  File "/usr/lib/python3.4/site-packages/django/core/management/__init__.py", line 354, in execute
    django.setup()
  File "/usr/lib/python3.4/site-packages/django/__init__.py", line 21, in setup
    apps.populate(settings.INSTALLED_APPS)
  File "/usr/lib/python3.4/site-packages/django/apps/registry.py", line 89, in populate
    "duplicates: %s" % app_config.label)
django.core.exceptions.ImproperlyConfigured: Application labels aren't unique, duplicates: rules

When I am disabling rules application:

INSTALLED_APPS = (
    # ...
    #'rules',
    'rules.apps.AutodiscoverRulesConfig',
)

I am getting another error (when I am trying to load any page):

ImportError at /
No module named 'rules.apps.AutodiscoverRulesConfig'; 'rules.apps' is not a package
Request Method: GET
Request URL:    http://localhost:8000/
Django Version: 1.7
Exception Type: ImportError
Exception Value:    
No module named 'rules.apps.AutodiscoverRulesConfig'; 'rules.apps' is not a package
Exception Location: /usr/lib/python3.4/site-packages/authority/__init__.py in autodiscover, line 21
Python Executable:  /usr/bin/python
Python Version: 3.4.1

What is the correct way of using autodiscover?

Some points about documentation

Thanks a lot for a great package, dfunkct!

The documentation is great, and I'm trying to get onboard. But even though a lot of things are pretty clear, certain things are still a bit confusing, and I think making them clearer in the documentation would really make this more approachable.

Namely;

  • You write rules.predicate and rules.add_rule, but at no point is there an import rules statement. For the decorator, this is kinda understandable, as it's common to import decorators from modules. But for add_rule, it wasn't clear what that variable was. Are these running on the module itself?
  • You mention that there are two rule sets, one shared one, and the other for Django. How do you access these? When I do rules.add_rule, which one does it get added to? Is the Django one implicit?
  • Where do we add these rules? Doing so in the view function is the most obvious choice. You mention adding a rule.py module in the app, which makes sense. What is the content of this module then? Just import rules, and then add the rules? And then do another import rules in views.py, and start using the decorators?

In general, I imagine many people will use rules from a Django app. A minimal example would really help. Also, a lot of info about settings things up is spread around the file. Installing using pip is a no-brainer. But there should also be a Django configuration section for things like:

  • Adding the app to INSTALLED_APPS
  • Adding rules to AUTHENTICATION_BACKENDS
  • Adding a rules.py file.

Sorry if this is long, but it's by no means a rant. On the contrary, I'm really excited about using this library, and I think it would really benefit from a few tweaks to the documentation!

Missing decorator for views?

In the documentation it's not specified how to limit a view to users that have object based permissions. Just like normally we can do:

@permission_required('app.action_object')
def my_view(request, ...):
    pass

I think that we should be able to somehow take incoming object IDs arguments and automatically block the request if the user does not meet the requirements.

For example, something along the lines of:

@permission_required('app.view_posts', 'post_id')
def view_post(request, post_id):
    pass

What do you think?

ImportError on python 2.7.10

Hello,

I'm getting the following error (I didn't install the other package, django-rules, 🤐 ) on Django 1.9.

Unhandled exception in thread started by <function wrapper at 0x104001758>
Traceback (most recent call last):
  File "/Users/dacian/.virtualenvs/venv/lib/python2.7/site-packages/django/utils/autoreload.py", line 226, in wrapper
    fn(*args, **kwargs)
  File "/Users/dacian/.virtualenvs/venv/lib/python2.7/site-packages/django/core/management/commands/runserver.py", line 109, in inner_run
    autoreload.raise_last_exception()
  File "/Users/dacian/.virtualenvs/venv/lib/python2.7/site-packages/django/utils/autoreload.py", line 249, in raise_last_exception
    six.reraise(*_exception)
  File "/Users/dacian/.virtualenvs/venv/lib/python2.7/site-packages/django/utils/autoreload.py", line 226, in wrapper
    fn(*args, **kwargs)
  File "/Users/dacian/.virtualenvs/venv/lib/python2.7/site-packages/django/__init__.py", line 18, in setup
    apps.populate(settings.INSTALLED_APPS)
  File "/Users/dacian/.virtualenvs/venv/lib/python2.7/site-packages/django/apps/registry.py", line 85, in populate
    app_config = AppConfig.create(entry)
  File "/Users/dacian/.virtualenvs/venv/lib/python2.7/site-packages/django/apps/config.py", line 116, in create
    mod = import_module(mod_path)
  File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/importlib/__init__.py", line 37, in import_module
    __import__(name)
  File "/Users/dacian/Desktop/dev/project_name/extras/dashboard/rules.py", line 6, in <module>
    from rules import predicate
ImportError: cannot import name predicate
INSTALLED_APPS = [
    'modeltranslation',
    'django.contrib.sites',
    'jet',
    'django.contrib.admin',
    'django.contrib.auth',
    'polymorphic',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'django.contrib.humanize',
    # 'rules',
    'rules.apps.AutodiscoverRulesConfig',
    'django_extensions',
    'rest_framework',
]

extras/dashboard/rules.py

# -*- coding: utf-8 -*-

from __future__ import absolute_import
from __future__ import unicode_literals

from rules import predicate


@predicate(name='has_menu_perm')
def has_menu_perm(user, menu_item):
    return any([user.has_perm(perm) for perm in menu_item.perms])

How to use PermissionRequiredMixin

I have start implementing django-rules to my application and I would like to know how can I integrate the permission backend with CBVs in django.

My settings are:

AUTHENTICATION_BACKENDS = ( 'account.auth_backends.EmailAuthenticationBackend',
                            'rules.permissions.ObjectPermissionBackend',
                            'django.contrib.auth.backends.ModelBackend')
INSTALLED_APPS = (
    ....,
    'rules.apps.AutodiscoverRulesConfig',
    ...,)

views.py

class BookEditView(SuccessMessageMixin, PermissionRequiredMixin, UpdateView):
    model = Book
    template_name = 'book.html'
    form_class = BookFormSettings

    success_message = "%(title)s was created successfully"

    ### PermissionRequiredMixin settings
    permission_required = 'books.change_course'

rules.py

@predicate
def is_author(user, course):
    return book.author == user

add_perm('books.change_book', is_author)

If this does not work, what are the best practices to integrate django-rules through views.

Thanks.

Support permission-based queryset filters

I really like how rules works for individual object permissions, but it doesn't really cover permission-based queryset filtering (which comes naturally for database-centric permission systems like django-guardian). While thinking about how to compensate for that, I thought of an extension to the API that could help fill that gap. Rather than rushing off to write a pull request, I figured I'd outline it here for feedback first. I'm envisioning something like this:

from django.db.models import Q

@rules.filter
def is_book_author(user):
    return Q(author=user)

is_book_author_or_superuser = is_book_author | rules.predicates.is_superuser

rules.add_filter('books.view_book', is_book_author_or_superuser)

Book.objects.filter(rules.q(user, 'books.view_book')

Filters would have to be defined separately from the object permission predicates, but would work very similarly; Q objects can be combined in ways that are pretty compatible with the predicate combinations already supported in rules. Existing predicates which only depend on properties of the user could be combined with Q-based filters, with predicate outcomes being represented as always-True (like Q(pk__isnull=False)) or always-False (like Q(pk__isnull=True)) Q objects.

This would also make it pretty straightforward to create a Django REST Framework filter that would use the filter associated with the correct permission:

from rest_framework.compat import get_model_name

class DjangoPermissionRulesFilter(BaseFilterBackend):

    perm_format = '%(app_label)s.view_%(model_name)s'

    def filter_queryset(self, request, queryset, view):
        user = request.user
        model_cls = queryset.model
        kwargs = {
            'app_label': model_cls._meta.app_label,
            'model_name': get_model_name(model_cls)
        }
        permission = self.perm_format % kwargs
        return queryset.filter(rules.q(user, permission))

Some of the things I like about this design:

  • Keeps implementation of the permissions out of the models and model managers, so they can be grouped together with the predicate definitions
  • Allows reuse of some basic filtering operations (at least within permissions on the same model or other ones with the same lookup path for the fields to compare)
  • Consistency with implementing predicates for the object-based permissions
  • Ability to reuse predicates that don't depend on the object in filters (this part just occurred to me and hasn't been as carefully thought through as the rest, but it seems like it should work)
  • Very simple to support in Django REST Framework with only one custom filter class that can be reused for many views

Some downsides that I don't see good ways to work around yet:

  • One permission can have 2 different implementations: a predicate function for a single object, or a Q object for a queryset. I really don't see any way around this without really limiting and complicating the case where you don't even need a filter for the permission (which is pretty common).
  • Models with different lookup paths to the user (or related models) generally can't share filter functions; one may need Q(author=user) while another has Q(owner=user), Q(status__user=user), or even Q(creator__organization=user.organization).
  • I'd kind of prefer a query filtering syntax like Book.objects.has_perm(user, 'books.view_book'), but it doesn't seem worth the effort to create a model manager mixin for it that would need to be explicitly included in all relevant models.

Thoughts? Do you think something like this would fit in rules or should go into a separate app which depends on it? And can you think of any good improvements on the API?

persistence

I'm new to django-rules. I like its simplicity and it's always helpful when there's more documentation than code. But I'm wondering how I should handle an app where I spin up a bunch of objects, each with its own set of permissions (and these are added to by users as time goes by) and, well, my server goes down. How do I rebuild that permission structure?

I (really) like that django-rules doesn't require a database hit each time I make a permission check, but its also important to record those permissions.

Thanks.

Can't import rules

with django-rules imported from pip:

....
Django==1.8.5
django-rules==0.2
django-social-auth==0.7.28
django-wysiwyg-redactor==0.4.9.1
djangorestframework==3.4.7
httplib2==0.9.2
...

and rules included in my INSTALLED_APPS

INSTALLED_APPS = (
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rules.apps.AutodiscoverRulesConfig',
    'rest_framework.authtoken',
    'rest_framework',
    'accounts',
    'bms',
    'cms',
    'socialmedia',
    # 'v1_api',
)

Attempting to start the django shell, I get the following error

Traceback (most recent call last):
  File "manage.py", line 10, in <module>
    execute_from_command_line(sys.argv)
  File "/home/vagrant/venv/local/lib/python2.7/site-packages/django/core/management/__init__.py", line 351, in execute_from_command_line
    utility.execute()
  File "/home/vagrant/venv/local/lib/python2.7/site-packages/django/core/management/__init__.py", line 325, in execute
    django.setup()
  File "/home/vagrant/venv/local/lib/python2.7/site-packages/django/__init__.py", line 18, in setup
    apps.populate(settings.INSTALLED_APPS)
  File "/home/vagrant/venv/local/lib/python2.7/site-packages/django/apps/registry.py", line 85, in populate
    app_config = AppConfig.create(entry)
  File "/home/vagrant/venv/local/lib/python2.7/site-packages/django/apps/config.py", line 112, in create
    mod = import_module(mod_path)
  File "/usr/lib/python2.7/importlib/__init__.py", line 37, in import_module
    __import__(name)
ImportError: No module named rules.apps

I can't think of what might be wrong..

Support for PEP 484 annotations

Using function annotations in rules will cause this error (Python 3.5):

ValueError: Function has keyword-only arguments or annotations, use getfullargspec() API which can support them

File "…/app/rules.py", line 3, in <module>
  from .predicates import (
File "…/app/predicates.py", line 13, in <module>
  def is_superuser(user: User):
File "…/django-rules/rules/predicates.py", line 246, in predicate
  return inner(fn)
File "…/django-rules/rules/predicates.py", line 241, in inner
  p = Predicate(fn, name, **options)
File "…/django-rules/rules/predicates.py", line 67, in __init__
  argspec = inspect.getargspec(fn)
File "/usr/lib64/python3.5/inspect.py", line 1045, in getargspec
  raise ValueError("Function has keyword-only arguments or annotations"

Ref: https://docs.python.org/3/library/inspect.html#inspect.getfullargspec

'rules.apps' is not a package

I'm not able to get django-rules working. I know that there is other issues that are similar #10 , #8 , and #16 ; but I don't have django-authority installed and I'm using python 3.4 and Django 1.7.6

This is the error I'm receiving:

ImportError at /
No module named 'rules.apps.AutodiscoverRulesConfig'; 'rules.apps' is not a package
Request Method: GET
Request URL:    http://localhost:8080/
Django Version: 1.7.6
Exception Type: ImportError
Exception Value:    
No module named 'rules.apps.AutodiscoverRulesConfig'; 'rules.apps' is not a package
Exception Location: /usr/lib/python3.4/importlib/__init__.py in import_module, line 109
Python Executable:  /usr/bin/python3.4
Python Version: 3.4.1

Thanks for the help.

Is predicates invocation too magic ?

Hi,
I'm raising a question here before submitting any pull request.
I think there is a design issue with Predicate.test(), because if I defined my predicate to accept only
two positional arguments and the caller gives only one argument, my predicate will be called with None for the second argument.

@rules.predicate
def are_equal(a, b):
  return a == b

rules.add_rule('test_this', are_equal)
rules.test_rule('test_this', 'a', 'a') # OK there is two positional argument
rules.test_rule('test_this', 'a')  # why b is None in this case ??

This behaviour troubles me, I do not know yet how it should behave, but I would like, first, to hear from you.
Do you think there is room for improvement ?

rules.contrib.views.PermissionRequiredMixin hides coding errors from the developer

        try:
            # Requires SingleObjectMixin or equivalent ``get_object`` method
            return self.get_object()
        except AttributeError:  # pragma: no cover
            return None

If the get_object() implementation has a bug that ends up causing AttributeError, the mixin gobbles it, which can lead to a very confusing result and debugging experience :)

Instead of looking at side effects of calling a missing method, what about using hasattr?

autodiscovery

I thought I understood autodiscovery (see #35), but I don't. So I've built a very simple Django project with a set of permissions stored in perms.py and loaded and used in views.py. The relevant line is probably

module = import_module('myapp.perms')

in myapp/views.py. This project app completely works: I can run it with app-setup.sh and (importantly) I can run the related tests with pytest. But if I try to move this app to another environment, say something built with Cookiecutter Django, then I have problems. Modifying import_module() to give it a relative path (.perms) also fails. In addition, note that my permissions file is named perms.py. If I rename it to rules.py and change the above line, it fails.

So can you show me an explicit way of loading a permissions file?

Thanks.

Global superuser override?

I need to create a lot of rulesets in a lot of models, and it appears that I will need to add an is_superuser predicate to every single one, which is not very DRY. Is there a way (or a best practice) to grant full access to superusers on everything touched by rules?

Return rule denial reasons (and improved integration with rest framework)

Thanks for the great library! I love the philosophy to use rules written in simple python functions over database tables.

I ran into a few troubles trying to integrate it with a django rest framework project - we don't use django permissions, which means we can't use DjangoObjectPermissions easily.

I thought to connect django-rules directly to drf permissions - ends up similar-ish to dry-rest-permissions but using django-rules.

I also wanted to be able to return custom error messages explaining why a permission was denied.

I managed to implement this by subclassing stuff in django-rules so that you can use it like this:

# in rules.py

@predicate(messages=('Team is archived', 'Team is not archived'))
def is_team_archived(_, team):
  return team.status = 'archived'

can_archive_team = ~is_team_archived

# standalone use

result, message = can_archive_team.test_with_message(user, team)
if result:
  print('yes')
else:
  print('no:', message) # prints "no: Team is archived"

# use in a @detail_route

# creates a drf compatible permission class
CanArchiveTeam = create_permission_class('CanArchiveTeam', can_archive_team)

@detail_route(
    methods=['POST'],
    permission_classes=(IsAuthenticated, CanArchiveTeam)
)
def archive(self, request, pk=None):
  # do archiving

If the CanArchiveTeam does not pass, then it returns a permission denied error with the message Team is archived.

There is a bit more information in our project discussion.

I wanted to avoid magic naming conventions (the approach taken by dry-rest-permissions) or referring to permissions using string names - explicit imports are much clearer to me.

Actually, the only API change for rules is changing/adding a method that returns (result, message) instead of result, and accepting messages as predicate args.

My questions are:

  • would you be interested in having something this inside django-rules?
  • if not, would you be interested in making django-rules officially extendable so I don't need to use private methods/variables which might break in future versions?

The alternative is a fork, but I don't want to contribute to the ever fragmenting django permissions ecosystem. Thanks!

assignment_tag is removed in django-2.0

Quoting the release docs:

Django 1.4 added the assignment_tag helper to ease the creation of template tags that store results in a template variable. The simple_tag() helper has gained this same ability, making the assignment_tag obsolete. Tags that use assignment_tag should be updated to use simple_tag.

As this prevents a timely upgrade to Django 2.0 for me, I'd ask you to consider merging a PR and releasing a new version soon-ish.

is_group_member() factory has undesirable caching side effect

Hey. Rules is proving quite useful, so thanks!, but a head scratcher in a test suite for a django app I am building led me to this issue.

In your built in factory is_group_member() you have the following code just before returning:

if not hasattr(user, '_group_names_cache'):  # pragma: no cover
    user._group_names_cache = set(user.groups.values_list('name', flat=True))

I'm not sure why you are caching the user's groups other than to save possible DB look ups in the future? A side effect is that if a user is removed from a group while your _group_names_cache attribute is in existence rules based on group membership will miss that change, as you've replaced a callable with a cached attribute. This results in unexpected rule failure. I'd suggest removing the attribute creation but you may other reasons for its existence?

To confirm:

  1. give a user membership in a group
  2. use is_group_member() to create a predicate for membership in that group
  3. test the new predicate - it returns True
  4. remove the user from the group
  5. test the predicate again - it still returns True, but should return False
  6. verify inconsistency by comparing user.groups.all() with user._group_names_cache

Admin page: model instance is None

I've gotten the Django permission tests to work as per the docs, but when attempting admin integration, I'm unable to get the model instance to be accepted by the predicate. The user is passed appropriately, but the model instance (book) in the example) is always None. I'm following the tutorials exactly, and am on Django 1.11. What could cause this?

@rules.predicate
def in_section(user, person_instance):
    #This always produces an exception, because person_instance is always None.
    return user.person.section == person_instance.section

rules.add_perm('myapp', rules.always_allow)
rules.add_perm('myapp.add_person', in_section) 

in python 3.6 the `inspect.getargspec()` used in the Predicate class is removed

In python 3.6 the getargspec is to be removed.

Django already provides some implementations to overcome this deprecation: see https://github.com/django/django/pull/4846/files (especially in here: https://github.com/django/django/pull/4846/files#diff-661c241427e347ab93c204317d4f68dc)

....
  File ".../_venv35/lib/python3.5/site-packages/rules/rulesets.py", line 1, in <module>
    from .predicates import predicate
  File ".../_venv35/lib/python3.5/site-packages/rules/predicates.py", line 260, in <module>
    always_true = predicate(lambda: True, name='always_true')
  File ".../_venv35/lib/python3.5/site-packages/rules/predicates.py", line 253, in predicate
    return inner(fn)
  File ".../_venv35/lib/python3.5/site-packages/rules/predicates.py", line 248, in inner
    p = Predicate(fn, name, **options)
  File ".../_venv35/lib/python3.5/site-packages/rules/predicates.py", line 70, in __init__
    argspec = inspect.getargspec(fn)
  File "/usr/lib/python3.5/inspect.py", line 1040, in getargspec
    stacklevel=2)
DeprecationWarning: inspect.getargspec() is deprecated, use inspect.signature() instead

django-rules does not work with DjangoObjectPermissions from django rest framework

My problem is accurately described by the comment I found here https://groups.google.com/forum/#!topic/django-rest-framework/M5q6pI8vcZQ:

I have been using DjangoObjectPermissions which inherits from DjangoModelPermissions. DjangoModelPermissions has a method has_permission which checks for Model level permissions. However, if you are working with object level permissions, usually the model level permissions evaluate to false. Consequently DjangoObjectPermissions raises a permission denied.

I have easily fixed that issue be overwriting the has_permission method of DjangoObjectPermissions, but again I am wondering, have I missed anything or should that not be the default behavior of DjangoObjectPermissions.

I have a code like this:

# rules.py

import rules


############################
# Predicates
############################

@rules.predicate
def is_tweet_owner(user, tweet):
    if not tweet:
        return False
    return tweet.owner == user


############################
# Permissions
############################

rules.add_perm('tweets.add_tweet', rules.is_authenticated)
rules.add_perm('tweets.change_tweet', is_tweet_owner)
rules.add_perm('tweets.delete_tweet', is_tweet_owner)


# views.py


from rest_framework import viewsets, mixins

from .models import Tweet
from .permissions import TweetPermission
from .serializers import TweetSerializer


class TweetViewSet(mixins.CreateModelMixin,
                   mixins.DestroyModelMixin,
                   mixins.RetrieveModelMixin,
                   mixins.UpdateModelMixin,
                   viewsets.GenericViewSet):
    queryset = Tweet.objects.all()
    serializer_class = TweetSerializer
    permission_classes = (TweetPermission, )

    def perform_create(self, serializer):
        serializer.save(owner=self.request.user)


# permissions.py


from rest_framework import permissions


class TweetPermission(permissions.DjangoObjectPermissions):
    authenticated_users_only = False

    def has_permission(self, request, view):
        """
        WTF?!
        """
        return True

I want the tweet can be updated or deleted only by the user that owns this tweet. But user.has_perms method will always return false, because django calls has_permission method before has_object_permission.

So, django will call this:

>>> user.has_perms(['tweets.change_tweet'])
False

But I want this:

>>> user.has_perms(['tweets.change_tweet'], tweet)
True

AttributeError: 'ObjectPermissionBackend' object has no attribute 'get_user'

I am trying to use Django's Client.force_login during tests, which appears to trigger an AttributeError when using ObjectPermissionBackend in settings.AUTHENTICATION_BACKENDS:

[25]   …/app/tests/test_middleware.py(23)test_ddt_middleware_normal()
-> response = client.get('/api/', HTTP_ACCEPT='text/html')
[26]   …/Vcs/django/django/test/client.py(531)get()
-> **extra)
[27]   …/Vcs/django/django/test/client.py(333)get()
-> return self.generic('GET', path, secure=secure, **r)
[28]   …/Vcs/django/django/test/client.py(409)generic()
-> return self.request(**r)
[29]   …/Vcs/django/django/test/client.py(478)request()
-> response = self.handler(environ)
[30]   …/Vcs/django/django/utils/six.py(686)reraise()
-> raise value
[31]   …/Vcs/django/django/core/handlers/exception.py(39)inner()
-> response = get_response(request)
[32]   …/app/middleware.py(37)__call__()
-> if not (request.user and
[33]   …/Vcs/django/django/utils/functional.py(234)inner()
-> self._setup()
[34]   …/Vcs/django/django/utils/functional.py(380)_setup()
-> self._wrapped = self._setupfunc()
[35]   …/Vcs/django/django/contrib/auth/middleware.py(24)<lambda>()
-> request.user = SimpleLazyObject(lambda: get_user(request))
[36]   …/Vcs/django/django/contrib/auth/middleware.py(12)get_user()
-> request._cached_user = auth.get_user(request)
[37] > …/Vcs/django/django/contrib/auth/__init__.py(187)get_user()
-> user = backend.get_user(user_id)

The pytest test looks like this:

def test_foobar(db, client, some_user, some_group):
    some_user.groups.add(some_group)
    some_user.save()
    client.force_login(some_user)

Setting a password and using login works:

def test_foobar(db, client, some_user, some_group):
    some_user.groups.add(some_group)
    some_user.set_password('password')
    some_user.save()
    assert client.login(username=some_user.username,
                        password='password')

According to the documentation the get_user method is required:
https://docs.djangoproject.com/en/1.10/topics/auth/customizing/#writing-an-authentication-backend.

help w/ example for docs

Hi @dfunckt, I've been building a test application w/ rules that I ultimately want to submit as a pull request. You can look at it in https://github.com/highpost/rules-testapp. It's a simple blog app with a number of users with different privilege levels.

I'm also writing an article that you can either use in your documentation or we can find another home. The first chunk (in explore.txt) is an overview of basic Django permissions. I'll get to rules after that.

My problem now is that my app works ... except with rules. If you look at views.py, you'll see that I'm using CBVs. If you remove PermissionRequiredMixin from each view, you can use the various test URLs in the README.txt file without any problem. But if you include them, then the buttons that access the views will fail with "127.0.0.1 redirected you too many times." I haven't had any luck tracking this down. I will note that the CreateView seems to work, and DetailView, UpdateView and DeleteView (with all take pk arguments) fail.

Thanks for your help.

Problem using template tags

I am having difficulty using template tags in my templates. For example this works

In the view:

from __future__                 import absolute_import
import rules
...
def detail(request, slug):
    obj = get_object_or_404(NewsStory, slug=slug)
    return render(request, 'news/detail.html', {
        'story': obj,
        'can_publish_newsstory': rules.has_perm('can_publish_newsstory', request.user)
        })

In the template

       {% if can_publish_newsstory %}
        <li><a href="{% url 'news:edit' story.slug %}" class="button expand secondary">Edit Story</a></li>
        <li><a href="{% url 'news:delete' story.slug %}" class="button expand secondary">Delete Story</a></li>
        {% endif %}

If I change this to remove 'can_publish_newsstory' from the view and include it in the template, nothing shows.

Revised Template

{% load rules %}
{% has_perm 'can_publish_newsstory' user as can_publish_newsstory %}
       {% if can_publish_newsstory %}
        <li><a href="{% url 'news:edit' story.slug %}" class="button expand secondary">Edit Story</a></li>
        <li><a href="{% url 'news:delete' story.slug %}" class="button expand secondary">Delete Story</a></li>
        {% endif %}

I am using Python 2.

Rules 2.0

Hey all, I'm thinking of releasing Rules 2.0 soon and was wondering if there are ideas for things that can be implemented now but couldn't due to having to keep backwards compatibility. I intend to include (breaking) changes related to #44 and #52. It would also be nice if we tackled #32 too (I remember there were some compatibility issues when I looked at it back when it was reported).

1.2.X will still be supported for some time, back-porting fixes for bugs that are severe enough to warrant it but no new features will be implemented.

What do you all think? I'll go ahead and release 2.0 in a couple of weeks if nothing comes up.

Redirection loop with CBV

If user already logged in, and have no specific permission for accessing View - redirect loop happen, here:

            # Check for permissions and return a response
            if not user.has_perms(perms, obj):
                # User does not have a required permission
                if raise_exception:
                    raise PermissionDenied()
                else:
                    return _redirect_to_login(request, view_func.__name__,
                                              login_url, redirect_field_name)

since raise_exception is always set to False (default value) with CBV.

Document usage of simple group membership on views

Documentation of the decorator for permissions on views is good, but focuses on views with passed-in objects and a user's permissions to access them. The simpler case, where you have set up group membership rules with django-rules and want to protect an entire view based on group membership, is not obvious.

I finally figured out that django-rules can be used in conjunction with Django's user_passes_test. I recommend adding to that section of the documentation something like this:

If you want to protect an entire view with a rules decorator, irrespective of any particular object, django-rules can be used in conjunction with Django's user_passes_test decorator. Rather than using the permission_required, use something like:

from django.contrib.auth.decorators import user_passes_test
import rules

is_participant = rules.is_group_member('Participants')

@user_passes_test(is_participant)
def participant_sample(request):
   ....

Predicates do not include the tested permission in the context

I'm implementing a custom rule system where a user can have different answers to a permission depending on the obj that is supplied to the predicates. In order to check these permissions fully, I need access to the name of the rule being checked in my predicates.

Is this possible without making obj in my predicates a tuple containing the name as well as the object I want to check on?

Please consider using user._group_names_cache again

Hi,

You've removed using the group_names_cache here because of #43.

As far as I can tell, that cache was only valid for the lifetime of one request and saves many duplicate DB queries. It wasn't 'undesired', in fact it was very desirable, imo. I have multiple {% has_perm %} on most pages, all based on group membership. Another example: I have a list of objects which many groups can view, but one part of determining whether an 'edit' link should be displayed for an object from that list is based on group membership.
What used to be 9 queries for this page is now over 60, all of them are fetching the group names for the same user again and again.

I'm not exactly sure what ljsjl's use case was, but I think that changing group membership and then checking it again for that same user within one single request is not the typical use case. Even the Django docs regarding Permission Caching say that it's fine to cache them by default.

If you decide against using the cache by default again, what would be the best way forward for people who want to use it? Right now I've basically copied the predefined is_group_member predicate and modified it. Is that the best solution? Or would you be willing to add another predicate to your package, something like cached_is_group_member? Or change the signature to

def is_group_member(*groups, cache=False):
    ...
    @predicate(name)
    def fn(user):
        if cache:
            # Use cache.
        else:
            # Refetch them every time.

and use it like this is_admin = is_group_member('admin', cache=True)? Not a big fan of the last one, but it would work.

Correct usage of rules py in module

Assuming I have a django project directory like so:

AppA:
-#views
-#models
-#rules.py
etc

AppB
-#settings
-#wsgi
etc

Rules
-compat
-contrib
-templatetags
-#apps
etc

In App A I am defining predicates in the rules.py file, which has the absolute import from future at the top. I want to use these predicates in the App A views file, so I go from 'rules.contrib.views import permission_required'. This however tries to import contrib.views from AppA/rules.py, rather than from the Rules package. What am I doing wrong?

The documentation isn't super clear about this I don't think! I have 'rules.apps.AutodiscoverRulesConfig' in my installed apps.

Django 1.10.4 autodiscover for rules.py modules failed

Replaced 'rules', with 'rules.apps.AutodiscoverRulesConfig', on INSTALLED_APPS settings to enable auto discover mode of rules.py modules as docs said and got this error:

ImportError: No module named 'rules.apps.AutodiscoverRulesConfig'; 'rules.apps' is not a package

P.S: I'm using Python 3.5.2 inside venv

Django 1.11 missing in tests

There is no Django 1.11 in the test matrix. Is this project still alive and being kept up to date with new Django releases?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.