【问题标题】:What is a common approach to scope records by those that an user can "read"?用户可以“阅读”的范围记录的常用方法是什么?
【发布时间】:2012-06-17 19:02:35
【问题描述】:

我正在使用 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)

但是,当我想(通常在我的控制器 index 操作中)通过准确检索 10 个对象来制作类似以下内容时

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

我仍然必须对每个对象执行授权检查。所以,我可以做些什么来对记录“列表”执行授权检查Article 对象数组)在“智能" / "performant" 方式?也就是说,例如,我应该加载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

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

那么,有一些方法可以以一种高效且“真正”正确的方式完成我想做的事情(也许,通过改变上述方法)?强>

【问题讨论】:

  • 我们可以得到readable_by_user? 的完整代码吗?
  • @patrickmcgraw - 出于“隐私政策”的原因,我无权发布readable_by_user? 方法的完整代码。我可以说,在数据库级别,它执行一堆“单独”/“不同”的 SQL 查询。
  • 好的,我只是想问问。我很难思考如何拥有一组无法通过 SQL 关联的表。特别是当 SQL 已经单独处理表以达到预期目的时。是否有任何readable_by_user? 检查完全依赖于数据库之外的东西,例如应用程序状态?
  • @patrickmcgraw - 感谢您对“隐私政策”的理解。我的回答是:不,readable_by_user? 检查确实完全依赖于数据库之外的东西。
  • 通过数据库逻辑授权是一种高效的方法。您可以用通用术语描述您的授权逻辑。它将帮助社区回答您的问题。

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


【解决方案1】:

这取决于您的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 查询。

【讨论】:

  • 使用选择块或其他传统方法遍历数组并选择您需要的。
  • 如果readable_by_user只依赖SQL,那么最好的办法就是为列表创建SQL查询。 Article.find_by_sql(["Select ...", user.id])。如果你坚持使用readable_by_user函数,那么不要只收集id-s,收集对象本身,因为它们已经在内存中,不需要通过id再次查询它们。任何进一步的优化都取决于系统,例如可读文章的稀有性,可读性规则等......
  • 我应该如何/在哪里“坚持”?我不明白您将如何设置“为列表创建 SQL 查询”方法的结构。
  • 更新了一个例子,我希望这能突出重点。确切的代码和结构取决于您的代码。
【解决方案2】:

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

【讨论】:

  • 我尝试使用 declarative_authorization gem,但它并没有完成我想做的事情,因为我的 readable_by_user,正如 Matzi 所说,“不是 易于翻译成 SQL”。我更新了问题。
【解决方案3】:

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

查看此页面了解更多信息:https://github.com/ryanb/cancan/wiki/Fetching-Records

【讨论】:

  • 我尝试使用 cancan gem,但它没有完成我想做的事情,因为我的 readable_by_user,正如 Matzi 所说,“不是 易于翻译成 SQL”。我更新了问题。
【解决方案4】:

我在当前使用的系统上遇到了同样的问题。

我发现最有效的方法是实现一个预先计算每条记录的授权状态的批处理作业。我使用了accessible_by_companies 之类的东西,并存储了一个包含所有可以访问这些记录的公司代码的数组,但如果是这样的话,你也可以使用accessible_by_users

在“show”动作上,我重新计算了授权公司列表以供记录,用它来进行授权检查,并再次存储。

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

【讨论】:

  • 你能再明确一点,也许可以提供一些简单的代码? P.S.:我已经考虑过存储一个包含所有对象ids“可读的数组”的解决方案” / 用户“可访问”,但正如我在问题内容中提到的“我认为它不是解决问题的常见/简单解决方案”(主要是因为您必须注意保留该数组更新)。但是,如果我理解您所做的,在“显示”控制器操作中,您会更新与当前显示的对象相关的 ids 数组,但只有用户“已经显示”的对象才能读取(所有其他,被忽略)。
【解决方案5】:

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

【讨论】:

    猜你喜欢
    • 2023-03-14
    • 1970-01-01
    • 2020-08-13
    • 1970-01-01
    • 2018-09-03
    • 1970-01-01
    • 1970-01-01
    • 2019-10-09
    • 1970-01-01
    相关资源
    最近更新 更多