Coming from Django, I was a little surprised/disappointed that permissions aren’t very tightly integrated with the Rails ActiveAdmin as they are with the django admin. Luckily, my search for better authorization for ActiveAdmin has led me to this very informative post by Chad Boyd. It makes things much easier so we can authorize resources more flexibly.
However, there were a couple of aspects that I still wasn’t 100% happy with:
- When an unauthorized action is attempted, the user is simply redirected with an error message. I personally like to return a 403 response / page. Yes, I’m nitpicking. I know.
- Default actions like Edit, View and Delete still appear. They are not dependent on the permission the user has. Clicking on those won’t actually allow you to do anything, but why have some option on the screen if they are not actually allowed??
So with my rather poor Ruby/Rails skill, and together with my much more experienced colleague, we’ve made a few tweaks to the proposal on Chad’s post to make it happen.
403 Forbidden (or “Don’t touch my coffee machine”)
There’s a subtle, but important difference between 401 (Unauthorized), and 403 (Forbidden) responses. The essence of it is that 401 means you don’t have a valid user account on the system. Usually this means you entered a wrong password, or need to login again. 403 however, means you might be logged-in, but your user account does not have the right permission to access the resource. As an example, I can invite you to my flat for dinner, and you’re welcome to walk into my kitchen too, but I won’t let you use my fancy coffee machine. My wife knows how to use it, so she’s obviously allowed (not sure why she always want me to make coffee, but that’s besides the point).
I think it’s good practice to clearly respond with the correct status code. If I don’t like you to use my coffee machine, I won’t push you away into the living room if you try to get closer. I’ll just say it. (that’s a rather crappy analogy, I admit). If the page doesn’t exist, return 404. If the user is not logged in – a 401. If the action is not authorized – 403 is the correct response. This makes it clearer to both the user and the developer. To track what’s going on in log files etc.
In the case of CanCan authorization, instead of doing this in the ApplicationController:
rescue_from CanCan::AccessDenied do |exception| redirect_to (super_user? ? franchises_path : root_path), :alert => exception.message end
We would do something like this
rescue_from CanCan::AccessDenied do |exception| return render_403(exception) end def render_403(exception) logger.warn("Unauthorized access. Request: #{request.env}") @forbidden_path = request.url @error_message = exception.message respond_to do |format| format.html { render template: 'errors/error_403', layout: 'layouts/application', status: 403 } format.all { render nothing: true, status: 403 } end end
This would render a nice 403 page as well as log the unauthorized access with some details about the request.
Hide actions that are not authorized
This is a subtle, but perhaps even more important aspect in my opinion. Blocking access is necessary of course, but why show an option that is not available in the first place? This just confuses users.
I’m not sure this is the best or most elegant solution, but it seems to work quite nicely. Please feel free to suggest a better way.
This is our modified lib/active_admin_can_can.rb
:
module ActiveAdminCanCan def active_admin_collection super.accessible_by current_ability end def resource resource = super authorize! permission, resource resource end private def permission case action_name when "show" :read when "new", "create" :create when "edit" :update else action_name.to_sym end end end # a small helper to make things shorter def can?(action, resource) controller.current_ability.can?(action, resource) end # call this from within your activeadmin Register block # This will only display the action items that are allowed for the user def active_admin_allowed_action_items config.clear_action_items! action_item :except => [:new, :show] do # New link if can?(:create, active_admin_config.resource_class) && controller.action_methods.include?('new') link_to(I18n.t('active_admin.new_model', :model => active_admin_config.resource_name), new_resource_path) end end action_item :only => [:show] do # Edit link on show if can?(:update, resource) && controller.action_methods.include?('edit') link_to(I18n.t('active_admin.edit_model', :model => active_admin_config.resource_name), edit_resource_path(resource)) end end action_item :only => [:show] do # Destroy link on show if can?(:destroy, resource) && controller.action_methods.include?("destroy") link_to(I18n.t('active_admin.delete_model', :model => active_admin_config.resource_name), resource_path(resource), :method => :delete, :confirm => I18n.t('active_admin.delete_confirmation')) end end end # Adds links to View, Edit and Delete # This override will only display the links that are allowed for the user def default_actions(options = {}) options = { :name => "" }.merge(options) column options[:name] do |resource| links = ''.html_safe if can?(:read, resource) && controller.action_methods.include?('show') links += link_to I18n.t('active_admin.view'), resource_path(resource), :class => "member_link view_link" end if can?(:update, resource) && controller.action_methods.include?('edit') links += link_to I18n.t('active_admin.edit'), edit_resource_path(resource), :class => "member_link edit_link" end if can?(:destroy, resource) && controller.action_methods.include?('destroy') links += link_to I18n.t('active_admin.delete'), resource_path(resource), :method => :delete, :confirm => I18n.t('active_admin.delete_confirmation'), :class => "member_link delete_link" end links end end
and then in our ActiveAdmin CircusController we use it like this:
ActiveAdmin.register Circus do controller do authorize_resource include ActiveAdminCanCan end # add this call - it will show only allowed action items active_admin_allowed_action_items index do column :id column :name column :clowns column :elephants # this will call our `default_actions` which only displays allowed actions default_actions end end
Hope it makes things reasonably DRY and re-usable, but somehow I wonder if there’s an even better way, in which ActiveAdmin just magically knows what to display. Until I find this magic, I’ll probably keep on with this.
p.s. if you are really interested, I’m happy to show you how to use the coffee machine too! I’m not that strict about it.
3 replies on “More ActiveAdmin Customizations with CanCan”
I am getting an uninitialized constant ActiveAdminCanCan
You probably need to make sure rails loads files from your lib folder. You can add this to you
config/application.rb
file:awesome! works great! thanks!