We now dig deeper in the customizations and options we have when working with controller helpers
CanCanCan makes two assumptions about your application:
- You have an
Ability
class which defines the permissions. - You have a
current_user
method in the controller which returns the current user model.
You can override both of these by defining the current_ability
method in your ApplicationController
. The default method looks like this.
def current_ability
@current_ability ||= Ability.new(current_user)
end
The Ability
class and current_user
method can easily be changed to something else.
# in ApplicationController
def current_ability
@current_ability ||= AccountAbility.new(current_account)
end
Sometimes you might have a gem in your project which provides its own Rails engine which also uses CanCanCan, in this case the current_ability override in the ApplicationController can also be useful.
# in ApplicationController
def current_ability
if request.fullpath =~ /\/rails_admin/
@current_ability ||= RailsAdmin::Ability.new(current_user)
else
@current_ability ||= Ability.new(current_user)
end
end
If your method that returns the currently logged in user just has another name than current_user
, it may be the easiest solution to simply alias the method in your ApplicationController like this:
class ApplicationController < ActionController::Base
alias_method :current_user, :name_of_your_method # Could be :current_member or :logged_in_user
end
If your parameters sanitization method does not follow the naming convention, load_and_authorize_resource
takes a param_method
option to specify a custom method in the controller to run to sanitize input.
You can associate the param_method
option with a symbol corresponding to the name of a method that will get called:
class ArticlesController < ApplicationController
load_and_authorize_resource param_method: :my_sanitizer
def create
@article.save
end
private
def my_sanitizer
params.require(:article).permit(:name)
end
end
You can also use a string that will be evaluated in the context of the controller using instance_eval
and needs to contain valid Ruby code.
load_and_authorize_resource param_method: 'permitted_params.post'
Finally, it's possible to associate param_method
with a Proc object which will be called with the controller as the only argument:
load_and_authorize_resource param_method: -> { |c| c.params.require(:article).permit(:name) }
If your model name and controller name differ, you can specify a class
option.
Note that the method will still be
articles_params
and notpost_params
, since we are inArticlesController
.
class ArticlesController < ApplicationController
load_and_authorize_resource class: 'Post'
def create
@article.save
end
private
def article_params
params.require(:article).permit(:name)
end
end
You can use CanCanCan with controllers that do not follow the traditional REST actions, however you should not use the load_and_authorize_resource
method since there is no resource to load. Instead you can call authorize!
in each action separately.
For example, let's say we have a controller which does some miscellaneous administration tasks such as rolling log files. We can use the authorize!
method here.
class AdminController < ActionController::Base
def roll_logs
authorize! :roll, :logs
# roll the logs here
end
end
And then authorize that in the Ability
class.
can :roll, :logs if user.admin?
Notice you can pass a symbol as the second argument to both authorize!
and can
. It doesn't have to be a model class or instance.
Alternatively you can use the authorize_resource
and specify that there's no class. This way it will pass the resource symbol instead. This is good if you still have a Resource-like controller but no model class backing it.
class ToolsController < ApplicationController
authorize_resource class: false
def show
# automatically calls authorize!(:show, :tool)
end
end
You can use the skip_load_and_authorize_resource
, skip_load_resource
or skip_authorize_resource
methods to skip any of the applied behavior and specify specific actions like in a before filter. For example:
class ProductsController < ActionController::Base
load_and_authorize_resource
skip_authorize_resource only: :new
end
If the model is named differently than the controller, then you may explicitly name the model that should be loaded; however, you must specify that it is not a parent in a nested routing situation, ie:
class ArticlesController < ApplicationController
load_and_authorize_resource :post, parent: false
end
If the model class is namespaced differently than the controller you will need to specify the :class
option.
class ProductsController < ApplicationController
load_and_authorize_resource class: "Store::Product"
end
If you want to fetch a resource by something other than id
it can be done so using the find_by
option.
load_resource find_by: => :permalink # will use find_by!(permalink: params[:id])
authorize_resource
The resource will only be loaded into an instance variable if it hasn't been already. This allows you to easily override how the loading happens in a separate before_action
.
class BooksController < ApplicationController
before_action :find_published_book, only: :show
load_and_authorize_resource
private
def find_published_book
@book = Book.released.find(params[:id])
end
end
If you want to be certain authorization is not forgotten in some controller action, add check_authorization
to your ApplicationController
.
class ApplicationController < ActionController::Base
check_authorization
end
This will add an after_action
to ensure authorization takes place in every inherited controller action. If no authorization happens it will raise a CanCan::AuthorizationNotPerformed
exception. You can skip this check by adding skip_authorization_check
to that controller. Both of these methods take the same arguments as before_action
so you can exclude certain actions with :only
and :except
.
class UsersController < ApplicationController
skip_authorization_check :only => [:new, :create]
# ...
end
The check_authorization
method supports :if
and :unless
options. Either one takes a method name as a symbol. This method will be called to determine if the authorization check will be performed. This makes it very easy to skip this check on all Devise controllers since they provide a devise_controller?
method.
class ApplicationController < ActionController::Base
check_authorization unless: :devise_controller?
end
Here's another example where authorization is only ensured for the admin subdomain.
class ApplicationController < ActionController::Base
check_authorization if: :admin_subdomain?
private
def admin_subdomain?
request.subdomain == "admin"
end
end
Note: The
check_authorization
only ensures that authorization is performed. If you haveauthorize_resource
the authorization will still be performed no matter what is returned here.
The default operation for CanCanCan is to authorize based on user and the object identified in load_resource
. So if you have a WidgetsController
and also an Admin::WidgetsController
, you can use some different approaches.
You can create differing authorization rules that depend on the controller namespace.
In this case, just override the current_ability
method in ApplicationController
to include the controller namespace, and create an Ability
class that knows what to do with it.
class Admin::WidgetsController < ActionController::Base
#...
private
def current_ability
# I am sure there is a slicker way to capture the controller namespace
controller_name_segments = params[:controller].split('/')
controller_name_segments.pop
controller_namespace = controller_name_segments.join('/').camelize
@current_ability ||= Ability.new(current_user, controller_namespace)
end
end
class Ability
include CanCan::Ability
def initialize(user, controller_namespace)
case controller_namespace
when 'Admin'
can :manage, :all if user.has_role? 'admin'
else
# rules for non-admin controllers here
end
end
end
Another way to achieve the same is to use a completely different Ability class in this controller:
class Admin::WidgetsController < ActionController::Base
#...
private
def current_ability
@current_ability ||= AdminAbility.new(current_user)
end
end