Django-guardian: Add roles

Created on 16 Jan 2011  ·  14Comments  ·  Source: django-guardian/django-guardian

First of all, let me explain why we didn't add roles in the first place. In short, Django's policy is to include simple but well done batteries. django.contrib.auth is best example. How many times people need to exchange auth with own implementation? I believe not so many. guardian is intended to be kind of "extension" to auth app, with sane & simple interface. And thats it. No much of extra functionality included (except some shortcut functions - but this refer to "sane" part).

Now, whats with roles? All they always needed? In fact, I believe not. Same with groups, really. User is needed as it is a fundamental entity for vast majority of all applications.

Ok, here we are, v1.0 almost out there. So for v1.1, roles support would be probably most important feature. Still, in my view, it should be "optional" in some way.

API change Enhancement

All 14 comments

+1 But I do agree roles is something that is almost never needed in the majority of apps that are written in django.
Every now and then somebody like us do try to implement an app (custom document mangement system) for which django wasn't really made for, and when you have a lot a lot of perms, you start thinking about roles :) roles and inheritance are the two things that should make it into v1.1

the optional part is easy though. just like using django doesn't make you use groups, don't make people have to define roles.

thx for the app btw.
ash

+1 for roles, depending on implementation.

+1 for roles and inheritance

I think about roles to be a "group of permissions", I don't know if this is the same for you. With this class of roles assigning a bunch of permissions is easier. I suggest to implement this using Python sets in order to substract one role to other, or get the intersection, and so on:

http://docs.python.org/library/stdtypes.html#set-types-set-frozenset
http://en.wikibooks.org/wiki/Python_Programming/Sets

+1 for roles, inheritance and optional

Hi all,
I am one of the guys who needs - or would like to have - role based object permissions.

I understand from this discussion that for very good reasons it is not a priority for django-guardian. So I will create my own app. However, I think most of the features and components of django-guardian, are fine and if possible, I would not want to re-invent the wheel but reuse those. So my question is, if you are willing to make django-guarding a little bit more pluggable. For example (that is just the first thing the poped up in my mind, while going through the code): make the a setting for ObjectPermissionChecker, that will allow other apps to replace the Checker class with something that takes care of roles?

What do you guys think?

Thanks
Juergen

Hej @schacki, that's actually pretty nice idea. Tests would most probably fail for not-default checker class but we can mark those or make sure they are run with default checker only. Can you create a specific task for it, please?

I would make any non-default checker classes not part of django-guardign but of the "other" app - so it is the job of the other app to the non-default checker classes. Please let me do a more thorough analysis of what might need to be adapted and changed before opening a task. With "task" I assume you mean a separate issue? btw: it might take a couple of weeks before this moves forward.

this[1] project help with anything?

[1] https://github.com/vintasoftware/django-role-permissions

Copied following text by @suriya from #330:

There has been some discussion in the past #23 about adding roles support in django-guardian. However, it did not move far. This is probably because adding roles to django-guardian seems like a very complex task.

I think I have a very simple proposal. I would like to put it forward and if there is interest, implement it. At present, I have my own fork of django-guardian which this functionality. It would be great to see this used by others as well.

The core of django-guardian's permission checking for a view is implemented in the function get_403_or_None(). This function checks that a user has all the required permissions. https://github.com/lukaszb/django-guardian/blob/112c373f213a19d93baa81fa4a941a41333115b5/guardian/utils.py#L98

has_permissions = all(request.user.has_perm(perm, obj) for perm in perms)

Supporting roles is a minor change. Instead of checking that a user has all permissions, we simply have to check whether a user satisfies at least one permission. Something like

if require_all_permissions:
    has_permissions = all(request.user.has_perm(perm, obj) for perm in perms)
else:
    has_permissions = any(request.user.has_perm(perm, obj) for perm in perms)

should do it. To add this support, all we need is to define a new flag in get_403_or_None() and its callers that specifies whether we want all or just some permissions to be satisfied.

I opened #386 and I am realizing that this is probably the same. What I would need is that in the code where I am checking for permission I can do "does this user has edit permissions on this object", but when I am assigning to users permissions, I assign them "moderator" permission, which is a superset of multiple permissions, including "edit" permission. This allows me later on to add more permissions to "moderator" without having to assign them manually to all users.

We can name this hierarchy of permissions, of roles.

+1 for roles, inheritance and optional

I implemented a role concept like bellow:

customize get_40x_or_None

from django.conf import settings
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
from django.http import HttpResponseForbidden, HttpResponseNotFound
from django.shortcuts import render
from guardian.conf import settings as guardian_settings


def get_40x_or_None(request, perms, global_perms, obj=None, login_url=None,
                    redirect_field_name=None, return_403=False,
                    return_404=False):
    """check if the given perms and global_perms are both granted by the user in combination"""
    login_url = login_url or settings.LOGIN_URL
    redirect_field_name = redirect_field_name or REDIRECT_FIELD_NAME

    # Handles both original and with object provided permission check
    # as ``obj`` defaults to None

    has_permissions = False
    if not has_permissions:
        has_permissions = all(request.user.has_perm(perm, obj) for perm in perms) and \
                          all(request.user.has_perm(perm) for perm in global_perms)

    if not has_permissions:
        if return_403:
            if guardian_settings.RENDER_403:
                response = render(request, guardian_settings.TEMPLATE_403)
                response.status_code = 403
                return response
            elif guardian_settings.RAISE_403:
                raise PermissionDenied
            return HttpResponseForbidden()
        if return_404:
            if guardian_settings.RENDER_404:
                response = render(request, guardian_settings.TEMPLATE_404)
                response.status_code = 404
                return response
            elif guardian_settings.RAISE_404:
                raise ObjectDoesNotExist
            return HttpResponseNotFound()
        else:
            from django.contrib.auth.views import redirect_to_login
            return redirect_to_login(request.get_full_path(),
                                     login_url,
                                     redirect_field_name)

implement custom GlobalPermissionRequiredMixin

from django.core.exceptions import PermissionDenied, ImproperlyConfigured
from guardian.mixins import PermissionRequiredMixin
from collections.abc import Iterable

from permission.utils import get_40x_or_None


class GlobalPermissionRequiredMixin(PermissionRequiredMixin):
    global_permission_required = None

    def get_global_required_permissions(self, request=None):
        """
        Returns list of permissions in format *<app_label>.<codename>* that
        should be checked against *request.user* and *object*. By default, it
        returns list from ``global_permission_required`` attribute.

        :param request: Original request.
        """
        if isinstance(self.global_permission_required, str):
            perms = [self.global_permission_required]
        elif isinstance(self.global_permission_required, Iterable):
            perms = [p for p in self.global_permission_required]
        else:
            raise ImproperlyConfigured("'GlobalPermissionRequiredMixin' requires "
                                       "'global_permission_required' attribute to be set to "
                                       "'<app_label>.<permission codename>' but is set to '%s' instead"
                                       % self.global_permission_required)
        return perms

    def check_permissions(self, request):
        """
        Checks if *request.user* has all permissions returned by
        *get_required_permissions* method.

        :param request: Original request.
        """
        obj = self.get_permission_object()

        forbidden = get_40x_or_None(request,
                                    perms=self.get_required_permissions(request),
                                    global_perms=self.get_global_required_permissions(request),
                                    obj=obj,
                                    login_url=self.login_url,
                                    redirect_field_name=self.redirect_field_name,
                                    return_403=self.return_403,
                                    return_404=self.return_404,
                                    )
        if forbidden:
            self.on_permission_check_fail(request, forbidden, obj=obj)
        if forbidden and self.raise_exception:
            raise PermissionDenied()
        return forbidden

In django built in crud views it is now possible to define global_required_permissions and required_permissions:

class ResourceDeleteView(GlobalPermissionRequiredMixin, DeleteView):
    ...
    global_permission_required = [app.delete_resource]
    permission_required = [app.view_resource]
    ...

Conclusion

This solution is based on that a User has the global permission for deleting resources, which could be granted by a Group from the django auth models (The Role in context of this issue). But he can only delete resources for that he has the object based view_resource Permission from the django auth models. I think this could be a common way to implement roles with django guardian.

If i get some thumbs up i will open a pull request with a conflict free code version of my code above.

Really interesting approach @jokiefer. I like that, I had originally been trying to implement guardian along those lines. Users would need the global view or edit permission, but then to access a specific object an individual permission for view or edit was required. The global permission was going to ask act more of an enabling permission without which the object permissions didn't matter. Obviously that didn't actually work so I restructured things.

Would your sample allow for that? i.e. the global delete permission is needed, but to actually delete an object the object-based delete permission is also required. Based on your example, the global delete is needed, and then an object specific view? Is that a bit of a mismatch?

@dwasyl

Would your sample allow for that?
Yes this would work for your approach.

But i didn't focus on the sample above. In our project we implement a simple as possible acl app (AccessControlList). In this app there is one core model called acl. This model is an abstraction of the atomic guardian permissions. It stores a set of users, set of permissions, set(s) of accessible objects. The magic thing behind this model is implemented in some signals. For example ownable_models_handling.py is a generic signal which appends dynamically signals to all other models which shall be accessible with guardian permissions. IF we like to secure more models, we simple need to implement a new m2m field in the AccessControlList model, which must be called with a leading accessible_MODELNAME to work automatically. In the acl_handling.py there is the implementation which adds/removes the guardian permissions based on the given AccessControlList. With that we don't need to modify the guardian core.

Maybe this acl app helps you more than the sample above. With some extra work on the acl app maybe it could be outsource from our code for a generic way to add roles to guadian (an on top app solution for guardian).

Was this page helpful?
0 / 5 - 0 ratings

Related issues

BenDevelopment picture BenDevelopment  ·  5Comments

Dzejkob picture Dzejkob  ·  28Comments

johnthagen picture johnthagen  ·  9Comments

brianmay picture brianmay  ·  16Comments

ad-m picture ad-m  ·  13Comments