avatar

Derek Zeng

A programmer

How to design a proper permission system

by coderek

Any content management system must have a robust "auth" component. "Auth" stands for authentication and authorization. Authentication is an well talked topic. Authorization is the interest of this post.

If you are familiar with OAuth, it is one application/implementation of authorization for sharing the resources with third party. Linux user permission system is another example of authorization management. They are all good examples. In my career as a web developer, I saw many other implementations, some by frameworks, some by the developers themselves. Most of times, the one designed by developers are quite ugly, fragile and inflexible. But in one of my recent projects I saw a very elegant and robust design that I would like to share here.

The project is a django application, but it was started before version 1.5 so there was no permission built into the framework.

Before I show the neat part of the design, let me talk about the general design first.

Key models

User and Group

Permissions are usually managed by 2 levels, user and group. User in a group inherits all the permissions given in the group. However user's own permission take precedence over everything else.

User group is sometimes referred to as Role. Examples of user groups are root/admin/manger/operation etc. User and group are many-to-many relations. Of course, super user should live beyond the permission system.

User can be authenticated or anonymouse. Authenticated users are known to the system, and they are distinct individuals with protected resources. Anonymouse users is unknown to the system. So the system categorize them in one Anonymouse group with very limited permissions that is the subset of all other groups. Some system also skips the creation of such group, and defaults read-access to all public resources.

Permission

Permission itself should be designed as a separate entity. It is like a token when you want to do something in the system. You can only proceed when you have the token in the permission space.

Permissions are associated with user and group entities. They are many-to-many relations respectively.

Permission ususally has two types, read and write. The targets of the permission also has multiple types. More generally, we can have permissions for a feature. Then, we have permissions for each database column. In our project, we have another type called 'Report'. Reports are special readonly data, they are by no means changable. And in our project they are data from our reporting database.

Permission inference

Since all user resources are stored inside database (at least the indices), all levels of permissions are boiled down to read/write access on database columns. How to achieve this? We use permission inference. We have a config file that store all the permissions and inferences in dictionary formats. When the system starts/restarts, all column level permissions are synced into the database. Then while the application is running, we compute the inferred permissions from the config for the required user/group whenever it's requested. Since this computation can take some time, we also store the computed permission-set in the cache. So whenever the user is asked for a permission, we can easily check from cache. For user, this inference also includes the inherited group permissions, so the order should be

  1. compute all permissions for group
  2. compute infered permissions for group
  3. compute all permissions for user based on groups he/she belongs
  4. compute infered permissions for user

Infered permissions greatly reduced the complexity of the system. When we assign permissions to user/group, we can assign a more abstract permission that infers to many smaller permissions. This also provides better user experience.

Permissions

The downside is that you have to manage the cache, but since this is one way write and read cache, it won't cause too much trouble.

Use the permissions

These are some example resources protected by permissions.

  1. html views, e.g. reports, details about something
  2. images
  3. data
  4. database models

In order for these to work we need to create permission access methods in various levels. Since session contains user object, so we can define a can method on user model.

class User(models.Model):

    def can(self, perm):
        if self.is_super_user:
            return True

        elif perm in self.get_all_perms():
            return True

        return False

Then in the template we can easily use it like

{% if user.can('write:user_form') %}
    {% include 'user_form.html' %}
{% endif %}

This is very handy.

JSON Response

We use ajax extensively to obtain json data from server side. The ajax response is also enforced by permissions. When the some field is not allowed to return, they will be stripped off from the response. In order to reduce the complexity of permission checking, we delay the checking to the last minute and only perform the checking on the ready-to-send data.

(End of article)