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.compileExpression → compileCelToFilter 返回 !ok → return null → 单策略路径变成 RLS_DENY_FILTER(fail closed)。

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

为什么没有随 #4698 一起做

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

  • RLS 的决策过程是 isSupportedRlsExpression,它先经 sqlPredicateToCel 把遗留 SQL-ish 子集(= → ==,IN → in)桥接成 canonical CEL,再问 isPushdownableCel。
  • sqlPredicateToCel 和 isSupportedRlsExpression 两个都住在 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。

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions