feat(formula,lint): 把 RLS 判据上提到 formula,并接上 ADR-0056 D4 的 authoring gate (#4983) - #5008
Merged
Merged
Conversation
…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
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 4 package(s): 21 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
xuyushun441-sys
marked this pull request as ready for review
August 3, 2026 20:24
xuyushun441-sys
enabled auto-merge
August 3, 2026 20:24
This was referenced Aug 3, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #4983
isSupportedRlsExpression的 TSDoc 从 ADR-0056 D4 起就写着它存在的唯一理由:让 authoring 命令拒绝一条运行期会被静默丢弃的谓词。而它在仓内从来没有任何非测试消费者 —— 一个为了修 declared-but-never-read 而写的函数,自己就是 declared-but-never-read。本 PR 补上那个消费者,分两步,顺序是硬性的。第一步:判据上提(行为保持的搬迁)
sqlPredicateToCel+isSupportedRlsExpressionFROMpackages/plugins/plugin-security/src/rls-compiler.tsTOpackages/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:rls-predicate-unenforceablerls-predicate-unparseableAND/OR/LIKE、子查询)&&、双竖线)模式照抄刚合入的 #4985:不建模、不 grep,直接调用消费者本人的判定
isSupportedRlsExpression—— 那正是RLSCompiler.compileFilter用来判断"这条被丢弃的策略要不要 WARN"的同一个函数,所以两边的判决按构造是同一个布尔值(测试里双向钉住)。报错文案说的是实测后果,不是"不支持"
顺着
plugin-security读出来的,不是推断:compileExpression→compileCelToFilter返回!ok→null,策略不贡献任何 filter;compileFilter打一行请求期 WARN(has an uncompilable predicate … and was DROPPED (no enforcement))—— 这是全部信号;using):若它是该对象+操作下唯一适用的策略,compileFilter返回RLS_DENY_FILTER哨兵并被 AND 进 where,于是该对象上每一次 select / update / delete 都匹配零行;若还有别的策略适用,它只是从 OR 里消失,写它的那份访问权根本不存在;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-security的default-permission-sets.ts平台种子、examples/、dogfood fixtures、skills/objectstack-data/SKILL.md—— 全部可下推,新 gate 不会让任何现有代码变红。仓内所有不可下推的谓词都只出现在测试文件和 ADR 文档里。另外单独钉住桥接最锋利的那条边:note = 'a = b'(引号内的=不被重写)判绿。依赖方向
方向是
security → formula与lint → formula,绝不反向。formula/src/rls-predicate.test.ts用源码断言钉死:该模块的 import 说明符只能是['./cel-to-filter'],且@objectstack/formula的 dependencies 只有@marcbachmann/cel-js与@objectstack/spec。写成测试而不是注释,因为只在构建期出错的依赖方向不会产生任何失败断言。测试与验证(已 merge
origin/main后重跑)搬迁前后测试全绿:被搬函数的两个 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。已 mergeorigin/main,零冲突,合并后 lint 全量 1052 tests 绿。本 PR 自始至终没有碰那两个文件。范围
只覆盖
permissions[].rowLevelSecurity[]。rowLevelSecurity仅声明在PermissionSetSchema上(ObjectSchema没有这个键),permissions是StackSchema声明的唯一 permission-set 键。顺手走一遍objects[].rowLevelSecurity或permissionSets会加进一条任何 spec 合法 stack 都到不了的分支 —— 正是 #4984 那个缺陷:读被拒别名的红线对每一个 schema 接受的 stack 都是惰性的。别名容忍属于 schema 的拒绝处,不属于消费者(Prime Directive #12)。