1
votes

I am using the Pundit gem to role scope my application and have found some difficulties merging ActiveRecord queries. I am working with Rails 5.1.4.

See I have three models, lets say Classroom, Student and Exam with:

  • Classroom has_many :students & has_many :exams, through: :students
  • Student belongs_to: :classroom & has_many :exams
  • Exam belongs_to: :student

Each have a policy scope with varying queries that I want to merge together to ensure a user has access to the Exam model that's within the Student scope, that's also within the Classroom scope.

Pundit let us do that by passing whatever ActiveRecord relationship from the controller to the Scope class inside a scope variable.

With that in mind, my scope classes appear as follow:

class ClassroomPolicy::Scope
  def resolve
    if user.reviewer?
      # A reviewer can only see specific classes
      scope.where(type: :exam).where(reviewer_id: user.id)
    end
  end
end

class StudentPolicy::Scope
  def resolve
    if user.reviewer?
      # A reviewer can only see students from classes he's allowed in
      scope.joins(:classroom).merge(policy_scope(Classroom))
    end
  end
end

class ExamPolicy::Scope
  def resolve
    if user.reviewer?
      # A reviewer can only see/grade exams from students he supervised
      scope.joins(:student).merge(policy_scope(Student))
    end
  end
end

All works well, if I do policy_scope(Exam), I do get all exams from students who where inside a reviewer's classroom.

The issue arises when passing an already joined query such as policy_scope(Classroom.find(params[:classroom_id]).exams).
ActiveRecord effectively generates a strange query which is unusable because both a_classroom.exams and our scope do a join on :student.

In addition, removing joins from our exam scope, giving us scope.merge(policy_scope(Student)), makes our call policy_scope(Classroom.find(params[:classroom_id]).exams) work while breaking policy_scope(Exam).

Is there a way to work around this so that both use cases work ?
Or is my approach to Pundit wrong ?
Is this the limit of ActiveRecord ?
Any help regarding this would be appreciated !

1

1 Answers

2
votes

policy_scope is intended to be used from Controllers. For e.g. refer the examples given in section https://github.com/varvet/pundit#scopes and you should find that this method is being demonstrated to be used either from controllers or views.

However as per your code you are trying to use policy_scope in your scope classes (i.e classes extending ApplicationPolicy::Scope class) themselves. And I think I am understanding the intention behind doing so to reuse the already defined scopes. But in my opinion that's a wrong way to think about it.

Once inside YourScopeClass#resolve method you should use normal querying mechanism like you do elsewhere in your Rails application.

I don't have a complete know-how of the fields you have in your model. But based on the comments you have added in resolve method for a reviewer, you should do something like:

class StudentPolicy::Scope
  def resolve
    if user.reviewer?
      # A reviewer can only see students from classes he's allowed in
      arel = scope.joins(:classroom)
      arel = arel.where(classrooms: { type: : exam, reviewer_id: user.id })
      arel
    end
  end
end

Then use it from controller like policy_scope(Student)

Similarly

class ExamPolicy::Scope
  def resolve
    if user.reviewer?
      # A reviewer can only see/grade exams from students he supervised
      arel = arel.joins(student: [ :classroom ])
      arel = arel.where(classrooms: { type: : exam, reviewer_id: user.id })
      arel
    end
  end
end

Then use it from controller like policy_scope(Exam)

And if you want to reuse some queries then try creating some generic methods which can be passed the query represented by Pundit Policy Scope.

For e.g.

class MyClass
  class << self
    def accessible_students(arel:, user_id:)
      arel.where(classrooms: { type: : exam, reviewer_id: user_id })
    end
  end
end

class StudentPolicy::Scope
  def resolve
    if user.reviewer?
      # A reviewer can only see students from classes he's allowed in

      arel = scope.joins(:classroom)
      arel = MyClass.accessible_students(arel: arel, user_id: user.id)
      arel
    end
  end
end

class ExamPolicy::Scope
  def resolve
    if user.reviewer?
      # A reviewer can only see/grade exams from students he supervised
      arel = arel.joins(student: [ :classroom ])
      arel = MyClass.accessible_students(arel: arel, user_id: user.id)
      arel
    end
  end
end

I hope you find this helpful.