2 Comments

Managing Complex Permissions in CanCanCan for Rails

We’re using the CanCanCan authorization gem to control access to resources in our current Rails project. It’s a great way to get started and covers a lot of ground with its basic and extended syntaxes, and it has decent support for adding more complex permissions checks that may not fit into more conventional patterns.

It’s very easy to express rules, like granting users the ability to manage their own User record:

can :manage, User, id: user.id

Indirect resource relationships can be traversed intuitively, say, to restrict WorkSchedule management solely to the owner of a Location:

can :manage, WorkSchedule, work_schedule: { location: { user_id: user.id } }

However, when it came time for us to support dynamic, domain-level permissions for the application, we needed slightly more complex, tailored code to get answers. For example, if we want to know whether a given User is permitted to manage pricing data when setting up Promotions at a given Location, we can figure that out by retrieving the relationship between user and location, and querying that relationship for the proper permission:

relation = LocationUser.where(user: user, location: location)
relation = LocationUser.new if relation.nil?
relation.can_manage_pricing?

This code answers the compound question: “Does this user have a managing relationship with the given Location, and if so, is she permitted to manage pricing?”

Some problems with this code:

  • It’s big, clunky, and repetitive. This is shorter, but not better:
    Manager.where(user: user, location: location) || Manager.new).can_manage_pricing?
  • It exposes details unnecessarily: users and locations are linked via a Manager class, how to find Managers properly in the database, and that permissions are accessed directly on the Manager objects.
  • Requires careful reading to determine the intent and function.

Contrast with the following equivalent code:

user.managing(promotion.location).can_manage_pricing?

This code is concise and clear: “Is the user allowed to manage pricing at the promotion’s location?” We’re also decoupled from the details of the LocationUser relationship.

Extend the User model by implementing the #managing method:


class User < ActiveRecord::Base def managing(location) managed_location(location) or Manager.new end # Helper method: def managed_location(location) Manager.where(user: self, location: location).first end end[/code] Leverage #managing in your Ability file when expressing complex permissions that don't fit the mold of straightforward user-resource access control: [code lang="ruby"]class Ability include CanCan::Ability def initialize(user) ... can [:show,:update], StoreHours do |store_hours| currently = user.managing(store_hours.location) currently.can_open_store? or currently.can_set_schedules? end can :manage, Promotion do |promotion| user.managing(promotion.location).can_manage_pricing? end ... end end[/code] Though we first used this technique to improve the code in our Ability class, the ideas and code are not bound to CanCanCan, and stand alone as a neat little trick to improve our code in any context.