Reputation: 52758
I noticed users could access an action they shouldn't be able to access.
I debugged in the rails console with something like
user = User.first
physician = Physician.first
ability = Ability.new(user)
ability.can?(:send_message, physician)
# => false
The above says that user can't access the send_message action for that physician, which is the desired behaviour, yet I know they can in the app!
I think this narrows down the cause to a problem with cancancan loading the wrong model instance for some reason. And that's hinted to in the cancan docs too:
Note: this assumes that the model instance is being loaded properly.
But the problem is I'm not sure how to diagnose the problem from here, since the console says it should be working. I don't know how to view the model instance that cancancan has set, and I don't know what else to try.
Any ideas?
I did manage to work around this by using authorize! :send_message, physician
in the controller, but since I only stumbled upon this behaviour by chance, I think it's much more important to figure out why the wrong model instance was being loaded (especially so I can see if that was happening elsewhere too).
I think this was happening because I had many custom actions, and some had @physician = Physician.find(current_user.physician.id)
(i.e they're the current user), whereas others were more like @physician = Physician.find_by_id(physician_params[:id])
. I'm not sure how cancan sets the instance model, but I do know it's not psychic, so it wouldn't know whether to set it to the current user's physician instance, or the physician instance for the physician id passed in.
How does cancancan set the model instance for custom methods (I presume it tries something, and if that doesn't work, tries something else, etc etc)?
Small notes that help:
load_and_authorize_resource
does attempt to load the model instance for non RESTful actionsWhen I returned slug it breaks this behaviour and I can edit all pokemons.
Upvotes: 2
Views: 1507
Reputation: 52758
Leaving my notes here in case they are helpful to anyone else.
TL;DR, there are a lot of nuanced assumptions cancancan makes, which you won't know about from the outset. I discovered many of them by thoroughly reading the comments in the cancancan readme, code, and defining abilities docs
So here goes..
authorize!
in the controller action itself, cancancan will look for an instance variable in each controller action.load_and_authorize_resource
at the start of your controller, that will do two things:
load_and_authorize_resource
will still try to load a model instance, but how does it know what to load? It doesn't, it guesses, which, for me, I do not like, so be aware of that.
load_and_authorize_resource
myself in two separate steps, so I know exactly what's going on.
load_and_authorize_resource
after the before action that sets the model instance
load_and_authorize_resource
should always go after any before actions that set the model instance variableThe name of the instance variable depends on the action. If we have an articles controller, then:
load_and_authorize_resource
checks to see if the model instance is set, and if not, it sets one. So if you have a before action that creates @article/@articles, then load_and_authorize_resource
won't do it for you (i.e. it won't overwrite it), but if you didn't set one, cancan will try to set one. See comments from the source code:
A resource is not loaded if the instance variable is already set. This makes it easy to override the behavior through a before_action on certain actions.
current_user
in ability.rb, it will error silently (!!), so be sure to use user
instead :)Upvotes: 5
Reputation: 230521
Here's what is happening: https://github.com/CanCanCommunity/cancancan/blob/585e5ea54c900c6afd536f143cde962ccdf68607/lib/cancan/controller_additions.rb#L342-L355
# Creates and returns the current user's ability and caches it. If you
# want to override how the Ability is defined then this is the place.
# Just define the method in the controller to change behavior.
#
# def current_ability
# # instead of Ability.new(current_user)
# @current_ability ||= UserAbility.new(current_account)
# end
#
# Notice it is important to cache the ability object so it is not
# recreated every time.
def current_ability
@current_ability ||= ::Ability.new(current_user)
end
Upvotes: 2