1. General introduction
- CanCanCan is a decentralized library for Ruby and Ruby on Rails, which limits the resources that a certain user is allowed to access.
- All permissions can be specified in one or more capable files and are not duplicated on the controller, view and query DB, keeping the authorization logic in one place for easy maintenance and testing.
2. Abilities in Database
What if you or the client want to change permissions without having to re-deploy the App? In that case, it’s best to store the right logic in the database: it’s easy to use database records when defining capabilities.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | class Ability include CanCan::Ability def initialize user if user.admin? can :manage, :all cannot :create, Borrowing, user_id: user.id cannot :read, Borrowing else can :create, Borrowing can :read, Borrowing, user_id: user.id can :read, Borrowing, ["user_id = ?", user.id] do |borrowing| borrowing.created_at.year >= 2021 end end end end |
In CanCanCan the actions are shown as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | def eval_cancan_action(action) case action.to_s when "index", "show", "search" cancan_action = "read" action_desc = I18n.t :read when "create", "new" cancan_action = "create" action_desc = I18n.t :create when "edit", "update" cancan_action = "update" action_desc = I18n.t :edit when "delete", "destroy" cancan_action = "delete" action_desc = I18n.t :delete else cancan_action = action.to_s action_desc = "Other: " << cancan_action end return action_desc, cancan_action end |
3. Ability for Other User
What if you wanted to define a User permission other than current_user
? Let’s say I want to see if another user has the right to see the loan or not
some_user.ability.can? :update, @borrowing
Add the ability
method to the User
model:
1 2 3 4 | def ability @ability ||= Ability.new(self) end |
Use delegate to be able to call directly from User
:
1 2 3 4 | class User < ActiveRecord::Base delegate :can?, :cannot?, to: :ability end |
The results will be as follows:
1 2 3 4 5 6 | User.last.can? :read, Borrowing.first (0.7ms) User Load (0.7ms) SELECT `users`.* FROM `users` WHERE `users`.`deleted_at` IS NULL ORDER BY `users`.`id` DESC LIMIT 1 Borrowing Load (1.3ms) SELECT `borrowings`.* FROM `borrowings` WHERE `borrowings`.`deleted_at` IS NULL ORDER BY `borrowings`.`id` ASC LIMIT 1 => false |
Finally, if you want to see which object current_user has access to then it’s best to override the mothod current_ability in the ApplicationController.
1 2 3 4 | def current_ability @current_ability ||= current_user.ability end |
The results will be tested as follows:
1 2 3 | current_ability.can? :create, Borrowing => false |
4. Ability Precedence
An ability rule will overwrite the previous ability rule. Suppose the Admin has full control over Borrowing, but cannot delete them.
1 2 3 | can :manage, Borrowing cannot :destroy, Borrowing |
The important thing is cannot :destroy, Borrowing
after the line can :manage, Borrowing
. Thus, cannot :destroy
will be overridden by can :manage
.
Adding can
does not override the previous rule.
1 2 3 4 5 | can :read, Borrowing, user_id: user.id can :read, Borrowing do |borrowing| borrowing.created_at.year > 2021 end |
can? :read
will always return true if user_id = user.id even if Borrowing has creation time less than 2021
5. Accessing request data
What if you need to modify permissions based on something outside of the User object? Let’s say you want to blacklist certain IP addresses from comment creation. The IP address is accessible via request.remote_ip but the Ability class does not have access to this address. The simple way to modify what you pass to the Ability object is by overriding the current_ability method in the ApplicationController.
1 2 3 4 5 6 7 8 | class ApplicationController < ActionController::Base private def current_ability @current_ability ||= Ability.new(current_user, request.remote_ip) end end |
1 2 3 4 5 6 7 8 | class Ability include CanCan::Ability def initialize(user, ip_address=nil) can :create, Comment unless BLACKLIST_IPS.include? ip_address end end |
This concept can also be applied to Session or Cookie.
6. Authorization for Namespaced Controllers
By default in the CanCanCan gem is permissions based on the user and object defined in the load_resource. But if you have a SearchController and Admin :: SearchController, you can use some other approach.
In this case, just override the current_ability
method in the ApplicationController to include the controller namespace, and create a class Ability
knows what to do with it.
1 2 3 4 5 6 7 8 9 10 11 12 | class ApplicationController < ActionController::Base private def current_ability 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 |
1 2 3 4 5 6 7 8 9 10 11 12 13 | class Ability include CanCan::Ability def initialize(user, controller_namespace) case controller_namespace when "Admin" can :manage, :all if user.admin? else # rules for non-admin controllers here end end end |
Another way is to use another Ability class on this controller:
1 2 3 4 5 6 7 8 9 10 | class Admin::BaseController < ActionController::Base #... private def current_ability @current_ability ||= AdminAbility.new(current_user) end end |
Conclude
Through this article, I hope to help you understand more about CanCanCan gem.
References [( https://github.com/CanCanCommunity/cancancan/wiki )]