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.rbfile:config.autoload_paths += %W(#{config.root}/lib)awesome! works great! thanks!