How to use pundit scopes?
Asked Answered
T

2

22

I have just made the switch to Pundit from CanCan. I am unsure about a couple of things, and how Pundit is best used.

For example:

If you have a resource that can have multiple parent objects, for instance lets say a Goal belongs to a student and instructor. Therefor, a student can have many goals and an instructor can have many goals. In a controller index action you might do:

if params[:student_id].present?
  @account = Student.find(params[:student_id])
  @goals = @account.goals
elsif params[:instructor_id].present?
  @account Instructor.find(params[:instructor_id])
  @goals = @account.goals
end

params are not usable inside policies, so the logic needs to be done here. I think. For what I can tell, if you skip the policy_scope you will get an unauthorized error when viewing the index page for goals.

Would you:

@goals = policy_scope(@account.goals)

OR

@goals = policy_scope(Goal.scoped).where( account_id: @account.id)

What happens when you throw a bunch of includes in the mix?

  @example = policy_scoped(@school.courses.includes(:account => :user, :teacher ))

Or when needed to order...is this correct?

 policy_scope(Issue.scoped).order("created_at desc")

When using scopes: What is :scope here? Is :scope an instance of the model being evaluated? I've tried accessing its attributes via :scope, but didn't work.

  class Scope < Struct.new(:user, :scope)
Tuchun answered 31/1, 2014 at 0:47 Comment(0)
P
31

Reading through this from a security perspective I can see a couple things that bear mentioning. For example, if you are allowing users to specify the student_id and instructor_id param fields, what's to stop them from passing in an ID for someone other than themselves? You don't ever want to let a user specify who they are, especially when you are basing policies on the users type.

For starters, I would implement Devise and add an additional boolean field called instructor that would be true when the user was an instructor but default to false for students.

Then your Users would automatically have an instructor? method defined, which will return true if the value in the instructor column is true.

You could then add a helper for students:

def student?
  !instructor?
end

Now using Devise (which gives us access to a current_user variable) we can do things like current_user.instructor? which will return true if they are an instructor.

Now on to the policy itself. I just started using Pundit a few weeks ago, but this is what I'd do in your situation:

class GoalPolicy < ApplicationPolicy
  class Scope < GoalPolicy
    attr_reader :user, :scope

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

    def resolve
      @scope.where(user: @user)
    end
  end
end

Then your (I'm assuming GoalsController class and index method) method can look like:

def index
  policy_scope(Goal) # To answer your question, Goal is the scope
end

If you wanted to order you could also do

def index
  policy_scope(Goal).order(:created_at)
end

I just realized that you asked this question half a year ago, but hey! Maybe it'll answer some questions other people have and maybe I'll get some feedback on my own budding Pundit skills.

Pathfinder answered 20/8, 2014 at 0:9 Comment(1)
it would probably be better to use getter methods to fetch user and scope inside resolve, also make them part of the private interface.Moua
S
0

Follow @Saul's recommendation on adding devise or other means of authentication.

Then you'll want to do this (Entity is Goal in your case):

@entities = policy_scope(Entity).where(...)

In entity_policy.rb:

class EntityPolicy < ApplicationPolicy
  class Scope < ApplicationPolicy::Scope
    def resolve
      # Here you have access to `scope == Entity` and `user == current_user`
      scope.where(entity: user.entity)
    end
  end
end

You might wonder why is where duplicated. The answer is (and here is the answer to your question): they serve different purposes. Although currently they are identical, but consider this:

You now have an admin user who has access to everything. Your policy changes:

class EntityPolicy < ApplicationPolicy
  class Scope < ApplicationPolicy::Scope
    def resolve
      if user.admin?
        scope.all
      else
        scope.where(entity: user.entity)
      end
    end
  end
end

If you have organizations with goals and the following restful endpoint:

/organizations/:organization_id/goals

When a user visits /organizations/1/goals you want to make sure the user is only allowed access to goals when the user is part of the organization:

scope.where(organization: user.organization) in the policy

And you also want to make sure that when an admin visits they can only see the goals related to that organization:

policy_scope(Goal).where(organization_id: params[:organization_id]) in the controller.

Shenika answered 17/2, 2021 at 11:28 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.