从 #4583 拆出,照 #4479 的先例:退役 PR 负责删掉假合规的键,不负责就地发明一个机制。
背景
datasource.capabilities.readOnly 在 #4583 里被移除——它读起来像安全属性,实际没有任何写路径读它,一个标着「只读副本」的 datasource 照常接受写入。
移除时给作者的处方是 external.allowWrites: false。但那个处方只覆盖一半场景。
缺口
ObjectQLEngine.assertWriteAllowed(packages/objectql/src/engine.ts):
const ds = this.datasourceDefs.get(dsName);
// No recorded definition, or an explicitly managed one ⇒ allow.
if (!ds || !ds.schemaMode || ds.schemaMode === 'managed') return;
const dsAllows = ds.external?.allowWrites ?? false;
const objAllows = object?.external?.writable ?? false;
闸门在第一个 if 就对 managed(以及没声明 schemaMode 的)datasource 直接放行。external.allowWrites 是联邦所有权闸门——它回答的是「这个外部库归谁写」,不是「这个连接是不是只读」。
所以对一个本地的、managed 的 datasource:
capabilities.readOnly —— 已删除,从来没生效过
external.allowWrites: false —— 不生效(在 managed 分支就返回了)
- 没有第三个选项
examples/app-crm 里那个 crm_analytics(sqlite、无 schemaMode、label 写着 "CRM Analytics Read Replica")就是这个形状:#4583 之后它不再声称自己只读,但也依然无法只读。
要请的是决策
A. 建闸门。 让 assertWriteAllowed 认一个与联邦无关的只读位。设计要点:位放在哪(datasource 顶层 readOnly?复用 schemaMode?)、和 external.allowWrites 的关系(两个都要过,还是短路)、以及 isSystem / 迁移 / DDL 是否豁免——一个连 schema sync 都拦的只读库多半不是作者想要的。
B. 不建,明确记录。 承认「只读连接」不是平台提供的能力,只读靠数据库账号权限做(GRANT SELECT),并把这句话写进 datasource 文档。这也是一个诚实的答案——而且很可能是更诚实的那个:数据库侧的只读账号是真闸门,应用层的布尔位不是。
我倾向 B,除非有明确需求要应用层拦截。理由是 #4583 的教训本身:readOnly 之所以危险,不是因为它不存在,而是因为它看起来存在。再造一个只拦 ObjectQL 写路径、拦不住直连 / 迁移 / DDL 的位,会重演同一个形状——只是这次带着一个能跑的测试当背书。
如果选 A,请先回答「它拦不住什么」,并把答案写进 describe(),而不是等下一次审计来问。
参考
未指派 —— 这是决策而非补丁,认领前先在下面说结论。
从 #4583 拆出,照 #4479 的先例:退役 PR 负责删掉假合规的键,不负责就地发明一个机制。
背景
datasource.capabilities.readOnly在 #4583 里被移除——它读起来像安全属性,实际没有任何写路径读它,一个标着「只读副本」的 datasource 照常接受写入。移除时给作者的处方是
external.allowWrites: false。但那个处方只覆盖一半场景。缺口
ObjectQLEngine.assertWriteAllowed(packages/objectql/src/engine.ts):闸门在第一个
if就对 managed(以及没声明schemaMode的)datasource 直接放行。external.allowWrites是联邦所有权闸门——它回答的是「这个外部库归谁写」,不是「这个连接是不是只读」。所以对一个本地的、managed 的 datasource:
capabilities.readOnly—— 已删除,从来没生效过external.allowWrites: false—— 不生效(在 managed 分支就返回了)examples/app-crm里那个crm_analytics(sqlite、无schemaMode、label 写着 "CRM Analytics Read Replica")就是这个形状:#4583 之后它不再声称自己只读,但也依然无法只读。要请的是决策
A. 建闸门。 让
assertWriteAllowed认一个与联邦无关的只读位。设计要点:位放在哪(datasource 顶层readOnly?复用schemaMode?)、和external.allowWrites的关系(两个都要过,还是短路)、以及 isSystem / 迁移 / DDL 是否豁免——一个连 schema sync 都拦的只读库多半不是作者想要的。B. 不建,明确记录。 承认「只读连接」不是平台提供的能力,只读靠数据库账号权限做(GRANT SELECT),并把这句话写进 datasource 文档。这也是一个诚实的答案——而且很可能是更诚实的那个:数据库侧的只读账号是真闸门,应用层的布尔位不是。
我倾向 B,除非有明确需求要应用层拦截。理由是 #4583 的教训本身:
readOnly之所以危险,不是因为它不存在,而是因为它看起来存在。再造一个只拦 ObjectQL 写路径、拦不住直连 / 迁移 / DDL 的位,会重演同一个形状——只是这次带着一个能跑的测试当背书。如果选 A,请先回答「它拦不住什么」,并把答案写进 describe(),而不是等下一次审计来问。
参考
capabilities.*(本 issue 的来源)readReplicas移除后另立读写分离立项readOnly从config挪进capabilities的那两次搬家,都没让它生效未指派 —— 这是决策而非补丁,认领前先在下面说结论。