Skip to content

finding: an approver routed a request can be unable to open the record it concerns — the inbox's record link dead-ends on "Record not found" #7345

Description

@os-zhuang

Observation-class finding recorded during the #7331 browser verification run. Filed unassigned, no pm:queue — for triage to grade. It is not a routing regression from #7213; the routing half was verified correct and is stated below so the next reader does not re-derive it.

What happens

A non-admin who is a pending approver opens the Approvals Inbox, sees the request, and clicks the record chip in its row (INV-1002). The record page renders "Record not found — The record you are looking for does not exist or may have been deleted."

The record exists and the request is live.

The routing is correct — that part was ruled out first

The rendered href is exactly what #7213 intended: it carries the current app's segment, the right object and the right record id.

INV-1002 -> /_console/apps/com.objectstack.account/showcase_invoice/record/9oAya50sd_E-PEWH

Loading the same URL shape under the same app with a record the persona can read renders the record page normally (verified with showcase_project/record/1YtAy4jz7Wsi9m9k under com.objectstack.account). So the account app can host a record page for a foreign business object; nothing about the app segment is wrong.

The real cause: record-level visibility

The authoritative server answer for that persona is a 404, not a 403:

GET /api/v1/data/showcase_invoice/9oAya50sd_E-PEWH
404 {"error":"Record 9oAya50sd_E-PEWH not found in showcase_invoice","code":"RECORD_NOT_FOUND"}

The persona holds contributor, whose showcase_invoice grant is owner-scoped; the invoice is owned by another user, so it is filtered out of the row set before the by-id read. The same read as admin returns the record. Transferring owner_id to the persona did not change the outcome either (the seeded showcase rows carry organization_id: null), so the showcase fixture makes this easy to hit — but the general shape is not showcase-specific: approver resolution routes on positions, record visibility is a separate gate, and nothing reconciles the two.

Why it is worth recording rather than shrugging at

The drawer degrades well: it carries the request's payload snapshot (record title, object, subtotal, account, status), so an approver can decide without opening the record, and the decision itself works — the run approved this exact request from the drawer successfully. So this is not a blocked approval.

What is unsatisfying is the affordance: the inbox offers a record link that, for the person it is offered to, leads to a page saying the record may have been deleted. The message is also the least useful of the available truths — the honest one is "you do not have access to this record", which the platform renders elsewhere (sys_inbox_message renders exactly that for the same persona).

Candidate dispositions (not decided here)

  1. Leave it. Approvals decide from the snapshot by design; record access is the security model's business. Cheapest, and defensible.
  2. Suppress the record link in an inbox row when the viewer cannot read the target — needs a per-row readability probe the inbox does not do today.
  3. Keep the link and fix only the landing message: distinguish "not visible to you" from "does not exist" on the record page. Smallest user-facing improvement, no security change.
  4. Grant an approver implicit read on the record under approval for the life of the request. The largest surface by far — it makes approver routing a source of record visibility, which is a real security decision and should not be taken as a UX fix.

No option recommended; 4 in particular is a security-model change, not a polish item.

Evidence

Measured 2026-08-10, examples/app-showcase, framework 88154bee1, console dist stamp 8aad9fd50b16750aebdd294392ff79540f17cd32. Non-admin persona holding contributor + finance. Screenshots of the "Record not found" landing, of the same URL shape rendering a readable record, and the paired API reads were captured in the run.


Generated by Claude Code

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions