我的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。
让我们以Department模型为例:
# 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
。
现在,我有一个解决方案,默认作用域触发权限检查,但这导致查询的数量增加了两倍,并且对于具有大量对象的类,内存问题和时间/性能问题肯定会出现。
我的目标是使用after_initialize
回调来预先授权对象。
但是,看起来after_initialize
无法阻止返回原始对象。它确实允许我重置对象的属性值,但不能省去它。
有人知道怎么做到这一点吗?
编辑:
非常感谢到目前为止提供的所有答案和评论;希望这个问题的扩展版本能澄清一些事情。
发布于 2015-08-21 14:31:54
基本上,在返回数据库查询结果之前,您需要检查访问权限。您正在尝试将此逻辑集成到您的模型中。
这是可能的,但不是使用您在问题中描述的设计。直接在ActiveRecord适配器方法(如first
、all
、last
等)中实现这一点是不干净的。你需要重新考虑你的设计。
(如果这篇文章读得太多,跳到'D‘点)
您有几种选择,它们都取决于您权限的定义方式。让我们来看几个案例:
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实现授权可以满足我的所有需求。
让我们通过一个简单的例子来看一下。我希望管理员能够访问整个数据库记录;普通用户只能访问状态=打开的部门,并且只能在操作时间(例如上午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方法链接功能。但是你也可以引发一个异常并从你的控制器中拯救它,然后重定向到一个“未授权”的页面。
发布于 2015-08-19 00:34:57
这不是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
这样的方法,因为肯定会有意想不到的副作用。
一种实用的替代方案是在Department
和User
之间创建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 << @user
的部门,或将部门添加到具有@user.departments << @department
的用户。这应该可以实现您正在寻找的功能。
@user.departments
将只返回该用户的部门,@user.departments.first
将返回该用户的第一个部门,如果没有部门,则返回nil
,而@user.departments.find(1)
仅返回属于该用户的相应部门,否则将抛出异常。
发布于 2015-08-23 19:24:42
如果check权限为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
https://stackoverflow.com/questions/32077572
复制相似问题