Despite moving from Ryan Bates’ curation to a community based maintenance a number of years ago, CanCan (now known as CanCanCan) still sees prevalent use in the Rails community and is the leader amongst authorization gems in monthly downloads.

Therefore, I assume, I am not the first developer to run into the problem of managing a small number of high-value clients with this authorization library.

Let me explain by example.

You’ve built a simple SaaS application, and, lo and behold, you land your first paying client, the Kingdom of Grimmdor!

They are very happy with the system, under the current ability file, and pay you handsomely every month.

 class Ability
  include CanCan::Ability

  def initialize(user)
    return if user.blank?

    case user.role.name
    when "King"
      can :manage, all

    when "Blacksmith"
      can :manage, Anvil
      can :create, Sword
      cannot :wield, Sword
      can [:create, :update], Income do |inc|
        inc.legal_tender?
      end

    when "Knight"
      can :wield, Sword
      cannot :create, Sword
      cannot :manage, Anvil

    when "Merchant"
      cannot :create, Sword
      cannot :wield, Sword
      can :manage, Income do |inc|
        inc.legal_tender?
      end
    end
  end
end

Thanks to a very sucessful drip campaign and word of mouth, before you know it, the Kingdom of MaxWoodia approaches you with a sackful of gold asking you to join your client base.

All is great, until you find an angry note via carrier pigeon that there is a problem. In MaxWoodia, all citizens are reared with swordsmanship from birth and therefore your blacksmiths and merchants must also be able to wield the weapon. This ability is in direct contravention with Grimmdor’s needs.

You resolve that you’re not going to throw away perfectly good gold and resolve the issue with the following updated ability file:

 class Ability
  include CanCan::Ability

  def initialize(user)
    return if user.blank?

    case user.role.name
    when "King"
      can :manage, all

    when "Blacksmith"
      can :manage, Anvil
      can :create, Sword
      cannot :wield, Sword do |bsmith|
        bsmith.kingdom == "Grimmdor"
      end
      can [:create, :update], Income do |inc|
        inc.legal_tender?
      end

    when "Knight"
      can :wield, Sword
      cannot :create, Sword
      cannot :manage, Anvil

    when "Merchant"
      cannot :create, Sword
      cannot :wield, Sword do |merchant|
        merchant.kingdom == "Grimmdor"
      end
      can :manage, Income do |inc|
        inc.legal_tender?
      end
    end
  end
end
 

Okay, that’s not exactly awe inspiring, but cash is king and you’re here to make money, right? You’re a SaaS-entrepreneur and quickly taking over the world. You think.

A third client approaches you, more excited about your app than the rest and with more money than the other two combined. There’s just one catch. The Kingdom of Tenderlovistan is made up of a collection of super-mutant cats that are able to do anything they want.

To boot, you are having meetings with two other potential clients next week who have already indicated their needs to have slightly different Abilities. How far do you twist your Abilities file before it breaks?

My solution is Oligarchical CanCan.

Requirements: small number of high value SaaS clients that rely on identical systems but for the Abilities file.

Instead of having one monster Ability file, consider spreading it out.

You can turn your ability file into a case/when that instantiates different files depending on the client/kingdom.

  class Ability
    include CanCan::Ability
    include Ability::Oligarchy

    def initialize(user)
      return if user.blank?

      case user.kingdom
      when "Grimmdor"
        grimmdor_abilities(user)
      when "Maxwoodia"
        maxwoodia_abilities(user)
      when "Tenderlovistan"
        tenderlovistan_abilities(user)
      end
    end
  end

Then move the ability file where is most useful for you:

  #app/models/concerns/abilities/grimmdor_abilities.rb
  module Ability::Oligarchy
    extend ActiveSupport::Concern

    included do
      def grimmdor_abilities(user)
        case user.role.name
        when "King"
          can :manage, all

        when "Blacksmith"
          can :manage, Anvil
          can :create, Sword
          cannot :wield, Sword
          can [:create, :update], Income do |inc|
            inc.legal_tender?
          end

        when "Knight"
          can :wield, Sword
          cannot :create, Sword
          cannot :manage, Anvil

        when "Merchant"
          cannot :create, Sword
          cannot :wield, Sword
          can :manage, Income do |inc|
            inc.legal_tender?
          end
        end
      end
    end
  end

Extending this out to all clients, you can effectively cater to their special needs without turning your Ability file into something of terror.

FINAL NOTE: For longer term scalability, or for larger Oligarchical setups, I would heartily recommend a specific feature test folder to handle derogations (such as spec/features/abilities/tenderlovistan_derogations_spec.rb) to document and test their unique ability features. It would also be highly recommended to have a default “example_abilities” file that new clients are started off with.

Stay in Touch

I like to write about Ruby and building things, typically once every month or so. Get an email when I have written something new.