Categories
rails ruby Security Technology

More ActiveAdmin Customizations with CanCan

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:

  1. 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.
  2. 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”

You probably need to make sure rails loads files from your lib folder. You can add this to you config/application.rb file:

config.autoload_paths += %W(#{config.root}/lib)

Leave a Reply

Your email address will not be published. Required fields are marked *