Part 1: Motivation
It’s a common theme: as our business requirements grow, so does our application. One day we discover we have coupled multiple business function sets in a monolithic application. In order to manage complexity and meet scaling requirements, we segregate the functional groups into several separately deployed applications, or services.
This is typically what we would call service-oriented architecture (SoA). I’m not writing here about the pros or cons. That’s already been done pretty well.
Here, I talk about the challenges of scaling an authorization framework, the problems we run into, and finally a solution we can adopt.
Can you CanCan?
In the beginning, we have a small Rails app. We have the notion of users and these users interact with a few resources. The domain model is simple. We can fit the entire thing in our head. CanCan is very flexible with how we define our authorization model, and let’s pretend we’ve chosen a good design for it.
CanCan allows us to define our authorization rules or abilities in the
Ability class. Here is what a few lines from my Ability class looks like:
class Ability include CanCan::Ability def initialize(user) user ||= User.new # guest user (not logged in) if user.ageny_principle? manage_agency else ... end end end # Declare a rule def manage_agency can :manage, Agency, id: current_user.agency_id end
This is some simple, role-based authorization.
There are some limitations to this approach, which are not apparent at first. As our system grows, so does our domain model, the types of users/roles, and the number of resources.
Our authorization system might be doing more complex things, like assigning resource-specific roles to users. Here is a simple, naive example to just illustrate:
# user.rb def manager_access?(resource) resource.managers.include?(user) end def limited_access?(resource) resource.limited_users.include?(user) end def standard_access?(resource) resource.standard_users.include?(user) end
(Note: I don’t recommend this approach.)
So, now users might be assigned system-wide roles (admin, employee, etc.), and resource-level roles (manager, limited, standard, etc.). The logic grows and so does our
Abilities are defined in the application code. This allows us to be lazy or sloppy and allow the encapsulation of the rules to leak outside the proper abstraction (
For example, before purchasing an insurance policy, I check that the user has been certified to sell policies for that particular year:
# policy.rb def purchase if self.agent.certified?(Date.current) ... else false end end
This seems harmless, however, we’ve just broken encapsulation. This is what the code should look like.
First, the HTTP API to allow purchasing,
PUT/api/v1/policies//purchase # policies_controller.rb
def purchase policy = Policy.find(params[:id]) if current_user.can?(:purchase, policy) policy.purchase else # Render a useful error message end end
Then the purchase logic itself, encapsulated in the
# policy.rb def purchase do_purchasing_stuff end
Hard to read, hard to maintain
At first glance, this might seem like a contrived example. The point I’m trying to illustrate is that as our application grows it becomes harder and hard to reason about all the rules we’ve set in place for our business requirements.
Even if we managed to put all our authorization logic into the
Abilityclass, our application code is tightly coupled to the definition of the rules. As our application grows and spans multiple domains and products, we can see how the complexity begins to grow. We have multiple products, each with its own model of authorization, and each with its own business requirements.
Now, as we try to split our monolithic Rails application into multiple services, we have to carefull deconstruct the
Ability class into two separate pieces. If we’re lucky and our new service will be a Ruby application, we can reuse much of what we’ve already written. What if the new service will be written in Java or Scala? We’ll end up rewriting much of our authorization logic. This is a bad idea.
To be continued…
In Part 2 I will talk about a new way to design our authorization framework, and the advantages we’ll get over it.
In Part 3 I’ll share the framework that my team and I have designed to deal with the challenge.