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.