Handling Authorization in a Service-Oriented Architecture  

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.

Limitations #

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 Ability class.

Breaking encapsulation #

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 (Ability).

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/[id]/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 class:

# 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 Ability class, 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.

 
4
Kudos
 
4
Kudos

Now read this

Rake tasks from a Rubygem

While recently working on IronHide, specifically the CouchDB Adapter, I needed a way to have the gem expose custom Rake tasks to an end user’s application. For example, to make working with the IronHide CouchDB Adapter easier, I wanted... Continue →