-->

Pundit policy_scope error: undefined method `admin

2020-02-15 03:02发布

问题:

Running into something I don't understand with Pundit,

Using Rails 4.2.5.1, Pundit 1.1.0 with Devise for authentication.

I'm trying to use a policy scope for the BlogController#Index action.

  • If user is admin, display all posts (drafts, published)
  • If user is standard, display posts marked published only
  • If no user / user not logged in, display posts marked published only

Getting an error:

undefined method `admin?' for nil:NilClass

Live shell reveals:

>> user
=> nil

# ApplicationController
class ApplicationController < ActionController::Base
  include Pundit
  rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized

  # Prevent CSRF attacks by raising an exception.
  # For APIs, you may want to use :null_session instead.
  protect_from_forgery with: :exception

  private

    def user_not_authorized
      flash[:error] = "You are not authorized to perform this action."
      redirect_to(request.referrer || root_path)
    end
end

# BlogController

# == Schema Information
#
# Table name: blogs
#
#  id         :integer          not null, primary key
#  title      :string           default(""), not null
#  body       :text             default(""), not null
#  published  :boolean          default("false"), not null
#  created_at :datetime         not null
#  updated_at :datetime         not null
#

class BlogsController < ApplicationController
  before_action :set_blog, only: [:show, :edit, :update, :destroy]
  before_action :authenticate_user!, except: [:index, :show]

  after_action :verify_authorized, except: [:index, :show]
  after_action :verify_policy_scoped, only: [:index]

  def index
    @blogs = policy_scope(Blog)
    authorize @blog
  end

  def show
  end

  def new
    @blog = Blog.new
    authorize @blog
  end

  def edit
    authorize @blog
  end

  def create
    @blog = Blog.new(blog_params)
    @blog.user = current_user if user_signed_in?

    authorize @blog

    if @blog.save
      redirect_to @blog, notice: "Blog post created."
    else
      render :new
    end
  end

  def update
    authorize @blog

    if @blog.update(blog_params)
      redirect_to @blog, notice: "Blog updated."
    else
      render :edit
    end
  end

  def destroy
    authorize @blog
    @blog.destroy
    redirect_to blogs_url, notice: "Blog post deleted."
  end

  private

    def set_blog
      @blog = Blog.friendly.find(params[:id])
    end

    def blog_params
      params.require(:blog).permit(*policy(@blog|| Blog).permitted_attributes)
    end
end

# Application Policy

class ApplicationPolicy
  attr_reader :user, :record

  def initialize(user, record)
    @user = user
    @record = record
  end

  def index?
    false
  end

  def show?
    scope.where(:id => record.id).exists?
  end

  def create?
    false
  end

  def new?
    create?
  end

  def update?
    false
  end

  def edit?
    update?
  end

  def destroy?
    false
  end

  def scope
    Pundit.policy_scope!(user, record.class)
  end

  class Scope
    attr_reader :user, :scope

    def initialize(user, scope)
      @user = user
      @scope = scope
    end

    def resolve
      scope
    end
  end
end

# Blog Policy

class BlogPolicy < ApplicationPolicy
  class  Scope < Scope
    def resolve
      if user.admin?
        scope.all
      else
        scope.where(published: true)
      end
    end
  end

  def new?
    user.admin?
  end

  def index?
    true
  end

  def update?
    user.admin?
  end

  def create?
    user.admin?
  end

  def destroy?
    user.admin?
  end

  def permitted_attributes
    if user.admin?
        [:title, :body]
    end
  end
end

In the Pundit BlogPolicy scope I've created:

  class  Scope < Scope
    def resolve
      if user.admin?
        scope.order('id DESC')
      else
        scope.where('published: true')
      end
    end
  end

If I log in as an admin user it works fine.

I'm able to view all blog posts.

If I log in as a standard user it works.

Standard user sees blog posts that are marked published.

If I'm not logged in where user is nil I get an error:

NoMethodError at /blog
undefined method `admin?' for nil:NilClass

I can add another clause elsif user.nil? before user.admin? or a case when statement but I thought if the user is not an admin it should just display what is in the else block?

 # This seems wrong?

  class  Scope < Scope
    def resolve
      if user.nil?
        scope.where('published: true')
      elsif user.admin?
        scope.all
      else
        scope.where('published: true')
      end
    end
  end

Any pointers much appreciated

回答1:

You can use try:

if user.try(:admin?)
  # do something
end

http://api.rubyonrails.org/v4.2.5/classes/Object.html#method-i-try



回答2:

This happens because there is no user when you are not logged in. (Probably to user variable nil value is assigned somewhere, so you are trying to call admin? method on nil object)

If you use ruby 2.3.0 or newer you had better use safe navigation

  if user&.admin?
    scope.order('id DESC')
  else
    scope.where('published: true')
  end

If you user other ruby version

if user.try(:admin?)
  scope.order(id: :desc)
else
  scope.where(published: true)
end