/iron_hide

IronHide is an authorization library for Ruby

Primary LanguageRubyMIT LicenseMIT

IronHide

Gem Version Build Status Code Climate

Experimenting with a new way to implement authorization.

IronHide is an authorization library. It uses a simple, declarative language implemented in JSON.

For more details around the motivation for this project, see: http://eng.climate.com/2014/02/12/service-oriented-authorization-part-1/

For a tiny example, look here https://github.com/TheClimateCorporation/iron_hide_sample_app

Installation

Add this line to your application's Gemfile:

gem 'iron_hide', path: '/path/to/source'

And then execute:

$ bundle install

Or build and install it yourself as:

$ gem build '/path/to/iron_hide.gemspec'
$ gem install iron_hide.gem

Usage

Rules Language

Authorization rules are JSON documents. Here is an example of a document:

  [
    {
      // [String]
      "resource": "namespace::Test",

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

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

      // [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::manager_id": ["user::id"]
          }
        },
        {
          "not_equal": {
            "user::disabled": [true]
          }
        }
      ]
    }
  ]

The language enables a context-aware attribute-based access control (ABAC) authorization model. The language allows references to the user and resource objects. The library (i.e., IronHide) should guarantee that it is able to parse the attributes of these objects (e.g., user::attribute::nested_attribute), while maintaining immutability of the object itself.

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
    • 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:

  • A logical AND is applied to conditions within a condition block and to the keys with that condition.
  • A logical OR is applied to the values of a single key.
  • All conditions must be met (logical AND across all conditions) to return an allow or deny decision.

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]
  }
}

Configuration

IronHide must be configured during application load time.

This is an example configuration that uses authorization rules defined in a JSON file.

# config/application.rb
require 'iron_hide'

IronHide.config do |c|
  c.adapter   = :file

  # This can be one or more files
  c.json      = '/path/to/json/file'

  # This is helpful if you have multiple projects with similarly named
  # resources
  c.namespace = 'com::myproject' # Default 'com::IronHide'

  # See Memoizing below
  c.memoize = true # Default
end

Public API

There are two ways to perform an authorization check. If you have used CanCan, then these should look familiar.

Given a very simple relational schema, with one table (users):

users
id
manager_id

Given a rule like this:

    {
      "resource": "namespace::User",
      "action": ["read", "manage"],
      "description": "Allow users and managers to read and manage users",
      "effect": "allow",
      "conditions": [
        {
          "equal": {
            // The user's ID must be equal to the resource's ID or the resource's manager's ID
            "user::id": ["resource::id", "resource::manager_id"]
          }
        }
      ]
    }

Authorize one user for "reading" another:

current_user = User.find(2)
IronHide.authorize! current_user, :read, User.find(1)
#=> Raises an IronHide::Error if authorization fails
current_user = User.find(2)
IronHide.can? current_user, :read, User.find(1)
#=> true

Attribute Memoization

Each time ::can? or ::authorize! is called, 0 or more rules are evaluated. Each of these rules could depend on the evaluation of an unbounded number of expressions.

In the last example of the previous section, the :id attribute of a user must match the :manager_id attribute of a resource. We can imagine the case where the method call, resource.manager_id could potentially be expensive (e.g., it's not a simple DB attribute and requires a complex SQL query).

Memoization caches the method call, resource.manager_id, so that subsquent rules that attribute do not repeat the call. Here is a simple example where two rules need to be evaluated for a single action, read and memoization can improve performance.

[
  {
    "resource": "namespace::User",
    "action": ["read"],
    "description": "Allow users read users",
    "effect": "allow",
    "conditions": [
      {
        "equal": {
          "user::id": ["resource::id", "resource::manager_id"]
        }
      }
    ]
  },
  {
    "resource": "namespace::User",
    "action": ["read", "manage"],
    "description": "Allow users to read and manage users",
    "effect": "allow",
    "conditions": [
      {
        "equal": {
          "user::id": ["resource::manager_id"]
        }
      }
    ]
  }
]

Adapters

IronHide works with rules defined in the canonical JSON language. The storage back-end is abstracted through the use of adapters.

An available adapter type must be specified in a configuration file, which gets loaded with the application at start time.

The default adapter is the File Adapter.

File Adapter

The File adapter allows rules to be written into a flat file. See spec/rules.json for an example.

CouchDB Adapter

See: https://github.com/TheClimateCorporation/iron_hide-storage-couchdb_adapter

Contributing

  • bundle install to install dependencies
  • rake to run tests
  • yard to generate documentation
  • Pull requests, issues, comments are welcome

Further Reading

TODO

  • Write a more detailed language specification
  • Better README
  • Admin interface for modifying policies