0

I'm new to using Pundit with Rails and have a new policy for my Artist model that is working as I expect it to, but I'm not clear on a good way to refactor it to make it more DRY. Specifically, it seems that I'm calling authorize @artist way too many times in artists_controller.rb and that there's a lot of code duplication in my artist_policy.rb.

For context, an Artist has a name, such as "Claude Monet" and that's it.

Here's my artist_policy.rb: https://gist.github.com/leemcalilly/799d5f9136b92fcf92c6074e6a28bfdb

And, my application_policy.rb: https://gist.github.com/leemcalilly/09d37a42c6f2500f98be3f1518c945e9

And here's my artists_controller.rb: https://gist.github.com/leemcalilly/c0dd8f33416b002f3b4c9a7baf0a3a75

And, models/artist.rb: https://gist.github.com/leemcalilly/c190322af41f3e91739b53391d8b7834

Is the way I currently have it working ok because I'm clearing expressing each policy (in the same way that some duplicate code across integration tests is ok), or should I refactor this? If so, is there a standard way that people structure their Pundit policies that I'm missing?

Lee McAlilly
  • 9,084
  • 12
  • 60
  • 94

1 Answers1

4

It seems that I'm calling authorize @artist way too many times in artists_controller.rb

Honestly, I think what you have there is fine.

There are a few ways that you could try to be clever about this and "automatically call authorize" for each controller action, but (warning: opinion-based answer) from past experience I've found that such attempts to make it more DRY adds significant confusion. Especially when you end up writing some controller action(s) that don't need authorising, or need authorising in an unusual way.

There's a lot of code duplication in my artist_policy.rb

One step at time... Here's the original:

class ArtistPolicy < ApplicationPolicy
  attr_reader :user, :artist

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

  def create?
    if user.admin? || user.moderator? || user.contributor?
      true
    elsif user.banned?
      false
    end
  end

  def update?
    if user.admin? || user.moderator? || user.contributor? && user.id == @artist.user_id
      true
    elsif user.banned?
      false
    end
  end

  def destroy?
    if user.admin? || user.moderator? || user.contributor? && user.id == @artist.user_id
      true
    elsif user.banned?
      false
    end
  end
end

It's unnecessary to define your own initialize method like that, so long as you're happy to reference the more generic variable name: record, instead of artist (which should be defined in the ApplicationPolicy):

class ArtistPolicy < ApplicationPolicy
  def create?
    if user.admin? || user.moderator? || user.contributor?
      true
    elsif user.banned?
      false
    end
  end

  def update?
    if user.admin? || user.moderator? || user.contributor? && user.id == record.user_id
      true
    elsif user.banned?
      false
    end
  end

  def destroy?
    if user.admin? || user.moderator? || user.contributor? && user.id == record.user_id
      true
    elsif user.banned?
      false
    end
  end
end

Next, in situations like this, it's fine to reference one policy rule from another - so long as they apply equally to user types:

class ArtistPolicy < ApplicationPolicy
  def create?
    if user.admin? || user.moderator? || user.contributor?
      true
    elsif user.banned?
      false
    end
  end

  def update?
    if user.admin? || user.moderator? || user.contributor? && user.id == record.user_id
      true
    elsif user.banned?
      false
    end
  end

  def destroy?
    update?
  end
end

Next, note that the record.user_id is the logged in user, for the create action! So you can simplify this even further:

class ArtistPolicy < ApplicationPolicy
  def create?
    if user.admin? || user.moderator? || user.contributor? && user.id == record.user_id
      true
    elsif user.banned?
      false
    end
  end

  def update?
    create?
  end

  def destroy?
    create?
  end
end

And lastly, the logic in that method is actually little wrong. (You could have picked this up with tests...) If a user is an admin and they are banned, then you still presumably want this to return false, not true. With this in mind, we can fix + simplify the code again to:

class ArtistPolicy < ApplicationPolicy
  def create?
    return false if user.banned?
    user.admin? || user.moderator? || user.contributor? && user.id == record.user_id
  end

  def update?
    create?
  end

  def destroy?
    create?
  end
end
Tom Lord
  • 27,404
  • 4
  • 50
  • 77
  • You *could* simplify this even further, but I wouldn't bother unless you find this logic gets repeated a lot *elsewhere* in the application. For example, you could define a method: `User#admin_or_moderator?`, and you could also define a method in the `ApplicationPolicy`, e.g. `record_belongs_to_contributor?`. – Tom Lord May 17 '18 at 15:31
  • Thanks so much Tom! This is an incredibly helpful answer. Really appreciate you taking the time to walk me through it step-by-step. Learned a lot in that. – Lee McAlilly May 17 '18 at 19:58
  • You could also use `alias_method` now. https://github.com/varvet/pundit#just-plain-old-ruby – Vishal Nov 10 '20 at 16:00