用户可以“阅读”的范围记录的常用方法是什么?

时间:2012-06-17 19:02:35

标签: ruby-on-rails ruby-on-rails-3 performance activerecord authorization

我正在使用Ruby on Rails 3.2.2并且我想知道当必须检查用户是否具有“读取”记录“列表”中存在的记录的适当授权时,什么是常用方法。也就是说,此时我有以下内容:

class Article < ActiveRecord::Base
  def readable_by_user?(user)
    # Implementation of multiple authorization checks that are not easy to
    # translate into an SQL query (at database level, it executes a bunch of
    # "separate" / "different" SQL queries).

    ... # return 'true' or 'false'
  end
end

通过使用上面的代码,我可以对单个文章对象执行授权检查:

@article.readable_by_user?(@current_user)

然而,当我想通过检索恰好10个对象时(通常在我的控制器index动作中)做出类似下面的事情

Article.readable_by_user(@current_user).search(...).paginate(..., :per_page => 10)

我仍然必须对每个对象执行授权检查。那么, 我可以对记录的“列表” (一组Article个对象) 执行授权检查“智能”/“高性能”方式? 也就是说,例如,我应该加载Article.all(可能通过创建数据排序,将SQL查询限制为10条记录,...... )然后迭代每个对象,以执行授权检查?或者我应该做一些不同的东西(可能有一些SQL查询技巧,一些Ruby on Rails工具或其他东西)?

@Matzi回答

后更新

我尝试“手动”检索用户可读的文章,例如使用find_each方法:

# Note: This method is intended to be used as a "scope" method
#
#   Article.readable_by_user(@current_user).search(...).paginate(..., :per_page => 10)
#
def self.readable_by_user(user, n = 10)
  readable_article_ids = []

  Article.find_each(:batch_size => 1000) do |article|
    readable_article_ids << article.id if article.readable_by_user?(user)

    # Breaks the block when 10 articles have passed the readable authorization 
    # check.
    break if readable_article_ids.size == n
  end

  where("articles.id IN (?)", readable_article_ids)
end

此时,上面的代码是我能想到的最“高效的妥协”,即使它有一些陷阱:它“”检索到的对象的数量限制为给定的数量给定id s的记录数(上例中默认为10条记录);实际上,它“确实”不会检索用户可读的所有对象,因为当您尝试进一步确定相关的ActiveRecord::Relation“其中”/“的范围时”{{1使用范围方法(例如,当你还要通过readable_by_user添加另一个SQL查询子句来搜索文章时),它会记录限制为那些title(那个是,它将“检出”和“可限制”对象的数量“限制”/“限制”为前10个,而在where("articles.id IN (?)", readable_article_ids)搜索时,将忽略用户可读的所有其他文章。为了使title方法能够正确使用其他范围方法,问题的解决方案可能是 readable_by_user块,以便加载所有可读文章,但是当存在大量记录时,由于性能原因而没有好处(也许,另一种解决方案可能是存储用户可读的所有文章break,但我认为不是解决问题的常用/简单解决方案。)

所以, 有一些方法可以用高性能和“真正”正确的方式完成我想做的事情(也许,通过改变上述方法)?

5 个答案:

答案 0 :(得分:3)

这取决于您的readable_by_user功能。如果很容易转换成SQL,那么它就是前进的方向。如果它比那更复杂,那么你很可能必须手动进行检查。

更新: 为了阐明为可读列表创建SQL查询的要点,我提供了一个示例。 假设文章对给定用户的可读性取决于以下内容:

  • 用户自己的文章(SELECT a.user == ? FROM Articles a WHERE a.id = ?
  • 该文章向所有人开放(SELECT a.state == 0 FROM Articles a WHERE a.user = ?
  • 该用户是可以访问文章的组的成员

SQL:

SELECT max(g.rights) > 64
FROM Groups g 
JOIN Groups_users gu on g.id = ug.group_id
WHERE gu.id = ?
  • 用户被分配到给定的文章

SQL:

SELECT 1
FROM Articles_users au
WHERE au.article_id = ? AND au.user_id = ?

这些可以在以下查询中进行总结:

def articles_for_user(user) 
  Articles.find_by_sql(["
    SELECT a.*
    FROM Articles a
    LEFT OUTER JOIN Articles_users au on au.article_id = a.id and au.user_id = ?
    WHERE a.user_id = ? 
       OR au.user_id = ?
       OR 64 <= (SELECT max(g.rights) 
                 FROM Groups g 
                 JOIN Groups_users gu on g.id = ug.group_id
                 WHERE gu.id = ?)
  ", user.id, user.id, user.id, user.id])
end

这确实是一个复杂的查询,但却是最有效的解决方案。数据库应该做数据库的东西,如果你只使用SQL查询和一些逻辑来评估你的readable_bu_user,那么你可以将它翻译成一个纯SQL查询。

答案 1 :(得分:1)

我认为你应该寻找declarative_authorization gem。使用with_permissions_to方法,您可以轻松执行此类数据库查询。例如:Article.with_permissions_to(:read).limit(10).offset(20)

答案 2 :(得分:0)

cancan gem具有此功能。实际上,从版本1.4开始,它会自动调整查询范围,以返回当前用户可以访问的对象。

有关详细信息,请参阅此页:https://github.com/ryanb/cancan/wiki/Fetching-Records

答案 3 :(得分:0)

我在目前正在处理的系统上遇到了同样的问题。

我找到的最有效的方法是实现预先计算每条记录的授权状态的批处理作业。我选择了类似accessible_by_companies的内容,并存储了一个包含可以访问这些记录的公司代码的数组,但如果是这种情况,您也可以使用accessible_by_users

在“show”操作中,我重新计算记录的授权公司列表,使用它来执行授权检查,然后再次存储。

我使用ElasticSearch来存储预先计算的值以及执行查询和列表所需的所有数据。仅在查看记录或批处理作业时触摸数据库。这种方法有很大的性能提升,试一试。

答案 4 :(得分:0)

为了获得最佳性能,我建议在会话中存储用户可读文章列表 - 用户不会在会话中更改,您可以单独考虑刷新频率和/或条件。假设您的Articles.list()可以通过id进行过滤,您需要做的就是将用户可读ID列表传递给Articles.list()功能。 重新用户可读列表更新:您应该相对不频繁地更新它 - 每次搜索最多一次,您不希望在每个页面加载时刷新完整列表,原因很简单,新结果可能出现在用户已经存在的页面中无论如何都滚动了。