Skip to content

Commit

Permalink
Add support for automatic permission registration with Django models
Browse files Browse the repository at this point in the history
  • Loading branch information
Robert Schindler committed Aug 7, 2019
1 parent fcf3711 commit bb09e04
Show file tree
Hide file tree
Showing 5 changed files with 217 additions and 11 deletions.
73 changes: 65 additions & 8 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,10 @@ Table of Contents
- `Using Rules with Django`_

- `Permissions`_
- `Rules and permissions in views`_
- `Rules and permissions in templates`_
- `Rules and permissions in the Admin`_
- `Permissions in models`_
- `Permissions in views`_
- `Permissions and rules in templates`_
- `Permissions in the Admin`_

- `Advanced features`_

Expand Down Expand Up @@ -399,8 +400,63 @@ Now, checking again gives ``adrian`` the required permissions:
False
Rules and permissions in views
------------------------------
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.


Permissions in views
--------------------

``rules`` comes with a set of view decorators to help you enforce
authorization in your views.
Expand Down Expand Up @@ -472,7 +528,8 @@ For more information refer to the `Django documentation`_ and the

.. _Django documentation: https://docs.djangoproject.com/en/1.9/topics/auth/default/#limiting-access-to-logged-in-users

Rules and permissions in templates

Permissions and rules in templates
----------------------------------

``rules`` comes with two template tags to allow you to test for rules and
Expand Down Expand Up @@ -502,8 +559,8 @@ Then, in your template::
{% endif %}


Rules and permissions in the Admin
----------------------------------
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
Expand Down
90 changes: 90 additions & 0 deletions rules/contrib/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
from django.core.exceptions import ImproperlyConfigured
from django.db.models import Model
from django.db.models.base import ModelBase

from ..permissions import add_perm


class RulesModelBaseMixin:
"""
Mixin for the metaclass of Django's Model that allows declaring object-level
permissions in the model's Meta options.
If set, the Meta attribute "rules_permissions" has to be a dictionary with
permission types (like "add" or "change") as keys and predicates (like
rules.is_staff) as values. Permissions are then registered with the rules
framework automatically upon Model creation.
This mixin can be used for creating custom metaclasses.
"""

def __new__(cls, name, bases, attrs, **kwargs):
model_meta = attrs.get("Meta")
if hasattr(model_meta, "rules_permissions"):
perms = model_meta.rules_permissions
del model_meta.rules_permissions
if not isinstance(perms, dict):
raise ImproperlyConfigured(
"The rules_permissions Meta option of %s must be a dict, not %s."
% (name, type(perms))
)
perms = perms.copy()
else:
perms = {}

new_class = super().__new__(cls, name, bases, attrs, **kwargs)
new_class._meta.rules_permissions = perms
new_class.preprocess_rules_permissions(perms)
for perm_type, predicate in perms.items():
add_perm(new_class.get_perm(perm_type), predicate)
return new_class


class RulesModelBase(RulesModelBaseMixin, ModelBase):
"""
A subclass of Django's ModelBase with the RulesModelBaseMixin mixed in.
"""


class RulesModelMixin:
"""
A mixin for Django's Model that adds hooks for stepping into the process of
permission registration, which are called by the metaclass implementation in
RulesModelBaseMixin.
Use this mixin in a custom subclass of Model in order to change its behavior.
"""

@classmethod
def get_perm(cls, perm_type):
"""Converts permission type ("add") to permission name ("app.add_modelname")
:param perm_type: "add", "change", etc., or custom value
:type perm_type: str
:returns str:
"""
return "%s.%s_%s" % (cls._meta.app_label, perm_type, cls._meta.model_name)

@classmethod
def preprocess_rules_permissions(cls, perms):
"""May alter a permissions dict before it's processed further.
Use this, for instance, to alter the supplied permissions or insert default
values into the given dict.
:param perms:
Shallow-copied value of the rules_permissions model Meta option
:type perms: dict
"""


class RulesModel(RulesModelMixin, Model, metaclass=RulesModelBase):
"""
An abstract model with RulesModelMixin mixed in, using RulesModelBase as metaclass.
Use this as base for your models directly if you don't need to customize the
behavior of RulesModelMixin and thus don't want to create a custom base class.
"""

class Meta:
abstract = True
19 changes: 16 additions & 3 deletions tests/testapp/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9 on 2015-12-05 09:27
from __future__ import unicode_literals
# Generated by Django 2.2.2 on 2019-08-05 08:04

import sys

from django.conf import settings
from django.db import migrations, models
Expand All @@ -26,3 +26,16 @@ class Migration(migrations.Migration):
],
),
]

# TestModel doesn't work under Python 2
if sys.version_info.major >= 3:
import rules.contrib.models
operations += [
migrations.CreateModel(
name='TestModel',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
],
bases=(rules.contrib.models.RulesModelMixin, models.Model),
),
]
18 changes: 18 additions & 0 deletions tests/testapp/models.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
from __future__ import absolute_import

import sys

from django.conf import settings
from django.db import models
from django.utils.encoding import python_2_unicode_compatible

import rules


@python_2_unicode_compatible
class Book(models.Model):
Expand All @@ -11,3 +17,15 @@ class Book(models.Model):

def __str__(self):
return self.title


if sys.version_info.major >= 3:
from rules.contrib.models import RulesModel

class TestModel(RulesModel):
class Meta:
rules_permissions = {"add": rules.always_true, "view": rules.always_true}

@classmethod
def preprocess_rules_permissions(cls, perms):
perms["custom"] = rules.always_true
28 changes: 28 additions & 0 deletions tests/testsuite/contrib/test_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from __future__ import absolute_import

import sys
import unittest

from django.core.exceptions import ImproperlyConfigured
from django.test import TestCase

import rules


@unittest.skipIf(sys.version_info.major < 3, "Python 3 only")
class RulesModelTests(TestCase):
def test_preprocess(self):
from testapp.models import TestModel

self.assertTrue(rules.perm_exists("testapp.add_testmodel"))
self.assertTrue(rules.perm_exists("testapp.custom_testmodel"))

def test_invalid_config(self):
from rules.contrib.models import RulesModel

with self.assertRaises(ImproperlyConfigured):

class InvalidTestModel(RulesModel):
class Meta:
app_label = "testapp"
rules_permissions = "invalid"

0 comments on commit bb09e04

Please sign in to comment.