【问题标题】:Return nil object if after_initialize callback returns false如果 after_initialize 回调返回 false,则返回 nil 对象
【发布时间】:2015-11-11 16:40:25
【问题描述】:

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

业务需求是高权限用户可以与新的或现有的“普通”用户“共享”层次结构中的任何单个元素。在没有与他们共享对象的情况下,普通用户无权查看(或执行其他任何操作)层次结构中任何级别的任何对象。

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

共享任何对象会授予该对象和层次结构中所有较低级别对象的 R/O、R/W 或 CRUD 权限,以及授予对象的所有直接祖先的 R/O 权限。对象集合有机地增长,因此权限系统仅通过记录 user_id、共享的 object_id 和共享的性质(R/O、CRUD 等)来工作。由于此层次结构中的对象数量一直在增长,因此在数据库中为每个用户/对象组合创建明确的权限记录是不切实际的。

相反,在用户请求周期开始时,ApplicationController 收集所有权限记录(用户 X 对部门 #5 具有 CRUD 权限)并将它们保存在内存中的哈希中。 Permissions 模型知道在将任何对象传递给它时如何评估散列 - 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

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

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

我的目标是使用after_initialize 回调来预授权对象。

然而,after_initialize 似乎无法阻止返回原始对象。它确实允许我重置对象属性的值,但不能放弃它。

有人知道如何实现吗?

编辑:

非常感谢到目前为止提供的所有答案和 cmets;希望这个问题的扩展版本可以澄清问题。

【问题讨论】:

  • after_initialize 没有按照您的预期去做。它在那里执行一些副作用。它不会给你任何回报。您的需求/要求到底是什么?
  • 我认为您不应该尝试这样做,我认为没有权限系统可以在该级别进行拦截,因为这不是您应该执行的级别。相反,您可能应该在 User 和 Department 之间建立关系。 check_permission 到底在检查什么?
  • 用户和部门之间没有直接关系。有问题的 Rails 应用程序具有对象层次结构(例如租户、部门、产品类别、产品等)。可以在任何级别向特定对象授予权限;所有子级都继承完全权限,所有上级继承只读权限。 “check_permission”方法调用一个 ActiveModel 模型来找出用户允许的特定对象(部门 #1 等)。

标签: ruby-on-rails activerecord callback rails-activerecord


【解决方案1】:

如果检查权限为假,您可以使用 before_create 回调来停止创建记录。只需在 check_permission 过滤器中返回 false 并不会创建记录。

 class Department < ActiveRecord::Base

  before_create :check_permission

  private

  def check_permission
    # return false if permission is not allowed 
  end

end

【讨论】:

  • 对不起,这是不可能的,权限系统应该在验证和创建之前就在实例化步骤上工作
  • 事实上,在创建或更新时强制执行权限很简单,您的解决方案就是我的做法(因此您是正确的)。但是,最初的问题是专门关于从数据库中读取数据的,受权限限制。
  • 我认为您应该检查您希望用户访问部门的位置并使用某种授权机制,如 CanCan 或编写您自己的。我看到您在评论中写道,如果用户没有权限让我们说 dep3 那么 Department.find(3) 应该返回 nil ,因为 find 不知道访问者是谁。您必须限制用户不访问此记录。你应该使用CanCan,它有一个方法authorize!如果该用户在控制器级别没有对该记录的权限,则可以限制该用户查看该特定记录
  • @MuhammadJunaid - 实际上,我从 CanCanCan 开始,不得不放弃它。它导致在每个请求周期都会触发数百个查询(因为我们的系统授予权限的方式)。在 Ability 模型中,我们实例化了一个具有计算明确权限的 Ability 对象,该对象对用户有权访问的每个对象都有明确的权限——因此会有很多查询。 CanCan(Can) 通常是一个很好的解决方案,但不适用于这种情况(非常不幸)。
【解决方案2】:

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

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

(如果阅读过多,请跳至点“D”)

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

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

您可以简单地将其实现为 has_many/belongs_toActive Record Associations 的关联

B. 用户和部门是独立的(换句话说:没有前面案例中描述的所有权)并且可以为每个用户和每个用户单独设置权限部门。

同样,您可以实现has_and_belongs_to_manyActive Record Associations 的关联。您将需要创建 Web 逻辑,以便您的应用程序的管理员可以添加/编辑/删除访问权限。

C.更复杂的案例:现有的授权库

大多数人会转向授权解决方案,例如cancanpunditother

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)

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

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

【讨论】:

  • 感谢您对此的看法。我同意您关于“零”与“空结果”的观点。然而,这个 Rails 应用程序中的权限设置要复杂得多。在用户和允许的对象之间不可能有直接的 RDBMS 类型的关系。 “部门”是层次结构中的多个级别之一。例如,层次结构可能看起来像租户 > 部门 > 类别 > 产品 > 评论。层次结构中的任何对象都可以被授予权限;然后,所有子对象都从被授权者那里继承所有权限,直接父对象变为只读。
  • 您的具体答案是在我的“D”点中详细说明,它不依赖于数据库关系,因此我相信您没有阅读/理解它。请记住,这个问题和答案很容易通过谷歌获得,这就是为什么我通过解释存在的不同解决方案来概括我的答案。请检查更多;这个解决方案没有限制,你可以通过这种方式实现非常复杂的权限逻辑。如果你需要一个框架,还要检查点“C”。
  • 实际上,这是我在创建问题之前提出的解决方案。我的做法略有不同:我要求权限模型给我一个 ID 列表(作为一个数组),用于所有被授予权限的对象,然后基于它的范围。这种方法的问题在于它会导致 2X 查询。我认为我觉得有必要创建一个新的回调并将它提供给 Rails 霸主。 :)
  • 你不能通过使用嵌套查询来避免 2X 查询吗?更具体地说,如果一个查询必须将一系列 ID 返回给另一个查询。
  • 你提供的细节越多,我就越明白你的问题并没有完全描述整个画面。向您提出的每个解决方案都需要披露更多详细信息。你为什么不解释原始帖子中的所有内容?祝你好运,但我会在这里放弃。
【解决方案3】:

这不是 after_initialize 回调的用途。相反,您可以只定义一个做同样事情的方法。例如,把它放在你的 Department 模型中,它应该会达到你想要的结果:

def self.get_first
  check_permission ? first : nil
end

更新

我不确定这样的事情有多安全,但您可以重写 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

不过,你最好还是使用一些授权框架。

更新 2

再考虑一下,我强烈建议使用不同的方法。你真的不应该覆盖像all 这样的方法,因为肯定会有意想不到的副作用。

一种实用的替代方法是在DepartmentUser 之间创建has_and_belongs_to_many 关系。以下是您的设置方式:

user.rb

class User < ActiveRecord::Base
  has_and_belongs_to_many :departments
  ...
end

department.rb

class Department < ActiveRecord::Base
  has_and_belongs_to_many :users
  ...
end

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

rails g migration CreateJoinTableDepartmentsUsers departments users
rake db:migrate

现在您可以使用@department.users &lt;&lt; @user 将用户添加到部门,或使用@user.departments &lt;&lt; @department 将部门添加到用户。这应该可以实现您正在寻找的功能。

@user.departments 将只返回该用户的部门,@user.departments.first 将返回该用户的第一个部门或nil,如果它没有任何部门,@user.departments.find(1) 将只返回相应的部门,如果它属于给用户,否则抛出异常。

【讨论】:

  • 我使用 Department.first 作为使用通用记录检索命令的示例。我想要 Department.all 等的相同功能。有什么想法吗?
  • 不幸的是,这不起作用。 check_permission 是一个实例方法。我尝试了这个,但成功有限; def self.all super.collect { |x| x.check_permission}.exclude?(false) ? super : super.none end Department.first,Department.all 工作。 Department.find(1) 没有。
  • 好的,那么您能否更清楚地解释您正在寻找的功能?您是否希望 all 仅返回 check_permission 返回 true 的部门?你想让first 返回check_permission 为真的第一个部门吗?等等。此外,Department.find 不像大多数其他查询方法那样基于 all。相反,你必须这样做Department.all.find
  • 是的,你理解正确。假设我有三个部门 [1,2,3],并且用户 A 可以访问 1 和 2,但不能访问 3。 Department.all 应该返回一个 AR::Relation,其中包含部门 1 和 2; Department.first 应该给部门 1,Department.last 应该给部门 2,Department.find(3) 应该返回 nil。 Department.find(1) 和 Department.find(2) 应该返回请求的部门。
  • Re: 更新 2 – 这里的问题是对对象的隐式权限可以从对父级(或祖父级)的显式权限派生。这意味着每次创建子对象时,我们都必须生成大量不可预测的权限记录。权限表将成为系统中最大的表。每次权限发生变化,或者对象被创建或销毁时,都会有大量的权限记录需要更新。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-11-19
  • 2012-11-21
  • 2018-10-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多