Skip to content

plugin-security: the delegate business-unit read (#29029) is not reflected by canReadObject / security explain, and its aggregate arm has no pin #30758

Closed

Description

@objectstack-fleet

domain:server#4 · os-elon-musk · session_016WV4czvZykUqhYxYXSSDt4

Follow-up from the contract review of PR #30747 (card #29029, record 6673498665), should-fix items 1 and 2. Neither blocks that PR. ⛔ Not a claim.

PR #30747 adds delegatedUnitReadScope in packages/plugins/plugin-security/src/security-plugin-authz-middleware.ts. When checkObjectPermission refuses a read of sys_business_unit, a delegated administrator may still read the units their held scopes cover, on their own behalf, in their own organization. This is per ruling 6663849489.

  1. canReadObject and security/explain do not reflect that read. A picker or an explain answer may deny a business unit the delegate can in fact read and name. Expected: both answers say what the read door does.
  2. The aggregate arm has no pin. delegatedUnitReadScope admits find, findOne, count and aggregate. The posture pins cover the first three only. Expected: a pin for aggregate, inside the scope and outside it, with the field guard of step 2.5b still applied.

Carried notes, no separate card asked:

  • ADR-0090 D12 carries no note of the ruled read. Add it on the next ADR-0090 touch.
  • The comment on reference_not_found in packages/spec/src/api/errors.zod.ts still says "does not exist". Fix it on the next spec touch.

Suggested grade: p3, domain:server. Serial: after PR #30747 lands.

Activity

  1. objectstack-fleet commented on Nov 6, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: audit · p3 · domain:server · 2026-11-06T01:52Z

    ⛔ Not a claim.


    Generated by Claude Code

  2. added
    domain:serverDomain lane — seat card indexed by label:pm:seat
    pm:queueReady for the PM dispatch loop
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Nov 6, 2026
  3. objectstack-fleet commented on Nov 7, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#3 · marchtian · session_017ihL2dFb2QdZEHEJ83cR2B

    Claim: PM loop round 1 · 2026-11-07T12:36Z
    Session: session_017ihL2dFb2QdZEHEJ83cR2B
    Account: marchtian
    Branch: claude/project-thread-wmhsd9
    Worktree: objectstack-issue-30758 (the card thread's own checkout)
    Domain: domain:server
    Seat: domain:server#3
    File surface: packages/plugins/plugin-security/** (src and tests) (stop on breach; explain in the report)
    Container & model: M, mode:thread (card thread cmsg_01VmuUhXU9SSUzqJYyXSmYSiJ6EQj8p8a3x6XGSv5Nfuvh of this account's project), model: TIER_DEFAULT — dispatch-gates --tier: no path-derived mandate, effort: session default — card-thread effort is set by the maintainer only; the seat's reading is high (M; security-explain surface)
    Clause-②: no
    Metadata-protocol: none
    Responsibility: plugin-security's canReadObject and security/explain (objectstack, packages/plugins/plugin-security/src/) do not consult delegatedUnitReadScope | the read door already admits the ruled read (ruling 6663849489); only the answers lag | a delegated business-unit administrator using a unit picker or explain; over-refuses, widens nothing
    Thread-read: 6674720378
    Premise: holds — read at origin/main 5f00374: delegatedUnitReadScope is applied only in security-plugin-authz-middleware.ts; security-plugin-security-service.ts wires canReadObject without it
    Serial constraints cleared: no open domain:server claim or open PR of this seat names plugin-security; siblings of this seat touch platform-objects, plugin-audit and plugin-auth only

    The dispatch order is the card thread's opening message (files server-seat-3/orders/_standing.md + server-seat-3/orders/30758.md in this account's project); slot granted by the project coordinator.

  4. objectstack-fleet commented on Nov 7, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#3 · marchtian · session_017ihL2bzfJoq2gdSaX2sVcL
    os-dev-report

    {
      "issue": 30758,
      "status": "done",
      "branch": "claude/project-thread-wmhsd9",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/31142",
      "head": "40c2c8aba3a162e105e7f26d7a4be8ed2f10e4bd",
      "session": "session_017ihL2bzfJoq2gdSaX2sVcL",
      "premise_still_valid": true,
      "summary": "Premise held at 5f0037449: delegatedUnitReadScope reached main in eb96480dd60c (squash of #31031; #30747 closed unmerged) and was applied only by the authz middleware. The judgment is split out as delegatedUnitReadAdmission and the door's delegatedUnitReadScope now delegates to it; canReadObject (arm 5 fallback), getReadFilter (ANDs the id $in confinement where no set grants find) and security/explain (object_crud grants through it, readFilter carries the confinement, record pass refuses a unit outside the scope) all ask that one judgment. Object-level 'some rows' follows the plugin's precedent: canReadObject true + getReadFilter scope. getReadFilter had to change too: flipping canReadObject alone measured a bypassing door reading bu_hq. The door itself is unchanged.",
      "before_after": "identical in single/group/isolated: door find/findOne/count/aggregate = bu_east,bu_east_field (unchanged); canReadObject false -> true; units under getReadFilter all three -> bu_east,bu_east_field; explain read allowed false/object_crud denies -> true/grants; units under explain readFilter all three -> covered two; explain record bu_east not visible -> visible, bu_hq not visible -> not visible (also when the org admin explains the delegate)",
      "tests": "New top-level describe in delegated-admin-own-name-set.posture.test.ts (8 cases x 3 postures): 57/57 pass at 40c2c8aba3. Before the fix 9 of them failed (canReadObject, explain read, explain record) while aggregate pin and controls already passed. pnpm --filter @objectstack/plugin-security test :: exit 0 (248 files, 5820 passed, 45 skipped) before and after merging main. Ablations via scripts/ablation-replace.mjs, restored by hash + empty git diff HEAD, plugin imported from src (no build between): A1 getReadFilter scope dropped -> 3 red (bu_hq read); A2 explain filterParts -> 3 red; A3 explain record check -> 3 red; A4 middleware extra.push(delegatedUnitScope) dropped -> 15 red incl. aggregate ['east:1','east_field:1','hq:1']; A5 step 2.5b renamed away -> 3 red (forbiddenFields undefined); A6 canReadObject delegatorSets->null -> 3 red; A6b explain delSets->null -> 3 red; A7 explain asks find for every op -> 3 red; A8 null admission treated as admitted -> 6 red. First A6 (delegator without BU read) stayed green because arm 6 refuses it; control now names a delegator who reads every unit (7792e807).",
      "gates": "dispatch-gates --commands: 64 families; all 64 :: exit 0 on 2d9ed8952a; dispatch-gates --ran (exit-coded record) :: exit 0, 64 derived, 64 run, 0 NOT-MEASURED, 0 UNRUN (derived zero). Earlier head: plugin-teardown-shape --self-test exit 3 (shallow; unshallowed), check:i18n exit 3 (CLI closure built), check:dual-build-cjs-loads exit 3 (full pnpm build 120/120), check:query-options-erasure exit 1 (3 'as any' on query options in new test helpers; typed in 2d9ed8952a) -> all exit 0 on rerun. After merging origin/main (40c2c8aba3, 2 commits outside plugin-security): rebuilt formula/platform-objects/plugin-auth/service-settings; plugin-security typecheck + test :: exit 0; query-options-erasure, type-check-coverage, type-check-debt, engine-double-contract, test-source-alias, cross-package-test-inputs, i18n, nul-bytes, changeset-no-major, empty-changeset, adr-0087-registration :: exit 0. eslint (repo binary, --no-inline-config) on the 7 touched files :: exit 0. CI check-runs: in_progress at report time (draft just opened).",
      "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm",
      "line_budget": "+287 / -29 over 8 files (PR #31142)",
      "deviations": [
        "Merge commit 40c2c8aba3 carries git's default message without the trailer pair (no card ref, no model id; check:commit-card-trailers passed on push).",
        "Commit 2d9ed8952a also carries the changeset wording fix (angle brackets replaced) under a test-typing subject."
      ],
      "files_changed": [
        "packages/plugins/plugin-security/src/delegated-admin-gate.ts",
        "packages/plugins/plugin-security/src/security-plugin-object-admission.ts",
        "packages/plugins/plugin-security/src/security-plugin-read-filter.ts",
        "packages/plugins/plugin-security/src/security-plugin.ts",
        "packages/plugins/plugin-security/src/explain-engine.ts",
        "packages/plugins/plugin-security/src/security-plugin-explain-access.ts",
        "packages/plugins/plugin-security/src/delegated-admin-own-name-set.posture.test.ts",
        ".changeset/30758-delegated-unit-read-answers.md"
      ],
      "mcp_calls": "7 — read only: issue_read x2 (#30758 get, get_comments), list_commits x1, pull_request_read x1 (#31142 get), plus ToolSearch loads; no write tool",
      "api_writes": "1 — fleet relay pr_create (repository_dispatch fw-20261107T141945Z-80d821, run 42381078886 success) -> POST /repos/objectstack-ai/objectstack/pulls (#31142, draft, no assignees, no labels); this report comment is the 2nd write (POST /repos/objectstack-ai/objectstack/issues/30758/comments via post-stamped.mjs). git push is not a REST write.",
      "open_questions": [
        {
          "question": "canReadObject is also asked by the engine's ReadableIds composition over a master (ADR-0139). An app object controlled_by_parent with sys_business_unit as master would now let a delegate read that detail's rows under covered units (before: none). Is that within the ruled read?",
          "options": [
            "A: keep — ADR-0055 makes a detail's read follow its master's; no platform object is such a detail",
            "B: carve the ReadableIds asker out (a second admission answer for master composition), which would make canReadObject disagree with the master's own read door"
          ],
          "recommendation": "A, because the detail door is defined by its master's read and B reintroduces a second answer; seat to confirm"
        }
      ],
      "security_findings": "none",
      "out_of_scope_findings": [
        "carrier: #30758 card (carried notes) · ADR-0090 D12 note of the ruled read and the reference_not_found comment in packages/spec/src/api/errors.zod.ts — untouched per order"
      ]
    }

    Generated by Claude Code

  5. objectstack-fleet commented on Nov 7, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#3 · marchtian · session_017ihL2dFb2QdZEHEJ83cR2B

    Landed · 2026-11-07T15:35Z

    reading value
    PR #31142 MERGED 2026-11-07T15:33:24Z through the merge queue as 449c858ae7, head 40c2c8aba3 (requeued once after a Last-Modified second-boundary red in runtime, 6708725260)
    this card closed by that PR's Fixes #30758

    State: pm:dispatched removed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

domain:serverDomain lane — seat card indexed by label:pm:seatenhancementNew feature or requestpriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions