Authority
Authority helps you authorize actions in your Rails app. It's ORM-neutral and has very little fancy syntax; just group your models under one or more Authorizer classes and write plain Ruby methods on them.
Authority will work fine with a standalone app or a single sign-on system. You can check roles in a database or permissions in a YAML file. It doesn't care! What it does do is give you an easy way to organize your logic, define a default strategy, and handle unauthorized actions.
It requires that you already have some kind of user object in your application, accessible from all controllers and views via a method like current_user
(configurable).
Contents
- Overview
- The flow of Authority
- Installation
- Defining Your Abilities
- Wiring It Together
- Security Violations & Logging
- Credits
- Contributing
Overview
The goals of Authority are:
- To allow broad, class-level rules. Examples:
- "Basic users cannot delete any Widget."
- "Only admin users can create Offices."
- To allow fine-grained, instance-level rules. Examples:
- "Management users can only edit schedules with date ranges in the future."
- "Users can't create playlists more than 20 songs long unless they've paid."
- To provide a clear syntax for permissions-based views. Example:
link_to 'Edit Widget', edit_widget_path(@widget) if current_user.can_update?(@widget)
- To gracefully handle any access violations: by default, it displays a "you can't do that" screen and logs the violation.
- To do all this with minimal effort and mess.
The flow of Authority
Authority encapsulates all authorization logic in Authorizer
classes. Want to do something with a model? Ask its authorizer.
You can group models under authorizers in any way you wish. For example:
Simplest case Logical groups Most granular
default_strategy default_strategy default_strategy
+ + +
| +--------+-------+ +-------------------+-------------------+
+ + + + + +
EverythingAuthorizer BasicAuthorizer AdminAuthorizer CommentAuthorizer ArticleAuthorizer EditionAuthorizer
+ + + + + +
+-------+-------+ +-+ +------+ | | |
+ + + + + + + + +
Comment Article Edition Comment Article Edition Comment Article Edition
The process generally flows like this:
current_user.can_create?(Moose) # You ask this question, and the user
+ # automatically asks the model...
|
v
Moose.creatable_by?(current_user) # The model automatically asks
+ # its authorizer...
|
v
MooseAuthorizer.creatable_by?(current_user) # *You define this method.*
+ # If it's missing, the default
| # strategy is used...
v
config.default_strategy.call(:creatable, MooseAuthorizer, user) # *You define this strategy.*
If the answer is false
and the original caller was a controller, this is treated as a SecurityViolation
. If it was a view, maybe you just don't show a link.
(Diagrams made with AsciiFlow)
Installation
Starting from a clean commit status, add authority
to your Gemfile, bundle
, then rails g authority:install
.
Defining Your Abilities
Edit config/initializers/authority.rb
. That file documents all your options, but one of particular interest is config.abilities
, which defines the verbs and corresponding adjectives in your app. The defaults are:
config.abilities = {
:create => 'creatable',
:read => 'readable',
:update => 'updatable',
:delete => 'deletable'
}
This option determines what methods are added to your users, models and authorizers. If you need to ask user.can_deactivate?(Satellite)
and @satellite.deactivatable_by?(user)
, add those to the hash.
Wiring It Together
Users
# Whatever class represents a logged-in user in your app
class User
# Adds `can_create?(resource)`, etc
include Authority::UserAbilities
...
end
Models
class Article
# Adds `creatable_by?(user)`, etc
include Authority::Abilities
# Without this, 'ArticleAuthorizer' is assumed
self.authorizer_name = 'AdminAuthorizer'
...
end
Authorizers
Add your authorizers under app/authorizers
, subclassing Authority::Authorizer
.
These are where your actual authorization logic goes. Here's how it works:
- Instance methods answer questions about model instances, like "can this user update this particular widget?" (Within an instance method, you can get the model instance with
resource
).- Any instance method you don't define (for example, if you didn't make a
def deletable_by?(user)
) will fall back to the corresponding class method. In other words, if you haven't said whether a user can update this particular widget, we'll decide by checking whether they can update any widget.
- Any instance method you don't define (for example, if you didn't make a
- Class methods answer questions about model classes, like "is it ever permissible for this user to update a Widget?"
- Any class method you don't define (for example, if you didn't make a
def self.updatable_by?(user)
) will call that authorizer'sdefault
method. - The inherited
default
method calls a default strategy proc (NOTE: this will be removed in version 2.0).
- Any class method you don't define (for example, if you didn't make a
For example:
# app/authorizers/schedule_authorizer.rb
class ScheduleAuthorizer < Authority::Authorizer
# Class method: can this user at least sometimes create a Schedule?
def self.creatable_by?(user)
user.manager?
end
# Instance method: can this user delete this particular schedule?
def deletable_by?(user)
resource.in_future? && user.manager? && resource.department == user.department
end
end
As you can see, you can specify different logic for every method on every model, if necessary. On the other extreme, you could simply supply a default strategy that covers all your use cases.
Default Strategies
Any class method you don't define on an authorizer will call the default
method on that authorizer. If you don't define that, the inherited default
method from Authority::Authorizer
will call your configured default strategy proc (NOTE: this proc will be removed in version 2.0; defining a default
method is a more standard OOP approach.)
The default default strategy proc simply returns false
, meaning that everything is forbidden. This whitelisting approach will keep you from accidentally allowing things you didn't intend.
You can configure a different default strategy. For example, you might want one that looks up permissions in your database:
# In config/initializers/authority.rb
# Example args: :creatable, AdminAuthorizer, user
config.default_strategy = Proc.new do |able, , user|
# Does the user have any of the roles which give this permission?
(roles_which_grant(able, ) & user.roles).any?
end
If your system is uniform enough, this strategy alone might handle all the logic you need.
Testing Authorizers
One nice thing about putting your authorization logic in authorizers is the ease of testing. Here's a brief example.
# An authorizer shared by several admin-only models
describe AdminAuthorizer do
before :each do
@user = FactoryGirl.build(:user)
@admin = FactoryGirl.build(:admin)
end
describe "class" do
it "should let admins update in bulk" do
AdminAuthorizer.should be_bulk_updatable_by(@admin)
end
it "should not let users update in bulk" do
AdminAuthorizer.should_not be_bulk_updatable_by(@user)
end
end
describe "instances" do
before :each do
# A mock model that uses AdminAuthorizer
@admin_resource_instance = mock_admin_resource
end
it "should not allow users to delete" do
@admin_resource_instance..should_not be_deletable_by(@user)
end
end
end
Custom Authorizers
If you want to customize your authorizers even further - for example, maybe you want them all to have a method like has_permission?(user, permission_name)
- just use normal Ruby inheritance. For example, add your own parent class, like this:
# lib/my_app/authorizer.rb
module MyApp
class Authorizer < Authority::Authorizer
def self.(user, )
# look that up somewhere
end
end
end
#app/authorizers/badger_authorizer.rb
class BadgerAuthorizer < MyApp::Authorizer
# contents
end
If you decide to place your custom class in lib
as shown above (as opposed to putting it in app
), you should require it at the bottom of config/initializers/authority.rb
.
Controllers
Anytime a controller finds a user attempting something they're not authorized to do, a Security Violation will result. Controllers get two ways to check authorization:
authorize_actions_for Transaction
protects multiple controller actions with abefore_filter
, which performs a class-level check. If the current user is never allowed to delete aTransaction
, they'll never even get to the controller'sdestroy
method.authorize_action_for @transaction
can be called inside a single controller action, and performs an instance-level check. If called insideupdate
, it will check whether the current user is allowed to update this particular@transaction
instance.
The relationship between controller actions and abilities - like checking readable_by?
on the index
action - is configurable both globally, using config.controller_action_map
, and per controller, as below.
class LlamaController < ApplicationController
# Check class-level authorizations before all actions except :create
# Before this controller's 'neuter' action, ask whether current_user.can_update?(Llama)
authorize_actions_for Llama, :actions => {:neuter => :update}, :except => :create
# Before this controller's 'breed' action, ask whether current_user.can_create?(Llama)
authority_action :breed => 'new'
...
def edit
@llama = Llama.find(params[:id])
@llama.attributes = params[:llama] # Don't save the attributes before authorizing
authorize_action_for(@llama) # failure == SecurityViolation
if @llama.save?
# etc
end
end
Views
Assuming your user object is available in your views, you can do all kinds of conditional rendering. For example:
link_to 'Edit Widget', (@widget) if current_user.can_update?(@widget)
If the user isn't allowed to edit widgets, they won't see the link. If they're nosy and try to hit the URL directly, they'll get a Security Violation from the controller.
Security Violations & Logging
Anytime a user attempts an unauthorized action, Authority calls whatever controller method is specified by your security_violation_handler
option, handing it the exception. The default handler is authority_forbidden
, which Authority adds to your ApplicationController
. It does the following:
- Renders
public/403.html
- Logs the violation to whatever logger you configured.
You can specify a different handler like so:
# config/initializers/authority.rb
...
config.security_violation_handler = :fire_ze_missiles
...
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
def fire_ze_missiles(exception)
# Log? Set a flash message? Dispatch minions to
# fill their mailbox with goose droppings? It's up to you.
end
...
end
If you want different error handling per controller, define fire_ze_missiles
on each of them.
Credits, AKA 'Shout-Outs'
- adamhunter for pairing with me on this gem. The only thing faster than his typing is his brain.
- nkallen for writing a lovely blog post on access control when he worked at Pivotal Labs. I cried sweet tears of joy when I read that a couple of years ago. I was like, "Zee access code, she is so BEEUTY-FUL!"
- jnunemaker for later creating Canable, another inspiration for Authority.
- TMA for employing me and letting me open source some of our code.
Contributing
What should you contribute? Try the TODO file for ideas, or grep the project for 'TODO' comments.
How can you contribute?
- Let's talk! Before you do a bunch of work, open an issue so we can be sure we agree.
- Fork this project
- Create your feature branch (
git checkout -b my-new-feature
) bundle install
to get all dependenciesrspec spec
to run all tests.- Update/add tests for your changes and code until they pass.
- Commit your changes (
git commit -am 'Added some feature'
) - Push to the branch (
git push origin my-new-feature
) - Create a new Pull Request