-
Notifications
You must be signed in to change notification settings - Fork 115
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, 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
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
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: :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_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, :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
end
This 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 => :destroy
Note 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_action
and raisingAcl9::AccessDenied
exception on occasion. - a method, installed with
before_action
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_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
# ...
end
Subject method can also be changed globally. Place the following into config/initializers/acl9.rb
:
Acl9.config[:default_subject_method] = :current_account
TODO - 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
# ...
end
access 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
# ...
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
.
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
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_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
# ...
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 %>
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)