Django-Guardian: A Full Access Control Logic (ACL) Example

This image has absolutely nothing to do with django except that the word guardian reminds of an angel and angels remind me of my old magic the gathering days.

Django-Guardian is an object level permission library used in Django. I’ve recently implemented in place of my own home rolled solution. Boy, was I glad I did it. This blog post will take you through the entire process. If you have any good ideas on how to improve it, do leave a comment!

So the product that will be using this library an Academic Event Management Software. It is a SaaS site that allows academics to setup events. The role requirements are such:

  1. Each newly setup event will have a set of pre-defined roles
  2. Each role will have differing permissions
  3. It is possible for one user to have multiple roles for multiple events at the same time

For simplicity sakes, I will be reducing the number of roles I will be implementing to just 2 and they are a Delegate role and a Admin role.

Step 1: Event Creation

The save method of each newly created event instance calls a role_init method that creates the groups and permissions objects for each event.

Event Model

class Event(models.Model):
    name = models.CharField('Event Name', max_length=255)
    slug = models.SlugField(blank=True)

    class Meta:
        permissions = (
                ('change_registration', 'Change Registration Setup'),
                ('change_submission', 'Change Submission Setup'),
                ('view_registration_admin', 'View Registration Admin'),
                ('add_users', 'Add New Users'),
                )

    def save(self, *args, **kwargs):
        from role.utils import roles_init_new

        is_create = False
        if not self.id:
            is_create = True

        if not self.slug:
            self.slug = unique_slugify(self, self.name)

        super(Event, self).save(*args, **kwargs)

        if is_create:
            roles_init_new(self)

GroupEvent Model

class GroupEvent(models.Model):
    '''
    This model associates a group with an Event.
    Allows for easy retrieval of Group (Role Name) per event
    '''
    group = models.ForeignKey(Group)
    event = models.ForeignKey(Event)
    modified = models.DateTimeField(auto_now=True)
    created = models.DateTimeField(auto_now_add=True)

    def __unicode__(self):
        return u"%s" % self.group.name

    class Meta:
        unique_together = ['group', 'event']

roles_init_new function

def roles_init_new(event):
    '''
    Create new groups for the event
    '''
    g1 = Group.objects.get_or_create(name='%s: Admin - All' % event.name)[0]
    g2 = Group.objects.get_or_create(name='%s: Delegate' % event.name)[0]

    # ADMIN ALL GROUPS #
    assign('change_event', g1, event)  # change event details
    assign('change_registration', g1, event)  # change submission settings
    assign('change_submission', g1, event)  # change registration setup
    assign('view_registration_admin', g1, event)  # change registration tools
    assign('add_users', g1, event)  # user create functionality

    GroupEvent.objects.get_or_create(group=g1, event=event)
    GroupEvent.objects.get_or_create(group=g2, event=event)
    return True

The following things happen with the creation of a new event:

  1. 2 Group objects are created using the event name as part of the name of the Group. So an Event called Naruto would have the following groups:
    1. Naruto: Admin – All
    2. Naruto: Delegate
  2. Each group is that assigned a set of permissions. These permissions are defined in the Event model meta Class permissions. I am using a shortcut provided by django-guardian to assign permissions object level permissions to these groups. In my case, I am only assigning them to the Naruto: Admin – All group as the Naruto: Delegate group needs no permissions for now
  3. I created a intermediary table called GroupEvent which is essentially a model containing a FK to my Event model and a FK to the Group model. This is to allow for easy querying of results
  4. Django-guardian creates permissions associated with each group and object, which in this case is the newly created event, in the GroupObjectPermissions table. This is the model where the magic happens.

Step 2: User Creation & Association

This part is easy. Once you’ve set up the groups in step 1, all that’s left is a simple matter of associating a user to a desired group. Recall that each Group has a set of object level permissions associated with it and assigning a user into that Group will give the user all of those permissions as well. In code speak, it will look like this

event = Event.objects.get(name='Naruto')
user = User.objects.get(email=new_user@email.com)
group = Group.objects.get(name='%s: Admin - All' % event.name)
user.groups.add(group)

Step 3: Usage

So now that you have a user object that has been added to a group that has been assigned permissions and associated with an event , you’re ready to use it!

I have in my context processor, a method that retrieves all of the roles for the user regardless of Event

def user_roles(request):
    '''
    Return a list of all the user roles
    '''
    try:
        g = Group.objects.filter(user=request.user)
        ge_list = []

        for group in g:
            if GroupEvent.objects\
                    .filter(group=group)\
                    .exists():
                ge = GroupEvent.objects.get(group=group)
                ge_list.append(ge)
    except:
        ge_list = []

    return {'USER_ROLES': ge_list}

In my template, I can check for a user’s permissions like this and vary what I want to show:

{% load guardian_tags %}

{% get_obj_perms request.user for request.session.event as 'perms' %}

{% if 'change_event' in perms %}

show something

{% else %}

dont show something

{% endif %}

I can also protect my views by adding a decorator like so

@login_required
@permission_required_or_403('change_event', (Event, 'slug', 'event_slug'))
def event_admin_dashboard(request, event_slug):
    '''
    Admin dashboard for a particular Event
    '''
    event = get_object_or_404(Event, slug=event_slug)
    template = 'events/event_admin_dashboard.html'
    template_vars = {'event': event}
    return render(request, template, template_vars)

To get all the Groups a user is part of for a particular event, you can use a method like this

def get_roles_for_event(user, event):
    '''
    Returns a list of Group Event objects that the user
    is a group of
    '''
    group_events = GroupEvent.objects.filter(event=event)
    groups = Group.objects.filter(user=user)
    roles = []
    for group_event in group_events:
        for group in groups:
            if group == group_event.group:
                roles.append(group_event)
    return roles

Lastly, in my settings.py file, I have the following options:

ANONYMOUS_USER_ID = -1
GUARDIAN_RENDER_403 = True

I also have a template called 403.html in my main templates folder which will be shown if a user tries to access a page he doesn’t have permissions to.

Advertisements

One thought on “Django-Guardian: A Full Access Control Logic (ACL) Example

  1. Thanks! I have done a couple of small django projects, (which are password protected, sorry: bishopchatardextra.org, edc-info-org) and now I’m adding django-userena to one of them. Which has lead me to guardian. So, your blog was great!, for me to get my head around. Would it be possible for you to send me, or provide a zipped file with this, as a working example? I think I can figure it out, but I’d appreciate a small working example. If not, okay. Mostly, I wanted to say thanks for this post. (If you publish this, please take out my url examples)

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s