Setting
How to install CanCanCan gem is as simple as any other gem
- gem 'cancancan'
- bundle install
Basic usage
Assuming the project already has the basic components. Each user has their own role.
- User rights are defined in the Ability.rb file
- To create this file, run: rails g cancan: ability
- In this file, to allow the user to access the resource, we can use the can function to allow or cannot to disallow it.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | <span class="token keyword">class</span> <span class="token class-name">Ability</span> include <span class="token constant">CanCan</span> <span class="token punctuation">:</span> <span class="token punctuation">:</span> <span class="token constant">Ability</span> <span class="token keyword">def</span> initialize user can <span class="token symbol">:read</span> <span class="token punctuation">,</span> <span class="token punctuation">[</span> <span class="token constant">Product</span> <span class="token punctuation">,</span> <span class="token constant">Table</span> <span class="token punctuation">]</span> <span class="token comment">#user chưa đăng nhập có thể xem Product, Table</span> <span class="token keyword">return</span> <span class="token keyword">unless</span> user <span class="token comment"># Nếu đăng nhập thì tiếp tục</span> can <span class="token punctuation">[</span> <span class="token symbol">:show</span> <span class="token punctuation">,</span> <span class="token symbol">:update</span> <span class="token punctuation">]</span> <span class="token punctuation">,</span> <span class="token constant">User</span> <span class="token punctuation">,</span> id <span class="token punctuation">:</span> user <span class="token punctuation">.</span> id <span class="token comment"># user nào thì xem và sửa profile của user đó</span> <span class="token keyword">case</span> user <span class="token punctuation">.</span> role <span class="token keyword">when</span> <span class="token string">"admin"</span> can <span class="token symbol">:manage</span> <span class="token punctuation">,</span> <span class="token symbol">:all</span> <span class="token comment"># Admin có toàn quyền</span> <span class="token keyword">when</span> <span class="token string">"staff"</span> can <span class="token symbol">:manage</span> <span class="token punctuation">,</span> <span class="token punctuation">[</span> <span class="token constant">Admin</span> <span class="token punctuation">,</span> <span class="token constant">OrderDetail</span> <span class="token punctuation">,</span> <span class="token constant">OrderTable</span> <span class="token punctuation">,</span> <span class="token constant">Order</span> <span class="token punctuation">]</span> <span class="token comment"># Staff có toàn quyền đối với trang admin dashboard, Order, OrderDetail, OrderTable</span> <span class="token keyword">when</span> <span class="token string">"guest"</span> can <span class="token symbol">:create</span> <span class="token punctuation">,</span> <span class="token constant">Order</span> <span class="token comment"># Guest có thể tạo Order</span> <span class="token keyword">end</span> <span class="token keyword">end</span> <span class="token keyword">end</span> |
- In the view or in the controller, to check permissions, we use can? or cannot ?.
1 2 3 4 | <span class="token operator"><</span> <span class="token operator">%</span> <span class="token keyword">if</span> can <span class="token operator">?</span> <span class="token symbol">:read</span> <span class="token punctuation">,</span> <span class="token variable">@order</span> <span class="token string">%> <%= link_to "View", @order %></span> <span class="token operator"><</span> <span class="token operator">%</span> <span class="token keyword">end</span> <span class="token operator">%</span> <span class="token operator">></span> |
- To get the list of records that a user can access, we can use the function:
1 2 | <span class="token function">accessible_by</span> <span class="token punctuation">(</span> current_ability <span class="token punctuation">)</span> |
- However, in order for CanCanCan to work properly, we need to check user rights for actions in the controller. To do so, we need to add
authorize!
into each action in each controller. Example in orders_controller.rb:
1 2 3 4 5 | <span class="token keyword">def</span> show <span class="token variable">@order</span> <span class="token operator">=</span> <span class="token constant">Order</span> <span class="token punctuation">.</span> <span class="token function">find_by</span> <span class="token punctuation">(</span> id <span class="token punctuation">:</span> params <span class="token punctuation">[</span> <span class="token symbol">:id</span> <span class="token punctuation">]</span> <span class="token punctuation">)</span> <span class="token punctuation">)</span> authorize <span class="token operator">!</span> <span class="token symbol">:read</span> <span class="token punctuation">,</span> <span class="token variable">@order</span> <span class="token keyword">end</span> |
But doing so is laborious, so CanCanCan has load_and_authorize_resource
to support us authorize all actions according to RESTful standards.
1 2 3 4 5 6 | <span class="token keyword">class</span> <span class="token class-name">UsersController</span> <span class="token operator"><</span> <span class="token constant">ApplicationController</span> load_and_authorize_resource <span class="token keyword">def</span> show <span class="token punctuation">;</span> <span class="token keyword">end</span> <span class="token keyword">end</span> |
- So, if the user does not have access to the resource, what happens?
- Then exception CanCan :: AccessDenied will be created and we will handle it in ApplicationController.
1 2 3 4 5 | rescue_from <span class="token constant">CanCan</span> <span class="token punctuation">:</span> <span class="token punctuation">:</span> <span class="token constant">AccessDenied</span> <span class="token keyword">do</span> <span class="token operator">|</span> exception <span class="token operator">|</span> flash <span class="token punctuation">[</span> <span class="token symbol">:warning</span> <span class="token punctuation">]</span> <span class="token operator">=</span> exception <span class="token punctuation">.</span> message redirect_to root_url <span class="token keyword">end</span> |
So we have already decentralized the user already, followed by some ways to decentralize in some special cases.
Some options
1. Some ways of defining user rights
In addition to using RESTful actions to define user rights, we can also define other actions.
In Ability.rb there are:
1 2 | can <span class="token symbol">:asign_roles</span> <span class="token punctuation">,</span> <span class="token constant">User</span> <span class="token keyword">if</span> user <span class="token punctuation">.</span> admin <span class="token operator">?</span> |
Update role in user_controller:
1 2 3 4 5 | <span class="token keyword">def</span> update authorize <span class="token operator">!</span> <span class="token symbol">:assign_roles</span> <span class="token punctuation">,</span> <span class="token variable">@user</span> <span class="token keyword">if</span> params <span class="token punctuation">[</span> <span class="token symbol">:user</span> <span class="token punctuation">]</span> <span class="token punctuation">[</span> <span class="token symbol">:assign_roles</span> <span class="token punctuation">]</span> <span class="token comment">#..update roles</span> <span class="token keyword">end</span> |
- Alternatively, you can create action aliases to combine multiple actions into one
1 2 | alias_action <span class="token symbol">:show</span> <span class="token punctuation">,</span> <span class="token symbol">:index</span> <span class="token symbol">:to</span> <span class="token operator">=</span> <span class="token operator">></span> <span class="token symbol">:read</span> |
- We can assign more conditions when decentralizing:
1 2 3 4 5 | can <span class="token symbol">:read</span> <span class="token punctuation">,</span> <span class="token constant">Order</span> <span class="token punctuation">,</span> active <span class="token punctuation">:</span> <span class="token keyword">true</span> <span class="token punctuation">,</span> user_id <span class="token punctuation">:</span> user <span class="token punctuation">.</span> id <span class="token comment"># chỉ user nào sở hữu order mới có thể đọc được order đó</span> can <span class="token symbol">:read</span> <span class="token punctuation">,</span> <span class="token constant">Product</span> <span class="token punctuation">,</span> <span class="token constant">Category</span> <span class="token punctuation">:</span> <span class="token punctuation">{</span> status <span class="token punctuation">:</span> <span class="token string">"enable"</span> <span class="token punctuation">}</span> <span class="token comment"># chỉ có thể đọc được product nào mà category nó thuộc về còn "enable"</span> |
2. Separation of Ability
- Besides using the Ability class to decentralize, we can separate the rights therein for each different class, maybe separated by model, separated by controller.
- To split the ability, we need to override the current_ability method in application_controller
1 2 3 4 | <span class="token keyword">def</span> current_ability <span class="token variable">@current_ability</span> <span class="token operator">||</span> <span class="token operator">=</span> <span class="token constant">AccountAbility</span> <span class="token punctuation">.</span> <span class="token keyword">new</span> <span class="token punctuation">(</span> current_account <span class="token punctuation">)</span> <span class="token keyword">end</span> |
3. Disregard decentralization
- To be able to check permissions without having to add the load_and_authorize_resource function to all controllers, add it instead to application_controller.
- If no controller's permissions need to be checked, add skip_authorize_resource to that controller.
4. Check decentralization.
- If a certain controller is omitted when delegating, then you can add check_authorization to the application_controller.
- And if you want to skip checking on a certain controller then add skip_authorization_check to that controller.
5. Filter params
- If in actions like create, update there are merely commands to create data without any commands to filter the params, avoiding the risks when the params have bad data, CanCanCan will help us with that.
1 2 3 4 5 6 7 8 9 10 | <span class="token keyword">class</span> <span class="token class-name">UsersController</span> <span class="token operator"><</span> <span class="token constant">ApplicationController</span> <span class="token keyword">def</span> create <span class="token keyword">if</span> <span class="token variable">@user</span> <span class="token punctuation">.</span> save <span class="token comment"># hurray</span> <span class="token keyword">else</span> render <span class="token symbol">:new</span> <span class="token class-name">end</span> <span class="token keyword">end</span> <span class="token keyword">end</span> |
CanCanCan has 4 ways to filter params and priorities in the following order:
- Call the params handler function by action name:
1 2 3 4 5 6 7 8 | <span class="token keyword">def</span> create_params params <span class="token punctuation">.</span> <span class="token keyword">require</span> <span class="token punctuation">(</span> <span class="token symbol">:user</span> <span class="token punctuation">)</span> <span class="token punctuation">.</span> <span class="token function">permit</span> <span class="token punctuation">(</span> <span class="token symbol">:name</span> <span class="token punctuation">,</span> <span class="token symbol">:email</span> <span class="token punctuation">)</span> <span class="token keyword">end</span> <span class="token keyword">def</span> update_params params <span class="token punctuation">.</span> <span class="token keyword">require</span> <span class="token punctuation">(</span> <span class="token symbol">:user</span> <span class="token punctuation">)</span> <span class="token punctuation">.</span> <span class="token function">permit</span> <span class="token punctuation">(</span> <span class="token symbol">:name</span> <span class="token punctuation">)</span> <span class="token keyword">end</span> |
- Call the params handler function by model name:
1 2 3 4 | <span class="token keyword">def</span> user_params params <span class="token punctuation">.</span> <span class="token keyword">require</span> <span class="token punctuation">(</span> <span class="token symbol">:user</span> <span class="token punctuation">)</span> <span class="token punctuation">.</span> <span class="token function">permit</span> <span class="token punctuation">(</span> <span class="token symbol">:name</span> <span class="token punctuation">)</span> <span class="token keyword">end</span> |
- Call resource_params:
1 2 3 4 | <span class="token keyword">def</span> resource_params params <span class="token punctuation">.</span> <span class="token keyword">require</span> <span class="token punctuation">(</span> <span class="token symbol">:user</span> <span class="token punctuation">)</span> <span class="token punctuation">.</span> <span class="token function">permit</span> <span class="token punctuation">(</span> <span class="token symbol">:name</span> <span class="token punctuation">)</span> <span class="token keyword">end</span> |
- Call the function to handle any params:
1 2 3 4 5 6 | load_and_authorize_resource param_method <span class="token punctuation">:</span> <span class="token symbol">:my_params</span> <span class="token keyword">def</span> my_params params <span class="token punctuation">.</span> <span class="token keyword">require</span> <span class="token punctuation">(</span> <span class="token symbol">:user</span> <span class="token punctuation">)</span> <span class="token punctuation">.</span> <span class="token function">permit</span> <span class="token punctuation">(</span> <span class="token symbol">:name</span> <span class="token punctuation">)</span> <span class="token keyword">end</span> |
Conclude
Above are some ways to customize decentralization with CanCanCan. There are many options, but not all options work for our project. Therefore, we need to think carefully, select the options that suit our project to apply. Otherwise, the decentralization will become cumbersome, making our project become slow and may lead to other unnecessary errors.