Skip to content

fix(fields): a lookup whose record read is refused draws the restricted-record placeholder, not a linked raw id - #29708

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-98b1gk
Nov 3, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-98b1gk

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Steedos · project thread

Fixes #29641

Clause-②: no
Metadata-protocol: none

Claim: 6592434215

维护者速读

  • 改了什么:lookup 字段单元格在读取被引用记录时,如果服务端以「无权限」拒绝(403 或 PERMISSION_DENIED),现在显示「受限记录」占位:不显示原始 id、不带链接,与「记录不存在」(404)同一外观;拒绝结果会缓存,同一记录只读一次。网络错误、5xx、401 等其他失败保持原来的「未解析」外观(id + 问号)。
  • 为什么改:目标对象的读权限拒绝读者时,服务端对记录读取回 403 而不是 404,单元格把它当成读取失败,画出带链接的原始 id,点进去是一个拒绝访问的页面,看起来像坏数据。
  • 风险与回滚:只改 useLookupName 的 catch 分支,判定复用 @object-ui/react 已有的 classifyLoadError(forbidden),不改数据适配器、不改 findOne 的返回值;404 占位、可读记录、在途读取的外观全部不变。回滚即 revert 本 PR。
  • 先看:ui/packages/fields/src/index.tsx 的 useLookupName catch 分支与 refused 缓存状态;新测试 lookupCell.forbiddenRead-29641.test.tsx。
  • 两分钟验证:pnpm --dir ui exec vitest run packages/fields/src/__tests__/lookupCell.forbiddenRead-29641.test.tsx,预期 12 个用例全部通过。
  • 需要你做的:无,席位落地。

What changes

A lookup cell names its referenced record by reading it as the viewer (useLookupName, ui/packages/fields/src/index.tsx). objectui#13335 drew a record the read answered "not found" (findOne answers null, the adapter's 404) as "Restricted record", with no id and no link. When the target object's read gate refuses the viewer, the server answers the same read with 403 PERMISSION_DENIED, which the adapter throws, and the hook caught every throw as a failure: the cell drew the raw id beside a question mark, inside a link to a record page that refuses the viewer.

The viewer's read of the referenced record Before Now
answers null (404, hidden by sharing or missing) Restricted record, no link unchanged
throws a refusal: status 403 (httpStatus, status or statusCode) or code PERMISSION_DENIED / FORBIDDEN the id with a question mark, linked Restricted record, no id, no link; cached, read once
throws anything else (network, 5xx, 401) the id with a question mark unchanged
  • The refusal is read through @object-ui/react's classifyLoadError (forbidden), which @object-ui/fields already depends on and imports; no new predicate and no message matching. The lookup cache gains a refused state, which the hook answers as absent, so LookupCellRenderer is unchanged apart from a comment.
  • Measured live before the edit (pnpm dev:crm, a member refused rnv_payroll): the adapter's findOne throws a plain Error whose own members are httpStatus: 403, code: 'PERMISSION_DENIED' and details (error, code, object), with no status / statusCode; classifyLoadError answers forbidden on it. A missing id under the same object answers the same 403, so the placeholder discloses nothing a not-found would not.
  • user cells: unchanged. UserCellRenderer never reads the referenced person (a bare id is UnresolvedUserReference), so it has no read answer to classify.
  • Not changed: @object-ui/data-objectstack and what findOne returns.

Tests

  • ui/packages/fields/src/__tests__/lookupCell.forbiddenRead-29641.test.tsx (new, 12): the measured 403 draws the placeholder with no id in text or attributes, no anchor, one findOne and no find; the four single spellings (httpStatus, status, statusCode 403, code PERMISSION_DENIED) each draw it; the refusal is cached (a second cell for the same id draws it with no read) and a 502 on another id afterwards keeps the unresolved face; in a multi-value cell only the first, read entry is restricted. CONTROLS: a null answer keeps the placeholder; a 500 with a code, a 503, a network TypeError and a 401 each keep the unresolved face with their id.
  • Unchanged and green: lookupCell.restrictedRecord-13335.test.tsx, lookupCell.referenceNameVisible-29462.test.tsx, the other lookupCell files and DetailSection.referenceNameVisible-29462.test.tsx: 12 files, 97 tests.
  • Ablations (scripts/ablation-replace.mjs, anchor 1 to 0, restored to the HEAD blob, git diff HEAD empty), run on the new file plus the [finding] skills/**/references/_index.md is generator-owned but not routed to merge=os-regen — two branches that regenerate it text-merge instead #13335 file: A1 the catch records every throw as err: 7 failed / 14 passed; A2 the refused entry no longer answers absent: 7 failed / 14 passed. The 7 are exactly the refusal cases; every control stays green.

Acceptance notes

  • Seat additions honoured: the cache pin (refusal cached like a not-found, no cross-key bleed to a later transient failure) is the third test above.

  • The cell's useRefObjectSchema reads the target's object definition (/meta/object/rnv_payroll) before and after this change alike; it is not part of the record read and is left as it is.

  • Local gates on the final head 214f2289 (main merged twice; none of the merged commits touches ui/packages/fields): dispatch-gates --commands derives 35; all 35 exit 0 (33 in an unlocked loop, check:type-check-debt and pnpm --dir ui exec vitest run scripts/ (170 files) under the verify lock); --ran: 35 derived, 35 run, 0 NOT-MEASURED. The same 35 also exit 0 on the earlier head 647dc13a. ui/scripts/check-spec-symbol-derivation.mjs exit 0 (PR spec(data): the name-only reference value a refused reader is served has a named schema, and referenceNameVisible says which name it carries #29623 edits the same file's ReferenceNameOnlyValue; this diff stays clear of it). turbo run type-check --filter=@object-ui/fields exit 0 (48 tasks). @object-ui/fields suite: 296 files passed, 1 skipped, 4449 tests. eslint on the two touched ui files: 0 errors. The repo-wide sweep is CI's.

Verification

Live, pnpm dev:crm -- --fresh plus the source Console (Vite), a member refused the payroll object
  • Setup is the ui: render a referenceNameVisible lookup's name as plain text, and add its Studio form row (#29171 ruling B) #29462 recipe: a scratch package com.example.rnv with rnv_payroll (private, admin-owned) and rnv_expense (public_read) carrying Payroll (no key), a lookup without referenceNameVisible; a member granted read on rnv_expense only, impersonated. GET /api/v1/data/rnv_payroll/ID as the member: 403 PERMISSION_DENIED.
  • Before (main fc558923): list and record page each draw Payroll (no key) as the unresolved face with the id, inside a link to rnv_payroll (1 anchor).
  • After (this branch): list and record page each draw "Restricted record" for it, not inside a link, and no anchor to rnv_payroll remains. The referenceNameVisible fields still draw their served names as text. Screenshots and the per-page readings are in the project folder (issue-29641/shots/), not on GitHub.

Generated by Claude Code

…ed placeholder

useLookupName now records a read the reader is refused (classifyLoadError
answers 'forbidden': status 403 or PERMISSION_DENIED / FORBIDDEN) as a
settled answer, read as absent, like the adapter's null for a 404. Every
other failure stays 'err' and keeps the unresolved face.

Claude-Session: https://claude.ai/code/session_01QsR2jRxjiKm4UH4VWn45pF
Co-authored-by: Claude <noreply@anthropic.com>
…ct-definition read

Claude-Session: https://claude.ai/code/session_01QsR2jRxjiKm4UH4VWn45pF
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tooling labels Nov 3, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#3 · zhuangjianguo · session_01QsR2jV4dWohojjbTJTQq5m

席位核对 · ACCEPT · 2026-11-03T02:31Z

Head-sha: 214f2289f98d4e7be3086585c5e8d0f0054e7a5f

  • Shape: draft → main, Fixes #29641. 3 files, +219 / −6. No governed path: check-governed-merges --pr 29708 reports NOT governed. Clause-②: no and Metadata-protocol: none are line-initial in the body and in the changeset (patch for @object-ui/fields), as claimed in 6592434215.
  • Contract review: none owed (Clause-②: no). No export changes, and classifyLoadError is an existing @object-ui/react export that fields already depends on.
  • File surface: inside the claim: fields/src/index.tsx, the new test lookupCell.forbiddenRead-29641.test.tsx, and the changeset. The edits stay clear of the ReferenceNameOnlyValue block that PR spec(data): the name-only reference value a refused reader is served has a named schema, and referenceNameVisible says which name it carries #29623 rewrites; the only import change is classifyLoadError added to the existing @object-ui/react line.
  • Read in the diff:
    • useLookupName's catch stores { state: 'refused' } when classifyLoadError(err) === 'forbidden' (status 403, or code PERMISSION_DENIED / FORBIDDEN), and { state: 'err' } otherwise.
    • A refused entry answers { state: 'absent' }, so LookupCellRenderer's existing restrictedId path draws objectui#13335's placeholder, with no id and no anchor.
    • A refused entry carries no promise, so the effect's guard does not re-read it, the same lifetime as err. No other site reads lookupNameCache.
    • Measured by the dev: the live 403 is a plain Error with httpStatus: 403 and code: 'PERMISSION_DENIED'. An unknown id under a gated object also answers 403, because the object gate runs first, so nothing leaks existence.
    • UserCellRenderer never calls useLookupName, so the user leg has nothing to change; the PR says so.
  • Tests (dev):
    • 12 cases: 403 and code-only refusals give the placeholder with no anchor. CONTROLs: 404/null is unchanged; 5xx, network and 401 keep the unresolved face. The refusal is cached (one read), and a transient error on another id stays err (no cross-key bleed).
    • Ablations A1 and A2 each go red (7 failed).
    • The fields suite is green (296 files, 4,449 tests). 35 of 35 derived gates exit 0, as do check-spec-symbol-derivation and the fields type-check.
  • Measured (dev): live on dev:crm, a member refused rnv_payroll. Before, the list and record showed the id inside a link. After, they show "Restricted record", with 0 anchors to rnv_payroll. Shots: /mnt/project-files/issue-29641/shots/. Report: 6593676465.
  • Gates (check-runs on this head): 44 success, 11 skipped (opt-in and schedule gates), 0 failed, 0 pending.
  • Landing: ordinary queue landing, pr_ready then automerge_enable.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 3, 2026 02:32
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge November 3, 2026 02:32
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 3, 2026
Merged via the queue into main with commit b824a66 Nov 3, 2026
56 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-98b1gk branch November 3, 2026 02:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Console: a lookup to a record whose object the reader may not read shows a linked raw id, not the restricted-record placeholder

1 participant