The declarative authorization plugin offers an authorization mechanism inspired by RBAC. The most notable distinction to other authorization plugins is the declarative approach. That is, authorization rules are not defined programmatically in between business logic but in an authorization configuration.
With programmatic authorization rules, the developer needs to specify which
roles are allowed to access a specific controller action or a part of a view,
which is not DRY. With a growing application code base roles' permissions
often change and new roles are introduced. Then, at several places of the
source code the changes have to be implemented, possibly leading to omissions
and thus hard to find errors. In these cases, a declarative approach as
offered by declarative_authorization
increases the development and maintenance
efficiency.
- Authorization at controller action level
- Authorization helpers for views
- Authorization at model level
- Authorize CRUD (Create, Read, Update, Delete) activities
- Query rewriting to automatically only fetch authorized records
- DSL for specifying Authorization rules in an authorization configuration
- Support for Rails ≥ 5
-
An authentication mechanism
- user object in
Controller#current_user
- (For model security) Setting
Authorization.current_user
- user object in
-
User objects need to respond to
#role_symbols
that returns an array of symbols describing the user's roles. For example:User.first.role_symbols #=> [:admin]
See below for installation instructions.
There is a decl_auth screencast by Ryan Bates, nicely introducing the main concepts.
Declarative Authorization comes with an installer to make setup easy.
First, include declarative_authorization
in your Gemfile
.
# In Gemfile
gem 'declarative_authorization', git: '[email protected]/breckenedge/declarative_authorization.git'
Next, bundle and install.
$ bundle
$ rails g authorization:install [UserModel=User] [field:type field:type ...] [--create-user --commit --user-belongs-to-role]
This installer will create a Role model, an admin and a user role, and set a
has_and_belongs_to_many
relationship between the User model and the
Role model. It will also add a role_symbols
method to the user model to
meet declarative_authorization
's requirements. The default User model is
User
. You can override this by simply typing the name of a model as above.
You can create the model with the fields provided by using the --create-user
option.
The --commit
option will run rake db:migrate
and rake db:seed
.
The --user-belongs-to-role
option will set up a one-to-many relationship
between Users and Roles. That is, each user has a role_id
column and can only
have one role. Role inheritance can be used in authorization rules.
Finally, the installer also copies default authorization rules, as below.
To copy a default set of authorization rules which includes CRUD privileges, run:
$ rails g authorization:rules
This command will copy the following to config/authorization_rules.rb
.
authorization do
role :guest do
# add permissions for guests here, e.g.
# has_permission_on :conferences, :to => :read
end
# permissions on other roles, such as
#
# role :admin do
# has_permission_on :conferences, :to => :manage
# end
#
# role :user do
# has_permission_on :conferences, :to => [:read, :create]
# has_permission_on :conferences, :to => [:update, :delete] do
# if_attribute :user_id => is {user.id}
# end
# end
#
# See the readme or GitHub for more examples
end
privileges do
# default privilege hierarchies to facilitate RESTful Rails apps
privilege :manage, :includes => [:create, :read, :update, :delete]
privilege :create, :includes => :new
privilege :read, :includes => [:index, :show]
privilege :update, :includes => :edit
privilege :delete, :includes => :destroy
end
For RESTful controllers, add filter_resource_access
:
class MyRestfulController < ApplicationController
filter_resource_access
...
end
For a non-RESTful controller, you can use filter_access_to
:
class MyOtherController < ApplicationController
filter_access_to :all
# or a group: filter_access_to [:action1, :action2]
...
end
Declarative Authorization will use current_user
to check authorization.
<%= link_to 'Edit Post', edit_post_path(@post) if permitted_to? :update, @post %>
----- App domain ----|-------- Authorization conf ---------|------- App domain ------
includes includes
.--. .---.
| v | v
.------. can_play .------. has_permission .------------. requires .----------.
| User |----------->| Role |----------------->| Permission |<-----------| Activity |
'------' * * '------' * * '------------' 1 * '----------'
|
.-------+------.
1 / | 1 \ *
.-----------. .---------. .-----------.
| Privilege | | Context | | Attribute |
'-----------' '---------' '-----------'
In the application domain, each User may be assigned to Roles that should define the users' job in the application, such as Administrator. On the right-hand side of this diagram, application developers specify which Permissions are necessary for users to perform activities, such as calling a controller action, viewing parts of a View or acting on records in the database. Note that Permissions consist of an Privilege that is to be performed, such as read, and a Context in that the Operation takes place, such as companies.
In the authorization configuration, Permissions are assigned to Roles and Role and Permission hierarchies are defined. Attributes may be employed to allow authorization according to dynamic information about the context and the current user, e.g. "only allow access on employees that belong to the current user's branch."
If authentication is in place, there are two ways to enable user-specific
access control on controller actions. For resource controllers, which more or
less follow the CRUD pattern, filter_resource_access
is the simplest
approach. It sets up instance variables in before filters and calls
filter_access_to
with the appropriate parameters to protect the CRUD methods.
class EmployeesController < ApplicationController
filter_resource_access
...
end
See Authorization::AuthorizationInController::ClassMethods
for options on
nested resources and custom member and collection actions.
By default, declarative_authorization
will enable filter_resource_access
compatibility with strong_parameters
. If you want to disable this behavior,
you can use the :strong_parameters
option.
class EmployeesController < ApplicationController
filter_resource_access :strong_parameters => false
...
end
If you prefer less magic or your controller has no resemblance with the
resource controllers, directly calling filter_access_to
may be the better
option. Examples are given in the following. E.g. the privilege index users
is required for action index. This works as a first default configuration for
RESTful controllers, with these privileges easily handled in the authorization
configuration, which will be described below.
class EmployeesController < ApplicationController
filter_access_to :all
def index
...
end
...
end
When custom actions are added to such a controller, it helps to define more
clearly which privileges are the respective requirements. That is when the
filter_access_to
call may become more verbose:
class EmployeesController < ApplicationController
filter_access_to :all
# this one would be included in :all, but :read seems to be
# a more suitable privilege than :auto_complete_for_user_name
filter_access_to :auto_complete_for_employee_name, :require => :read
def auto_complete_for_employee_name
...
end
...
end
For some actions it might be necessary to check certain attributes of the object the action is to be acting on. Then, the object needs to be loaded before the action's access control is evaluated. On the other hand, some actions might prefer the authorization to ignore specific attribute checks as the object is unknown at checking time, so attribute checks and thus automatic loading of objects needs to be enabled explicitly.
class EmployeesController < ApplicationController
filter_access_to :update, :attribute_check => true
def update
# @employee is already loaded from param[:id] because of :attribute_check
end
end
You can provide the needed object through before_action
s. This way, you have
full control over the object that the conditions are checked against. Just
make sure, your before_action
s occur before any of the filter_access_to
calls.
class EmployeesController < ApplicationController
before_action :new_employee_from_params, :only => :create
before_action :new_employee, :only => [:index, :new]
filter_access_to :all, :attribute_check => true
def create
@employee.save!
end
protected
def new_employee_from_params
@employee = Employee.new(employee_params)
end
end
If the access is denied, a permission_denied
method is called on the
current_controller
, if defined, and the issue is logged. For further
customization of the filters and object loading, have a look at the complete
API documentation of filter_access_to
in
Authorization::AuthorizationInController::ClassMethods
.
In views, a simple permitted_to?
helper makes showing blocks according to the
current user's privileges easy:
<% permitted_to? :create, :employees do %>
<%= link_to 'New', new_employee_path %>
<% end %>
Only giving a symbol :employees as context prevents any checks of attributes as there is no object to check against. For example, in case of nested resources a new object may come in handy:
<% permitted_to? :create, Branch.new(:company => @company) do
<!-- or @company.branches.new -->
<!-- or even @company.branches %> -->
<%= link_to 'New', new_company_branch_path(@company) %>
<% end %>
Lists are straight-forward:
<% for employee in @employees %>
<%= link_to 'Edit', edit_employee_path(employee) if permitted_to? :update, employee %>
<% end %>
See also Authorization::AuthorizationHelper
.
There are two distinct features for model security built into this plugin: authorizing CRUD operations on objects as well as query rewriting to limit results according to certain privileges.
See also Authorization::AuthorizationInModel
.
To activate model security, all it takes is an explicit enabling for each model that model security should be enforced on, i.e.
class Employee < ActiveRecord::Base
using_access_control
...
end
Thus,
Employee.create(...)
fails, if the current user is not allowed to :create
:employees
according to
the authorization rules. For the application to find out about what happened
if an operation is denied, the filters throw Authorization::NotAuthorized
exceptions.
As access control on read are costly, with possibly lots of objects being
loaded at a time in one query, checks on read need to be activated explicitly
by adding the :include_read
option.
When retrieving large sets of records from databases, any authorization needs to be integrated into the query in order to prevent inefficient filtering afterwards and to use LIMIT and OFFSET in SQL statements. To keep authorization rules out of the source code, this plugin offers query rewriting mechanisms through named scopes. Thus,
Employee.with_permissions_to(:read)
returns all employee records that the current user is authorized to read. In
addition, just like normal named scopes, query rewriting may be chained with
the usual where
and find
methods:
Employee.with_permissions_to(:read).where(...)
Employee.with_permissions_to(:read).find(employee_id)
If the current user is completely missing the permissions, an
Authorization::NotAuthorized
exception is raised. Through
Model.obligation_conditions
, application developers may retrieve the
conditions for manual rewrites.
Authorization rules are defined in config/authorization_rules.rb
(or redefine
rules files path via Authorization::AUTH_DSL_FILES
). E.g.
# in config/authorization_rules.rb
authorization do
role :admin do
has_permission_on :employees, :to => [:create, :read, :update, :delete]
end
end
There is a default role :guest
that is used if a request is not associated
with any user or with a user without any roles. So, if your application has
public pages, :guest
can be used to allow access for users that are not
logged in. All other roles are application defined and need to be associated
with users by the application.
If you need to change the default role, you can do so by adding an initializer that contains the following statement:
Authorization.default_role = :anonymous
Privileges, such as :create
, may be put into hierarchies to simplify
maintenance. So the example above has the same meaning as
authorization do
role :admin do
has_permission_on :employees, :to => :manage
end
end
privileges do
privilege :manage do
includes :create, :read, :update, :delete
end
end
Privilege hierarchies may be context-specific, e.g. applicable to :employees.
privileges do
privilege :manage, :employees, :includes => :increase_salary
end
For more complex use cases, authorizations need to be based on attributes.
Note that you then also need to set :attribute_check => true
in controllers
for filter_access_to
. E.g. if a branch admin should manage only employees of
his branch (see Authorization::Reader
in the API docs for a full list of
available operators):
authorization do
role :branch_admin do
has_permission_on :employees, to: :manage do
# user refers to the current_user when evaluating
if_attribute :branch => is { user.branch }
end
end
end
To reduce redundancy in has_permission_on
blocks, a rule may depend on
permissions on associated objects:
authorization do
role :branch_admin do
has_permission_on :branches, :to => :manage do
if_attribute :managers => contains { user }
end
has_permission_on :employees, :to => :manage do
if_permitted_to :manage, :branch
# instead of
# if_attribute :branch => { :managers => contains {user} }
end
end
end
Lastly, not only privileges may be organized in a hierarchy but roles as well. Here, project manager inherit the permissions of employees.
role :project_manager do
includes :employee
end
See also Authorization::Reader
.
declarative_authorization
provides a few helpers to ease the testing with
authorization in mind.
In your test_helper.rb
, to enable the helpers add
require 'declarative_authorization/maintenance'
class Test::Unit::TestCase
include Authorization::TestHelper
...
end
For using the test helpers with RSpec, just add the following lines to your
spec_helper.rb
(somewhere after require 'spec/rails'):
require 'declarative_authorization/maintenance'
include Authorization::TestHelper
Now, in unit tests, you may deactivate authorization if needed e.g. for test setup and assume certain identities for tests:
class EmployeeTest < ActiveSupport::TestCase
def test_should_read
without_access_control do
Employee.create(...)
end
assert_nothing_raised do
with_user(admin) do
Employee.find(:first)
end
end
end
end
Or, with RSpec, it would work like this:
describe Employee do
it "should read" do
without_access_control do
Employee.create(...)
end
with_user(admin) do
Employee.find(:first)
end
end
end
In functional tests, get, posts, etc. may be tested in the name of certain users:
get_with admin, :index
post_with admin, :update, :employee => {...}
See Authorization::TestHelper
for more information.
The requirements are
- Rails ≥ 5.
- An authentication mechanism.
- A user returned by
Controller#current_user
. - An array of role symbols returned by calling
#role_symbols
on user. - (For model security) Setting
Authorization.current_user
to the request's user
Currently, the main means of debugging authorization decisions is logging and
exceptions. Denied access to actions is logged to warn
or info
, including
some hints about what went wrong.
All bang methods throw exceptions which may be used to retrieve more information about a denied access than a Boolean value.
If your authorization rules become more complex, you might be glad to use the
authorization rules browser that comes with declarative_authorization
. It has
a syntax-highlighted and a graphical view with filtering of the current
authorization rules.
By default, it will only be available in development mode. To use it, add the
following lines to your authorization_rules.rb
for the appropriate role:
has_permission_on :authorization_rules, :to => :read
Then, point your browser to http://localhost:3000/authorization_rules.
The graphical view requires Graphviz
(which e.g. can be installed through the
graphviz
package under Debian and Ubuntu) and has only been tested under
Linux.
Note: for Change Support you'll need to have a #login
method on user that
returns a non-ambiguous user name for identification.
Originally created by
Steffen Bartsch TZI, Universität Bremen, Germany sbartsch at tzi.org
This fork maintained by Aaron Breckenridge.
Thanks to John Joseph Bachir, Dennis Blöte, Eike Carls, Damian Caruso, Kai Chen, Erik Dahlstrand, Jeroen van Dijk, Alexander Dobriakov, Sebastian Dyck, Ari Epstein, Jeremy Friesen, Tim Harper, John Hawthorn, hollownest, Daniel Kristensen, Jeremy Kleindl, Joel Kociolek, Benjamin ter Kuile, Brad Langhorst, Brian Langenfeld, Georg Ledermann, Geoff Longman, Olly Lylo, Mark Mansour, Thomas Maurer, Kevin Moore, Tyler Pickett, Edward Rudd, Sharagoz, TJ Singleton, Mike Vincent, Joel Westerberg
Copyright (c) 2008 Steffen Bartsch, TZI, Universität Bremen, Germany released under the MIT license