Trust
Trust is a no-nonsense framework for authorization control - for Ruby On Rails.
- Why yet another authorization framework you may ask?
Well, we used DeclarativeAuthorization for a while, but got stuck when it comes to name-spaces and inheritance. So, we investigated in the possibilities of using CanCan and CanTango, and found that CanCan could be slow, because all permissions has to be loaded on every request. CanTango has tackled this problem by implementing caching, but the framework is still evolving and seems fairly complex. At the same time, CanTango is role focused and not resource focused.
What will you benefit from when using Trust?
- Resource focused permissions, not role focused
- Complete support for inheritance in controllers
- Complete support for namespaces, both controllers and models
- Complete support for nested resources
- Complete support for shortened associations (e.g. if you have models in name spaces that relates to other models in the name space)
- Fast permission loading, where no cashing is needed. All permissions are declared on class level, so once loaded, they stay in memory.
- Support for inheritance in the permissions model
- Natural code evaluation in the permission declarations, i.e. you understand completely what is going on, because the implementation is done the way you implement condifitions in rails for validations and alike.
- Automatic loading of instances and parents in controller
- Mongoid support
What is not supported in Trust
- Loading datasets for the index action. You may use other plugins / gems for doing this, or you may implement your own mechanism.
Install and Setup
Install the gem
gem install trust
Or, in your Gemfile
gem 'trust'
Define permissions
Create the permissions file in your model directory. Example
module Permissions
class Default < Trust::Permissions
role :system_admin do
can :manage
can :audit
end
end
class Account < Default
role :support, can(:manage)
role :accountant do
can :edit, :show, :if => :associated_with_client?
end
role :department_manager, :accountant do
can :create, :if => lambda { parent }
end
def associated_with_client?
parent && parent.is_a?(Client) && parent.operators.find(user.id)
end
end
class Voucher < Default
member_roles :accountant do
can :edit, :show, :if => :associated_with_client?
end
def members_role()
user.member_role( subject_or_parent.team )
end
end
# Rails 4 - definitions for strong_params
class Invoice < Default
require :invoice # requires :invoice hash. This is set by default, so in practice not necessary to define
permit :date, :due_days # permitted parameters
role :accountant do
can :edit, :show, :if => :associated_with_client?
end
role :department_manager, :accountant do
can :new, :create, :if => lambda { parent }, permit: [:date, :due_days, :discount]
end
end
end
The members_role can be implemented if a user has multiple roles such as memberships of teams, projects or similar.
The following attributes will be accessible in a Permissions class:
-
subject
- the resource that is currently being tested for authorization -
parent
- the parent of the authorization when resource is nested -
user
- the user accessing the resource -
klass
- the resource class -
action
- the action that triggered the authorization
Keep in mind that the permission object will be instanciated to do authorization, and not the class. You can extend the Trust::Permissions with more functionality if needed.
You can also create aliases for actions. We have defined a predefined set of aliases. See Trust::Permissions.action_aliases. Processing of aliases are done in such way that permissions per action is expanded when the permissions are loaded, so thif you define :update when declaring the permissions, there will be one permission for :update and one for :edit
Apply access control in controller
Place trustee
in your controller after the user has been identified. Something like this:
class AccountsController < ApplicationController
login_required
trustee
end
The trustee statement will set up 3 before_filters (before_actions) in your controller:
before_filter :set_user
before_filter :load_resource
before_filter :access_control
Trust assumes that current_user
is accessible. The user object must respond to the method role_symbols
which should return an array of one or more roles for the user.
Handling access denied situations in your controller. Implement something like the following in your ApplicationController:
class ApplicationController < ActionController::Base
rescue_from Trust::AccessDenied do |exception|
redirect_to root_url, :alert => exception.message
# or some other redirect
end
end
Define associations in your controller
For nested resources you can easily define the associations using belongs_to
like this:
class AccountsController < ApplicationController
login_required
belongs_to :client
trustee
end
You can specify as many associations as you like.
The can? and permits? method
The can? method is accessible from controller and views. Here are some coding examples:
In controller or views you will use can?
can? :edit # does the current user have permission to edit the current resource?
# If there is a nested resource, the parent is automatically associated
can? :edit, @customer # does the current user have permission to edit the given customer?
# Parent is also passed on here.
can? :edit, @account, @client # is current user allowed to edit the account associated with the client?
On ActiveRecord objects you will use permits?
@customer.permits? :edit # does the current user have permission to edit the given customer?
Customer.permits? :create, @client # does the current user have permission to create customers for client?
Customer.permits? :create, @client, :by => @auditor # does the auditor have permission to create customers?
Customer.permits? :create, :for => @client, :by => @auditor # same as above
You can also designate actors in your model so you can test if other people has access to do operations on a subject
Include the Actor role in the class
class Auditor
include Trust::Actor
...
end
Now you can test if the auditor has permission to modify customer for client. Three ways of writing it
@auditor.can? :update, @customer, @client
@auditor.can? :update, @customer, :for => @client
@auditor.can? :update, @customer, :parent => @client
The above is the same as
@customer.permits? :update, @client, :by => @auditor
Instance variables
The filter load_resource
will automatically load the instance for the resource in the controller. It will by default use the controller_path to determine the name of the instance variable. Here are a couple of examples:
UsersController => @user
Account::CreditsController => @account_credit
If it is a nested resource, it will also instantiate the parent
class, using the namedefined in belongs_to to determine the name. E.g. if you have defined belongs_to :client, it will look for the parameter :client_id
and perform a find like Client.find(client_id)
. Finding the resource will be done through the association between the two, such as client.accounts.find(id)
.
You can override the naming by specifying model
like this
class AccountsController < ApplicationController
login_required
model :wackount
trustee
end
If you want to override the name with namespacing then
class Account::CreditsController < ApplicationController
login_required
model :"account/wreckit"
trustee
end
You can also access the instances in a generic manner if you like. Use following statements:
resource.instance => accesses the instance variable
resource.parent => accesses the parent instance
You can even assign these if you like. The resource is also exposed as helper, so you can access it in views.
For simplicity we have also exposed an instances
accessor that you can assign when you have a multirecord result,
such as for index action.
Accessing strong_params for updates (rails 4)
@invoice.update_attributes(resource.strong_params)
# or
resource.instance.update_attributes(resource.strong_params)
Overriding defaults
Overriding resource permits in the controller
Say you have a controller without a model or do not want to perform access control. You can turn off the featur in your controller
class ApplicationController < ActionController::Base
login_required
trustee # By default we want to test for permissions
end
class MyController < ApplicationController
trustee :off # turns off all callbacks
end
Alternatives
class MyController < ApplicationController
set_user :off # turns off set_user callback
load_resource :off # do not load resources
access_control :off # turn access control off
end
More specifically
For all call backs and trustee
you can use :only
and :except
options.
Example toggle create action off
class MyController < ApplicationController
load_resource :except => :create
access_control :except => :create
end
Yet another alternative, avoiding resource loading
Avoid resource loading on show
action
class MyController < ApplicationController
actions :except => :show
end
Overriding set_user
If you prefer to use some other user reference than current_user you can override the method set_user
like this in your controller:
def set_user
Trust::Authorization.user = User.current
end
Devise integration
If you have your ApplicationController
as the trustee you will need to reverse this in devise that inherits the ApplicationController
.
Add this to your devise initializer:
DeviseController.trustee :off