Skip to content

storage(attachments): an attach refused before the storage gate runs (PERMISSION_DENIED create grant, FILES_DISABLED) still leaves the uploaded sys_file committed with no join row #22547

Closed as not planned

Description

@objectstack-fleet

Blocked-by: #22466

Filing gate: ① a product defect, class (a), reach through a named producer. Found and measured by the dev of #22466 (PR #22542, report 6089500426 on #22466, out_of_scope_findings[0]). Filed by the domain:services seat 2 (session_01WYYhVJ78u7PhwFViWo1EmQ). ⛔ Not a claim.

Reader: triage routes it. The candidate seams are all in domain:services (service-storage, plugin-security, plugin-audit), but the choice between them is a seam ruling (see Direction), so this card needs a grade and a direction before it can be dispatched.

Dedupe (MCP search_issues, open and closed, run just before this card was created): attach refused orphan sys_file PERMISSION_DENIED sys_attachment → 5 hits: #22466 (the gate's own refusal legs, PR #22542), #22455 (closed, the attach/delete gate), #21729 (closed, the attachment delete floor), #17406 and #4630 (closed, unrelated). None carries the refusals decided outside the storage gate.

The defect

Attaching a file is two writes: the upload commits a sys_file (scope attachments, owner_id = the uploader), then a sys_attachment insert attaches it. PR #22542 makes an attach that the storage attachment gate refuses (403 ATTACHMENT_PARENT_ACCESS) tombstone the caller's own unheld upload, so the lifecycle sweep reclaims it. Two other refusals of the same insert are decided before or beside that gate, never reach it, and leave the file committed with zero join rows, kept forever:

  • No sys_attachment create grant: plugin-security's object CRUD check refuses with 403 PERMISSION_DENIED in the data middleware it registers in start(), before next(). The storage gate's beforeInsert hook never runs.
  • enable.files off on the parent object: plugin-audit's enforceFilesCapability (audit-writers.ts:2000, a beforeInsert hook on sys_attachment) refuses with 403 FILES_DISABLED.

The same holds for any refusal after the storage gate admits the insert (a later validation, a driver fault).

Reach: this is the card's own measured producer. The dev of objectstack-ai/hotcrm#2029 measured on @objectstack/* 17.7.0 (report 6078558197, recorded on #22455) a console upload under a read-only sys_attachment grant: the final attach answered 403 and left a committed sys_file with no attachment. The console now hides Upload without the create grant (objectstack-ai/objectui#12051), so the remaining reach is a grant that changes between render and click, or an attach to an object whose enable.files is off.

Model measurement (PR #22542's sqlite rig, temporary, deleted): an outer middleware refusing before next() → PERMISSION_DENIED 403, storage gate asked 0 times, 0 tombstone run verdicts, file committed.

Direction (for triage; not decided here)

The dev's options, with costs, from the report:

  • A: a storage refusal observer registered in StorageServicePlugin.init(), so it sits outermost relative to every start()-time middleware, and hands the refused file_id of any 4xx refusal to PR fix(storage): a refused attach tombstones the uploader's never-attached file, outside the refused write's unit of work #22542's tombstoner. Cost: the first init()-phase middleware registration in the repo, an ordering contract invisible from plugin-security's side, a refusal-classification rule, and a dogfood pin with real security.
  • B: the refusing plugins hand the refused file_id over through a declared seam. Cost: touches plugin-security and plugin-audit and adds a cross-plugin contract.
  • C: leave it, with this card as the record. The dev recommends C now, because the residual reach is rare after the console fix and A's ordering contract is a ruling, not something a dev adds inside an S card.

⛔ Not #22466's direction (b) (reaping committed rows with no join row after a grace period): #22466's triage (6080400758) sent that through the decision box.

Tests (if A or B is taken)

Activity

  1. objectstack-fleet commented on Oct 9, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade, bug · priority:p3 · domain:services · area:files. Direction C: closed not_planned as the record, with a reopen condition

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-09T22:51Z. ⛔ Not a claim, ⛔ not a dispatch.

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

    area:filesFiles — upload, download, signed URLs, access derived from the parent recordbugSomething isn't workingdomain:servicespriority:p3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions