Managing Devise’s current_user, current_admin and current_troll with CanCan
CanCan is awesome. It lets you manage user abilities easily and provides ways to define complex scenarios. I highly recommend using it for anyone who has more than one user type (like Troll).
Devise is great for authentication. When you have more than one user type as distinct classes, Devise will create current_* to be used in your controllers and views. So, User class corresponds to current_user
. Admin class corresponds to current_admin
. Troll class (used to identify Trolls under your application’s bridge) corresponds to current_troll
.
The problem
CanCan doesn’t work with current_admin
and current_troll
out-of-the-box. It assumes that current_user
is defined and current_user
’s abilities are defined in the Ability class. What if you want to break this paradigm? It turns out CanCan makes this pretty easy. Here are the current_user and Ability assumptions I am referring to:
def current_ability
@current_ability ||= ::Ability.new(current_user)
end
CanCan defines current_ability
on your controller. This grabs an instance of the Ability class for the current user. So it assumes that you have current_user
set and you have an Ability class defined. When your user types get more complex than what can be handled by one User model, it’s time to make some changes.
Working with numerous Ability classes
Up front, your project might not require many different user types that vary greatly from one another. It might make sense to use Rail’s nifty STI (Single Table Inheritance) and add all your abilities to one class. This can be nice in some respect. For instance, all users, no matter which type, can be reference by current_user
.
When your user types get too complex to use one User model, your Ability class is too complex as well. In it’s most simple form, say you have an Ability class that looks as follows:
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new # guest user (not logged in)
if user.is_a? Admin
# Admin abilities
elsif user.is_a? Troll
# Troll abilities
elsif user.new_record?
# Guest abilities
else
# Basic user abilities
end
end
end
This structure gives you some flexibility in how you define your abilities but it’s on it’s way to Maintenance Hell, a deep dark place with no exit.
It would be best to define your abilities in different classes. Here we define UserAbility, AdminAbility, TrollAbility, and GuestAbility.
class UserAbility
include CanCan::Ability
def initialize(user)
# Basic user abilities
end
end
class AdminAbility
include CanCan::Ability
def initialize
# Admin abilities
end
end
class TrollAbility
include CanCan::Ability
def initialize(user)
# Troll abilities
end
end
class GuestAbility
include CanCan::Ability
def initialize
# Guest abilities
end
end
Keep in mind that if your abilities are a subset of another user’s abilities, you can inherit from other ability class. So in our case a Troll is a user who lives under a bridge. We don’t want the Trolls to talk, so we limit their ability to post comments. Otherwise, they can do everything a User can do.
class TrollAbility < UserAbility
def initialize(user)
super(user)
cannot :create, Comment
# More Troll abilities
end
end
Hooking up the Ability classes
When you use CanCan’s can? :create, Comment
method, it refers to current_ability
to determine whether the given abilities include :create, Comment.
Since CanCan makes the assumption we’re working with current_user
and strictly Ability, we need to extend the built-in functionality. We do this by instantiating the new Ability classes based on the current user type (defined by Devise). CanCan has a brief wiki post on this topic.
def current_ability
@current_ability ||= case
when current_user
UserAbility.new(current_user)
when current_admin
AdminAbility.new
when current_troll
TrollAbility.new(current_troll)
else
GuestAbility.new
end
end
Now, when CanCan needs to check abilities (when you call can? :create, Comment
), your current_ability
method will return the appropriate Ability class.
Happy CanCaning!
4 comments