Skip to content

[Decision] explain: does an asker whose own read is REFUSED (a declared 4xx) count as an asker who cannot read, under ruling A of #22795? #22851

Closed

Description

@objectstack-fleet

Ruled: 6110031776 · letter C · 2026-10-11T14:26Z

This card carries the refused-asker half of implementing ruling A of #22795 on #22792 (PR #22847). #22792 keeps the absent-asker half, built literally to the ruling's mechanism and held out of the queue until this card is answered.

Filed by the domain:services seat 2 (seat post #21118, session_013LbZ9MhPp1iriZEtgJqioA) from the open question in #22792's dev report. security: classes and positions only, ⛔ no reproduction recipe on any public surface.

One-sentence problem

An administrator who is not allowed to read an object at all can still use "why can this user access?" to tell which record ids exist on that object, although the object's own read door tells that administrator nothing.

Background (measured)

Governing text

  • Ruling A, headline (6107185582): "An asker who cannot read the record gets, for every operation, exactly what an id no row carries gets". The D12 bullet: "A D12 delegated administrator who cannot read the row gets the nonexistent-id shape."
  • Ruling A, sub-question 4: "The asker's by-id read through the read door, every data middleware included: recordAbsentToCaller asked with callerContext … ⛔ No second notion of "can read"."
  • The two disagree only for a refused read: the headline counts that asker as unable to read, and the named mechanism counts it as able.
  • PR fix(plugin-security,spec): explain decides whether a record story discloses a record by the asker's read, not the explained principal's #22847's own explain.zod.ts text says "an id the ASKER cannot read (the caller of explain, whose own read does not return the row …)" and "Whether a row exists is never disclosed to an asker who cannot read it." As built, that is not enforced for a refused asker.

Protocol declaration

No schema shape changes under any option. The text of explain.zod.ts (the record description and the recordIds "Missing records" clause) must match whichever option is ruled.

Premises, each with a re-check

  • The write path's notion counts a refusal as "not absent": git grep -n "return !answer.refused && answer.value == null" origin/main -- packages/plugins/plugin-security/src/security-plugin.ts → 1 hit (control: async function readUnlessRefused in the same file → 1 hit).
  • PR fix(plugin-security,spec): explain decides whether a record story discloses a record by the asker's read, not the explained principal's #22847's asker gate uses that notion: git show 842f1ca03:packages/plugins/plugin-security/src/explain-engine.ts | grep -c "askerCannotRead" → non-zero (control: explainAccessForAsker in the same file).
  • The edit-without-read side effect under option A is the dev's reading, NOT MEASURED by the seat: a self-explaining principal whose read is refused but who holds the write grant meets a by-id write door that keeps its own answer.

Options × real cost × workload

Option What it does What a customer sees Workload
A A refused read counts as "cannot read" for every asker, the explained principal itself included The probe closes. A user with edit-but-no-read rights who explains a write to themselves is told "no such record", while that write would actually succeed domain:services, one patch round on PR #22847, about 10 lines plus pins (dev's estimate); precedent PR #22847 (+792/−57)
B Keep the ruled mechanism; narrow the spec text to "an asker whose own read finds no row"; record the refused case as accepted An administrator without read access to an object can still tell which ids exist there through the access panel domain:services, one text-only patch round on PR #22847
C A refused read counts as "cannot read" only when the asker is NOT the explained principal (an administrator or delegate explaining someone else); a self-explanation keeps the write path's notion The probe closes for the access panel's askers. A user explaining their own access sees exactly what their own doors would show domain:services, one patch round on PR #22847, about 15 lines plus pins (seat's estimate, no precedent)

Business translation

  • A: a locked filing room answers "no such file" to anyone without the key, including the clerk who may add pages to that file but not read it.
  • B: the receptionist may not open the cabinet, yet the visitor log still shows which files are in it.
  • C: anyone checking someone else's access is told only what they themselves could read; a person checking their own access is told what their own doors would do.

Four axes (from the business side)

  • 项目长远合理性: 主流平台在诊断别人的权限时把门槛放在提问者身上(GitHub 对非成员的私有仓库答 404;AWS IAM 策略模拟器是特权调用)。C 与 A 都让「披露与否」只看提问者的读门答案,终态一致;C 不把自解释和门的结果拆开,A 会在「能写不能读」的权限组合上让解释与门不一致,B 留下一个已知探针并写进协议。
  • 实际业务需求: 拉动是实测的:已上线的 Studio「为什么这个用户能访问」面板,提问者正是 manage_users 管理员与 D12 委派管理员,他们的数据可达范围是自己的(裁决 A 的前提)。被拒读的提问者是这块面板上的真实情形,不是假设。
  • 防 AI 犯错: 出错时谁看到什么:B 下,一个没有读权的管理员(或代它调用的 agent)静默拿到「这个 id 存在」的信号,没有任何拒绝;A、C 下它拿到与不存在的 id 完全一样的答案,不泄露。A 的副作用是解释对「能写不能读」的用户说「没有这条记录」,而门实际放行:那是解释说错,会把排障的人带偏。
  • 创业阶段不扩散: 三者都不加新面、不加门禁;B 最省,但把一个探针写成协议;A、C 是同一 PR 里十几行代码。

Recommendation, fallback, confidence gap

  • Recommended: C. Fallback: A.
  • On ① alone, C: two years on, the explain door should be "the asker learns only what its own doors would tell it", as on the platforms above. C is that rule exactly. A is that rule for third-party askers, but over-applies it to self-explanation, and B is not that rule.
  • Confidence gap: the edit-without-read side effect that separates A from C is the dev's reading, not measured by this seat. If no permission set can grant a by-id write without read, A and C give the same answers, and A is simpler.
  • 自检:只看①选 C;②③④ 是否翻转:否(② 支持关闭探针,不区分 A/C;③ 偏 C,A 有解释说错的副作用;④ 三者成本都小)。

After the ruling

os-decision-facets
① 项目长远合理性:C/A 让披露规则只有一条(提问者的读门答没答出这一行),缩小特例;B 把一个已知探针写进协议,扩大特例。
② 实际业务拉动:已上线的 Studio 访问说明面板,提问者正是可能没有对象读权的管理员与委派管理员;今天就撞得上。
③ 防 AI 犯错:A/C 下无读权的提问者得到与不存在 id 一模一样的答案(无泄露);B 下静默泄露存在性;A 另有解释说错的副作用。
④ 创业阶段不扩散:三者都不加键、不加门禁;A/C 约十几行加 pin,B 只改文字但留下永久的已知例外。

Prior rulings read: "explain asker", "recordAbsentToCaller refused", "existence signal explain" → 0 relevant hits; #22795 A 6107185582; #21771 A 5985885287; absentUnderCallerRead / readUnlessRefused docblocks (the #7505 three-outcome rule); thread: #22792 through 6108485513 and the dev report.

Dedupe (MCP search_issues, closed cards included): "explain asker refused read grant existence signal record story administrator explaining another user" → 0 hits; "recordAbsentToCaller declared 4xx refusal counts as can read explain disclosure" → 12 hits, none this question.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 11, 2026

    @objectstack-fleet
    ContributorAuthor

    Ruling: batch S2-1 item 1 · letter C · maintainer 「同意C」 2026-10-11T14:25Z

    domain:services seat 2 (#21118) · session_013LbZ9MhPp1iriZEtgJqioA, writing as objectstack-fleet[bot] via the relay. Provenance: the maintainer's word, verbatim 「同意C」, in this seat's session chat, answering batch S2-1 (this card alone), which the seat presented in the director's decision format at the maintainer's request (「22851 按总监决裁格式呈给我」). Freshness gate: the body and its 0 comments were re-read before this record. Thread-read: none on this card; #22792 through 6109818307.

    The ruling

    C. A refused read counts as "cannot read" only when the asker is not the explained principal. When an administrator (manage_users) or a D12 delegated administrator explains another user, an asker whose own by-id read of the record is refused (a declared 4xx, for example no read grant on the object) gets, for every operation, exactly what an id no row carries gets, as an asker the read door returns no row to does. A principal explaining itself keeps the write path's notion (recordAbsentToCaller: absent means no row), so a self-explanation shows what that principal's own doors would do.

    Execution

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsdomain:servicespriority:p2Medium: important, M3security

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions