Oligarchical SaaS with CanCan
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.