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

时间:2015-08-18 16:14:39

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

My 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

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

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

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

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

有人知道如何实现这个目标吗?

编辑:

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

3 个答案:

答案 0 :(得分:5)

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

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

(跳到点&#39; D&#39;如果这是太多阅读)

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

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

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

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

再一次,您可以与Active Record Associations实施has_and_belongs_to_many关联。您需要创建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)

定义范围迫使我们在代码中的任何地方使用它。你可以想到忘记&#34;忘记&#34;通过范围并直接访问模型(即写Department.all而不是Department.accessible_by(current_user))。这就是为什么你必须在规范中(在控制器或功能级别)稳固地测试你的权限的原因。

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

答案 1 :(得分:1)

这不是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那样重写方法,因为肯定会出现意想不到的副作用。

实际的替代方案是在has_and_belongs_to_manyDepartment之间建立User关系。以下是设置方法:

<强> 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 << @user的部门,或将部门添加到@user.departments << @department的用户。这应该实现您正在寻找的功能。

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

答案 2 :(得分:1)

如果检查权限为false,则可以使用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