Genuity Tech Blog

Tidbits of Ruby, Rails, and all things related

ActiveAdmin and CanCan

TL;DR

When using CanCan with ActiveAdmin, ability rules with conditons aren’t respected. This post talks about the customizations I made to get these rules to work in ActiveAdmin.



One of the projects I’m currently working on uses ActiveAdmin for the back end administration interface. I’ve used ActiveAdmin on several projects, and I really like it a lot. However, I always seem to find myself needing to go beyond the generated resource definitions and do some customization. On this particular project, I’ve had to do what I consider to be significant customization. The things I’ve had to do have either been not documented well by ActiveAdmin, or in some cases not documented at all. In an effort to help others, I’ve decided to write a series of blog posts on the customizations I’ve had to do to get ActiveAdmin to do what I need.

For this first post I would like to talk about using the popular authorization gem CanCan, written by Ryan Bates of Railscasts fame, for declaring authorization rules for the different user roles in this project’s admin interface.

ActiveAdmin’s wiki has a page titled How to work with cancan which details the steps to take in order to use CanCan with the ActiveAdmin framework. I followed these instructions, and for whatever reason this did not work as advertised. I’m going to share with you what I did in addition to these steps in order to get CanCan working with ActiveAdmin.

The Ability class

This project has a pretty simple Ability class so far:

app/models/ability.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
class Ability
  include CanCan::Ability

  def initialize(user)
    case user
    when SuperUser
      can :manage, :all
    when FranchiseUser
      can :create, Customer
      can [:read, :update, :destroy], Customer, franchise_id: user.franchise_id
    end
  end
end

As you can see, I’m simply using inheritance for defining roles. There are only two types of users in this project: A SuperUser who can administer any type of model, and a FranchiseUser who can only administer Customer models that belongs_to their Franchise. Both of these user classes extend a base User class. If there were more roles, or the authorization scheme was more complex, I would definitely go with something other than inheritance for roles. But this project’s needs were pretty simple.

Looking at the abilities for FranchiseUser, you can see I have a condition for :read, :update, and :destroy that constrains those actions to the user’s franchise, but the :create action is not constrained. I found that if I applied the condition to the :create action I could no longer access the customers/new view. I guess this makes sense because CanCan defines :create to encompass both :new and :create controller actions, and a new Customer doesn’t yet have a franchise_id associated with it. If I defined the customers resource as a nested route using ActiveAdmin’s belongs_to method, I probably could put :create down with the conditional, but I wanted it to be a top-level resource so the super user can also create customers for any franchise without having to first go through a particular franchise link.

When the customer is actually saved to the database in the create controller action, I’m restricting the franchise user’s ability to create customers outside their franchise by explicitly setting the franchise_id param to the current_user’s franchise_id:

app/admin/customers.rb
1
2
3
4
5
6
7
8
ActiveAdmin.register Customer do
  controller do
    def create
      params[:customer][:franchise_id] = current_user.franchise_id if franchise_user?
      create!
    end
  end
end

Authorizing ActiveAdmin resources with CanCan

Normally when using CanCan, you would include the load_and_authorize_resource call in your controller, or in our case the Customer resource definition, to load the resource, or collection thereof, and perform the authorization. However, ActiveAdmin’s wiki says not to use this method because it will cause problems. One of the reasons is because ActiveAdmin automatically decorates the resource collection with pagination. Instead of load_and_authorize_resource, you should just call authorize_resource and leave the loading up to ActiveAdmin. The problem with this is that by not letting CanCan load the resource, or collection thereof, ability rules with conditions don’t seem to be respected.

So what are we to do if we want to use those conditional rules and still let ActiveAdmin load the resources? We need to manually authorize the resources ourselves, just like CanCan normally does.

Authorizing resource collections

During the :index controller action, CanCan’s load_and_authorize_resource method authorizes the resources in the collection by calling .accessible_by on the resource class. This method constrains the result set to objects that can be read by the current_user. Knowing this, I created a module that would add this behavior to the ActiveAdmin method responsible for retrieving the resource collection:

lib/active_admin_can_can.rb
1
2
3
4
5
module ActiveAdminCanCan
  def active_admin_collection
    super.accessible_by current_ability
  end
end

Note that current_ability is defined in my ApplicationController, as recommended by the aforementioned ActiveAdmin wiki page:

app/controllers/application_controller.rb
1
2
3
4
5
6
7
8
9
10
11
class ApplicationController < ActionController::Base
  protect_from_forgery

  rescue_from CanCan::AccessDenied do |exception|
    redirect_to (super_user? ? franchises_path : root_path), :alert => exception.message
  end

  def current_ability
    @current_ability ||= Ability.new(current_user)
  end
end

Now we’ll just include this new module in the ActiveAdmin CustomersController:

app/admin/customers.rb
1
2
3
4
5
6
7
8
9
10
11
ActiveAdmin.register Customer do
  controller do
    authorize_resource
    include ActiveAdminCanCan

    def create
      params[:customer][:franchise_id] = current_user.franchise_id if franchise_user?
      create!
    end
  end
end

Now when visiting the customers index page, the table of customers is filtered to only those belonging to the current user’s franchise.

Authorizing singular resources

This takes care of resource collections and the index view, but creating, editing and destroying customers is still not restricted to customers that belong to the current user’s franchise. For this, we need to add to our newly created module:

lib/active_admin_can_can.rb
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
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

Here, we’ve added the resource method, as well as the private permission method. ActiveAdmin uses the inherited_resources gem under the covers for loading and modifying resources. The resource method is provided by inherited_resources, so this is the method we need to override to perform our authorization.

Now when we try to edit or destroy a customer that doesn’t belong to the user’s franchise, we are denied access. Awesome!

Conclusion

I hope this helps others that have struggled with getting CanCan and ActiveAdmin to play nicely together. Keep an eye out for subsequent posts in the near future on other ActiveAdmin customizations I’ve done for this project.

Comments