CanCan is probably the most widely used authorization solution for Ruby on Rails, and for good reason. But sometimes weird things happen.
Given the following ability:
can :read, User, role: :developer
I was experiencing the following:
@user = User.accessible_by(current_ability).first can? :read, @user #=> false ???
As you can see, the accessible_by method was returning the user list just fine, so I obviously had the permission to read the user at this point, but once I asked the authorize!
or can?
methods about the same user they promptly replied with false.
The culprit here is ActiveRecord's indifferent-access of strings and hashes. Something that had me scratching my head at more than one occasion over the last few years. ActiveRecord doesn't care if the keys in a hash are strings or symbols, so a beginner might be tempted to think Ruby does not differentiate between those.
But Ruby does care, 'test' != :test
will return true! Whereas ActiveRecord doesn't care:
User.where('role' => 'developer') # or User.where(:role => :developer)
This is due to the heavy use of ActiveSupport::HashWithIndifferentAccess that makes this possible in Rails:
a = {} a[:foo] = 'test' puts a['foo'] #=> 'test'
Now if we look at the conditions derived from the above ability you will quickly see the problem:
current_ability.model_adapter(User, :read).conditions # => { role: :developer }
ActiveRecord doesn't care about the symbol, it will happily return all Users with the role column set to the string developer, whereas CanCan will do the can?
check in Ruby, doing essentially this:
puts @user.role #=> 'developer' # user.role is a string - the condition a Symbol puts @user.role == :developer #=> false
Conclusion: Always make sure your conditions are valid both in ActiveRecord AND Ruby! Where ActiveRecord and Rails in general promote a sloppy type-free behavior, other frameworks don't necessarily see it that way. So keep in mind that a symbol is not a string!
Also when you know you might get a symbol and want to compare it to a string, don't use .to_sym
on the string but rather .to_s
on the symbol - symbols don't get garbage-collected, so they will stick around in memory forever. Never .to_sym
user input or bad things will happen™!