Acl9 is yet another solution for role-based authorization in Rails. It consists of two
subsystems which can be used separately.
Role control subsystem allows you to set and query user roles for various objects.
Access control subsystem allows you to specify different role-based access rules
inside controllers.
A bunch of access rules is translated into a complex
boolean expression. Then it’s turned into a lambda or a method and can
be used with before_filter
. Thus you can block unprivileged access to certain
actions of your controller.
An example:
class VerySecretController < ApplicationController
access_control do
allow :superadmin
allow :owner, :of => :secret
action :index do
allow anonymous, logged_in
end
allow logged_in, :to => :show
allow :manager, :of => :secret, :except => [:delete, :destroy]
deny :thiefs
end
def index
# ...
end
# ...
end
Acl9 is hosted on the GitHub.
You may find tutorials and additional docs on the wiki page.
Rdocs are available here.
If you have questions, please post to the
acl9-discuss group
Acl9 can be installed as a gem from GitHub.
Add the following line to your config/environment.rb
:
config.gem "be9-acl9", :source => "http://gems.github.com", :lib => "acl9"
Then run rake gems:install
(with possible rake gems:unpack
thereafter) and you’re done!
Alternatively you can install Acl9 as a plugin:
script/plugin install git://github.com/be9/acl9.git
Both words start with “auth” but have different meaning!
Authentication is basically a mapping of credentials (username, password) or
OpenID to specific user account in the system.
Authorization is an authenticated user’s permission to perform some
specific action somewhere in the system.
Acl9 is a authorization solution, so you will need to implement authentication
by other means. I recommend Authlogic
for that purpose, as it’s simple, clean and at the same time very configurable.
Role is an abstraction. You could directly assign permissions to user accounts in
your system, but you’d not want to! Way more manageable solution is to assign permissions
to roles and roles further to users.
For example, you can have role called admin which has all available permissions. Now
you may assign this role to several trusted accounts on your system.
Acl9 also supports the notion of object roles, that is, roles with limited scope.
Imagine we are building a magazine site and want to develop a permission system. So, what roles
and permissions are there?
Journalists should be able to create articles in their section and edit their own articles.
Section editors should be able to edit and delete all articles in their sections and
change the published flag.
Editor-in-chief should be able to change everything.
We clearly see that journalists and section editors are tied to a specific section, whereas
editor-in-chief is a role with global scope.
All permission checks in Acl9 are boiled down to calls of a single method:
subject.has_role?(role, object)
That should be read as “Does subject have role on object?”.
Subject is an instance of a User
, or Account
, or whatever model you use for
authentication. Object is an instance of any class (including subject class!)
or nil
(in which case it’s a global role).
Acl9 builtin role control subsystem provides has_role?
method for you, but you can
also implemented it by hand (see Coming up with your own role implementation below).
Role control subsystem has been lifted from
Rails authorization plugin,
but undergone some modifications.
It’s based on two tables in the database. First, role table, which stores pairs [role_name, object]
where object is a polymorphic model instance or a class. Second, join table, which joins users and roles.
To use this subsystem, you should define a Role
model.
class Role < ActiveRecord::Base
acts_as_authorization_role
end
The structure of roles
table is as follows:
create_table "roles", :force => true do |t|
t.string "name", :limit => 40
t.string "authorizable_type", :limit => 40
t.integer "authorizable_id"
t.datetime "created_at"
t.datetime "updated_at"
end
Note that you will almost never use the Role
class directly.
class User < ActiveRecord::Base
acts_as_authorization_subject
end
You won’t need any specific columns in the users
table, but
there should be a join table:
create_table "roles_users", :id => false, :force => true do |t|
t.integer "user_id"
t.integer "role_id"
t.datetime "created_at"
t.datetime "updated_at"
end
Place acts_as_authorization_object
call inside any model you want to act
as such.
class Foo < ActiveRecord::Base
acts_as_authorization_object
end
class Bar < ActiveRecord::Base
acts_as_authorization_object
end
A call of acts_as_authorization_subject
defines following methods on the model:
subject.has_role?(role, object = nil)
. Returns true
of false
(has or has not).
subject.has_role!(role, object = nil)
. Assigns a role
for the object
to the subject
.
Does nothing is subject already has such a role.
subject.has_no_role!(role, object = nil)
. Unassigns a role from the subject
.
subject.has_roles_for?(object)
. Does the subject
has any roles for object
? (true
of false
)
subject.has_role_for?(object)
. Same as has_roles_for?
.
subject.roles_for(object)
. Returns an array of Role
instances, corresponding to subject
’s roles on
object
. E.g. subject.roles_for(object).map(&:name).sort
will give you role names in alphabetical order.
subject.has_no_roles_for!(object)
. Unassign any subject
’s roles for a given object
.
subject.has_no_roles!
. Unassign all roles from subject
.
A call of acts_as_authorization_object
defines following methods on the model:
object.accepts_role?(role_name, subject)
. An alias for subject.has_role?(role_name, object)
.
object.accepts_role!(role_name, subject)
. An alias for subject.has_role!(role_name, object)
.
object.accepts_no_role!(role_name, subject)
. An alias for subject.has_no_role!(role_name, object)
.
object.accepts_roles_by?(subject)
. An alias for subject.has_roles_for?(object)
.
object.accepts_role_by?(subject)
. Same as accepts_roles_by?
.
object.accepts_roles_by(subject)
. An alias for subject.roles_for(object)
.
You may want to deviate from default User
and Role
class names. That can easily be done with
arguments to acts_as_...
.
Say, you have Account
and AccountRole
:
class Account < ActiveRecord::Base
acts_as_authorization_subject :role_class_name => 'AccountRole'
end
class AccountRole < ActiveRecord::Base
acts_as_authorization_role :subject_class_name => 'Account'
end
class FooBar < ActiveRecord::Base
acts_as_authorization_object :role_class_name => 'AccountRole', :subject_class_name => 'Account'
end
Or… since Acl9 defaults can be changed in a special hash, you can put the following snippet:
Acl9::config.merge!({
:default_role_class_name => 'AccountRole',
:default_subject_class_name => 'Account',
})
… into config/initializers/acl9.rb
and get rid of that clunky arguments:
class Account < ActiveRecord::Base
acts_as_authorization_subject
end
class AccountRole < ActiveRecord::Base
acts_as_authorization_role
end
class FooBar < ActiveRecord::Base
acts_as_authorization_object
end
Note that you’ll need to change your database structure appropriately:
create_table "account_roles", :force => true do |t|
t.string "name", :limit => 40
t.string "authorizable_type", :limit => 40
t.integer "authorizable_id"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "account_roles_accounts", :id => false, :force => true do |t|
t.integer "account_id"
t.integer "account_role_id"
t.datetime "created_at"
t.datetime "updated_at"
end
user = User.create!
user.has_role? 'admin' # => false
user.has_role! :admin
user.has_role? :admin # => true
user
now has global role admin. Note that you can specify role name either
as a string or as a symbol.
foo = Foo.create!
user.has_role? 'admin', foo # => false
user.has_role! :manager, foo
user.has_role? :manager, foo # => true
foo.accepts_role? :manager, user # => true
user.has_roles_for? foo # => true
You can see here that global and object roles are distinguished from each other. User
with global role admin isn’t automatically admin of foo
.
However,
user.has_role? :manager # => true
That is, if you have an object role, it means that you have a global role with the same name too!
In other words, you are manager if you manage at least one foo
(or a bar
…).
bar = Bar.create!
user.has_role! :manager, bar
user.has_no_role! :manager, foo
user.has_role? :manager, foo # => false
user.has_role? :manager # => true
Our user
is no more manager of foo
, but has become a manager of bar
.
user.has_no_roles!
user.has_role? :manager # => false
user.has_role? :admin # => false
user.roles # => []
At this time user
has no roles in the system.
The described role system with its 2 tables (not counting the users
table!)
might be an overkill for many cases. If all you want is global roles without
any scope, you’d better off implementing it by hand.
The access control subsystem of Acl9 uses only subject.has_role?
method, so
there’s no need to implement anything else except for own convenience.
For example, if each your user can have only one global role, just add role
column to your User
class:
class User < ActiveRecord::Base
def has_role?(role_name, obj=nil)
self.role == role_name
end
def has_role!(role_name, obj=nil)
self.role = role_name
save!
end
end
If you need to assign multiple roles to your users, you can use serialize
with role array or a special solution like
preference_fu.
By means of access control subsystem you can protect actions of your controller
from unauthorized access. Acl9 provides a nice DSL for writing access rules.
Access control is mostly about allowing and denying. So there are two
basic methods: allow
and deny
. They have the same syntax:
allow ROLE_LIST, OPTIONS
deny ROLE_LIST, OPTIONS
ROLE_LIST is a list of roles (at least 1 role should be there). So,
allow :manager, :admin
deny :banned
will match holders of global role manager and holders of global role admin as allowed.
On the contrary, holders of banned role will match as denied.
Basically this snippet is equivalent to
allow :manager
allow :admin
deny :banned
which means that roles in argument list are OR’ed for a match, and not AND’ed.
Also note that:
- You may use both strings and :symbols to specify roles (the latter get converted into strings).
- Role names are singularized before check.
Thus the snippet above can also be written as
allow :managers, :admins
deny 'banned'
or even
allow *%w(managers admins)
deny 'banned'
Examples in the previous section were all about global roles. Let’s see how we can
use object and class roles in the ACL block.
allow :responsible, :for => Widget
allow :possessor, :of => :foo
deny :angry, :at => :me
allow :interested, :in => Future
deny :short, :on => :time
deny :hated, :by => :us
To specify an object you use one of the 6 preposition options:
- :of
- :at
- :on
- :by
- :for
- :in
They all have the same meaning, use one that makes better English out of your rule.
Now, each of these prepositions may point to a Class or a :symbol. In the former case we get
a class role. E.g. allow :responsible, :for => Widget
becomes subject.has_role?('responsible', Widget)
.
Symbol is trickier, it means that the appropriate instance variable of the controller is taken
as an object.
allow :possessor, :of => :foo
is translated into
subject.has_role?('possessor', controller.instance_variable_get('@foo'))
.
Checking against an instance variable has sense when you have another before filter which is executed
before the one generated by access_control
, like this:
class MoorblesController < ApplicationController
before_filter :load_moorble, :only => [:edit, :update, :destroy]
access_control do
allow :creator, :of => :moorble
# ...
end
# ...
private
def load_moorble
@moorble = Moorble.find(params[:id])
end
end
Note that the object option is applied to all of the roles you specify in the argument list.
As such,
allow :devil, :son, :of => God
is equivalent to
allow :devil, :of => God
allow :son, :of => God
but NOT
allow :devil
allow :son, :of => God
There are three pseudo-roles in the ACL: all
, anonymous
and logged_in
.
allow all
will always match (as well as deny all
).
allow anonymous
and deny anonymous
will match when user is anonymous, i.e. subject is nil
.
You may also use a shorter notation: allow nil
(deny nil
).
logged_in
is direct opposite of anonymous
, so allow logged_in
will match if the user is logged in
(subject is not nil
).
No role checks are done in either case.
By default rules apply to all actions of the controller. There are two options that
narrow the scope of the deny
or allow
rule: :to
and :except
.
allow :owner, :of => :site, :to => [:delete, :destroy]
deny anonymous, :except => [:index, :show]
For the first rule to match not only the current user should be an owner of the site, but also
current action should be delete or destroy.
In the second rule anonymous user access is denied for all actions, except index and show.
You may not specify both :to
and :except
.
Note that you can use actions block instead of :to
(see Actions block
below). You can also use :only
and :except
options in the
access_control
call which will serve as options of the before_filter
and thus
limit the scope of the whole ACL.
You may create conditional rules using :if
and :unless
options.
allow :owner, :of => :site, :to => [:delete, :destroy], :if => :chance_to_delete
Controller’s :chance_to_delete
method will be called here. The rule will match if the action
is ‘delete’ or ‘destroy’ AND if the method returned true
.
:unless
has the opposite meaning and should return false
for a rule to match.
Both options can be specified in the same rule.
allow :visitor, :to => [:index, :show], :if => :right_phase_of_the_moon?, :unless => :suspicious?
right_phase_of_the_moon?
should return true
AND suspicious?
should return false
for a poor visitor to
see a page.
Currently only controller methods are supported (specify them as :symbols). Lambdas are not supported.
Rule matching system is similar to that of Apache web server. There are two modes: default allow
(corresponding to Order Deny,Allow
in Apache) and default deny (Order Allow,Deny
in Apache).
Mode is set with a default
call.
default :allow
will set default allow mode.
default :deny
will set default deny mode. Note that this is the default mode, i.e. it will be on
if you don’t do a default
call at all.
First of all, regardless of the mode, all allow
matches are OR’ed together and all deny
matches
are OR’ed as well.
We’ll express this in the following manner:
ALLOWED = (allow rule 1 matches?) OR ((allow rule 2 matches?) OR ...
NOT_DENIED = NOT ((deny rule 1 matches?) OR (deny rule 2 matches?) OR ...)
So, ALLOWED is true
when either of the allow
rules matches, and NOT_DENIED is true
when none
of the deny
rules matches.
Let’s denote the final result of algorithm as ALLOWANCE. If it’s true
, access is allowed, if false
– denied.
In the case of default allow_:
ALLOWANCE = ALLOWED OR NOT
DENIED
In the case of default deny_:
ALLOWANCE = ALLOWED AND NOT
DENIED
Same result as a table:
Rule matches | Default allow mode | Default deny mode |
---|---|---|
None of the allow and deny rules matched. |
Access is allowed. | Access is denied. |
Some of the allow rules matched, none of the deny rules matched. |
Access is allowed. | Access is allowed. |
None of the allow rules matched, some of the deny rules matched. |
Access is denied. | Access is denied. |
Some of the allow rules matched, some of the deny rules matched. |
Access is allowed. | Access is denied. |
Apparently default deny mode is more strict, and that’s because it’s on by default.
You may group rules with the help of the actions
block.
An example from the imaginary PostsController
:
allow :admin
actions :index, :show do
allow all
end
actions :new, :create do
allow :managers, :of => Post
end
actions :edit, :update do
allow :owner, :of => :post
end
action :destroy do
allow :owner, :of => :post
end
This is equivalent to:
allow :admin
allow all, :to => [:index, :show]
allow :managers, :of => Post, :to => [:new, :create]
allow :owner, :of => :post, :to => [:edit, :update]
allow :owner, :of => :post, :to => :destroy
Note that only allow
and deny
calls are available inside actions
block, and these may not have
:to
/:except
options.
action
is just a synonym for actions
.
By calling access_control
in your controller you can get your ACL block translated into…
- a lambda, installed with
before_filter
and raisingAcl9::AccessDenied
exception on occasion. - a method, installed with
before_filter
and raisingAcl9::AccessDenied
exception on occasion. - a method, returning
true
orfalse
, whether access is allowed or denied.
First case is by default. You can catch the exception with rescue_from
call and do something
you like: make a redirect, or render “Access denied” template, or whatever.
Second case is obtained with specifying method name as an argument to
access_control
(or using :as_method
option, see below) and may be helpful
if you want to use skip_before_filter
somewhere in the derived controller.
Third case will take place if you supply :filter => false
along with method
name. You’ll get an ordinary method which you can call anywhere you want.
Acl9 obtains the subject instance by calling specific method of the controller. By default it’s
:current_user
, but you may change it.
class MyController < ApplicationController
access_control :subject_method => :current_account do
allow :nifty
# ...
end
# ...
end
Subject method can also be changed globally. Place the following into config/initializers/acl9.rb
:
Acl9::config[:default_subject_method] = :current_account
:debug => true
will output the filtering expression into the debug log. If
Acl9 does something strange, you may look at it as the last resort.
In the case
class NiftyController < ApplicationController
access_control :as_method => :acl do
allow :nifty
# ...
end
# ...
end
access control checks will be added as acl
method onto MyController, with before_filter :acl
call thereafter.
Instead of using :as_method
you may specify the name of the method as a positional argument
to access_control
:
class MyController < ApplicationController
access_control :acl do
# ...
end
# ...
end
If you set :filter
to false
(it’s true
by default) and also use
:as_method
(or method name as 1st argument to access_control
, you’ll get a
method which won’t raise Acl9::AccessDenied
exception, but rather return
true
or false
(access allowed/denied).
class SecretController < ApplicationController
access_control :secret_access?, :filter => false do
allow :james_bond
# ...
end
def index
if secret_access?
_secret_index
else
_ordinary_index
end
end
# ...
private
def _secret_index
# ...
end
def _ordinary_index
# ...
end
end
The generated method can receive an objects hash as an argument. In this example,
class LolController < ApplicationController
access_control :lolcats?, :filter => false do
allow :cats, :by => :lol
# ...
end
end
you may not only call lolcats?
with no arguments, which will basically return
current_user.has_role?('cats', @lol)
but also as lolcats?(:lol => Lol.find(params[:lol]))
. The hash will be looked into first,
even if you have an instance variable lol
.
Sometimes you want to have a boolean method (like :filter => false
) accessible
in your views. Acl9 can call helper_method
for you:
class LolController < ApplicationController
access_control :helper => :lolcats? do
allow :cats, :by => :lol
# ...
end
end
That’s equivalent to
class LolController < ApplicationController
access_control :lolcats?, :filter => false do
allow :cats, :by => :lol
# ...
end
helper_method :lolcats?
end
Other options will be passed to before_filter
. As such, you may use :only
and :except
to narrow
the action scope of the whole ACL block.
class OmgController < ApplicationController
access_control :only => [:index, :show] do
allow all
deny :banned
end
# ...
end
is basically equivalent to
class OmgController < ApplicationController
access_control do
actions :index, :show do
allow all
deny :banned
end
allow all, :except => [:index, :show]
end
# ...
end
Apart from using :helper
option for access_control
call inside controller, there’s a
way to generate helper methods directly, like this:
module SettingsHelper
include Acl9Helpers
access_control :show_settings? do
allow :admin
allow :settings_manager
end
end
Here we mix in Acl9Helpers
module which brings in access_control
method and call it,
obtaining show_settings?
method.
An imaginary view:
<% if show_settings? %>
<%= link_to 'Settings', settings_path %>
<% end %>
Copyright © 2009 Oleg Dashevskii, released under the MIT license.