自动更新的安全屏障视图

来自 PostgreSQL Wiki
跳转到导航跳转到搜索

这是一个过去版本开发笔记页面。内容将过时。

自动更新的安全屏障视图

作为 行安全 工作的一部分,以及作为一项本身有用的功能,支持自动更新security_barrier视图是可取的。普通视图在 9.3 中是可更新的,但security_barrier视图不被视为“简单”视图,不可更新。

使安全屏障视图自动可更新 中讨论了一种替代方法。

相关列表讨论

主题 可更新安全屏障视图的 WIP 修补程序

9.3 中的状态

“简单”视图在 9.3 中通过将视图限定符扁平化到外部查询中而变得可更新。因此,给定

 CREATE TABLE t AS 
 SELECT n AS id, 'secret'||n AS secret FROM generate_series(1,10) n;
 CREATE VIEW t_even AS SELECT * FROM t WHERE id % 2 = 0;
 CREATE VIEW t_even_sb WITH (security_barrier) AS SELECT * FROM t WHERE id % 2 = 0;

您可以更新简单的非安全屏障视图,但也可以窃取值

 CREATE OR REPLACE FUNCTION f_leak(text) RETURNS boolean AS $$
 BEGIN
   RAISE NOTICE 'Saw secret=%',$1;
   RETURN true;
 END;
 $$ LANGUAGE plpgsql COST 1;
 test=>   UPDATE t_even SET id = id WHERE f_leak(secret);
 NOTICE:  Saw secret=secret1
 NOTICE:  Saw secret=secret3
 NOTICE:  Saw secret=secret5
 NOTICE:  Saw secret=secret7
 NOTICE:  Saw secret=secret9
 NOTICE:  Saw secret=secret2
 NOTICE:  Saw secret=secret4
 NOTICE:  Saw secret=secret6
 NOTICE:  Saw secret=secret8
 NOTICE:  Saw secret=secret10
 UPDATE 5

您根本无法更新安全屏障视图

 test=> UPDATE t_even_sb SET id = id WHERE f_leak(secret);
 ERROR:  cannot update view "t_even_sb"
 DETAIL:  Security-barrier views are not automatically updatable.
 HINT:  To enable updating the view, provide an INSTEAD OF UPDATE trigger or an unconditional ON UPDATE DO INSTEAD rule.

因此,没有安全的方法可以授予用户仅对表中某些行的更新权限。这是我们需要为行安全做的事情,并且它对一般用户来说也是一项有用的功能。

安全屏障支持问题

现有的可更新视图代码依赖于将视图的限定符扁平化到外部查询中。因此,对于上面的示例

 UPDATE t_even SET id = id WHERE f_leak(secret);

被视图扩展成逻辑上类似于

 UPDATE (SELECT * FROM t WHERE id % 2 = 0) SET id = id WHERE f_leak(secret)

但执行器不知道如何处理它,因此它不能按原样传递。相反,子查询被扁平化,产生

 UPDATE t SET id = id WHERE id % 2 = 0 AND f_leak(secret)

执行器可以处理它,因为它是对关系的简单更新。您可以在计划中看到这一点

 test=# explain  UPDATE t_even SET id = id WHERE f_leak(secret);
                        QUERY PLAN                        
 ---------------------------------------------------------
  Update on t  (cost=0.00..31.53 rows=2 width=42)
    ->  Seq Scan on t  (cost=0.00..31.53 rows=2 width=42)
          Filter: (f_leak(secret) AND ((id % 2) = 0))
 (3 rows)

看看f_leak(secret)限定符和id % 2 = 0位于同一级别?执行器将倾向于选择f_leak(secret)首先运行,因为它的COST被人工降低了。

这在安全屏障视图中行不通,因为我们必须确保谓词id % 2 = 0在任何行传递给用户提供的谓词之前过滤行,在本例中为f_leak(secret)。因此,当前代码拒绝对security_barrier视图进行操作。

9.3 中自动更新视图的工作原理

自动更新视图由 Dean Rasheed 的代码引入,由 Tom 在 a99c42f291421572aef2b0a9360294c7d89b8bc7 中提交。

此代码扩展了src/backend/rewrite/rewriteHandler.c,添加了以下函数到

  • get_view_query:获取_RETURN来自视图的规则
  • view_has_instead_trigger:检查是否存在INSTEAD触发器应该取代自动更新视图
  • view_is_auto_updatable:检查视图是否足够“简单”以自动更新。拒绝 sb 视图。
  • relation_is_updatable:由information_schema使用。仅对真实视图感兴趣。
  • adjust_view_column_set:将列权限从视图映射到子表。仅当它是真实视图时才感兴趣。
  • rewriteTargetView:核心。将视图重写为外部查询。它
    • 确保视图是自动可更新的view_is_auto_updatable
    • 在外部查询中找到视图的 RTE
    • 从其_RETURN规则中使用get_view_query
    • 锁定基关系(可能另一个视图)更新目标列表中的 resno 以引用基关系的列
    • 向上拉取视图(如果可能,应与优化器向上拉取代码合并)
    • 创建一个新的目标 RTE 来描述基关系,并将其添加到外部查询的范围表中
      • 将限定符复制到外部查询的限定符列表中,修复 varnos 以指向新的目标
      • 处理视图所有者 != 查询调用者的权限
      • 处理列权限
    • 修复外部关系中的变量,使其指向新基关系中的变量,而不是视图中的变量,使用ReplaceVarsFromTargetListTODO:我们是否可以将这种方法重新用于 RLS?)
    • 使用ChangeVarNodes
    • 修复引用视图的所有其他内容,使其指向新的基关系
    • 更新目标列表中的 resno 以指向基关系的列(仅限 UPDATE / INSERT)。

还更改了fireRIRrules

值得注意的是,这意味着可更新视图代码实际上并没有添加对更新视图的支持。相反,它添加了对重写简单视图的支持,以将其限定符向上拉到外部查询中,并将子查询扁平化。

我们不能对可更新的安全屏障视图这样做。

可更新 s.b. 视图的路径

要支持可更新的安全屏障视图,我们必须支持UPDATE直接在子查询上,而无需扁平化查询。这是因为我们必须仍然强制执行由security_barrier子查询上的标志。任何其他操作都需要实现对安全屏障的不同方法,并引入自身问题。

通过可更新子查询更新安全屏障视图

一种方法是允许重写器扩展作为子查询目标的视图。此 Wiki 页面上相当广泛的历史记录涵盖了实现该目标的尝试。

从该努力中产生的两个基本问题是

resultRelation 与 source relation

解析器和执行器的一部分被用于 resultRelation 与我们从 ExecModifyTable 中获取行的关系相同。如果更新子查询,则不再是这种情况。而不是在目标视图扁平化后

      SeqScan<baseRel> WHERE quals1 AND quals2 AND quals3
      |         ^
      |         |
      |         -------------
      v                     |
    ModifyTable             |
      -> heap_modify_tuple( | )
      -> RETURNING (mix of refs to baserel and other expressions)

我们得到

    (SELECT ctid, ... FROM (
      (SELECT ctid, ... FROM (
        (SELECT ctid, ... FROM SeqScan<baseRel> WHERE quals1)
      WHERE quals2) 
    WHERE quals3)
    |
    v
  ModifyTable
    -> resultRelation(baseRel)
    -> RETURNING (mix of refs to baserel and other expressions)

RETURNING列表可能包含引用 resultRelation 的表达式,它必须反映任何BEFORE触发器等的影响后的最终关系。表达式也可能包含子查询,可能引用视图查询中不是基于 resultRelation 的表达式,等等。因此,我们必须重写 tlist 以替换对 resultRelation 的引用,同时保持其他所有内容不变。

以下必须将其 'relation' 参数指向 resultRelation

  • heap_modify_tuple(...) 用于存储新元组
  • 通过 RowMarks 调用的 heap_lock_tuple(...)
  • RETURNING 列表中任何引用正在更新的关系的 Var

以下必须指向 source relation

  • 顶层查询上的 "ctid" resjunk Var
  • 顶层查询上的 "oid" resjunk Var
  • 所有引用目标视图的 TargetList 条目
  • "ctid1" 等用于 RowMark ctid 源

tlist 扩展

我们需要将一个 resjunk "ctid" 列注入到每个子查询中,以将 ctid 从子查询层提取到 ExecModifyTable 中。如果在表上使用 oid,我们还需要将 "oid" 作为 resjunk 获取。

此外,任何未在 SET 中明确指定的 tlist 条目都必须添加到每个嵌套子查询的 tlist 中,指向最内部的查询。

在重写时执行此操作不一定正确。我们可能会选择在 preprocess_targetlist 时间执行此操作,使用 'sourceRelation' 作为下一级 Var 指向位置的指导。这里的问题是我们必须从内部查询向外进行预处理,而目前我认为我们是以相反的方式进行的。


啊!

在重写器中执行此操作可能很容易。重写器是递归的,而不是迭代的。因此,堆栈状态被保留,并且堆栈展开。在 RewriteQuery 返回后,我们可以检查返回的内容并使用下一层外部层的知识对其进行修改。

先前消息

相同内容的早期解释。

1. 规划器中的 preprocess_targetlist 假设 resultRelation 是在新的 Var 中设置为 varno 的正确 RTE,它添加了该 Var 以作为 resjunk 属性获取行 ctid(用标签 "ctid1")。这会导致 tlist 的条目指向与最终 seqscan / indexscan 扫描的 RTE 不同的 RTE,尽管底层关系是相同的。tlist 验证检查不喜欢这样。

可能还有其他地方需要添加指向我们正在从中读取行的关系的 tlist 条目。它们还需要能够处理它不再是 resultRelation 的事实。


2. 尽管我对着它猛烈地敲打了好几个世纪,但我还没有弄清楚如何将对基关系的 ctid、oid(如果使用 WITH OIDS)以及 DML 语句中未指定的任何 tlist 条目的引用注入到子查询树中。

今天我一直在研究两个想法,但还没有找到如何使其中任何一个都起作用的方法

- 将下一层外部的 Query 传递到 RewriteQuery 调用中。

文档引用