Home > Enterprise >  Why does a Pundit policy for one controller is affected by another?
Why does a Pundit policy for one controller is affected by another?

Time:11-06

I am perhaps misunderstanding Pundit policies but I am facing an issue where the UserPolicy is clashing with the SongPolicy.

What happens if that a statement in UserPolicy is being asserted ignoring what's written in SongPolicy:

Pundit::NotAuthorizedError in SongsController#edit 

not allowed to edit? this User

  def authorization
    authorize Current.user
  end

The issue emerged after introducing a new role for users but I believe that I probably haven't configured it right and for some reason only UserPolicy is looked at for asserting authorization in the SongsController?

I have two controllers that check for the user to be signed in (require_user_logged_in) and another to check on Pundit's policies (authorization):

class UsersController < ApplicationController
  before_action :require_user_logged_in!, :authorization, :turbo_frame_check
  # Actions were removed for brevity.
end
class SongsController < ApplicationController
  before_action :require_user_logged_in!, :authorization, except: [:index, :show]
  # Actions were removed for brevity.
end

The authorization methods looks like this:

  def authorization
    authorize Current.user
  end

There's an application-level policy class, ApplicationPolicy:

# frozen_string_literal: true

class ApplicationPolicy
  attr_reader :user, :params, :record

  # Allows params to be part of policies.
  def initialize(context, record)
    if context.is_a?(Hash)
      @user = context[:user]
      @params = context[:params]
    else
      @user = context
      @params = {}
    end
    @record = record
  end

  def index?
    false
  end

  def show?
    false
  end

  def create?
    false
  end

  def new?
    create?
  end

  def update?
    false
  end

  def edit?
    update?
  end

  def destroy?
    false
  end

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

    def resolve
      raise NotImplementedError, "You must define #resolve in #{self.class}"
    end

    private

    attr_reader :user, :scope
  end
end

The UserPolicy to protect user views:

class UserPolicy < ApplicationPolicy
  class Scope < Scope
  end

  def index?
    user.has_role?(:admin)
  end

  def show?
    # Access if admin or the same user only.
    user.has_role?(:admin) || is_same_user?
  end

  def create?
    index?
  end

  def new?
    create?
  end

  def update?
    index? || is_same_user?
  end

  def edit?
    update? # This is called when accessing a view for `SongsController`.
  end

  def destroy?
    index? || is_same_user? 
  end

  def delete?
    destroy?
  end

  private

  # Used to keep a user from editing another.
  # Admins should be allowed to edit all users.
  def is_same_user?
    # Check if user being accessed is the one being logged in.
    params[:id].to_s == Current.user.username.to_s
  end
end

And the SongPolicy:

class SongPolicy < ApplicationPolicy
  class Scope < Scope
  end

  def index?
  end

  def show?
  end

  def create?
    user.has_role?(:admin) || user.has_role?(:collaborator) # This is ignored.
  end

  def new?
    create?
  end

  def update?
    create?
  end

  def edit?
    create?
  end

  def destroy?
    user.has_role?(:admin)
  end

  def delete?
    destroy?
  end
end

Not sure what else to try here, I'm sure I'm missing something, if someone with more knowledge of Pundit could let me know their thoughts on why a statement for one policy can leak into another, it would be really helpful.

CodePudding user response:

You're calling authorize on the current user, which is a User, so Pundit is going to infer the UserPolicy policy. It won't automatically infer the SongPolicy policy unless you provide a Song record, even if you're in the SongController controller.

If you want to use a different policy, you'll need to provide it via authorize(policy_class:).

authorize Current.user, policy_class: SongPolicy

Implicit authorization like this is generally a code smell. Ideally, you should be explicitly authorizing the current Song record(s) against the current user context.

  • Related