0

I don't know if I'm doing something wrong here but it seems like.

I use Pundit for authorization and I have set up a few models with it now.

Ive got a Category model which can only be created by admins. Also I don't want users to see the show/edit/destroy views either. I just want it to be accessed by admins. So far so good.

Will add some code below:

category_policy.rb

class CategoryPolicy < ApplicationPolicy
  def index?
    user.admin?
  end

  def create?
    user.admin?
  end

  def show?
    user.admin?
  end

  def new?
    user.admin?
  end

  def update?
    return true if user.admin?
  end

  def destroy?
    return true if user.admin?
  end
end

categories_controller.rb

class CategoriesController < ApplicationController
  layout 'scaffold'

  before_action :set_category, only: %i[show edit update destroy]

  # GET /categories
  def index
    @category = Category.all
    authorize @category
  end

  # GET /categories/1
  def show
    @category = Category.find(params[:id])

    authorize @category
  end

  # GET /categories/new
  def new
    @category = Category.new
    authorize @category
  end

  # GET /categories/1/edit
  def edit
    authorize @category
  end

  # POST /categories
  def create
    @category = Category.new(category_params)
    authorize @category
    if @category.save
      redirect_to @category, notice: 'Category was successfully created.'
    else
      render :new
    end
  end

  # PATCH/PUT /categories/1
  def update
    authorize @category
    if @category.update(category_params)
      redirect_to @category, notice: 'Category was successfully updated.'
    else
      render :edit
    end
  end

  # DELETE /categories/1
  def destroy
    authorize @category
    @category.destroy
    redirect_to categories_url, notice: 'Category was successfully destroyed.'
  end

  private

  # Use callbacks to share common setup or constraints between actions.
  def set_category
    @category = Category.find(params[:id])
  end

  # Only allow a trusted parameter "white list" through.
  def category_params
    params.require(:category).permit(:name)
  end
end

application_policy.rb

class ApplicationPolicy
  attr_reader :user, :record

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

  def index?
    false
  end

  def create?
    create?
  end

  def new?
    create?
  end

  def update?
    false
  end

  def edit?
    update?
  end

  def destroy?
    false
  end

  class Scope
    attr_reader :user, :scope

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

    def resolve
      scope.all
    end
  end
end

Ive got Pundit included in my ApplicationController and rescue_from Pundit::NotAuthorizedError, with: :forbidden this set up there as well.

The authorization itself works, if I'm logged in with an admin account I have access to /categories/*. If I'm logged out I get the following: NoMethodError at /categories undefined methodadmin?' for nil:NilClass` While writing the question I think I found the problem- I guess Pundit looks for a User that is nil because I'm not logged in. What would the correct approach of solving this issue look like?

Best regards

benl96
  • 274
  • 3
  • 18

2 Answers2

1

The most common approach is to redirect users from pages that are not accessible by not logged in users. Just add a before action in your controller:

class CategoriesController < ApplicationController
  before_action :redirect_if_not_logged_in

  <...>

  private

  def redirect_if_not_logged_in
    redirect_to :home unless current_user
  end
end

(I assume here that you have current_user method which returns user instance or nil. Please change :home to wherever you want to redirect users)

P. Boro
  • 716
  • 3
  • 12
  • Thanks for the quick answer. That is exactly what I was looking for. Don't really know why I didn't come up with a redirect. – benl96 Oct 10 '18 at 13:58
0

There are multiple ways of achieving what you want.

  1. The most obvious (but kind of dirty) and straightforward-looking way of doing that would be to add a check for user presence in every condition:

    user && user.admin?

    It won't fail with nil error as the second part of the condition won't get executed. But it doesn't look very nice, right? Especially if you have to copy this over to all methods you have in CategoryPolicy.

  2. What you can do instead, is to make Pundit "think" that you passed a User, by creating a GuestUser class which responds with false to admin? method (https://en.wikipedia.org/wiki/Null_object_pattern):

    In object-oriented computer programming, a null object is an object with no referenced value or with defined neutral ("null") behavior. The null object design pattern describes the uses of such objects and their behavior (or lack thereof)

    And use it when user is nil. In practice, it will look like this:

    class ApplicationPolicy
      attr_reader :user, :record
    
      def initialize(user, record)
        @user = user || GuestUser.new
        @record = record
      end
    
      # ...
    end
    
    class GuestUser
      def admin?
        false
      end
    end
    

    This way you won't have to alter any of your other code, as the model you passed responds to the method which is expected by policy (admin?). You may want to define this GuestUser somewhere else (not in the policy file), depending if you want other parts of the app to reuse that behavior.

  3. You can also proceed with the redirect approach from P. Boro answer. It's less flexible in some sense but can totally work fine if you don't need anything besides redirecting all non-logged in users.

Airat Shigapov
  • 575
  • 4
  • 19