Search code examples
deviseruby-on-rails-4.2pundit

Allow non registered users to view content with pundit


I have having trouble allowing non-registered/non-logged in users to view the index and show pages for a blog section. I am using Pundit for authorization and realize that at the moment I have my policies set to not allow non-users to view any part of the blog section, but I have no idea how to work around that to have no policy for the index and show page.

My goal is to have the following:

Allow Admin and Editors to view, create, edit, and delete blogs

This portion works pefect

Allow registered users to view blogs

This portion works perfect

Allow non-registered/non-logged in users to view blogs

This part does not work

When I try to view the index page as a non-registered/non-logged in user, I will get an access denied flash message that comes out of my application controller, which is doing what it is supposed to be doing given the current policies.

So my question is: How do I modify my policies to allow non-registered/non-logged in users to view the index and show pages only?

Application Controller

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
  before_filter :configure_permitted_parameters, if: :devise_controller?

  private

    def user_not_authorized(exception)
      flash[:danger] = "Access denied. You are not authorized to view that page."
      redirect_to (request.referrer || root_path)
    end


protected

  def configure_permitted_parameters
    devise_parameter_sanitizer.permit(:sign_up) { |u| u.permit(:username, :email, :password, :password_confirmation, :remember_me) }
    devise_parameter_sanitizer.permit(:sign_in) { |u| u.permit(:username, :email, :password, :remember_me) }
    devise_parameter_sanitizer.permit(:account_update) {|u| u.permit(:username, :email, :password, :password_confirmation, :current_password)}
  end


end

Application Policy

class ApplicationPolicy
  attr_reader :user, :record

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

  def index?
    true
  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

Post Policy

class PostPolicy < ApplicationPolicy
  attr_reader :post

  class Scope < Scope
    def resolve
      if user&.admin?&.editor?&.user?
        scope.all
      else user != admin? || editor? || user?
        scope
      end
    end
  end

  def permitted_attributes
    if user.admin? || user.editor?
      [:title, :body, :image, :permalink, :description, :tag_list, :username]
    else
      [:title, :body, :image, :username]
    end
  end

  def index?
    true
  end

  def show?
    true
  end

  def new?
    user.admin? || user.editor?
  end

  def create?
    user.admin? || user.editor?
  end

  def update?
    user.admin? || user.editor?
  end

  def destroy?
    user.admin? || user.editor?
  end
end

Post Controller

class PostsController < ApplicationController
  before_action :set_post, only: [:show, :edit, :update, :destroy]
  after_action :verify_authorized, only: [:destroy]

  def index
    @meta_title = "Blog"
    @meta_description = "page description here"
    @posts = Post.all.order("created_at DESC").paginate(:page => params[:page], :per_page => 4)
  end

  def show
    @meta_title = @post.title
    @meta_description = @post.description
  end

  def new
    @meta_title = "Add New Blog"
    @meta_description ="Add a new blog."
    @post = Post.new
    authorize @post
  end

  def edit
    @meta_title = "Edit Blog"
    @meta_description ="Edit an existing blog."
    authorize @post
  end

  def create
    @post = Post.new
    @post.update_attributes(permitted_attributes(@post))
    @post.user = current_user if user_signed_in?

    authorize @post

    if @post.save
      redirect_to @post, notice: 'Post was successfully created.'
    else
      render :new
    end
  end

  def update
    @post = Post.find(params[:id])
    if @post.update_attributes(permitted_attributes(@post))
      authorize @post
      redirect_to @post, notice: 'Post was successfully updated.'
    else
      render :edit
    end
  end

  def destroy
    if @post.present?
      @post.destroy
      authorize @post
    else
      skip_authorization
    end

    redirect_to posts_url, notice: 'Post was successfully deleted.'
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_post
      @post = Post.find(params[:id])
    end

    # Only allow the white list through.
    def post_params
      params.require(:post).permit(policy(@post).permitted_attributes)
    end
end

I've seen a similar question asked Pundit policy_scoper error, but the solution suggested there does not seem to work in my case.


Solution

  • After much frustration, I was finally able to solve the issue. Big thanks go out to @Scott for helping get the controller and testing set up as they should be, and nearly getting the policies working.

    Turns out that the raise Pundit::NotAuthorizedError, "must be logged in" unless user in the initializer section of the Application Policy was not allowing non-logged-in users from accessing the index page (just like it's supposed to when you want a closed system...). Since my application is open for anyone to view in the index and show pages of the blog, I needed to remove that line.

    Once removed the application would then throw a undefined method admin?' for nil:NilClass for non-logged-in users trying to access the blog index page. This was solved by using the correct conventions of identifying users in Post Policy. For each def in the policy I had user.admin? || user.editor?. That needed to be changed to user&.admin? || user&.editor?.

    The code ended up as follows:

    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
    

    Post Policy

    class PostPolicy < ApplicationPolicy
    
      attr_reader :post
    
    
      class Scope < Scope
        def resolve
          if user&.admin? || user&.editor?
            scope.all
          else 
          end
        end
      end
    
      def permitted_attributes
        if user.admin? || user.editor?
          [:title, :body, :image, :permalink, :description, :tag_list, :username]
        else
          [:title, :body, :image, :username]
        end
      end
    
      def index?
        true
      end
    
      def show?
        true
      end
    
      def new?
        admin_or_editor
      end
    
      def create?
        admin_or_editor
      end
    
      def update?
        admin_or_editor
      end
    
      def destroy?
        admin_or_editor
      end
    
      private
    
      def admin_or_editor
        user&.admin? || user&.editor?
      end
    end
    

    Post Controller

    class PostsController < ApplicationController
      before_action :set_post, only: [:show, :edit, :update, :destroy]
      before_action :authenticate_user!, except: [:index, :show]
      after_action :verify_authorized, except: [:index, :show]
    
      def index
        @meta_title = "Blog"
        @meta_description = "blog description"
        @posts = Post.all.order("created_at DESC").paginate(:page => params[:page], :per_page => 4)
      end
    
      def show
        @meta_title = @post.title
        @meta_description = @post.description
      end
    
      def new
        @meta_title = "Add New Blog"
        @meta_description ="Add a new blog."
        @post = Post.new
        authorize @post
      end
    
      def edit
        @meta_title = "Edit Blog"
        @meta_description ="Edit an existing blog."
        authorize @post
      end
    
      def create
        @post = Post.new
        @post.update_attributes(permitted_attributes(@post))
        @post.user = current_user if user_signed_in?
    
        authorize @post
    
        if @post.save
          redirect_to @post, notice: 'Post was successfully created.'
        else
          render :new
        end
      end
    
      def update
        @post = Post.find(params[:id])
        authorize @post
        if @post.update_attributes(permitted_attributes(@post))
          redirect_to @post, notice: 'Post was successfully updated.'
        else
          render :edit
        end
      end
    
      def destroy
        if @post.present?
          @post.destroy
          authorize @post
        else
          skip_authorization
        end
    
        redirect_to posts_url, notice: 'Post was successfully deleted.'
      end
    
      private
      # Use callbacks to share common setup or constraints between actions.
      def set_post
        @post = Post.find(params[:id])
      end
    
      # Only allow the white list through.
      def post_params
        params.require(:post).permit(policy(@post).permitted_attributes)
      end
    end