Permissions are hard. Ensuring the javascript front-end of your application is working with the same set of permissions for a given entity as the backend is even harder. The first step towards madness is trying to code the business rules that grant/remove permissions in the frontend. This is opinion not fact. Some people may have managed to do this successfully but I’ve never met them. I like to manage permissions as follows:

Step 1 - Enumerate the permissions

For each entity type in your system define an Enum that encapsulates all the possible permissions. Namespace your permissions. Choose good names.

class EntityPermissionsEnum:
    EDIT = 'entity:edit'
    PUBLISH = 'entity:publish'
    VIEW = 'entity:view'

Step 2 - Centralize the permissions business logic

For each entity type in your system define a Permissions Class that has a get_permissions(entity) class method on it

class EntityPermissions:

    @classmethod
    def get_permissions(cls, requesting_user, entity):
        """
        Calculates the maximal set of permissions the requesting user has on the specified entity
        :param user requesting_user: the user in context
        :param model entity: the entity to get permissions on
        :return set: 
        """
        
        # Start with maximal permissions defined as a Set
        permissions = {
            EntityPermissionsEnum.VIEW,
            EntityPermissionsEnum.EDIT,
            EntityPermissionsEnum.PUBLISH,
        }
        
        # Discard permissions according to your business logic needs
        if entity.owner != requesting_user:
            permissions.discard(EntityPermissionsEnum.EDIT):
            
        # Use fancy set operations to keep code clean
        if not requesting_user.is_admin:
            permissions.intersection_update({EntityPermissionsEnum.VIEW, EntityPermissionsEnum.VOTE})
            
        return permissions
        

Step 3 - Check permissions on server-side using set membership

Checking for permissions is now a case of calling the get_permissions() method and testing for the required permission or permissions within the permissions set.

permissions = EntityPermissions.get_permissions(requesting_user=requesting_user, entity=entity)
if EntityPermissionsEnum.VIEW not in permissions:
    raise PermissionsError()

Step 4 - Serialize the permissions along with the rest of entity on the way out

Wherever you serialize the entity in REST responses, add a generated field called permissions that contains the list of permissions. If you are using Django REST Framework you can use method serializer fields to dynamically call get_permissions() and then mix this into a base serializer class.

def rest_view():
    # get the entity...
    entity_dict = dict(entity)
    permissions = EntityPermissions.get_permissions(requesting_user=requesting_user, entity=entity)
    entity_dict['__permissions__'] = list(permissions)
    return json.dumps(entity_dict) 

The serialized entity will now look like:

{
    id: 1,
    title: 'blah',
    owner: 4,
    __permissions__: ['entity:view', 'entity:edit', 'entity:publish']
}

Step 5 - Use javascript Array methods to check permissions in frontend

Use the new Array methods in ES6 to check permissions. We could use Maps, but they don’t serialize to/from json nicely so I find Arrays work best.

const ENTITY_PERMISSIONS = {
    VIEW: 'entity:view',
    EDIT: 'entity:edit',
    PUBLISH: 'entity:publish',
}

if(entity.__permissions__.includes(ENTITY_PERMISSIONS.EDIT)){
    // Render Edit Button
}

Step 6 - Improvements

  • Automatically generate the javascript permissions constants from the enum classes to keep them in sync.
  • Cache the entities on the server for the lifetime of the request if they are expensive to generate