Skip to content

降级 analytics shim 把 ExecutionContext 丢在门口 —— /analytics/query 在没装 service-analytics 的装配里不注入 RLS/租户谓词,契约字段 where 也被静默忽略 #3891

Description

@os-zhuang

#3878 分出来的第二条分歧。#3878 是"同一个 URL 两种请求体";这一条是同一个 URL 两种作用域语义 —— 降级实现把调用者身份丢在门口,聚合以无主体身份落到引擎,RLS/租户谓词整条不注入。

链路(逐跳,均核对于 origin/main @ 93f267f

  1. dispatcher 正确地把身份传了下去:
    packages/runtime/src/domains/analytics.ts:45analyticsService.query(body, context?.executionContext)
    该行上方的注释正是 authz(D10): getReadFilter 缺 delegator 交集 — analytics read-scope 对 agent 上下文不与委托人 RLS 求交(原 Gap 2 /analytics/query 无 context 已修复) #2852 修真引擎时留下的:"Without it, getReadScope(object, undefined) returned no filter and the query ran UNSCOPED — an authenticated caller saw rows RLS would otherwise hide."

  2. 降级 shim 的 query一元函数,第二个参数当场丢弃:
    packages/metadata-protocol/src/plugin.ts:139query: async (body: any) => {

  3. analyticsQuery 调引擎时也不带 context(engine.aggregate 的第三参 EngineReadOptions 没传,query.context 也没设):
    packages/metadata-protocol/src/protocol.ts:3675await this.engine.aggregate(object, { where, groupBy, aggregations })

  4. 引擎侧把两个来源合并,两个都是 undefined:
    packages/objectql/src/engine.ts:3696context: mergeReadContext(query?.context, options?.context)undefined
    (引擎只有 txStore 一个 AsyncLocalStorage,用于事务;没有任何环境态身份兜底。)

  5. 安全中间件的空主体分支放行:
    packages/plugins/plugin-security/src/security-plugin.ts:847positions.length === 0 && explicitPermissionSets.length === 0 && !opCtx.context?.userIdreturn next()
    RLS 过滤与租户谓词都不再注入。

真引擎不走这条路:AnalyticsService.query(query, context?) 收下 context,并有自己的 fail-closed getReadScope 预解析(#3597 那一轮补的)。#2852 / #3597 修的都是真引擎那一半,降级这一半从未跟上。

可达性(写清楚,免得高估也免得低估)

需要同时满足三条:宿主挂了 HTTP dispatcher + 挂了 SecurityPlugin + AnalyticsServicePlugin

  • os serve 默认/full preset:serve.ts:202ALWAYS_ON_CAPABILITIESanalytics,真引擎装上,不受影响。
  • os serve --preset minimal:tiers 只剩 ['core']serve.ts:1401tierEnabled('auth') 为 false,AuthPlugin/SecurityPlugin 都不装 —— 此配置下本来就没有 RLS 可绕,不构成越权。
  • ❌ 托管环境:宿主侧 defaultRequires 强制 analytics,真引擎装上。
  • 程序化嵌入createStandaloneStack() / createObjectQLKernel() + SecurityPlugin + dispatcher,但没有单独挂 AnalyticsServicePlugin —— 这是被文档支持的用法,且 analytics 槽位默认已被 shim 占住,作者不会收到任何"你没装分析引擎"的信号。
  • ✅ 任何按 requires 装配、而该 bundle 未声明 analytics 的多环境宿主。

结论:不是发行默认配置里的洞,但落在一个合法且无提示的装配组合上,且没有任何一层会告诉部署者作用域已经失效 —— 它返回 200 和一份看起来正常的聚合。

同一处的第二个后果:契约里的过滤条件被静默忽略

AnalyticsQuery 的规范过滤字段是 wherespec/data/analytics.zod.ts:154,"canonical Query DSL FilterCondition")。降级实现只读 query.filtersprotocol.ts:3661),而 filters 根本不是 AnalyticsQuery 的字段。

所以一个完全符合契约的带过滤请求,在降级路径上过滤条件被丢掉,返回全表聚合。objectui 的 dashboard 正是按契约发的(payload.where = params.filter)。两条叠加:既没有 RLS 谓词,也没有调用者自己的过滤 —— 返回的是该对象的全量聚合。

修法

倾向 A:按 #3878 的结论退役 shim,槽位空着,路由回到 domains/analytics.ts:33 已有的 404。一条路径消失,就不用再给它补第二遍安全门(#3770 vs #3875 已经补过一次两遍了)。

若决定保留 shim,则至少要:query 收下第二参并原样传给 analyticsQueryanalyticsQueryengine.aggregate(object, ast, { context }) 传下去 → 同时认 where。并补一条门:降级实现与真实现在同一份 RLS 断言下都要绿,否则下次还是只修一半。

关联:#3878(同一 URL 两种请求体)、#2852#3597#3770/#3866#3867/#3875、ADR-0076 D10/D12。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions