Reputation: 19312
I just created a rails app with the rails composer with devise/cancan/rolify and i'm looking at some of the code that it generated and I'm not sure where @user
comes from in this snippet:
/controllers/users_controller.rb
class UsersController < ApplicationController
def index
authorize! :index, @user, :message => 'Not authorized as an administrator.'
@users = User.all
end
end
/controllers/application_controller.rb
class ApplicationController < ActionController::Base
# Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead.
protect_from_forgery with: :exception
before_filter :authenticate_user!
rescue_from CanCan::AccessDenied do |exception|
redirect_to root_path, :alert => exception.message
end
end
/models/ability.rb
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new # guest user (not logged in)
if user.has_role? :admin
can :manage, :all
end
end
Where does the @user
variable get set? I would have expected to see current_user
there.
EDIT
I feel like I must not be explaining myself clearly so lets try this.
1) This works
def index
authorize! :index, @user, :message => 'Not authorized as an administrator.'
@users = User.all
end
2) This works
def index
authorize! :index, current_user, :message => 'Not authorized as an administrator.'
@users = User.all
end
3) This does NOT work
def index
authorize! :index, @my_user, :message => 'Not authorized as an administrator.'
@users = User.all
end
My question is why does 1) work, but 3) does not?
Upvotes: 2
Views: 2852
Reputation: 5611
You are not getting a @user
variable instantiated here. You are simply working with a method that get a user
local variable passed to.
What this user
variable contains really depends on the cancan
gem. It will work reading the commonly used current_user
helper method so that cancan
works fine with devise
and other similar gems.
In fact, when no user is logged in, you force
user ||= User.new
to be able to handle guests users. But this is all matter of method-local variables.
Probably you are misunderstanding how the authorize!
and can?
methods work.
So, cancan
will try to verify if current_user
is capable of doing something. What the current logged in user (current_user
) is able to do has to be defined in the ability.rb
file.
Abilities have this form (easiest case):
_user_ can _action_ on _model_or_instance_
When you wright this on the Ability
class:
def initialize(user)
can :read, Document
end
you are really saying that the current logged in user can read all instances of the Document
model:
user can :read Document #pseudo-code
That user
that will be passed to the initialize
method will always be current_user
or nil
.
In your controllers you want to check for abilities. Ok. So you go with:
authorize! :index, @user
And this will be converted in something like
verify if current_user can :index @user #pseudo-code
When you change it to:
authorize! :index, current_user
It's the equivalent of
verify if current_user can :index current_user #pseudo-code
You are trying to authorize current_user
to do what? :index
on what controller/model?
Eg. I want to be able to manager users only if the current logged in user is an administrator
The best way to do this is, keeping your Ability
definitions:
authorize! :index, User
so that you are authorizing the current_user to operate on the whole User
models.
You should not pass to the authorize!
and can?
methods the user you want to check the abilites for. That user will always be current_user
. Eg. you cannot get an instance of User
and check if that instance has this or that ability. cancan
will alway use as "main character" the logged in user (or nil, if there is none).
3) will work as intended (and as 1) and 2)) if and only if you logged in to the application and the current user is an administrator. So, it will not work if:
In all other cases (you logged in as an admin) it will work. Please, check this first.
If you're an administrator (given your Ability
definition) this will always be true:
can? :any_action_of_your_choice, nil
since you stated that admins
can :manage, :all
From your examples, no matter what you set after authorize! :index,
, it will always authorize if you logged in as an admin (eg. current_user.has_role? admin
).
Upvotes: 3
Reputation: 16565
Ahh man - I should be working not answering questions on SO however...
TLDR; The @user
variable is NOT being set and that's why your code is odd.
Let me take your first use case and explain what's happening:
# Case 1
class UsersController < ApplicationController
def index
authorize! :index, @user, :message => 'Not authorized as an administrator.'
@users = User.all
end
end
At this stage you actually have a bug. You're getting confused between the person who's accessing the resource, current_user
and the resource you're giving them access which is @user
. I am Peter (current_user
) but I might be trying to access John's (@user
) user page. John is the resource that I am requesting access to.
In your code you've not actually loaded the resource you're wanting to grant access to. Let me break down step by step what's going on.
I'm going to use the show action because it's easier to see what's going on but the same things applies to index
too
Let's assume I, Peter am trying to access your (Catfish's) page:
class UsersController < ApplicationController
def show
# first let's get explicit about who's accessing the page
@current_user = current_user
# now we need to load up the page I'm trying to access
@user = User.find params[:id]
# can I, Peter, access the :show action of this profile?
authorize! :show, @user, :message => 'Not authorized as an administrator.'
# rest of your controller...
@users = User.all
end
end
This loads up me, current_user
and then loads up you, @user
and then asks whether I have permission to access your :show
action.
The fact that you're using the index action is doubly confusing as there isn't any resource to load there. I'm therefore going to explain what's going on using the show action:
# case 1
# this requests access FOR current_user TO @user
# I have no idea what the value of @user is (probably nil)
def show
authorize! :show, @user, :message => 'Not authorized as an administrator.'
@users = User.all
end
# case 2
# this requests access FOR current_user TO current_user
def show
# this is not what you want
authorize! :show, current_user, :message => 'Not authorized as an administrator.'
@users = User.all
end
# case 3)
# This requests access FOR current_user TO @my_user
# I have no idea what the value of @my_user is (probably nil)
def show
authorize! :show, @my_user, :message => 'Not authorized as an administrator.'
@users = User.all
end
Your current user is loaded for you by the authenticate_user!
method. However you need to make sure that the resource you're trying to access is also loaded. In the case of the :show
action this would happen like this:
class UsersController < ApplicationController
def show
# load the resource you want to protect access to
@user = params[:id]
# use cancan to see whether anyone can access it
authorize! :show, @user
@users = User.all
end
end
However depending on whether this is the last level in nesting the id for the resource may be id
or user_id
. On the :index
action there won't be either of these params at all. All this is a pain however cancan handles this nicely for you using load_resource
class UsersController < ApplicationController
def show
# CanCan figures out that the resource is a User and then gets it using the params
load_resource
# @user is now set.
# If the URL is http://yoursite.com/users/5, then @user.id == 5
authorize! :show, @user
@users = @user # not actually necessary since load_resource has already done this
end
end
Now cancan makes it even easier by combining the load and authorize steps
class UsersController < ApplicationController
def show
# figures out what you want to load and authorizes it (sweet)
load_and_authorize_resource
# @user will now be equal to User.find(params[id])
# no need to load @user - it's already been done
end
end
Finally, you can pull these out into a before_filter so you don't have to write them at all
class UsersController < ApplicationController
before_filter load_and_authorize_resource :user
def show
# in a pure CRUD app you literally won't need any code in your show action
end
end
Don't use the users resource to learn cancan
Use Pages, Articles or anything else. It will help make it clear to you which part is the resource, @page
, and which part is the current user @current_user
Don't learn on the index action
The index action doesn't actually load a particular resource since there isn't one to load. This again makes things more difficult to understand. See what's happening on :show
or :edit
and then move to the actions which don't take a resource (:new
, :index
, :create
)
Upvotes: 1
Reputation: 9523
As mentioned in the cancan README:
CanCan expects a current_user method to exist in the controller.
So cancan does depend on devise's current_user
in authorization, it checks if the current_user
is authorize!
d to do the :action
, and doesn't (as you might have guessed) check if the second parameter (which is in this case @user
) is authorized to do that action.
The second parameter of authorize!
is actually the object on which the action (first parameter) is to be checked. In other words (and assuming the case in your code):
cancan will check if the current_user
has the permissions to :index
the @user
Explaining the case in your comment:
The weirdest thing is that it somehow works too. When i login as a user without the admin role, i get the message "not authorized as an administrator", but when i login as an admin i don't get that message
Since you have not given any permissions to non-:admin
users in your /models/ability.rb
, then they will not be able to perform any action, no matter what action it is and what object(s) it's performed on, even if on a nil
! - which seems to be your case. On the other hand, you have granted users with the :admin
role all (:manage
) permissions on all (:all
) objects in your /models/ability.rb, so they're able to do any action on any object no matter what it is, even a nil
!
Upvotes: 5