0

I'm using the pundit gem in order to give permissions to three different users(Admin, seller, viewer). Currently I got everything working, admin has access to everything, seller to his own products and viewer can just view the products.

The only glitch I'm having is that I would like the non signed_up/signed_in users to be able to view the products via search results. Right now non sign_up/signed_in users can see the search results but don't have access to the show view.

Here is the setup I have:

class ItemPolicy < ApplicationPolicy
  attr_reader :item

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

  def update?
    @user.is_a?(Admin) ? item.all : @user.items
  end

  def index?
    @user.is_a?(Admin) ? item.all : @user.items
  end

  def show?
    @user.is_a?(Admin) ? item.all : @user.items
  end

  def create?
   @user.is_a?(Admin) ? item.all : @user.items
  end

  def new?
    @user.is_a?(Admin) ? item.all : @user.items
  end

  def edit?
    @user.is_a?(Admin) ? item.all : @user.items
  end

  def destroy?
    @user.is_a?(Admin) ? item.all : @user.items
  end

  class Scope < Struct.new(:user, :scope)
    def resolve
      if user.is_a?(Admin)
        scope.where(:parent_id => nil)
      elsif user.is_a?(Seller)
        scope.where(:id => user.items)
      end
    end

    def show?
      return true if user.is_a?(Admin)
      return true if user.seller_id == seller.id && user.is_a?(Seller)
     false
    end
  end
end

the controller:

class ItemsController < ApplicationController
  before_action :set_item, only: [:show, :edit, :update, :destroy]


  def index
    authorize Item
    @items = policy_scope(Item)
  end

  def search
    if params[:term]
      @items = Item.search(params[:term]).order("created_at DESC")
    else
      @items = []
    end
  end


  def show
    @comments = Comment.where(item_id: @item).order("created_at DESC")
    @items = policy_scope(Item).find(params[:id])
    authorize @item
  end


  def new
    @item = Item.new
    authorize @item
    @categories = Category.order(:name)
  end


  def edit
   authorize @item
   @categories = Category.order(:name)
  end


  def create
    @item = Item.new(item_params)
    authorize @item

    respond_to do |format|
       if @item.save
        format.html { redirect_to @item, notice: 'Item was successfully created.' }
        format.json { render :show, status: :created, location: @item }
       else
        format.html { render :new }
        format.json { render json: @item.errors, status: :unprocessable_entity }
      end
    end
  end


  def update
    authorize @item
    respond_to do |format|
      if @item.update(item_params)
        format.html { redirect_to @item, notice: 'Item was successfully updated.' }
        format.json { render :show, status: :ok, location: @item }
      else
        format.html { render :edit }
        format.json { render json: @item.errors, status: :unprocessable_entity }
      end
    end
  end


   def destroy
    authorize @item
    @item.destroy
     respond_to do |format|
       format.html { redirect_to items_url, notice: 'Item was successfully destroyed.' }
       format.json { head :no_content }
     end
  end

  private
    def set_item
      @item = Item.find(params[:id])
      authorize @item
    end


    def item_params
      params.require(:item).permit(:title, :description, :image, :price, :category_id)
    end
end

application_contoller.rb

  class ApplicationController < ActionController::Base
  include Pundit
  protect_from_forgery prepend: true

  rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized

  def pundit_user
    current_seller || current_admin || current_viewer
  end

  private

  def user_not_authorized(exception)
    policy_name = exception.policy.class.to_s.underscore
    flash[:warning] = t "#{policy_name}.#{exception.query}", scope: "pundit", default: :default
    redirect_to(request.referrer || root_path)
  end
end

Update 1

class ApplicationPolicy
  attr_reader :user, :record

  def initialize(user, record)
    raise Pundit::NotAuthorizedError, "must be logged in" unless user
    @user = user
    @record = record
  end

  def index?
    true # anybody can view
  end

  def show?
    true # anybody can view
  end

  def search?
    index?
  end

  def new?
    create?
  end

  def edit?
    update?
  end

  def destroy?
    update?
  end

  private
  # don't repeat yourself
  def admin?
    user.is_a?(Admin)
  end

  def seller?
    user.is_a?(Seller)
  end
end


class ItemPolicy < ApplicationPolicy
  attr_reader :item

  class Scope < Struct.new(:user, :scope)
    def resolve
      if admin?
        scope.where(parent_id: nil)
      elsif seller?
        # avoids a query for user.items
        scope.where(seller: user)
      end
    end
  end

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

  def update?
    admin? || is_owner?
  end

  def create?
    # just guessing here
    admin? || seller?
  end

  private

  def is_owner?
    # or whatever the association between the item and its owner is
    item.seller == user
  end
end
Theopap
  • 715
  • 1
  • 10
  • 33

1 Answers1

1

You are confusing scopes and the authorization methods in Pundit. The new?, show? etc. methods should return a boolean indicating if the user is allowed to perform the action at all.

To allow unauthorized users to perform an action you simply return true. Scopes are used to narrow down which records the user has access to. They only have a resolve method.

class ApplicationPolicy
  # ...

  private
  # don't repeat yourself  
  def admin?
    user.is_a?(Admin)
  end

  def seller?
    user.is_a?(Seller)
  end
end


class ItemPolicy < ApplicationPolicy
  attr_reader :item

  class Scope < Struct.new(:user, :scope)
    def resolve
      if admin?
        scope.where(parent_id: nil)
      elsif seller?
        # avoids a query for user.items
        scope.where(seller: user)
      end
    end
  end

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

  def update?
    admin? || is_owner?
  end

  def index?
    true # anybody can view
  end

  def show?
    true # anybody can view
  end

  def search?
    index?
  end

  def create?
    # just guessing here
    admin? || seller?
  end

  def new?
    create?
  end

  def edit?
    update?
  end

  def destroy?
    update?
  end

  private

  def is_owner?
     # or whatever the association between the item and its owner is
     item.seller == user 
  end
end

Instead of repeating yourself (the conditions) you can shortcut many of the actions since the permissions for editing for example is the same as updating. You can even do this in the ApplicationPolicy so that you don't have to repeat it in each policy class:

class ApplicationPolicy
  # ...

  def index?
    true # anybody can view
  end

  def show?
    true # anybody can view
  end

  def search?
    index?
  end

  def new?
    create?
  end

  def edit?
    update?
  end

  def destroy?
    update?
  end

  private
  # don't repeat yourself  
  def admin?
    user.is_a?(Admin)
  end

  def seller?
    user.is_a?(Seller)
  end
end

class ItemPolicy < ApplicationPolicy
  attr_reader :item

  class Scope < Struct.new(:user, :scope)
    def resolve
      if admin?
        scope.where(parent_id: nil)
      elsif seller?
        # avoids a query for user.items
        scope.where(seller: user)
      end
    end
  end

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

  def update?
    admin? || is_owner?
  end

  def create?
    # just guessing here
    admin? || seller?
  end

  private

  def is_owner?
     # or whatever the association between the item and its owner is
     item.seller == user 
  end
end

You are also authorizing the user twice at many places in your controller as it is as already performed by the set_item callback:

class ItemsController < ApplicationController
  before_action :set_item, only: [:show, :edit, :update, :destroy]

  def index
    authorize Item
    @items = policy_scope(Item)
  end

  def search
    if params[:term]
      @items = Item.search(params[:term]).order("created_at DESC")
    else
      @items = Item.none # Don't use [] as @items.none? for example would blow up.
    end
  end


  def show
    @comments = Comment.where(item_id: @item).order("created_at DESC")
    authorize @item
  end


  def new
    @item = authorize(Item.new)
    @categories = Category.order(:name)
  end


  def edit
    @categories = Category.order(:name)
  end


  def create
    @item = authorize(Item.new(item_params))

    respond_to do |format|
      if @item.save
        format.html { redirect_to @item, notice: 'Item was successfully created.' }
        format.json { render :show, status: :created, location: @item }
      else
        format.html { render :new }
        format.json { render json: @item.errors, status: :unprocessable_entity }
      end
    end
  end

  def update
    respond_to do |format|
      if @item.update(item_params)
        format.html { redirect_to @item, notice: 'Item was successfully updated.' }
        format.json { render :show, status: :ok, location: @item }
      else
        format.html { render :edit }
        format.json { render json: @item.errors, status: :unprocessable_entity }
      end
    end
  end


  def destroy
    @item.destroy
    respond_to do |format|
      format.html { redirect_to items_url, notice: 'Item was successfully destroyed.' }
      format.json { head :no_content }
    end
  end

  private
    def set_item
      @item = authorize( Item.find(params[:id]) )
    end


    def item_params
      params.require(:item).permit(:title, :description, :image, :price, :category_id)
    end
end
max
  • 96,212
  • 14
  • 104
  • 165
  • Thanks for the info and help once again @max!!! I updated my code according your answer but still cant get the `unauthorized users` to view the show page... also when a user is signed-in he can view and edit all the items now. I have added update 1 to my question so u can see what I have done thus far. – Theopap Oct 06 '17 at 13:00
  • An admin can see and edit all the items. If thats not what you want change the logic in the #update? method. Also to allow unathorized users to access a resource you need to skip the Devise 'authorize_user!' (Or whatever it is) callback. – max Oct 06 '17 at 13:08
  • Yes thats is exactly I want, the admin to have access to all, the seller only his on items, the viewer only the search results index and show.... but currently with your changes... a logged-in seller can edit/view/delete all items. – Theopap Oct 06 '17 at 13:14
  • Well, then change the logic in the `#is_owner?` method, although I'm kind of suprised that it returns true. Anyways you actually have to implement this yourself and understand the code. Stackoverflow is not 1-on-1 tutoring or a code writing service. – max Oct 06 '17 at 13:21