Rails 7中如何通过Pundit Scope实现多对多关联的员工权限控制?
问题:如何用Pundit Scope实现公司员工列表的权限控制
模型间多对多关联如下:
# company.rb has_many :employees, dependent: :destroy has_many :users, through: :employees # employee.rb belongs_to :company # user ...
Employee有published(已发布)和未发布两种状态,需求为:
- 普通用户在公司员工列表页仅能查看已发布的Employee
- 该公司的员工可查看全部Employee
目前在employees_controller#index中的实现逻辑:
def index @employees = if current_user.is_employee?(@company) @company.employees.all else @company.employees.select { |employee| employee.published == true } end end
希望通过Pundit的Scope实现相同逻辑,但无法从Scope中获取公司信息,尝试的伪代码如下:
# EmployeePolicy class Scope def initialize(user, scope) @user = user @scope = scope end def resolve if user.is_employee?(...) # 无法从scope中获取公司信息 scope.all else scope.where(published: true) end end end
不想使用繁琐的join方法,求其他简洁的实现方式。
解决方案
这里提供两种实用的实现思路:
方法一:调用Scope时传入公司实例
Pundit支持在初始化Scope时传递额外上下文参数,你可以修改Scope的初始化方法,直接传入目标公司实例:
- 修改
EmployeePolicy的Scope类:
# app/policies/employee_policy.rb class EmployeePolicy < ApplicationPolicy class Scope attr_reader :user, :scope, :company def initialize(user, scope, company) @user = user @scope = scope @company = company end def resolve if user.is_employee?(company) scope.all else scope.where(published: true) end end end end
- 在控制器中调用
policy_scope时传入公司实例:
def index @company = Company.find(params[:company_id]) @employees = policy_scope(@company.employees, company: @company) end
注:Rails 6+可直接通过
policy_scope第三个参数传递额外参数;旧版本可使用policy_scope(@company.employees, context: { company: @company }),再在Scope初始化时从context中提取。
方法二:从Scope中推导公司信息
如果不想额外传递参数,也可以从当前Scope的ActiveRecord关系中提取公司信息——因为你的Scope是@company.employees,本质上是带有company_id条件的查询集合,可直接获取对应的公司:
# app/policies/employee_policy.rb class EmployeePolicy < ApplicationPolicy class Scope attr_reader :user, :scope def initialize(user, scope) @user = user @scope = scope end def resolve # 从scope的查询条件中提取company_id company_id = scope.where_values.detect do |cond| cond.is_a?(Arel::Nodes::Equality) && cond.left.name == 'company_id' end&.right&.value company = Company.find(company_id) if company_id if company && user.is_employee?(company) scope.all else scope.where(published: true) end end end end
控制器中正常调用即可:
def index @company = Company.find(params[:company_id]) @employees = policy_scope(@company.employees) end
提示:原控制器中的
select方法会加载所有员工再过滤,建议改成where查询,提升性能:@company.employees.where(published: true)
内容的提问来源于stack exchange,提问作者Anders
相关产品推荐
相关产品推荐

