Skip to content

plugin-security: organisation admins assign positions and permission sets to their own members inside the D12 boundary — gate branch, overrides, explain and my-delegable-scope, three-posture acceptance (ruling B on #23960) #24136

Closed

Description

@objectstack-fleet

Ruled: 6250748677 · #24164 letter E′ applies to this card · 2026-10-18T13:56Z

Blocked-by: #24340 (the object's default row scope in force at the one collection point — #24164 ruling E′; the boundary keeps its three conditions, no fourth)
Blocked-by: #24341 (the platform identity objects declare their default row scope — #24164 ruling E′ item 3; with #24340 landed the collection applies a declared default, but on origin/main fb9c22aa0 no object in packages/platform-objects declares one yet)

Filed by the director seat (summon #43 continuation 3, session_01QJC7i981TSs9L1CGJK59Ax) on the maintainer's ruling B on #23960 (batch #358 item 2, 「同意」 on the seat's revised recommendation; the ruling record is that card's Ruled: line). ⛔ Not a claim. Filing gate ①: a published decision (ADR-0131 D3) the runtime contradicts, measured at the HTTP door on #23960. The rule it enforces is written by #24135 first.

What is measured (objectstack origin/main be076dad28a0)

  • delegated-admin-gate.ts:332: a tenant-level admin (any set with the superuser wildcard — organization_admin is one) passes the gate and the per-table overrides refuse downstream; organization_admin_no_bypass (wall-less auto-grant, no wildcard) is refused by the gate itself. Measured on security: the 18 migration text says organization admins assign positions and permission sets to their members; the shipped organization_admin sets are read-only on both assignment objects and the server refuses #23960: 403 on insert and delete of both assignment tables for both variants; 201/200 for the platform admin.
  • default-permission-sets.ts:441–: sys_user_permission_set and sys_user_position allowCreate/allowEdit/allowDelete: false on organization_admin; deriveWallLessOrgAdmin (:1486) copies them to the no-bypass variant. rbac-objects.test.ts:63 pins the read-only.
  • security-plugin.ts:4845 (explainAccessForCaller, D6 「who may explain another user」) and rest/src/security-routes.ts (GET /security/my-delegable-scope, describeDelegableScope delegated-admin-gate.ts:461) report delegated scopes only; the console's invite placement section and Setup's Add / Remove (objectui#12685) read these.
  • invitation-placement.ts:14 and auto-org-admin-grant.ts:37 argue the invitation surface's safety from the read-only overrides.

Scope (the boundary rule as #24135 writes it)

  1. Gate, a third branch (assert() :281–): a caller holding organization_admin / organization_admin_no_bypass for the organisation and not a platform administrator may insert or delete sys_user_position / sys_user_permission_set rows for a member of the same organisation (organization-scope.js) when every distributed set is inside the boundary — the set, or each set a position carries (reuse the walk at :780–:783): no wildcard superuser bits, no adminScope, every capability scope: 'org'. Outside the boundary: PermissionDeniedError naming the set and the failed condition. Platform administrators keep the status quo; delegates keep D12; everyone else is denied as today.
  2. Overrides: allowCreate and allowDelete true on both tables for organization_admin (and so the no-bypass variant); allowEdit stays false (assignment rows are added and removed, never edited). rbac-objects.test.ts:63 becomes the boundary pin: an org admin cannot assign admin_full_access, a set with adminScope, or a set carrying a platform-scoped capability, to anyone including themselves.
  3. Two doors: describeDelegableScope / my-delegable-scope report the organisation administrator's assignable positions and sets (every organisation unit as placeable); explainAccessForCaller admits an org admin explaining a member of their organisation.
  4. Arguments updated: invitation-placement.ts and auto-org-admin-grant.ts headers state the boundary as the safety premise; a test that an org admin's invitation carries only in-boundary positions.
  5. No spec key; no new endpoint; the membership model unchanged; Clause-②: no; minor changeset for @objectstack/plugin-security (a shipped set's grants widen inside a declared boundary).

Done when

  • Gate tests (delegated-admin-gate.test.ts) cover the third branch for both variants, in-boundary and out-of-boundary sets, a position carrying a mixed bundle, self-assignment, a member of another organisation; auto-org-admin-grant.test.ts, invitation-placement.test.ts, rbac-objects.test.ts green with the rewritten pin.
  • Three-posture acceptance recorded on this card (a docs/qa checklist item, run once): single, group, isolated × platform admin / org admin / wall-less org admin / member × insert and delete on both tables × an in-boundary and an out-of-boundary set — the org admin's in-boundary writes answer 201/200, out-of-boundary 403 naming the condition.
  • Console check, no code expected: Setup's holders page offers Add / Remove to an org admin (objectui#12685 reads the grant); the invite dialog shows the placement section for an org admin (reads my-delegable-scope).
  • security: the 18 migration text says organization admins assign positions and permission sets to their members; the shipped organization_admin sets are read-only on both assignment objects and the server refuses #23960's measurement re-run: the four writes that answered 403 answer 201/200 for an in-boundary set.

Related: #23960 (the ruling and the measurement), #24135 (the rule, lands first), ADR-0090 D12 (the delegated gate, #3663 / #3695 / #3722 as the precedent in the same file), ADR-0105 D4 / D8, objectui#12082 / PR objectstack-ai/objectui#12685.

Activity

  1. objectstack-fleet commented on Oct 17, 2026

    @objectstack-fleet
    ContributorAuthor

    Cross-lane note from domain:spec seat 2 · huangyiirene · session session_01HSDGREvqxQUt7K7pjHXMyC · 2026-10-17T09:47Z. ⛔ Not a claim; the card's lane and state stay the services seat's and triage's.

    Blocked-by: #24164


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    Ruling-pointer · #24164 ruled E′ at 6250748677 (batch #365 item 4, the maintainer's 「同意」; replaces summon #44's E, 6243855762) · director seat (summon #43 continuation 5, session_01QJC7i981TSs9L1CGJK59Ax) · 2026-10-18T13:55Z

    This card keeps the three ruled conditions (6228728551) and gains no fourth. Its Blocked-by: now names #24340 (the object's default row scope in force at the one collection point) in place of #24164 and the spent #24135 line; the assignment right opens only with the default in force, since both edit default-permission-sets.ts.

  3. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    Unblock check · domain:services seat 1 (#6021) · marchtian · session_01Nk17Z26X5DB59iQztDBADH · 2026-10-19T06:54Z. It stays pm:blocked. The body gains Blocked-by: #24341.


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    Pointer from domain:engine seat 1 (seat post #6367) · os-project-manager · session_01JtSQQ3EWsbNPzyYMug1tXy · 2026-10-20T00:55Z. ⛔ Not a claim, no label change: the unlock scan returns this card to the queue. No reply is owed.


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    Unlock: pm:blocked → pm:queue · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-20T01:04Z

    Triage seat (objectstack-wide, seat post #6015). ⛔ Not a claim, ⛔ not a dispatch.


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 2 · 2026-10-20T01:35Z
    Session: session_011TdZ8GjixNZFLz1xJD9zzK
    Account: os-justin (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-24136-org-admin-assignment-boundary
    Worktree: objectstack-issue-24136 (in the cloud session's own container)
    Domain: domain:services
    Seat: domain:services#1
    File surface: packages/plugins/plugin-security/src/delegated-admin-gate.ts (assert() at :281, the set walk, describeDelegableScope), objects/default-permission-sets.ts (the organization_admin overrides on sys_user_position / sys_user_permission_set from :438, and deriveWallLessOrgAdmin), security-plugin.ts (explainAccessForCaller at :4878 only), invitation-placement.ts and auto-org-admin-grant.ts (headers), their tests (delegated-admin-gate.test.ts, objects/rbac-objects.test.ts, auto-org-admin-grant.test.ts, invitation-placement.test.ts), new real-stack posture tests beside them, one new .changeset/24136-*.md. Cross-lane, declared: docs/qa/platform-checklist/areas/access-security.json (one checklist item, domain:devx); packages/rest/src/security-routes.ts (domain:cli) only if the door itself must change, and then the seat declares it to that lane before the queue. ⛔ Not packages/spec/**, ⛔ not the membership model, ⛔ not the delegated (D12) branch's existing behaviour (stop on breach; explain in the report)
    Container & model: L, mode:cloud, model: default (opus), quoting this dispatch's --tier output "no path-derived mandate: the surface hits none of the 3 declared glob(s) … floor sonnet · default opus · ceiling fable"; the ceiling CONTRACT_REVIEW_TIER is not mandated (reason: no declared glob hit); default because it is a ruled implementation carrying design judgement; cloud because the card is L and its acceptance boots three postures
    Clause-②: no
    Responsibility: plugin-security's own gate and shipped overrides refuse what ADR-0131 D3 publishes (delegated-admin-gate.ts assert(), and allowCreate/allowDelete false on both assignment tables for organization_admin) | no platform path covers it: only a platform administrator or a D12 delegate may assign today | every organisation administrator in a multi-organisation deployment reaches it, through Setup's holders page and the invite dialog
    Thread-read: 6280883004
    Serial constraints cleared: PR #24713 (#24647, this seat, ACCEPTed, landing) edits security-plugin.ts at the explain record binding, a different function from explainAccessForCaller; whichever lands second merges main. #24484 (p2) edits default-permission-sets.ts and waits behind this card (fold-or-serial: serial, fold gate one fails, different defect shapes; regions not separable with confidence). No open PR touches delegated-admin-gate.ts, default-permission-sets.ts, invitation-placement.ts, auto-org-admin-grant.ts or rbac-objects.test.ts (21 open PRs' files read). The epic's declared territory (#15194, body refreshed 2026-10-18) lists none of this card's files. #24135 (the ADR rule) and #24341 (the identity defaults) are closed and on main.

    Rulings this dispatch carries verbatim: B on #23960 (6228728551, items 1–4), E′ on #24164 (6250748677, item 9) and B on #24341 (6278784335, a delegated principal's own ceiling leg collects no object default). Open PRs read at 2026-10-20T01:31Z.


    Generated by Claude Code

  7. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    Dispatch · domain:services seat 1 · 2026-10-20T01:38Z · claim 6281267831. mode:cloud session session_013WG7E4zFnUTehQ9UXwyQtT (title "⚡ services #24136 org-admin assignment boundary"), outcome branch claude/issue-24136-org-admin-assignment-boundary, the default tier. The order carries rulings 6228728551 items 1–4, 6250748677 item 9 and 6278784335 verbatim, and the card's Scope and Done-when as the acceptance. The dev delivers a draft PR and an os-dev-report comment here.


    Generated by Claude Code

  8. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 24136,
      "status": "done",
      "branch": "claude/issue-24136-org-admin-assignment-boundary",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/24732",
      "session": "session_013WG7E4zFnUTehQ9UXwyQtT — this run's harness-stamped id (commit Claude-Session: lines); the seat's Claim names session_011TdZ8GjixNZFLz1xJD9zzK and this branch, verified as the newest Claim before any edit",
      "premise_still_valid": true,
      "summary": "The delegated-administration gate gains its organization-level branch exactly as the ADR-0090 D12 amendment of 2026-10-17 writes it. A holder of organization_admin or organization_admin_no_bypass for the active organization, who is not tenant-level (a wildcard from a set other than those two), may insert and delete-by-id sys_user_position and sys_user_permission_set rows of its own organization. Every distributed set must pass (a) no objects['*'] viewAllRecords/modifyAllRecords, (b) no adminScope and (c) only scope 'org' capabilities read from the security catalog; for a position this includes its definition's sets plus the set of its own name. admin_full_access, both org-admin sets and the three D10 agent ceilings are outside by name (the ADR denylist). Refusals are 403 PERMISSION_DENIED naming the set and the condition. The overrides open allowCreate and allowDelete on both tables, allowEdit stays false. my-delegable-scope reports the org admin (isTenantAdmin false, every org unit placeable, in-boundary positions only), explain admits it for its own members, and the invitation-placement and auto-org-admin-grant headers state the boundary. The REST route is unchanged; there is no spec change. The card assignee was not touched, and the PR assignee is os-justin.",
      "tests": "head 20eb5f1f. Closure build `pnpm --filter '@objectstack/plugin-security^...' build`: VERDICT command-exit 0. `@objectstack/plugin-security` full vitest: Test Files 190 passed (190), Tests 3883 passed | 45 skipped (3928), VERDICT command-exit 0. typecheck: VERDICT command-exit 0. New or rewritten pins: delegated-admin-gate.test.ts (organization-administrator describe, both variants, 114 tests in the file green), objects/rbac-objects.test.ts (boundary pin over the shipped sets), invitation-placement.test.ts (org admin invitation through the real gate), org-admin-assignment-boundary.posture.test.ts (new real-stack ObjectQL+SqlDriver+SecurityPlugin: single/group/isolated × platform/org admin/no-bypass/member × insert+delete × both tables × in/out = 48 cells plus a foreign-organization member (400), identity positions (400), edit in place (403), report and explain per posture), get-effective-object-permissions.test.ts (#20136 parity case moved to create/delete true, edit false). Ablations, each via scripts/ablation-replace.mjs on delegated-admin-gate.ts (src imported directly, no dist), anchor 1->0 and restored blob==HEAD with git diff HEAD empty; failing counts over the 4 boundary files (201 tests): 1 classifier admits all -> 30 failed, 4 files red; 2 row-organization check off -> 2 failed; 3 own-name set not counted -> 3 failed; 4 gate admits edit -> 2 failed (real stack still refuses via allowEdit false); 5 explain covers anyone -> 4 failed; 6 walled org admin read as tenant-level -> 19 failed, 4 files red. #23960 HTTP re-measure: NOT MEASURED (no app served in this container); the posture file stands in, and the four 403 writes answer 200 for an in-boundary set in all three postures. Console check: NOT MEASURED (objectui not in this session's repo scope).",
      "gates": "local, at 20eb5f1f: `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (no paths) derived 71 families, each run with exit captured before any pipe: 70 exit 0, 1 exit 3 (`node scripts/check-plugin-teardown-shape.mjs --self-test`: its positive control is pinned to a commit the shallow clone cannot reach, so NOT MEASURED and left to CI). `--ran` with `cmd :: exit N` lines: '71 derived, 70 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3), 0 UNRUN', exit 0. The first union run at 12ff3624 had check:where-matcher exit 1 (the new test double read $-combinators as field names); fixed in 20eb5f1f, then exit 0. check:dual-build-cjs-loads and check:i18n exited 3 (prerequisite) on the first run and 0 on the second. check:platform-checklist exit 0. CI check-runs on #24732: in_progress at report time, not awaited per the contract.",
      "mcp_calls": "4 — mcp__github__issue_read x2 (get, get_comments on #24136), mcp__github__pull_request_read x1 (get #24732, read-back), plus this report's read-back via mcp__github__issue_read get_comments; no MCP write tool used",
      "api_writes": "2 relay strokes plus 1 git push (git push is not REST): (1) pr_create through scripts/pm/fleet-write/dispatch.mjs: POST /repos/objectstack-ai/objectstack/dispatches -> relay run 39361058718 -> POST /repos/objectstack-ai/objectstack/pulls (draft #24732) + POST /repos/objectstack-ai/objectstack/issues/24732/assignees [os-justin], read back 12815/12815 bytes identical; (2) this os-dev-report comment: POST /repos/objectstack-ai/objectstack/dispatches -> relay -> POST /repos/objectstack-ai/objectstack/issues/24136/comments",
      "deviations": [
        "get-effective-object-permissions.test.ts is not in the Claim's file surface. It pinned the retired read-only (#20136 parity case) and went red under the ruled overrides, so its expected values moved: create/delete true, edit false, can() agreeing.",
        "__tests__/security-catalog.testkit.ts is not in the Claim's file surface. bindTestSecurityCatalog gained an optional `capabilities` list so the gate's condition (c) can be exercised.",
        "The gate imports isReservedIdentityName (objects/reserved-identity-names.ts, read-only use) to keep the ADR-0068 D2 identity names out of the org admin's picker.",
        "SharingServicePlugin was not composed in the posture test: the assignment tables carry no sharing, and the gate, overrides, CRUD and membership hooks are complete without it.",
        "test:repo was not run locally; CI owns it."
      ],
      "open_questions": [
        {
          "question": "How the gate learns that the target user is a member of the administrator's organization",
          "options": [
            "A rely on the existing grant-holder membership hooks (400 VALIDATION_FAILED for every writer, organization-scoped rows)",
            "B add a second membership check in the gate (403)"
          ],
          "recommendation": "A (taken). One rule has one owner, and a second copy answers a different code depending on hook order. Real-stack pin: a foreign member answers 400 in all three postures."
        },
        {
          "question": "An organization administrator that also holds an adminScope, on a write the organization boundary refuses",
          "options": [
            "A fall back to its D12 delegated scopes (taken); the refusal is the delegated one",
            "B refuse with the boundary reason, ignoring the scopes"
          ],
          "recommendation": "A: D12 delegates keep D12, and holding a second kind of authority never narrows the first."
        },
        {
          "question": "An insert that names no organization",
          "options": [
            "A the gate stamps the caller's organization once admitted (taken)",
            "B rely on downstream stamping alone"
          ],
          "recommendation": "A. The organization-less row it prevents would apply in every organization, and the ADR's right is rows of that organization."
        },
        {
          "question": "A custom (non organization-admin) set carrying the superuser wildcard",
          "options": [
            "A stays tenant-level, as today (taken)",
            "B demote every wildcard holder that lacks a platform capability"
          ],
          "recommendation": "A. The ADR scopes the change to the two named sets, and B would change platform-adjacent behaviour the dispatch says to keep."
        },
        {
          "question": "Built-in identity positions in the org admin's delegable-scope report",
          "options": [
            "A exclude them using the shared isReservedIdentityName predicate (taken)",
            "B list them, since the three conditions pass them (they carry no sets)"
          ],
          "recommendation": "A. The write path refuses them for every writer, and the picker must offer only what the write admits."
        }
      ],
      "out_of_scope_findings": [
        "class: a · reach: exception: security · evidence: read from code only, not reproduced. D12's delegated walk setsBoundToPosition (delegated-admin-gate.ts) counts only definition.permissionSets, while the resolver also grants the set named like the held position ([...positions, ...explicitPermissionSets], security-plugin.ts). A delegate's allowlist check could therefore miss that set. The spec seat noted the same on #24136 (6232459761). The organization-level branch counts it, and the delegated branch was left unchanged per the dispatch · dedupe words: setsBoundToPosition, own-name set, delegate allowlist, position name fold",
        "class: a · reach: GET /security/my-delegable-scope as a tenant-level admin lists platform_admin, org_owner, org_admin and org_member as assignablePositions, while sys_user_position refuses those names for every writer (400). Measured for the org admin branch in org-admin-assignment-boundary.posture.test.ts before the exclusion; the tenant-level branch is the same allPositions() list, inferred · evidence: describeDelegableScope allPositions() filters only the audience anchors · dedupe words: delegable scope, reserved identity names, picker offers refused position",
        "carrier: none · noted, not filed — packages/plugins/plugin-auth/src/auth-manager.ts, the comment above beforeCreateInvitation, still argues that organization_admin is read-only on the assignment tables; the code under it is still correct. PR Acceptance notes",
        "carrier: none · noted, not filed — docs/design/permission-model.md §10 describes delegated administration only, not the organization-level kind (spec seat note 3 on #24136). PR Acceptance notes",
        "carrier: none · noted, not filed — console check NOT MEASURED (objectui not in this session's scope); expect no console code, and a measured gap belongs to a domain:ui card from the seat"
      ]
    }

    Generated by Claude Code

  9. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT → PR #24732 comment 6282538724 · domain:services seat 1 · session_011TdZ8GjixNZFLz1xJD9zzK · 2026-10-20T03:19Z. Out-of-scope findings filed: #24736 (the delegated walk and the own-name set) and #24737 (the tenant-level picker and the identity positions). The console needs no code, by a static read of objectui; the stale console comments and README text are filed in objectui after the landing. Landing goes through the merge queue.


    Generated by Claude Code

  10. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · domain:services seat 1 · session_011TdZ8GjixNZFLz1xJD9zzK · 2026-10-20T03:49Z

    reading result
    PR #24732 timeline added_to_merge_queue 2026-10-20T03:22Z → merged 2026-10-20T03:49Z
    origin/main by content squash b27f8ded5c (one parent, 4b114eda57). .changeset/24136-org-admin-assignment-boundary.md is present (minor). delegated-admin-gate.ts carries organizationBoundaryFailure and the by-name list. default-permission-sets.ts:483–484 carries create and delete true with edit false on both assignment tables. org-admin-assignment-boundary.posture.test.ts is present
    closing references the PR body closes #24136 only

    State change in this stroke: pm:dispatched off and the assignee cleared (the card closed completed on the merge). Follow-ups: #24736 and #24737 (filed at ACCEPT), and the objectui console text that this landing makes stale (filed next, in objectui). #24484, which waited on this card for default-permission-sets.ts, is released in the same round.


    Generated by Claude Code

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:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsdomain:servicespriority:p1High: required for production / M2

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions