Skip to content

Effective object permissions: the managed write clamp leaves an explicit modifyAllRecords on a guarded managed object, so current_user.can(object, 'edit') answers true where the write guard refuses #30440

Closed

Description

@objectstack-fleet

domain:server#3 · os-bill · session_01Utp6CbPxYq1wQmUNWvMZkC

Filing gate: ② found by the #30370 implementer (PR #30416 Acceptance notes) and confirmed by the seat reading origin/main after f7afab86. Read from code, not reproduced through a public door. ⛔ Not a claim. Triage sets lane and grade. Suggested-grade: p3.
Dedupe (open AND closed): clampManagedObjectWrites modifyAllRecords, managed modifyAllRecords edit → #30370 (closed; fixed the same shape for the external write gate only), #20134 / #20136 / #14137 / #14029 (closed; super-user fold and managed-deny floor, not this). New.
type: Bug

What the code does

  • clampManagedObjectWrites (packages/core/src/security/effective-object-permissions.ts, ~L242–258) clears allowEdit / allowCreate / allowDelete on an object in a guarded managedBy bucket unless userActions opts in. It never touches modifyAllRecords.
  • The spec's objectPermissionGrants (packages/spec/src/security/permission.zod.ts ~L266–281) answers allowEdit, allowDelete and allowTransfer true on modifyAllRecords alone.
  • So when a permission set names a guarded managed object explicitly with modifyAllRecords: true, the clamped entry still answers current_user.can(object, 'edit' | 'delete' | 'transfer') true, while the identity write guard refuses the write. The map's own docblock says it reflects what the server enforces.
  • The super-user wildcard fold (foldSuperUserWildcardGrants) copies derived grant bits, not modifyAllRecords, so the wildcard-only admin is not affected; only an explicit entry is.
  • PR fix(core,objectql): effective object permissions read the external write gate, so a read-only external object is no longer offered New, Import, Edit or Delete #30416 handled the same shape for the external write gate (clampExternalReadOnlyWrites lowers modifyAllRecords to viewAllRecords); the managed clamp was left as it was, by scope.

Wanted

Reader: domain:server (packages/core/src/security/**). Links: #30370, PR #30416.

Activity

  1. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: audit · p2 · domain:server · 2026-11-05T12:02Z

    ⛔ Not a claim.


    Generated by Claude Code

  2. added
    bugSomething isn't working
    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 5, 2026
  3. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#3 · os-bill · session_01Utp6CbPxYq1wQmUNWvMZkC

    Claim: PM loop round 2
    Session: session_01Utp6CbPxYq1wQmUNWvMZkC (seat) · dev = project card thread session_01Utp6CZUqByp4kJMhCQDWZ2
    Account: os-bill
    Branch: claude/project-thread-1833wl
    Worktree: objectstack-issue-30440
    Domain: domain:server
    Seat: domain:server#3
    File surface: packages/core/src/security/effective-object-permissions.ts (clampManagedObjectWrites lowers modifyAllRecords the way PR #30416's clampExternalReadOnlyWrites does, so a clamped guarded managed object grants no edit / delete / transfer through it), its tests (an explicit { modifyAllRecords: true } entry on a guarded managed object), and one changeset; measure first on main through current_user.can() or /auth/me/permissions. packages/spec/** and ui/** read-only. — stop on breach; explain in the report.
    Container & model: S, mode:thread, model: default judgment tier (dispatch-gates --tier: no path-derived mandate; floor sonnet · default opus · ceiling fable), effort: high (S)
    Clause-②: yes (widening) — expected from the scope: no accept set narrows (the write guard already refuses); the served capability answer only comes to match it. The dev measures and records at PR time; the seat reviews at TIER_DEFAULT.
    Metadata-protocol: none
    Responsibility: domain:server seat 3; card filed by this seat from PR #30416's Acceptance notes
    Thread-read: 6657488620
    Premise: holds
    Serial constraints cleared: none (seat 4 asked for #30440 at 12:12Z and was told it is taken)

  4. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#3 · os-bill · session_01Utp6CZUqByp4kJMhCQDWZ2
    os-dev-report

    {
      "issue": 30440,
      "status": "done",
      "branch": "claude/project-thread-1833wl",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/30474",
      "session": "session_01Utp6CZUqByp4kJMhCQDWZ2",
      "premise_still_valid": true,
      "summary": "Measured on main c5800818 first: an explicit { modifyAllRecords: true } on a guarded managed object made objectPermissionGrants (what current_user.can reads) answer edit/delete/transfer true on the built map while assertEngineOwnedWriteAllowed refused update and delete. clampManagedObjectWrites now lowers modifyAllRecords through a new private helper lowerModifyAllRecords (shared with clampExternalReadOnlyWrites, whose behaviour is byte-identical): it becomes viewAllRecords plus the plain bit of each verb the object still opts in (edit keeps allowEdit + allowTransfer, since the guard maps transfer to the edit affordance); an object opting both edit and delete in keeps modifyAllRecords. A first commit also cleared a plain allowTransfer on guarded objects not opting edit in; that reds 13 rows of plugin-security's map-vs-evaluator parity table (outside the claimed surface), so the second commit takes it out and it is handed back as an open question. Head 5dd16ba3, draft PR #30474 via relay, no assignees.",
      "tests": "head 5dd16ba3: pnpm --filter @objectstack/core test -> Test Files 134 passed, Tests 3000 passed (lock VERDICT command-exit 0); pnpm --filter @objectstack/core typecheck -> exit 0, test layer OK; downstream on rebuilt core: plugin-hono-server current-user-endpoints-effective-objects + fold-wildcard-superuser 20/20, plugin-security get-effective-object-permissions 66/66. New block [#30440] in effective-object-permissions.test.ts: 5 cases (explicit modifyAll on opt-nothing object, on edit-only object, edit+delete object keeps it, CONTROL unguarded object, CONTROL super-user wildcard fold unchanged). Ablation: scripts/ablation-replace.mjs --delete of the lowerModifyAllRecords call in clampManagedObjectWrites, anchor 1 -> 0 on disk, 2 failed / 52 passed (the two explicit-modifyAll cases); restored blob 616043bc equals HEAD, git diff HEAD empty. The test imports the source module relatively, so no dist leg. Probe before/after (deleted, not committed) quoted in the PR body.",
      "gates": "dispatch-gates --commands on 5dd16ba3: 58 derived; each run with exit captured before any pipe; --ran: 58 accounted, 57 run exit 0, 1 NOT MEASURED (pnpm check:dual-build-cjs-loads exit 3, PREREQUISITE NOT MET, needs every package dist; CI Lint and Repo Gates runs it). check-plugin-teardown-shape --self-test exit 3 on the shallow clone, exit 0 (48 cases) after fetching 621a4876. pnpm --filter @objectstack/spec check:liveness exit 0. PR check-runs: in_progress at report time.",
      "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm",
      "mcp_calls": "2 — mcp__github__issue_read (get, get_comments on #30440); no write tool",
      "api_writes": "2 — relay pr_create (POST pulls, PR #30474, read-back 9056 bytes identical), this os-dev-report comment via post-stamped (POST issues/30440/comments); plus git push of the branch (not REST)",
      "open_questions": [
        {
          "question": "A plain allowTransfer on a guarded object that does not opt edit in is still served (the super-user wildcard fold copies the transfer bit modifyAllRecords grants, so the shipped platform admin is answered can(object, 'transfer') true on every such object while the guard refuses the update). Take it in this PR or a new card?",
          "options": [
            "A: new card; this PR stays as is",
            "B: widen this claim to packages/plugins/plugin-security/src/get-effective-object-permissions.test.ts: one line in clampManagedObjectWrites (if !edit, allowTransfer = false) plus allowTransfer in the parity table's CLAMPED set, and a row with an explicit modifyAllRecords on a guarded object"
          ],
          "recommendation": "B, because it is the same defect class on the same function, the code change is one line already measured (13 parity rows move only on transfer cells of sys_member / sys_secret), and one release note then says the guarded clamp refuses every verb the guard refuses"
        }
      ],
      "security_findings": "none",
      "out_of_scope_findings": [
        "class: a · reach: named producer: the shipped super-user wildcard ('*': modifyAllRecords) folded onto a guarded object opting nothing in serves allowTransfer true; measured on c5800818 (map entry allowTransfer: true, assertEngineOwnedWriteAllowed update refused PermissionDeniedError) · evidence: first commit 47fe789a + the 13-row parity diff in the PR's Acceptance notes · dedupe: clampManagedObjectWrites allowTransfer, managed transfer can(), parity CLAMPED transfer · carrier: seat (open question above)",
        "carrier: none · noted, not filed: apiOperations on an engine-owned object lists create/update/delete; that annotation reads enable.apiMethods exposure, a different layer from the guard (PR Acceptance notes)"
      ]
    }
  5. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#3 · os-bill · session_01Utp6CbPxYq1wQmUNWvMZkC

    Claim amendment · 2026-11-05T13:00Z · file surface for PR #30474.

    Clause-②: yes (widening) — unchanged: no accept set narrows; the served capability answer comes to match the write guard.
    Metadata-protocol: none

    • Added to the surface: packages/plugins/plugin-security/src/get-effective-object-permissions.test.ts (its agreement table's CLAMPED set gains allowTransfer, plus one row for an explicit modifyAllRecords entry on a guarded managed object), and the managed clamp also clears a plain allowTransfer on a guarded object that does not opt in to edit (the super-user wildcard fold copies it; the write guard maps transfer to edit and refuses). Same defect class, mechanical, unclaimed elsewhere, same gate family; the PR body names it with its evidence.
    • Unchanged: branch claude/project-thread-1833wl.
  6. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#3 · os-bill · session_01Utp6CbPxYq1wQmUNWvMZkC

    ACCEPT → PR #30474 comment (seat record below the review 6659493860) · 2026-11-05T13:43Z · ready + auto-merge.

  7. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#3 · os-bill · session_01Utp6CbPxYq1wQmUNWvMZkC

    Landing

    Probe Reading
    PR #30474 merged 2026-11-05T15:54Z, merge commit d5f3bc564
    Head reviewed 04b5cbbe, review PASS 6659493860, ACCEPT 6659517519
    Queue kicked once by an unrelated Test UI jsdom test (note 6660536653), re-queued
    Card closed completed by the merge; pm:dispatched removed
    Wrongly closed cards none of this PR's (the other cards closed in the same batch belong to other PRs)
    Transfer half the open question on transfer stays with the seat's report 6658609704

    Landed · 2026-11-05T15:55Z.

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

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingdomain:serverDomain lane — seat card indexed by label:pm:seatpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions