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:
1 2 3 4 5 6 7 8 9 10 11 12 13 | |
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:
1 2 3 4 5 6 7 8 | |
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:
1 2 3 4 5 | |
Note that current_ability is defined in my ApplicationController, as
recommended by the aforementioned ActiveAdmin wiki page:
1 2 3 4 5 6 7 8 9 10 11 | |
Now we’ll just include this new module in the ActiveAdmin
CustomersController:
1 2 3 4 5 6 7 8 9 10 11 | |
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:
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 | |
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.