Catfish
Catfish

Reputation: 19312

Rails - Trying to learn cancan, but where does @user object come from

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

Answers (3)

marzapower
marzapower

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.

For a more detailed insight

Defining abilities

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.

Checking/authorizing abilities

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

What you want to do

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.

Warning

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).

Why 3) is not working

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:

  1. You are not logged in, and
  2. You are not an admin

In all other cases (you logged in as an admin) it will work. Please, check this first.

Bonus

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

Peter Nixey
Peter Nixey

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.

You're conflating the resource with the person accessing it

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

You already have real (but confusing) bug

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.

What was happening in your cases

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

You need to load the resource first and pass that resource to the ability file

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

Ways to make this all easier to understand

  1. 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

  2. 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

Tamer Shlash
Tamer Shlash

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.

Then what is the second parameter of authorize! ?

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

Related Questions