Skip to content

feat(attachments): v2 follow-ups — read visibility, authed downloads, edit-on-parent, lifecycle, enforce-or-remove (#2970)#2984

Merged
os-zhuang merged 3 commits into
mainfrom
claude/attachments-v1-followups-8jhvin
Jul 16, 2026
Merged

feat(attachments): v2 follow-ups — read visibility, authed downloads, edit-on-parent, lifecycle, enforce-or-remove (#2970)#2984
os-zhuang merged 3 commits into
mainfrom
claude/attachments-v1-followups-8jhvin

Conversation

@os-zhuang

@os-zhuang os-zhuang commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Completes all of the #2970 tracking issue (follow-up to #2755 / merged #2969). Six items, verified by unit + dogfood + a real-browser pass.

1. Read visibility inheritance

sys_attachment reads (find/list/count/aggregate) now inherit the parent record's visibility, via a sys_attachment-scoped engine middleware (not a find-hook — so list total, which comes from engine.count(), is filtered identically and can't leak the hidden-row count). Generalizes ADR-0055 controlled_by_parent to the polymorphic parent: resolves visible parent ids per parent_object through the caller-scoped engine, ANDs a $or of {parent_object, parent_id:{$in}}; deny-all when none; fail-closed on error.

2. Authenticated, parent-scoped downloads

GET /storage/files/:fileId(/url) were anonymous capability URLs. For scope==='attachments', non-public_read files they now gate on authorizeFileRead: 401 AUTH_REQUIRED without a session, 403 ATTACHMENT_DOWNLOAD_DENIED when the caller is neither the owner nor able to read a parent record (full caller context via resolveAuthzContext), else a short-lived (downloadTtl, 300s) signed URL. Non-attachments files (avatars, field images embedded in <img src>) keep the stable anonymous URL.

3. Edit-on-parent for attach (Salesforce parity)

Creating a sys_attachment now requires EDIT on the parent (sharing canEdit), not merely read — public parents unchanged, private/owner-scoped require own/edit.

4. sys_upload_session lifecycle

An ADR-0057 transient lifecycle (TTL 1d past expires_at; retention 7d for terminal statuses) reaps abandoned/terminal chunked-upload sessions. Row reap only; a backend multipart-abort guard is a filed follow-up.

5. sys_attachment.enable.trashfalse (ADR-0049)

The flag is dead in the liveness ledger and deletes are hard (the reap guard reclaims bytes), so a restore would dangle — declare the honest state.

6. Showcase files-enabled object

showcase_project opts into enable.files so the Attachments panel renders for browser dogfooding.

Verification

  • Unit: @objectstack/service-storage 93 ✓ (read-visibility middleware, download gate, edit-on-parent, access hooks), platform-objects 197 ✓, objectql/rest unchanged-green.
  • Dogfood: the attachments matrix is now 15 cases (read-visibility denial, download gate anon/cross-user/owner/parent-inherited, edit-on-parent read-but-not-edit, session reap); full suite 256 ✓ / 3 skipped.
  • Real-server E2E (booted objectstack dev showcase): 15/15 — admin create/upload/attach/list, FILES_DISABLED, anon download 401, admin signed URL 200, signed URL serves the bytes, member RBAC-denied attach (grant-needed finding), member parent-inherited download 200.
  • Browser (objectui HMR console + booted server, both with these changes): the Attachments panel renders on a showcase_project detail page, lists the file, and the Download button fetches the authenticated /storage/files/:id/url signed-URL endpoint. Screenshot in the session.
  • Gates: check:liveness ✓, check:api-surface ✓.

ℹ️ The red Validate Dependencies check is the repo-wide retired-npm pnpm audit endpoint (410), unrelated to this diff.

Companion objectui PR: objectstack-ai/objectui#2538 (panel download via signed URL). Closes #2970.

🤖 Generated with Claude Code

…ity (#2970)

Follow-up to #2755. The create/delete gates landed, but a member could
still LIST sys_attachment rows (file_name, size, parent_id) pointing at
records they cannot read — an info leak, since attachment access derives
from the parent record. sys_attachment is public with no owner field, so
the sharing/RLS static predicates never narrowed it.

installAttachmentReadVisibility registers a sys_attachment-scoped engine
MIDDLEWARE (not a find-hook): middleware runs for find/findOne/count/
aggregate, so the list total (from engine.count(), never the find path)
is filtered identically and cannot leak the hidden-row count — a
before/afterFind hook would leave count() unfiltered. Generalizing
ADR-0055 controlled_by_parent to the polymorphic parent, each read
resolves the visible parent ids per parent_object through the
caller-scoped engine (the parent's own RLS/OWD/sharing apply) and ANDs a
$or of { parent_object, parent_id: { $in } } into the query; no visible
parent → deny-all sentinel; fails closed on compute error; the candidate
pre-scan cap logs (not silent) when it truncates. System/context-less
internal reads are not narrowed.

Dogfood: the (c) matrix pin flips from leak-asserted to a denial — a
member cannot list/read/by-id-fetch attachments of an invisible parent,
total does not leak the count, and the control proves they still see
attachments on records they CAN read. 19 unit tests; full dogfood 256 ✓.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0187NT3Qer9oep5dCRb9b8Lt
@vercel

vercel Bot commented Jul 15, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
spec Ready Ready Preview, Comment Jul 15, 2026 5:09pm

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/m labels Jul 15, 2026
@github-actions

github-actions Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/dogfood, @objectstack/platform-objects, packages/services.

9 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/automation/webhooks.mdx (via packages/services)
  • content/docs/kernel/runtime-services/audit-service.mdx (via packages/services)
  • content/docs/kernel/runtime-services/index.mdx (via packages/services)
  • content/docs/kernel/runtime-services/settings-service.mdx (via packages/services)
  • content/docs/permissions/authorization.mdx (via packages/dogfood)
  • content/docs/permissions/delegated-administration.mdx (via packages/dogfood)
  • content/docs/plugins/packages.mdx (via @objectstack/platform-objects, packages/services)
  • content/docs/protocol/kernel/i18n-standard.mdx (via packages/services)
  • content/docs/ui/setup-app.mdx (via @objectstack/platform-objects)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

The storage download endpoints (GET /storage/files/:fileId and
/files/:fileId/url) were anonymous capability URLs — anyone holding a
fileId could mint a download with no session or access check.

For scope='attachments', non-public_read files, both endpoints now gate
on a new authorizeFileRead seam: 401 AUTH_REQUIRED without a session, 403
ATTACHMENT_DOWNLOAD_DENIED when the caller is neither the file owner nor
able to READ a record the file is attached to (parent-derived, resolved
through the full caller context via resolveAuthzContext), else a
short-lived signed URL (downloadTtl, default 300s). Non-attachments files
(field files, avatars, org logos — embedded in <img src> which can't
carry a bearer) keep the stable anonymous capability URL; bare
kernels/tests without the seam stay open (back-compat).

Tests: storage-routes gate unit tests (401/403/allow/public/non-attach/
open); the dogfood matrix (e-read) pin flips from a leak to a denial —
anon 401, cross-user 403, owner 200, parent-inherited 200, 302 anon 401.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0187NT3Qer9oep5dCRb9b8Lt
@github-actions github-actions Bot added size/l and removed size/m labels Jul 15, 2026
…lse, showcase files (#2970 items 3-6)

- item 3: attach requires EDIT on the parent record (sharing.canEdit),
  not merely read (Salesforce parity); public parents unchanged, private
  owner-scoped parents require own/edit. Read fallback without sharing.
- item 4: sys_upload_session gains an ADR-0057 transient lifecycle (TTL 1d
  past expires_at + 7d retention for terminal statuses) so abandoned
  chunked-upload sessions are reaped. Row reap only; multipart-abort guard
  is a follow-up.
- item 5: sys_attachment enable.trash -> false. The flag is dead in the
  liveness ledger and deletes are hard (reap guard reclaims bytes), so a
  restore would dangle; declare the honest state (ADR-0049).
- item 6: showcase_project opts into enable.files so the attachments panel
  renders for browser dogfooding.
- dogfood: att_readonly (public_read) fixture + matrix cases for
  edit-on-parent (read-but-not-edit → 403, still listable) and
  sys_upload_session reap; access-hook unit tests for the canEdit path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0187NT3Qer9oep5dCRb9b8Lt
@os-zhuang os-zhuang changed the title feat(attachments): sys_attachment read inherits parent-record visibility (#2970) feat(attachments): v2 follow-ups — read visibility, authed downloads, edit-on-parent, lifecycle, enforce-or-remove (#2970) Jul 15, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review July 16, 2026 01:09
@os-zhuang
os-zhuang merged commit e1484bc into main Jul 16, 2026
17 checks passed
@os-zhuang
os-zhuang deleted the claude/attachments-v1-followups-8jhvin branch July 16, 2026 01:09
os-zhuang added a commit that referenced this pull request Jul 16, 2026
…ion guard note (#3009)

Documents the attachment access model landed across #2984/#2999/#2538
(#2970): parent-derived read/create/delete, inherited list visibility,
authenticated parent-scoped downloads, the enable.files opt-in gate, the
member_default no-delete-baseline caveat, the storage-byte lifecycle
(sys_file + sys_upload_session reap guards), and an enforcement-summary
+ error-code table. Registered in permissions/meta.json.

Also names sys_upload_session as the second reap-guard consumer in the
ADR-0057 amendment.


Claude-Session: https://claude.ai/code/session_0187NT3Qer9oep5dCRb9b8Lt

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Jul 17, 2026
…ad_session (objectstack-ai#2970)

The sys_upload_session lifecycle (objectstack-ai#2984) reaps abandoned/terminal
chunked-upload session rows but not the underlying backend multipart —
on S3 an initiated-but-not-completed multipart keeps its parts billable
and invisible until AbortMultipartUpload, so reaping only the row
stranded them (backend_upload_id, the sole pointer, gone).

createUploadSessionReapGuard registers a LifecycleReapGuard on
sys_upload_session that aborts the backend multipart before the row is
deleted: skips 'completed' sessions (multipart already an object — an
abort would NoSuchUpload-error), re-seeds the S3 uploadId->key map from
the row (cold sweeps lack the live in-process map), and vetoes (keeps
the row for retry) on abort failure so the pointer survives. Local
adapter parts dir removed the same way.

Verified: 7 new unit tests (S3 key re-seed, completed-skip, no-backend,
veto-on-failure, local adapter, no-abort adapter); service-storage 99;
dogfood matrix drives a real chunked upload → part on disk → sweep →
row gone AND parts dir aborted. Closes the last objectstack-ai#2970 sub-follow-up.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0187NT3Qer9oep5dCRb9b8Lt
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/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[attachments] v2 tracking: parent-visibility inheritance, authed downloads, remaining enforce-or-remove

2 participants