Handling Authorization in a Service-Oriented Architecture  

Part 2: Designing for Decoupling #

Last week, I closed the discussion on authorization with the observation that existing authorization libraries in the Ruby ecosystem rely on coupling the application code to the authorization rules.

Make things as simple as possible, but not simpler. – Albert Einstein

Applications tend towards complexity over time and it’s our job as engineers to keep our systems simple. I started with the assumption that it must be possible to design an application with a large set of authorization rules and a simple design.

With CanCan, complexity quickly grows as the number of rules grow. You need to quickly find ways to keep the Ability class in check. To provide a simple example of how this can happen, let’s distill our application’s user domain model into two sets of users:

  1. Growers: Users with insurance policies and Climate.com accounts
  2. Agents: Users who sell both insurance and Climate.com accounts to growers

Our Climate.com product and our insurance products are separate concerns, though the users that interact with each may be the same. The actions these users take and the resources they interact with (e.g., policies, fields, quotes, subscriptions) differ across the two domains. We could split our Ability class into two modules that might look like this:

class Ability
  include InsuranceAbilities
  include ClimateDotComAbilities
  ...
end

Then it should be obvious where to find authorization logic for Insurance vs. Climate.com. Unfortunately, we never took this approach. So, now the only choice I have is really to re-write our existing authorization rules. My team has a test-driven culture, so I can be relatively confident when refactoring this code.

Let’s step back for a moment, though…

Goals #

If I were to design the authorization framework for my application today, I would have two goals in mind: flexibility and scalability.

Flexibility #

To reach this goal, I needed the authorization framework to (1) be platform or language agnostic, and (2) exist outside the application code.

We chose to define our rules in a canonical, JSON-based language. JSON is great because it’s easy to read, for both humans and machines.

After several iterations, my team and I decided on this canonical representation for a set of rules. Here is how we would represent the rule: “A user may perform the action ‘read’ on an Agency if the id of the Agency matches the agency_id of the user AND the user is not disabled.”

[
  {
    // [String]
    "resource": "com::climate::Agency",

    // [Array<String>]
    "action": ["read"],

    // [String]
    "description": "",

    // [String]
    "effect": "allow",

    // [Array<Object>]
    "conditions": [
      // All conditions must be met (logical AND)
      {
        "equal": {
          // The numeric value of the key must be equal to any value in the array (logical OR)
          "resource::agency_id": ["user::agency_id"]
        }
      },
      {
        "not_equal": {
          "user::disabled": [true]
        }
      }
    ]
  }
]

This language is designed to be the minimally required syntax to provide all the desired traits and allow for future flexibility. The design of the language is heavily inspired by other similar frameworks, especially Amazon’s AWS policy language. I suggest checking it out

Following is a description of the top-level objects in each rule.

Resource #

The resource to which the rule applies. These should be namespaced properly, since multiple applications may share resources.

Action #

An array of Strings that specifies the set of actions to which the current rule applies.

Actions can be named anything you want and in Ruby/Rails these would typically be aligned with the instance methods for a class:

class User
  # The 'delete' action
  def delete
    ...
  end

  # The 'charge' action
  def charge
    ...
  end
end

Description #

A string that helps humans reading the rule JSON understand it more easily. It’s optional.

Effect #

This is required. It is the effect a rule has when a user requests access to conduct an action to which the rule applies. It is either ‘allow’ or ‘deny’.

Evaluation of Rules #

  1. Default: Deny
  2. Evaluate applicable policies
    • Match on: resource and action
  3. Does policy exist for resource and action?
    • If no: Deny
  4. Do any rules resolve to Deny?
    • If yes, Deny
    • If no, Do any rules resolve to Allow?
      • If yes, Allow
    • Else: Deny

If access to a resource is not specifically allowed, authorization will default to DENY. This should make it easy to reason about: “A user was denied this request. I should create a rule that specifically allows access.”

Conditions #

Conditions are expressions that are evaluated to decide whether the effect of a particular rule should or should not apply. The expression semantics are dictated by the consuming application and the implementation of the library code that is used to communicate with and parse our rules.

This object is optional (i.e., the rule is always in effect). It is an array of objects to allow multiple of the same type of condition to be evaluated (e.g., ‘equal’, ‘not_equal’).

When creating a condition block, the name of each condition is specified, and there is at least one key-value pair for each condition.

How conditions are evaluated:

In our early implementation, we started with two types of expressions: equal and not_equal.

Again, very similar to the approach taken for Amazon’s policy language.

The key/value pairs in an equal or not_equal condition allow the rule to reference chained method calls the user and resource.

For example, here the agency_id of a resource must equal the agency_id of a user.

// Condition
{
  "equal": {
    "resource::agency_id": ["user::agency_id"]
  }
}

The value of a key in a condition may be checked against multiple values. It must match at least one for the condition to hold.

// Condition
{
  "equal": {
    "user::role_id": [1,2,3,4]
  }
}

Scalability #

I like JSON because it helps keep things simple. This means designing for scalability becomes easier. In the initial implementation of this design, I am able to store all my rules on disk in flat files. As I start thinking about scaling this system, I’d like to move away from using flat files and JSON allows us to easily do that. There are many data stores available today that target JSON as a storage format (Couch and Mongo come to mind). With a good design, changing data-stores could be a simple choice.

Implementation #

In Part 3, tie everything together. I show our implementation of a Ruby gem we that serves as the bridge between the back-end storage of our rules and provides a nice, CanCan-like interface to perform authorization in the application code.

I address the scalability issue more closely. I show how we can specify multiple, swappable back-ends for our rules without changing any part of our application code. Finally, I discuss a few other advantages of this design, including the ability to specify and edit rules outside the application development lifecycle (i.e., commit, merge, build, and deploy).

 
2
Kudos
 
2
Kudos

Now read this

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... Continue →