0

I'm using devise and I followed this in order to setup three users (admin, seller, viewer). Each user has it's on model, session_controller, registration_conttroler and views folder with all the views associated to each user.

Now I'm trying to implement the pundit gem in order to setup permissions in each controller.

When trying to land on the localhost:3000/items I'm getting the following error: unable to find policy of nil Pundit::NotDefinedError in ItemsController#index

This is what I'm trying to do in the items_controller:

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


  def index
    authorize @item
    @items = Item.all
  end

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

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


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

  def create
    authorize @item
    @item = 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
    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])
    end
end

application_controller.rb

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

  rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized

  def pundit_user
    CurrentContext.new(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

models/current_context.rb

class CurrentContext
  attr_reader :seller, :admin, :viewer

  def initialize(seller, admin, viewer)
    @seller = seller
    @admin = admin
    @viewer = viewer
  end
end

policies/application_policy.rb

class ApplicationPolicy
  attr_reader :seller, :record, :admin, :viewer

  def initialize(context, record)
     raise Pundit::NotAuthorizedError, "must be logged in" unless context
    @seller = context.seller
    @admin = context.admin
    @viewer = context.viewer
    @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 :seller, :admin, :viewer, :scope

    def initialize(context, scope)
      @seller = context.seller
      @admin = context.admin
      @viewer = context.viewer
      @scope = scope
    end

    def resolve
      scope
    end
  end
end

policies/item_policy.rb

What I'm trying here is... the admin to have full access and the seller to create, edit, updated, delete only his own content.

class ItemPolicy < ApplicationPolicy
  attr_reader :item

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

  def update?
    @user.is_a?(Admin) || @item.try(:user) == @user
  end

  def index?
    @user.is_a?(Admin) || @item.try(:user) == @user
  end

  def show?
    @user.is_a?(Admin) || @item.try(:user) == @user
  end

  def create?
    @user.is_a?(Admin) || @item.try(:user) == @user
  end

  def new?
    @user.is_a?(Admin) || @item.try(:user) == @user
  end

  def edit?
    @user.is_a?(Admin) || @item.try(:user) == @user
  end

  def destroy?
   @user.is_a?(Admin) || @item.try(:user) == @user
  end
end
Theopap
  • 715
  • 1
  • 10
  • 33

2 Answers2

1

Check your controller for index action you have @item nil. Change your index action like this:

  def index
    authorize Item
    @items = Item.all
  end
Gaurav Gupta
  • 1,181
  • 10
  • 15
  • Thanks for the help @Gaurav Gupta!! It doesn't return the error anymore but I'm having one small glitch. It won't let me see the items added by the user that is currently logged in. Pundit returns: `You cannot perform this action` when landing on the `localhost:3000/items` page, and redirects me to the `root path`. I added the `authorize Item` to the `index` action inside the `items_controller` and this inside the `ItemPolicy` `index` action `@user.is_a?(Admin) || @item.try(:user) == @user` – Theopap Oct 05 '17 at 16:28
1

In Pundit you pass the class to authorize actions that do not correspond to a specific instance:

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

Also make a habit of using policy_scope - it lets you controll which records are available from the policy.

You're also using the @item instance variable before you are declaring it in #new and create:

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

You can also DRY the controller considerably by authorizing in your set_item callback instead:

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


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

  def show
    # Use the association
    @comments = @item.comments.order("created_at DESC")
  end

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


  def edit
    @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
    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]) )
      # Or if you are using an older version of Pundit
      # @item = Item.find(params[:id])
      # authorize @item
    end
end
max
  • 96,212
  • 14
  • 104
  • 165
  • In the older versions of pundit (don't remember exactly where its changed) `authorize` returns a boolean instead of the record. So if you are getting `undefined method "foo" for FalseClass` errors do it in two lines. – max Oct 05 '17 at 15:13
  • Thanks for the help @max!! It doesn't return the error anymore but I'm having one small glitch. It won't let me see the items added by the user that is currently logged in. Pundit returns: `You cannot perform this action` when landing on the `localhost:3000/items` page, and redirects me to the `root path`. I added the `authorize Item` to the `index` action inside the `items_controller` and this inside the `ItemPolicy` `index` action `@user.is_a?(Admin) || @item.try(:user) == @user` – Theopap Oct 05 '17 at 16:25
  • `@item.try(:user) == @user` won't work for an index action as it represents and index of all the items. If you want to restrict which items a user can see do it in the policy scope. `user.admin? ? Item.all : user.items` – max Oct 05 '17 at 16:35
  • I had to change it to this: `@user.admin? ? Item.all : user.items` but for some reason it returns this error: `NoMethodError in ItemsController#index` `undefined method 'admin?' for # Did you mean? admin` – Theopap Oct 05 '17 at 16:40
  • I guess it would be `user.is_a?(Admin) ? Item.all : user.items` – max Oct 05 '17 at 16:41
  • hmmmm.... I already tried that and returns this : `NoMethodError in ItemsController#index undefined method items for #` – Theopap Oct 05 '17 at 16:43
  • Well thats really wierd that @user in your policy would be something different than a User / Admin instance or nil. Have you overridden the devise `current_user` method in some quirky way? – max Oct 05 '17 at 16:48
  • Nope, I just added `def pundit_user`action inside the `application_controller.rb` and added the `models/current_context.rb`... you can check out both if you want, inside my question. – Theopap Oct 05 '17 at 16:52
  • Yeah, that seems really quirky and overcomplicated. Change it to return the currently signed in user. And base your authentication of what he/she can do. Not 4 different factors. That seems crazy. – max Oct 05 '17 at 16:56
  • Thanks for the help thus @max!!! Just a heads up, the only reason I changed the `current_user` is because if I change it back it returns this error: `undefined local variable or method current_user for # Did you mean? current_seller` – Theopap Oct 05 '17 at 17:01
  • You should probally create a `current_user` method which gets whatever kind of scope is logged in though. Can't help you there though - I have never done multiple classes/scopes in devise as it seems like a bad solution. I just use a single class and rolify instead. – max Oct 05 '17 at 17:04
  • Thanks anyway @max!! So, are you saying that it might be best if I switch to a single class and add rolify?? – Theopap Oct 05 '17 at 17:09
  • yeah either that or change it to `def pundit_user; current_seller || current_admin || current_viewer; end` (semicolons just to write it as single line). But the reason I think using different authorization classes to implement roles is crap is that in real life user have several roles in an application and it does not handle that. It also adds a bunch of duplication and complexity. – max Oct 05 '17 at 17:15