-
Notifications
You must be signed in to change notification settings - Fork 111
Access Control Subsystem
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, OPTIONSROLE_LIST is a list of roles (at least 1 role should be there). So,
allow :manager, :admin
deny :bannedwill 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 :bannedwhich 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 => :usTo 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
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 callback which is executed
before the one generated by access_control, like this:
class MoorblesController < ApplicationController
before_action :load_moorble, :only => [:edit, :update, :destroy]
access_control do
allow :creator, :of => :moorble
# ...
end
# ...
private
def load_moorble
@moorble = Moorble.find(params[:id])
end
endNote that the object option is applied to all of the roles you specify in the argument list. As such,
allow :devil, :son, :of => Godis equivalent to
allow :devil, :of => God
allow :son, :of => Godbut NOT
allow :devil
allow :son, :of => GodThere 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: :only and :except.
allow :owner, :of => :site, :only => [: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 :only and :except.
Note that you can use actions block instead of :only (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_action and thus
limit the scope of the whole ACL.
Note :only was called :to prior to version 2.1, and :to still works. If you happy to provide both a :to and :only option then their values will be merged together and it will work fine.
You may create conditional rules using :if and :unless options.
allow :owner, :of => :site, :only => [:delete, :destroy], :if => :chance_to_deleteController'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, :only => [: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. |
Allowed | Denied |
Some of the allow rules matched, none of the deny rules matched. |
Allowed | Allowed |
None of the allow rules matched, some of the deny rules matched. |
Denied | Denied |
Some of the allow rules matched, some of the deny rules matched. |
Allowed | 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
endThis is equivalent to:
allow :admin
allow all, :only => [:index, :show]
allow :managers, :of => Post, :only => [:new, :create]
allow :owner, :of => :post, :only => [:edit, :update]
allow :owner, :of => :post, :only => :destroyNote that only allow and deny calls are available inside actions block, and these may not have
:only/: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_actionand raisingAcl9::AccessDeniedexception on occasion. - a method, installed with
before_actionand raisingAcl9::AccessDeniedexception on occasion. - a method, returning
trueorfalse, 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_action 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
# ...
endSubject method can also be changed globally. Place the following into config/initializers/acl9.rb:
Acl9.config[:default_subject_method] = :current_accountTODO - add docs for protect_global_roles
: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
# ...
endaccess control checks will be added as acl method onto MyController, with before_action :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
# ...
endIf 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
endThe 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
endyou 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.
TODO - document _action override.
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
endThat's equivalent to
class LolController < ApplicationController
access_control :lolcats?, :filter => false do
allow :cats, :by => :lol
# ...
end
helper_method :lolcats?
endOther options will be passed to before_action. 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
# ...
endis basically equivalent to
class OmgController < ApplicationController
access_control do
actions :index, :show do
allow all
deny :banned
end
allow all, :except => [:index, :show]
end
# ...
endApart 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
endHere 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 %>show_to is predefined helper for your views:
<% show_to :admin, :supervisor do %>
<%= link_to 'destroy', destroy_path %>
<% end %>or even
<% show_to :prince, :of => :persia do %>
<%= link_to 'Princess', princess_path %>
<% end %>- Home
- Role Subsystem
- Access Control Subsystem
- Legacy Docs (some faults/errors may exist)