Skip to content

feat(formula,lint): 把 RLS 判据上提到 formula,并接上 ADR-0056 D4 的 authoring gate (#4983) - #5008

Merged
xuyushun441-sys merged 2 commits into
mainfrom
claude/issue-4983-rls-authoring-gate
Aug 3, 2026
Merged

feat(formula,lint): 把 RLS 判据上提到 formula,并接上 ADR-0056 D4 的 authoring gate (#4983)#5008
xuyushun441-sys merged 2 commits into
mainfrom
claude/issue-4983-rls-authoring-gate

Conversation

@xuyushun441-sys

@xuyushun441-sys xuyushun441-sys commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Fixes #4983

isSupportedRlsExpression 的 TSDoc 从 ADR-0056 D4 起就写着它存在的唯一理由:让 authoring 命令拒绝一条运行期会被静默丢弃的谓词。而它在仓内从来没有任何非测试消费者 —— 一个为了修 declared-but-never-read 而写的函数,自己就是 declared-but-never-read。本 PR 补上那个消费者,分两步,顺序是硬性的。

第一步:判据上提(行为保持的搬迁)

sqlPredicateToCel + isSupportedRlsExpression FROM packages/plugins/plugin-security/src/rls-compiler.ts TO packages/formula/src/rls-predicate.ts,可执行代码逐字未动,只换住址(参照 #4857 的做法)。plugin-security 改为从 @objectstack/formula 消费,不留副本;这两个符号从来没有出现在 plugin-security 的 entry point 导出里,所以两个包之外没有任何 import 路径变化。

为什么必须先搬:@objectstack/lint 的包契约是 "Depends on @objectstack/spec; never on a runtime",判据住在 runtime 里时,lint 只剩下抄一份桥接这一条路。而桥接的边界条件(引号内字面量不重写、对 CEL 输入幂等)就是这道 gate 判红判绿的分界线 —— 两份实现漂移一格,linter 就会否掉运行期其实能执行的策略,是比漏报更糟的误伤方向。ADR-0058 D1 要的正是 "a single canonical shape gate",桥接属于那个 gate。

第二步:lint 侧的 authoring gate

新规则 validateRlsPredicateEnforceability,error,三个 authoring 命令全覆盖,作用于 permissions[].rowLevelSecurity[].using.check:

id 判据 修法
rls-predicate-unenforceable 能解析成 CEL,但不在下推子集内(函数调用、算术、三元、跨对象路径) 改写进可下推子集 / 把值反规范化到本对象
rls-predicate-unparseable 过了遗留 SQL 桥接仍解析不了(SQL AND / OR / LIKE、子查询) 改写成 canonical CEL(&&、双竖线)

模式照抄刚合入的 #4985:不建模、不 grep,直接调用消费者本人的判定 isSupportedRlsExpression —— 那正是 RLSCompiler.compileFilter 用来判断"这条被丢弃的策略要不要 WARN"的同一个函数,所以两边的判决按构造是同一个布尔值(测试里双向钉住)。

报错文案说的是实测后果,不是"不支持"

顺着 plugin-security 读出来的,不是推断:

  1. compileExpressioncompileCelToFilter 返回 !oknull,策略不贡献任何 filter;
  2. compileFilter 打一行请求期 WARN(has an uncompilable predicate … and was DROPPED (no enforcement))—— 这是全部信号;
  3. 读路径(using):若它是该对象+操作下唯一适用的策略,compileFilter 返回 RLS_DENY_FILTER 哨兵并被 AND 进 where,于是该对象上每一次 select / update / delete 都匹配零行;若还有别的策略适用,它只是从 OR 里消失,写它的那份访问权根本不存在;
  4. 写路径(check,ADR-0058 D4):computeWriteCheckFilter 只收声明了 check 的策略,同样的丢弃让 post-image 谓词变成哨兵,matchesFilterCondition 必然为假,每一次 insert / update 抛 PermissionDeniedError

所以这不是安全漏洞 —— 运行期 fail closed,这正是它能一直活着的原因。它是一条看起来在授权、实际在一律拒绝的策略,而现场没有任何东西指向那一行:os validate / os build / os lint 今天全绿。

与 sharing-rule 那条不同,CEL 语法错误在这里报告而不是让给 expression-invalid:validateStackExpressions 根本不走 rowLevelSecurity,让给它等于让给没有人;而且它就算走了也判不对 —— owner_id = current_user.id 同时是一个 CEL 语法错误和一条能正常工作的 RLS 谓词,因为桥接先跑。

双向证明

判红:函数调用 / 跨对象路径 / 算术 / 三元 / SQL AND / 子查询,逐条点名 path(permissions[0].rowLevelSecurity[0].using)+ 说出后果 + 给修法。另有一条"没有任何别的 author-time 规则看得见这个 stack"的测试 —— 把 fixture 灌进整张 AUTHORING_RULES,断言只有本规则开口,这就是 #4983 抱怨的"过了整条工具链"的机械化版本。

判绿:实测扫过全仓每一条真实 RLS 谓词 —— plugin-securitydefault-permission-sets.ts 平台种子、examples/、dogfood fixtures、skills/objectstack-data/SKILL.md —— 全部可下推,新 gate 不会让任何现有代码变红。仓内所有不可下推的谓词都只出现在测试文件和 ADR 文档里。另外单独钉住桥接最锋利的那条边:note = 'a = b'(引号内的 = 不被重写)判绿。

依赖方向

方向是 security → formulalint → formula,绝不反向。formula/src/rls-predicate.test.ts 用源码断言钉死:该模块的 import 说明符只能是 ['./cel-to-filter'],且 @objectstack/formula 的 dependencies 只有 @marcbachmann/cel-js@objectstack/spec。写成测试而不是注释,因为只在构建期出错的依赖方向不会产生任何失败断言。

测试与验证(已 merge origin/main 后重跑)

pnpm --filter @objectstack/formula test         → 14 files, 323 tests passed
pnpm --filter @objectstack/plugin-security test → 33 files, 717 tests passed
pnpm --filter @objectstack/lint test            → 56 files, 1052 tests passed
typecheck (formula / lint / plugin-security)    → Done ×3
eslint --no-inline-config(全部改动文件)         → clean
node scripts/check-adr-anchors.mjs              → OK

搬迁前后测试全绿:被搬函数的两个 shape 单测逐字跟着搬进 formula/src/rls-predicate.test.ts;plugin-security 保留消费侧断言 —— 新增一条"编译器丢弃的谓词集合 == isSupportedRlsExpression 拒绝的集合"的 corpus 测试,比把断言原样复制一份更强,也正好是新 gate 依赖的那句话。

#4991 的重叠(已消解)

#4991 在本 PR 推送前经 #5004 合入,且它只动了 validate-org-axis-red-lines.ts / .test.ts,没有authoring-rules.ts。已 merge origin/main,零冲突,合并后 lint 全量 1052 tests 绿。本 PR 自始至终没有碰那两个文件。

范围

只覆盖 permissions[].rowLevelSecurity[]rowLevelSecurity 仅声明在 PermissionSetSchema 上(ObjectSchema 没有这个键),permissionsStackSchema 声明的唯一 permission-set 键。顺手走一遍 objects[].rowLevelSecuritypermissionSets 会加进一条任何 spec 合法 stack 都到不了的分支 —— 正是 #4984 那个缺陷:读被拒别名的红线对每一个 schema 接受的 stack 都是惰性的。别名容忍属于 schema 的拒绝处,不属于消费者(Prime Directive #12)。

…untime's own predicate (#4983)

`isSupportedRlsExpression` was written so an authoring command could REJECT a
predicate the runtime silently drops, and no authoring command ever called it —
a declared-but-never-read helper whose whole purpose was fixing
declared-but-never-read. Two steps, in this order.

1. Hoist `sqlPredicateToCel` + `isSupportedRlsExpression` FROM
   plugin-security/src/rls-compiler.ts TO formula/src/rls-predicate.ts.
   Executable code unchanged — an address change. plugin-security consumes them
   from @objectstack/formula and keeps no copy; neither symbol was ever exported
   from plugin-security's entry point, so no import path outside the two
   packages moves. @objectstack/lint may depend on spec and never on a runtime,
   so the alternative was forking the SQL->CEL bridge, whose boundary conditions
   (quoted literals never rewritten; canonical CEL idempotent) ARE the gate's
   red/green line. ADR-0058 D1: a single canonical shape gate.

2. New lint rule `validateRlsPredicateEnforceability`, error, on all three
   authoring commands, over permissions[].rowLevelSecurity[].using/.check:
   - rls-predicate-unenforceable: parses as CEL, outside the pushdown subset.
   - rls-predicate-unparseable: does not parse even after the legacy SQL bridge.
   The verdict is `isSupportedRlsExpression` itself — the same function
   RLSCompiler.compileFilter consults to decide whether a dropped policy earns
   its WARN — so lint and runtime are one boolean by construction, pinned in
   both directions over a shared corpus.

Runtime consequence, read from plugin-security rather than inferred: the policy
is DROPPED with one request-time WARN; on the read path, when it is the only
applicable policy, compileFilter returns RLS_DENY_FILTER, so every
select/update/delete matches zero rows; on the ADR-0058 D4 write path the
post-image check becomes that sentinel and every insert/update raises
PermissionDeniedError. Fail-closed, hence survivable — a policy that reads as an
authorization and behaves as a blanket refusal, with nothing at authoring time
naming the line.

Measured: every RLS predicate declared anywhere in this repo (platform seeds,
examples, dogfood fixtures, the authoring skill) is supported, so the gate turns
nothing red that works today.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018iARDqtrhQgz6fVHDeDkbQ
@vercel

vercel Bot commented Aug 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 3, 2026 8:20pm

Request Review

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/formula, @objectstack/lint, @objectstack/plugin-security, @objectstack/dogfood.

21 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/automation/hook-bodies.mdx (via @objectstack/lint)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/formula)
  • content/docs/data-modeling/validation.mdx (via @objectstack/formula)
  • content/docs/deployment/cli.mdx (via @objectstack/plugin-security)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via @objectstack/plugin-security)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/plugin-security)
  • content/docs/permissions/access-recipes.mdx (via packages/plugins/plugin-security)
  • content/docs/permissions/authorization.mdx (via @objectstack/lint, @objectstack/plugin-security, packages/qa/dogfood)
  • content/docs/permissions/delegated-administration.mdx (via packages/qa/dogfood)
  • content/docs/permissions/explain.mdx (via @objectstack/plugin-security)
  • content/docs/permissions/permissions-matrix.mdx (via packages/plugins/plugin-security)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/plugin-security)
  • content/docs/plugins/index.mdx (via @objectstack/plugin-security)
  • content/docs/plugins/packages.mdx (via @objectstack/formula, @objectstack/plugin-security)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/formula)
  • content/docs/releases/implementation-status.mdx (via @objectstack/plugin-security)
  • content/docs/releases/v15.mdx (via @objectstack/formula)
  • content/docs/releases/v16.mdx (via @objectstack/formula)
  • content/docs/releases/v17.mdx (via @objectstack/lint)
  • content/docs/ui/audience-based-interfaces.mdx (via packages/plugins/plugin-security)
  • content/docs/ui/dashboards.mdx (via @objectstack/plugin-security)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/xl and removed documentation Improvements or additions to documentation tests tooling labels Aug 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

2 participants