Skip to content

fix(documents): linked receipts follow the documents right - #1424

Merged
ulsklyc merged 6 commits into
mainfrom
fix/document-links-follow-access
Sep 22, 2026
Merged

ulsklyc merged 6 commits into
mainfrom
fix/document-links-follow-access

Conversation

@ulsklyc

@ulsklyc ulsklyc commented Sep 22, 2026

Copy link
Copy Markdown
Owner

Follow-up to #1413 for the rest of the document links.

Problem. Budget entries, shared expenses (receipts and settlement proofs) and inventory items returned the name and id of every linked document to anyone who could read the owning module - also to members with documents: none and to API tokens without documents:read. The path guard judges by the first segment (/budget, /inventory) and never asks about documents. Linking worked the same way, and its answers differed between a real and an unknown id.

Fix. One decision, in one place: mayReadDocuments(req) / documentViewer(req) in server/services/document-links.js (member right and token scope via hiddenModulesFor). Every function there now takes that viewer instead of a bare user id, and housekeeping's receiptAccess and new-link branch use the same helper - no second copy of the rule.

  • Read: without the right each link comes masked (document_id, name, original_name, mime_type, file_size are null), the per-document visibility filter stays as before. The inventory history drops document rows then. A settlement's proof_document_id in the POST and reverse responses is null unless the caller may read that document (the reverse response also leaked a private proof id to a group manager).
  • Write: without the right a non-empty attachment_document_ids or a proof_document_id throws DocumentLinkRefusedError -> the same 403 body for every id (stored, visible, unknown, or in the deletion window), before visibility and deletion-lock checks and before any row is written. An empty list or a missing field keeps everything. Housekeeping's new-link 403 now uses the same shared body.
  • Frontend: attachmentLinksNode (budget and split detail views) shows "Attached" (documentAttach.presentHidden, same wording as housekeeping.receiptPresent in all 24 locales) for a masked link. The inventory detail view had no documents-right check and would have linked to /documents/null/preview. It now follows the same rule: no row at documents: none, "Attached" for a masked link.
  • OpenAPI: shared DOCUMENT_LINKS_READ_NOTE plus a documentLinkRefusal 403 on the write operations. The reverse schema notes the nullable proof_document_id. CHANGELOG and SPEC are updated.

Tests (red on main, green now).

  • test:budget-attachments, test:split-expenses-attachments, test:inventory-item-documents: session with documents: none and token without documents:read, read (list, detail, PUT/POST response, history) and write (POST/PUT with valid, stored and unknown id, and a locked id, give an identical 403 and nothing is created; [] keeps the links). Also a settlement proof, and a reverse by a manager who cannot see the private proof. Red on main: masked fields came back filled (document_id: 20, name: 'Kassenbon Recht'), POST with a receipt was 201 instead of 403.
  • test:budget-readonly-ui: masked link shows "Attached" in budget, split and inventory, and nothing at documents: none. Red with the old document-attach.js and the old inventory mapping.
  • Counterproof: with mayReadDocuments() returning true, 10 tests go red, including 3 of the fix(housekeeping): receipt name follows document access #1413 housekeeping tests.

Suites run: housekeeping-routes, budget-attachments, budget-entries-routes, split-expenses-attachments, split-expenses-routes, split-settlement-reversal, inventory-item-documents, inventory-items-routes, inventory-service-log, inventory-tracked-dates, document-folders, budget-readonly-ui, module-write-access, module-readonly-ui, housekeeping-ui, openapi-structure, openapi-coverage, api, changelog, i18n, i18n-translated, frontend-audit, budget-routes-scope, search-permissions, dashboard-permissions, suite-chain.

Not in this PR: server/routes/tasks.js (GET/PUT /tasks/:id/documents, document_count) has its own document-link code outside document-links.js and the same missing module check.

Refs #1358

Ulas Kalayci added 2 commits September 22, 2026 09:50
Budget entries, shared expenses, settlement proofs and inventory items
delivered the name and id of every linked document to anyone who could
read the owning module, also with `documents: none` and to tokens without
documents:read. The documents-read decision (member right and token scope)
now lives once, as mayReadDocuments()/documentViewer() in
services/document-links.js, and housekeeping uses it too.

Without it links come masked (id and name null), the item history drops
them, a settlement's proof_document_id is null, and any non-empty
attachment_document_ids or proof_document_id is the same 403 for every id,
before visibility and deletion lock. The detail views show "Attached"
instead of a link into nothing.

Refs #1358
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@claude

claude Bot commented Sep 22, 2026

Copy link
Copy Markdown

Code review

No issues found. Checked for bugs and CONTRIBUTING.md compliance (this repo has no CLAUDE.md; its contribution rules live in CONTRIBUTING.md on main) at commit 14d6fc7.

@claude

claude Bot commented Sep 22, 2026

Copy link
Copy Markdown

Code review

This push only merges in unrelated upstream changes (the split-expenses ledger-author-on-account-deletion fix for #1309: server/db.js, server/routes/split-expenses.js, server/openapi/paths/splitexpenses.js, related tests/docs/CHANGELOG) — the document-links diff itself is byte-identical to the previously reviewed commit 14d6fc7, so this state (8054436) brings nothing new for this PR's actual subject.

Ulas Kalayci added 2 commits September 22, 2026 10:26
A new receipt link without documents access must answer the same 403 for
an unknown or deleted id and for a document in the deletion window as for
an existing one. Goes red when the refusal in
assertDocumentLinkTargetsAvailable() moves behind visibility and lock.

Refs #1358
@claude

claude Bot commented Sep 22, 2026

Copy link
Copy Markdown

Code review

No issues found. Compared to the last-reviewed state (8054436), this push adds one commit relevant to the PR's actual subject — 47bb132f (a housekeeping test verifying that an unknown, deleted, or lock-window document id refuses identically to a stored one when linking a new receipt) — which I reviewed and found correct and consistent with assertDocumentLinkTargetsAvailable()'s existing ordering (refusal before visibility/lock checks). The rest of the diff merges unrelated upstream changes (#1414, kitchen shopping-transfer gating: server/scopes.js, server/index.js, public/utils/kitchen-transfer.js, public/utils/module-access.js, public/pages/meals.js, public/pages/recipes.js, public/pages/shopping.js, related tests). Checked for bugs and CONTRIBUTING.md compliance (this repo has no CLAUDE.md; its contribution rules live in CONTRIBUTING.md on main) at commit abe792b.

@claude

claude Bot commented Sep 22, 2026

Copy link
Copy Markdown

Code review

Compared to the last-reviewed state (abe792b9), this push (f453d0d9) adds no new commits to this PR's own branch — it is only a merge of main that pulls in unrelated upstream changes (the sign-out-other-sessions / personal-account session feature: server/auth.js, public/settings/pages/personal-account.js, server/openapi/paths/auth.js, related tests, locales, CHANGELOG, SPEC), so there is nothing new to review here.

@ulsklyc
ulsklyc merged commit e242103 into main Sep 22, 2026
6 checks passed
@ulsklyc
ulsklyc deleted the fix/document-links-follow-access branch September 22, 2026 08:48
mingzhao2019 pushed a commit to mingzhao2019/yuvomi that referenced this pull request Sep 22, 2026
)

* fix(documents): linked receipts follow the documents right

Budget entries, shared expenses, settlement proofs and inventory items
delivered the name and id of every linked document to anyone who could
read the owning module, also with `documents: none` and to tokens without
documents:read. The documents-read decision (member right and token scope)
now lives once, as mayReadDocuments()/documentViewer() in
services/document-links.js, and housekeeping uses it too.

Without it links come masked (id and name null), the item history drops
them, a settlement's proof_document_id is null, and any non-empty
attachment_document_ids or proof_document_id is the same 403 for every id,
before visibility and deletion lock. The detail views show "Attached"
instead of a link into nothing.

Refs ulsklyc#1358

* test(housekeeping): unknown, deleted and locked receipt ids refuse alike

A new receipt link without documents access must answer the same 403 for
an unknown or deleted id and for a document in the deletion window as for
an existing one. Goes red when the refusal in
assertDocumentLinkTargetsAvailable() moves behind visibility and lock.

Refs ulsklyc#1358

---------

Co-authored-by: Ulas Kalayci <ulas.kalayci@icloud.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant