Permissions Inheritance

The DIP allows you to implement inheritance permissions for your objects. For example, a librarian does’t need to have explicit permissions to all their books in his library as long as the books make it clear that the library is an object in which it “belongs” to.

Class RoleOptions

The class RoleOptions works just like the Meta class in the Django models, helping us to define some attributes related to that specific model. This class has the following attributes:

Attribute Type Description
permission_parents list of str List of ForeignKey or GenericForeignKey fields on the model to be considered as parent of the model.
unique_together bool If True, this model only allows one assignment to any User instance.

Working with the inheritance

Let’s go back to that first example, the model Book. We are going to implement another model named Library and create a ForeignKey field in Book to create a relationship between them. So, our models.py will be something like that:

# myapp/models.py

from django.db import models

class Library(models.Model):
    name = models.CharField(max_length=256)


class Book(models.Model):
    title = models.CharField(max_length=256)
    content = models.TextField(max_length=1000)
    my_library = models.ForeignKey(Library)

    class Meta:
        permissions = (
            ('read_book', 'Can Read Book'),
            ('review_book', 'Can Review Book')
        )

We need to say to DIP that the my_library represents a parent of the Book model. In other words, any roles related to the Library model with inherit=True will be elected to search for more permissions.

The way to do this is implementing another inner class in the model, the class RoleOptions and defining the list permission_parents:

# myapp/models.py

from django.db import models
from improved_permissions.mixins import RoleMixin


class Library(models.Model, RoleMixin):
    name = models.CharField(max_length=256)


class Book(models.Model, RoleMixin):
    title = models.CharField(max_length=256)
    content = models.TextField(max_length=1000)
    my_library = models.ForeignKey(Library)

    class Meta:
        permissions = (
            ('read_book', 'Can Read Book'),
            ('review_book', 'Can Review Book')
        )

    class RoleOptions:
        permission_parents = ['my_library']

Let’s create a new role in order to represent the Library instances.

# myapp/roles.py

from improved_permissions.roles import Role
from myapp.models import Library

class LibraryManager(Role):
    verbose_name = 'Library Manager'
    models = [Library]
    allow = []
    inherit = True
    inherit_allow = ['myapp.read_book']

After that, the field my_library already represents a parent model of the Book. Now, let’s go to the terminal to make some tests:

# Django Shell

from django.contrib.auth.models import User
from improved_permissions.shortcuts import assign_role, has_permission
from myapp.models import Book, Library
from myapp.roles import LibraryManager

john = User.objects.get(pk=1)

library = Library.objects.create(name='Important Library')
book = Book.objects.create(title='New Book', content='Much content', my_library=library)

# John has nothing :(
has_permission(john, 'myapp.read_book', book)
>>> False

# John receives an role attached to "library".
assign_role(john, LibraryManager, library)

# Now, we got True by permission inheritance.
has_permission(john, 'myapp.read_book', book)
>>> True

Unique roles to a given object

There is a scenario where a model has several roles related to it, but a single user must be assigned to only one of them. In order to allow this behavior, we have the boolean attribute called unique_together.

Let’s say that one user must not be the Author and the Reviewer of a given Book instance at same time. Let’s see on the terminal:

# Django Shell

from django.contrib.auth.models import User
from improved_permissions.shortcuts import assign_role, has_permission
from myapp.models import Book
from myapp.roles import Author, Reviewer

john = User.objects.get(pk=1)
book = Book.objects.create(title='New Book', content='Much content', my_library=library)

# John is the Author.
assign_role(john, Author, book)

# And also the Reviewer.
assign_role(john, Reviewer, book)

# We cannot allow that :(
has_permission(john, 'myapp.read_book', book)
>>> True
has_permission(john, 'myapp.review_book', book)
>>> True

Now, let’s change the class RoleOptions inside of Book:

# myapp/models.py

from django.db import models
from improved_permissions.mixins import RoleMixin

class Book(models.Model, RoleMixin):
    title = models.CharField(max_length=256)
    content = models.TextField(max_length=1000)
    my_library = models.ForeignKey(Library)

    class Meta:
        permissions = (
            ('read_book', 'Can Read Book'),
            ('review_book', 'Can Review Book')
        )

    class RoleOptions:
        permission_parents = ['my_library']

        # new feature here!
        # --------------------
        unique_together = True
        # --------------------

Going back to the terminal to see the result:

# Django Shell

from django.contrib.auth.models import User
from improved_permissions.shortcuts import assign_role, has_permission
from myapp.models import Book
from myapp.roles import Author, Reviewer

john = User.objects.get(pk=1)
book = Book.objects.create(title='New Book', content='Much content', my_library=library)

# John is the Author.
assign_role(john, Author, book)

# Can be the Reviewer now?
assign_role(john, Reviewer, book)
>>> InvalidRoleAssignment: 'The user "john" already has a role attached to the object "book".'

Yeah! Now we are safe.