
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:
- Each newly setup event will have a set of pre-defined roles
- Each role will have differing permissions
- 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:
- 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:
- Naruto: Admin – All
- Naruto: Delegate
- 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
- 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
- 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.
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)