自动更新的安全屏障视图
这是一个过去版本开发笔记页面。内容将过时。
自动更新的安全屏障视图
作为 行安全 工作的一部分,以及作为一项本身有用的功能,支持自动更新security_barrier视图是可取的。普通视图在 9.3 中是可更新的,但security_barrier视图不被视为“简单”视图,不可更新。
在 使安全屏障视图自动可更新 中讨论了一种替代方法。
相关列表讨论
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 以指向新的目标
- 处理视图所有者 != 查询调用者的权限
- 处理列权限
- 修复外部关系中的变量,使其指向新基关系中的变量,而不是视图中的变量,使用ReplaceVarsFromTargetList(TODO:我们是否可以将这种方法重新用于 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 调用中。
文档引用
- Var (计划树VAR)
- PlaceHolderVar
- TargetEntry (计划树TARGETENTRY)
- Result (计划树RESULT)