Joshua Muheim
Joshua Muheim

Reputation: 13243

CanCanCan: How to handle an object that is nil in Ability model?

I have this controller:

class UsersController < ApplicationController
  load_and_authorize_resource

  def create
    @user.save
    respond_with @user
  end

  def update
    @user.update user_params
    respond_with @user
  end

  def destroy
    @user.destroy
    respond_with @user
  end

  private

  def user_params
    permitted_keys = [:name,
                      :email,
                      :password,
                      :password_confirmation,
                      :lock_version]

    permitted_keys << :role if can? :edit_role, @user

    params.require(:user).permit(permitted_keys)
  end
end

In my ability model:

    can :edit_role, User do |user|
      user.new_record?
    end

The problem is, that when creating a user (#new or #create action), @user is nil. So the role can't be set, as it will not be added to the permitted_keys on #create.

How can this be solved? I could change my controller to:

permitted_keys << :role if action_name == 'create'

But I don't like this, as the Ability can be tested much more easily than this.

Another workaround would be this:

permitted_keys << :role if can? :edit_role, @user || User.new

But this feels redundant.

Anybody has a better idea? And by the way - I'm quite surprised that passing nil as object to can? is allowed and doesn't raise an error. It seems that it doesn't even propagate to the actual configuration in Ability, as otherwise user.new_record? would raise a NoMethod error or similar.

Upvotes: 1

Views: 664

Answers (2)

Joshua Muheim
Joshua Muheim

Reputation: 13243

It seems that using CanCanCan gem, the user_params method is called before the @user variable is created; this kind of makes sense, as user_params is needed to create the @user variable, or at least it is needed to assign the values from the form.

If CanCanCan would assign a new @user variable (of type User, of course) before assigning the values from the form, the following code would work on #create:

private

def user_params
  permitted_keys = [:name,
                    :email,
                    :password,
                    :password_confirmation]

  permitted_keys << :role     if can? :edit_role,    @user
  permitted_keys << :disabled if can? :disable_user, @user

  params.require(:user).permit permitted_keys
end

But as it doesn't do that apparently, the following workaround works for me:

private

def user_params
  permitted_keys = [:name,
                    :email,
                    :password,
                    :password_confirmation]

  auth_object = @user || User.new
  permitted_keys << :role     if can? :edit_role,    auth_object
  permitted_keys << :disabled if can? :disable_user, auth_object

  params.require(:user).permit permitted_keys
end

Upvotes: 0

yez
yez

Reputation: 2378

The CanCan docs prescribe defining abilities with blocks a little differently than how you've done it. They suggest using a guard instead of a block for the case you described (when the instance is nil):

# don't do this
can :edit_role, User do |user|
  user.new_record? # this won't be called for User.accessible_by(current_ability, :edit_role)
end

# do this
can :edit_role, User if user.new_record?

I'd try this. For other information and to see where I got this example, check these docs

Upvotes: 1

Related Questions