如果after_initialize回调返回false,则返回nil对象

Dan*_*fan 12 activerecord ruby-on-rails callback rails-activerecord

我的Rails应用程序有许多形成层次结构的模型.例如:零售商>部门>产品类别>产品>审核.

业务要求是高权限用户可以与新的或现有的"普通"用户"共享"层次结构中的任何单个元素.如果没有与它们共享对象,普通用户无权查看(或执行任何其他操作)层次结构的任何级别中的任何对象.

共享过程包括选择共享是否授予对目标对象的只读,读取更新或完整CRUD的权限.

共享任何对象将授予该对象和层次结构中所有较低级别对象的R/O,R/W或CRUD权限,以及对该对象的所有直接祖先的R/O权限.对象集合有机增长,因此权限系统只需记录user_id,共享的object_id和共享的性质(R/O,CRUD等)即可.由于此层次结构中的对象总体一直在增长,因此在DB中为每个用户/对象组合创建显式权限记录是不切实际的.

相反,在用户请求周期开始时,ApplicationController收集所有权限记录(用户X对部门#5具有CRUD权限)并将其保存在内存中的哈希中.权限模型知道如何在传递任何对象时评估哈希值 - Permission.allow?(:show,Department#5)将返回true或false,具体取决于用户权限哈希的内容.

我们来看,例如,部门模型:

# app/models/department.rb
class Department < ActiveRecord::Base

  after_initialize :check_permission

  private

  def check_permission
    # some code that returns true or false 
  end

end
Run Code Online (Sandbox Code Playgroud)

check_permission方法返回true时,我想Department.first恢复数据库中的第一条记录,但是,如果check_permission返回false,我想返回nil.

现在,我有一个解决方案,默认范围触发权限检查,但这导致查询数量的2倍,对于具有大量对象的类,内存问题和时间/性能问题肯定会出现.

我的目标是使用after_initialize回调来预先允许对象.

然而,似乎after_initialize无法阻止原始对象被返回.它允许我重置对象属性的值,但不能免除它.

有谁知道如何实现这一目标?

编辑:

非常感谢到目前为止提供的所有答案和评论; 希望这个问题的扩展版本能够澄清事情.

Ben*_*enj 5

基本上,您需要在返回数据库查询结果之前检查访问权限(或权限).而且您正在尝试将此逻辑集成到您的模型中.

这是可能的,但不是您在问题中描述的设计.这是不干净的,直接在ActiveRecord的适配器方法来实现这一点(如first,all,last等...).你需要重新考虑你的设计.

(如果读数过多,请跳至'D')

您有多种选择,这些选择都取决于您的权限的定义方式.我们来看几个案例:

A.用户拥有他所拥有的部门列表,只有他可以访问它们

您可以简单地将其实现为与Active Record Associationshas_many/ belongs_to关联

B.用户和部门是独立的(换句话说:没有如前一种情况所述的所有权),并且可以为每个用户和每个部门单独设置权限.

再简单一次,您可以实现has_and_belongs_to_manyActive Record Associations的关联.您需要创建Web逻辑,以便应用程序的管理员可以添加/编辑/删除访问权限.

C.更复杂的情况:现有的授权库

大多数人会转向授权解决方案,如康康,专家其他

D.当这些授权库超出您的需求时(实际上,我在大多数项目中的情况),我发现通过rails scoping实现授权可以满足我的所有需求.

让我们通过一个简单的例子来看.我希望管理员能够访问整个数据库记录; 和常规用户只能访问status = open且仅在运营时间(例如上午8点至下午6点)的部门.我写了一个实现我的权限逻辑的范围

# Class definition
class Department
  scope :accessible_by -> (user) do
    # admin user have all access, always
    if user.is_admin?
      all
    # Regular user can access only 'open' departments, and only
    # if their request is done between 8am and 6pm
    elsif Time.now.hour >= 8 and Time.now.hour <= 18
      where status: 'open'
    # Fallback to return ActiveRecord empty result set
    else
      none
    end
  end
end

# Fetching without association
Department.accessible_by(current_user)

# Fetching through association
Building.find(5).departments.accessible_by(current_user)
Run Code Online (Sandbox Code Playgroud)

定义范围迫使我们在代码中的任何地方使用它.你可以想到"忘记"通过范围并直接访问模型的风险(即写作Department.all而不是写作Department.accessible_by(current_user)).因此,您必须在规范中(在控制器或功能级别)严格测试您的权限.

注意在此示例中,我们不会nil在权限失败时(如您在问题中提到的那样)返回,而是返回空结果集.通常更好,因此您保持ActiveRecord方法链接功能.但您也可以提出异常并从控制器中拯救它,然后重定向到"未授权"页面.


dho*_*uty 1

这不是after_initialize回调的用途。相反,您可以定义一个执行相同操作的方法。例如,将其放入您的Department模型中,它应该达到您正在寻找的结果:

def self.get_first
  check_permission ? first : nil
end
Run Code Online (Sandbox Code Playgroud)

更新

我不太确定这样的方法有多安全,但您可以覆盖该all方法,因为其他查询方法都基于它。

class Department < ActiveRecord::Base
  def self.all
    check_permission ? super : super.none
  end

  private

  def self.check_permission
    # some code that returns true or false 
  end
end
Run Code Online (Sandbox Code Playgroud)

不过,您最好使用一些授权框架。

更新2

多考虑一下这个问题,我强烈建议使用不同的方法。您确实不应该像这样重写方法,all因为肯定会产生意想不到的副作用。

一个实用的替代方案是在和has_and_belongs_to_many之间创建关系。设置方法如下:DepartmentUser

用户.rb

class User < ActiveRecord::Base
  has_and_belongs_to_many :departments
  ...
end
Run Code Online (Sandbox Code Playgroud)

部门.rb

class Department < ActiveRecord::Base
  has_and_belongs_to_many :users
  ...
end
Run Code Online (Sandbox Code Playgroud)

然后在终端中运行这些命令:

rails g migration CreateJoinTableDepartmentsUsers departments users
rake db:migrate
Run Code Online (Sandbox Code Playgroud)

现在您可以使用以下命令将用户添加到部门@department.users << @user,或将部门添加到具有 的用户@user.departments << @department。这应该可以实现您正在寻找的功能。

@user.departments将仅返回该用户的部门,@user.departments.first将返回该用户的第一个部门或nil没有任何@user.departments.find(1)部门,并且仅当该部门属于该用户时才返回相应的部门,否则抛出异常。