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
无法阻止原始对象被返回。它允许我重置对象属性的值,但不能免除它。
有人知道如何实现这个目标吗?
编辑:
非常感谢到目前为止提供的所有答案和评论;希望这个问题的扩展版能够澄清事情。
答案 0 :(得分:5)
基本上,您需要在返回数据库查询结果之前检查访问权限(或权限)。而且您正在尝试将此逻辑集成到您的模型中。
这是可能的,但不是您在问题中描述的设计。在ActiveRecord适配器方法(例如first
,all
,last
等)中直接实现它并不干净。你需要重新考虑你的设计。
(跳到点&#39; D&#39;如果这是太多阅读)
您有多种选择,这些选择都取决于您的权限的定义方式。让我们看几个案例:
A. 用户拥有他拥有的部门列表,只有他可以访问
您可以简单地将其作为与Active Record Associations的has_many
/ belongs_to
关联实现
B. 用户和部门是独立的(换句话说:没有所有权,如前一种情况所述),并且可以为每个用户和每个用户单独设置权限部门。
再一次,您可以与Active Record Associations实施has_and_belongs_to_many
关联。您需要创建Web逻辑,以便应用程序的管理员可以添加/编辑/删除访问权限。
C。更复杂的案例:现有的授权库
大多数人会转向授权解决方案,例如cancan,pundit或other
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_many
和Department
之间建立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