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
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
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.
The resource to which the rule applies. These should be namespaced properly, since multiple applications may share resources.
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
A string that helps humans reading the rule JSON understand it more easily. It’s optional.
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’.
- Default: Deny
- Evaluate applicable policies
- Match on: resource and action
- 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 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]
}
}
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
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
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"]
}
}
]
}
]
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
.
The File adapter allows rules to be written into a flat file. See spec/rules.json
for an example.
See: https://github.com/TheClimateCorporation/iron_hide-storage-couchdb_adapter
bundle install
to install dependenciesrake
to run testsyard
to generate documentation- Pull requests, issues, comments are welcome
- Service-Oriented Authorization blog posts:
- XACML(eXtensible Access Control Markup Language)
- Amazon: Access Policy Language
- Write a more detailed language specification
- Better README
- Admin interface for modifying policies