Skip to content

ADR-0056 D4 的 RLS authoring gate 从未接线:isSupportedRlsExpression 没有任何非测试消费者 #4983

Description

@xuyushun441-sys

发现于 #4698 的实现过程(packages/lint 侧已落地 sharing-rule 那条,见 PR)。这是同一缺陷类的姊妹面,但修不进那个 PR,原因写在下面。

事实

isSupportedRlsExpression (packages/plugins/plugin-security/src/rls-compiler.ts) 的 TSDoc 明确说明它存在的唯一理由:

ADR-0056 D4: exposed so an authoring-time gate (objectstack compile) can REJECT a predicate the runtime would silently drop — the class of bug where owner == current_user.name (==, unsupported) compiled to nothing and left an object unprotected. A false here means "this predicate will never enforce".

仓内消费者盘点(grep -rn isSupportedRlsExpression --include=*.ts packages/):

  • packages/plugins/plugin-security/src/rls-compiler.ts — 定义处
  • packages/plugins/plugin-security/src/security-plugin.test.ts — 它自己的单测
  • packages/qa/dogfood/test/authz-conformance.matrix.ts — 一致性矩阵里的一行描述文字

即:那个 authoring gate 从来没有被任何 authoring 命令调用过。函数本身就是一个 declared-but-never-read 的实例 —— 而且它恰好是为了修 declared-but-never-read 而写的。

后果

stack.permissions[].rowLevelSecurity[].using / .check 是可授权面(PermissionSetSchema.rowLevelSecurity)。一条不可下推的谓词在运行期走 RLSCompiler.compileExpressioncompileCelToFilter 返回 !okreturn null → 单策略路径变成 RLS_DENY_FILTER(fail closed)。

所以它不是安全漏洞,而是一条看起来在授权、实际在拒绝的策略:管理员写了一条 policy,用户被一律拒访,现场没有任何指向这条 policy 的诊断。os validate / os build / os lint 全绿。

为什么没有随 #4698 一起做

判据本身是可机械判定的(和 sharing rule 那条同一个编译器),但判据的实现位置不对:

  • RLS 的决策过程是 isSupportedRlsExpression,它先经 sqlPredicateToCel 把遗留 SQL-ish 子集(===,INin)桥接成 canonical CEL,再问 isPushdownableCel
  • sqlPredicateToCelisSupportedRlsExpression 两个都住在 plugin-security,那是一个 runtime。@objectstack/lint 的包契约是 "Depends on @objectstack/spec; never on a runtime"。
  • 于是 lint 只有两条路:import 一个 runtime(禁止),或者把桥接逻辑抄一份到 lint(把唯一判据 fork 成两份 —— 正是 validate/lint have no check for "declared but never read" metadata — three instances found in one app in a day #4698 那条规则的文档开头拒绝做的事)。

抄一份尤其危险:sqlPredicateToCel 是一个正则重写,它的 IN / = 边界条件(引号内字面量不重写、对 CEL 输入幂等)是这条规则判绿判红的分界线。两份实现只要漂移一格,lint 就会在 authoring 时否掉运行期其实能执行的策略 —— 误伤方向,比漏报更糟。

建议做法(engine / security 车道)

  1. sqlPredicateToCel 上提到 @objectstack/formula(它已经是 isPushdownableCel 的家,而且 lint 已经依赖它)。plugin-security 从那里 re-export 或直接引用,保持一份定义。ADR-0058 D1 说的就是 "a single canonical shape gate",桥接属于那个 gate 的一部分。
  2. packages/lint 加一条规则,判据 = isPushdownableCel(sqlPredicateToCel(expr)),覆盖 permissions[].rowLevelSecurity[].using.check,以及对象上直接授权的 objects[].rowLevelSecurity[](validateOrgAxisRedLines 已经在走这两条路径,可以照抄遍历)。
  3. 严重度建议 error,理由同 sharing-rule 那条:没有任何一种读法下这条 policy 会按作者写的那样授权。
  4. 落地前先跑一遍全仓实测。我在实现 validate/lint have no check for "declared but never read" metadata — three instances found in one app in a day #4698 时已经扫过一次:examples/packages/plugins/plugin-security/src/objects/default-permission-sets.ts 里现存的每一条 using / check 都能下推,新 gate 不会让任何现有代码变红

验证脚本(实测,非推断)

import { isPushdownableCel } from '@objectstack/formula';
isPushdownableCel('owner == current_user.name');      // => { ok: true }  (#4171 时代的例子如今能下推)
isPushdownableCel('size(record.tags) > 0');           // => { ok: false, reason: 'unsupported' }
isPushdownableCel("record.account.region == 'EU'");   // => { ok: false, reason: 'unsupported' }

相关:#4698(母议题,declared-but-never-read 的模式)、ADR-0056 D4、ADR-0058 D1。

Metadata

Metadata

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions