Skip to content

[Decision] should a file field's sys_file metadata (name, size, type) follow the holding record's read, as the download door and attachment rows already do? (access half of #22593) #22624

Description

@objectstack-fleet

Ruled: 6094990917 · letter B · 2026-10-10T07:12Z

Filing gate: ② a decision only the maintainer can make. This is the access half of #22593, which that card sends to the maintainer: whether a file field's sys_file metadata (name, size, type) should follow the holding record's read, the way the download door and attachment rows already do. Answering yes loosens sys_file's read boundary. Filed by domain:engine seat 2 (seat post #20966) · session_01Bw3y2DWhT9RPnrmDsNqEVG, from the read-only measurement in #22593's os-dev-report (access_half, 6094498292). ⛔ Not graded or routed here; ⛔ not a claim. Ruled B (6094990917); carrier: the domain:engine seat on #22593.

Who acts on it: the maintainer answers one letter; triage grades it; the domain:engine seat carries the answer. #22593's loud half (PR #22620) does not wait for it. It marks a refused hydration { id, metadataRefused: true } instead of a bare id that reads as "no file".

维护者速读

一句话问题

能读一条记录的人,能不能看到这条记录文件字段里文件的名称、大小和类型?

Background (read on faf689872c, from #22593's access_half; read-only, nothing changed)

  • sys_file read today.
    • SystemFile (service-storage/src/objects/system-file.object.ts:21) declares no access block and no sharingModel; it is tenant-scoped.
    • The read is the object CRUD gate PermissionEvaluator.checkObjectPermission('find', 'sys_file', …) (plugin-security/src/permission-evaluator.ts:205). It passes only when a set grants allowRead on sys_file or '*'.
    • Shipped sets: '*' read in admin_full_access, organization_admin and viewer_readonly. member_default, the everyone baseline, names neither, so a plain member has no sys_file read unless an application set grants it.
  • The hydration. ObjectQL.resolveFileReferences (objectql/src/engine.ts) reads the referenced sys_file rows in one batch, as the caller. A refusal arrives as PermissionDeniedError (403) for the whole sub-read, measured on the real stack.
  • The download door (service-storage/src/storage-routes.ts, GET /storage/files/:fileId and /url).
    • It reads the file by id under the system context, and never consults the caller's sys_file read.
    • authorizeDownload lets acl: 'public_read' through anonymously. A file with no scope and no field owner needs only a session.
    • Otherwise it goes to buildFileReadAuthorizer (storage-service-plugin.ts:1210):
      • the owner is allowed;
      • a field-owned file (ref_object + ref_id) is allowed if a declared fileAccessDelegate says so, else if the caller can read that record;
      • otherwise the file is allowed when any sys_attachment parent is caller-readable.
  • Attachment rows. installAttachmentReadVisibility (service-storage/src/attachment-access-hooks.ts:758) ANDs a caller-readable-parent filter into every sys_attachment read, behind an object grant.
  • The asymmetry. The same member can be allowed the bytes of a field-owned file and refused its name, size and type.

Governing text:

选项 × 真实代价

选项 做什么 客户感受到的后果
A 维持现状:元数据只给有 sys_file 读权限的人;想让成员看到文件名,应用自己在权限集里给 sys_file 读 成员看到「文件(详情无权查看)」,点开仍能下载;每个带文件字段的应用都要记得多授一项权限,忘了就是今天 hotclm 的样子
B 字段水合按下载入口的同一条推导规则读元数据:字段所属的文件(ref_object/ref_id 与这条记录一致),调用者能读这条记录就水合;其他情况仍按 sys_file 读权限;直接查询 sys_file 不变 能读记录的人看到完整的文件名、大小、类型,与能下载的范围一致;不在该记录名下的文件 id(例如从别处拷来的)照旧标为无权查看
C 给 sys_file 本身加一条行级规则(类似附件行跟随父记录),并让默认成员权限集带上 sys_file 读 直接查询 sys_file 也跟着开放到父记录可读的范围;改动面最大,要动默认权限集与新的行级闸门

业务含义直译:

  • A:你能拿到文件,但信封上的名字被涂掉;要看名字得另外申请。
  • B:你能拿到哪份文件,就能看到那份文件的名字。
  • C:不仅看得到自己记录里的文件,还能直接翻整个文件柜里你能看的那些。

os-decision-facets

Prior rulings read: sys_file read, file field authorization, attachment parent visibility → ADR-0104 D3 (parent-derived read for field files), #22431 (closed: an unowned file needs a session; public_read stays anonymous), #22455 (closed: the attachment write gate), #10702 (closed: sys_file metadata writability). Thread: #22593, #22590. None rules the hydration's read.

推荐

B:元数据的读权限与下载入口同源。

  • 终态句: 两年后,一个文件「谁能看」只有一个答案:能读持有它的记录,就能看它的名字和内容;直接浏览文件表仍需要文件表权限。主流平台也是这样:Salesforce 中,对记录有读权限的用户能看到挂在记录上的文件(ContentDocumentLink 从父记录推导共享),不需要单独授予文件对象的权限。
  • 自检: 只看①选 B;②③④ 是否翻转:否。
  • 回退: A(现状加 fix(objectql): a refused sys_file read marks the file field refused instead of reading as no file #22620 的显式标记)可以接受;C 不推荐,它新增闸门。
  • 置信缺口:
    • 「同一个人能下载却看不到名字」是读码结论,未用真实请求两端对照实测;
    • 行级收窄(对象可读、但部分行被过滤)的情形未测;
    • 是否有部署刻意依赖「成员看不到文件名」,测不到。

裁后执行

Dedupe: MCP search_issues, repo-scoped, open and closed, two queries:

Dedupe words: sys_file metadata read parent-derived · file field hydration refused name size · download door vs hydration authorization asymmetry

Activity

  1. objectstack-fleet commented on Oct 10, 2026

    @objectstack-fleet
    ContributorAuthor

    Ruling: batch #311 item 2 · letter B · maintainer 「同意」 2026-10-10T07:10Z

    Director seat, summon #36, session_019fWAt2renophxLVg5aJXMH (GitHub hotlong; written as objectstack-fleet[bot] via the relay). Batch #311 (three cards, this one second) was presented in chat with this seat's own readings and the recommendation B, fallback A; the maintainer answered 「同意」, which takes the recommendation. Freshness gate: the body (9,371 bytes, unedited since filing) and the comment list (none) were re-read before this record. Thread-read: none on this card.

    The ruling

    • B — a file field's metadata follows the same parent-derived verdict the download door already applies. When the caller's own sys_file object read is refused, resolveFileReferences still hydrates a referenced file's { id, name, size, mimeType, url } for exactly the files whose ref_object / ref_id name the record being read, judged by the rule buildFileReadAuthorizer applies to the bytes (storage-service-plugin.ts:1237-1265): a declared fileAccessDelegate decides when the owner object names one, else the caller's read of that record decides. At hydration that record is the one the caller has just read, so the verdict reduces to "the file's reference points at this record", with the delegate consulted where declared. A referenced id that does not point at the record being read keeps fix(objectql): a refused sys_file read marks the file field refused instead of reading as no file #22620's refused marker ({ id, metadataRefused: true }). Direct queries of sys_file are unchanged; public_read is unchanged.
    • ⛔ Not taken: A (the status quo plus the loud marker: two contradictory rules for one file, and every AI-authored application with a file field has to remember an extra sys_file grant or ship the hotclm symptom) and C (a row-level rule on sys_file plus a default-set change: a new gate and a new default grant, where B needs one engine seam).

    Why B on the first axis

    ADR-0104 D3 already rules it: "field-referenced files get parent-derived read checks, reusing the attachments authorizeFileRead verdict model", and the expanded form "is produced at read/expand time from the sys_file row". The download door implements D3; the hydration did not. B finishes the record's one verdict model instead of adding a second, and the population that gains the name, size and type is exactly the population that can already fetch the bytes, not one person more. Salesforce models it the same way: a record's reader sees the files linked to it (ContentDocumentLink) without a grant on the file object.

    Prior rulings read: ADR-0104 D3; ADR-0110 D3 (the loud half, PR #22620); #22593 (the loud half's card, pm:dispatched); #22431, #22455, #10702 (closed; none rules the hydration's read); thread: none. 自检:只看①选 B;②③④是否翻转:否。

    State and execution


    Generated by Claude Code

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions