Skip to content

[security] settings: tenant-scope settings are not isolated per organization on multi-organization deployments — detail withheld pending maintainer #22261

Description

@objectstack-fleet

Filing gate: ① a product defect, reach measured through public doors on a multi-organization deployment in production posture. Reader: triage routes it. By its package it lands in the services lane (service-settings). Filed by the repo:cloud seat (repo:cloud#1, R45, session session_011jobP72PwN3whNm55GetXQ) as the 裁后执行 of a maintainer ruling. ⛔ Not a claim.

Detail withheld pending maintainer. Ruling, verbatim letter: D3 withheld, maintainer 「同意」 (2026-10-08T09:23Z, director batch #292 item 1), following the objectstack#21124 precedent. The full reading, the door table and the fix draft live on the originating private card. The deployment that surfaced it is closing its own exposure separately, at p0.

What is known publicly

  • SettingsService's row identity omits the organization that the sys_setting object declares in its own identity.
  • On a multi-organization deployment, an organization administrator's tenant-scope settings write is therefore visible to, and replaceable by, other organizations.
  • A second, read-side half: the generic data API's read of sys_setting and sys_setting_audit does not apply each namespace's declared readPermission.

Fix direction (option A, as ruled on the originating card)

Acceptance

  • Two organizations on one kernel each set and read their own tenant-scope value. One organization's write or reset leaves the other's unchanged.
  • A global row is still read by both.
  • An organization-less tenant write under a walled posture is refused, asserting code and status.
  • The generic data-API read of sys_setting refuses a principal lacking the namespace's readPermission.
  • Measured over HTTP, with a positive control for each refusal.

Priority: p1 per the ruling. The deployment-side fix is p0 on its own card.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: routed domain:services · area:access · pm:queue; priority:p1 and security stand, as ruled

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

    Triage: lands in packages/services/service-settings (the settings row identity and its reads) and the generic read door's readPermission enforcement ⇒ domain:services; rationale: packages/services/* is that lane's. If the generic-door half sits in another lane's package, the claiming seat names it and that seat reviews.

  2. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 3 · 2026-10-08T11:24Z
    Session: session_01WkL6Eijt432S1Y7ekb6ovQ
    Account: os-bill (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-22261-settings-tenant-isolation
    Worktree: objectstack-issue-22261
    Domain: domain:services
    Seat: domain:services#1 (seat post #6021)
    ⛔ Classes, positions and functions only. Detail stays withheld (the objectstack#21124 precedent).

    Executes option A as ruled (card body; triage 6058273893).

    File surface at origin/main c8bb3c8d:

    Exclusions:

    Stop on breach; explain in the report.
    Container & model: M, mode:subagent, model: opus (dispatch-gates --tier: no path-derived mandate; default tier, a p1 security card)
    Clause-②: no (narrowing)

  3. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 22261,
    "status": "done",
    "branch": "claude/issue-22261-settings-tenant-isolation",
    "pr": "#22295",
    "session": "session_01WkL6Eijt432S1Y7ekb6ovQ (dispatching seat's session; this executor ran as its subagent)",
    "premise_still_valid": true,
    "summary": "Option A executed as ruled, at class level. SettingsService now carries the caller's organization in the row identity of tenant and user rows (rowIdentity, setMany), filters its loads explicitly by the caller's organization plus organization-less rows (loadScopedRows and the in-memory store), and prefers the caller organization's own row in the cascade and the lock pre-flight (preferredRow). Under a walled posture an organization-less tenant write is refused whole with SettingsValidationError (SETTINGS_VALIDATION, HTTP 400, field code invalid_value); the posture comes from the tenancy service through bindEngine and is read only for callers naming no organization on keys below the global rung. The generic read door is an engine middleware registered by service-settings on sys_setting, sys_setting_audit and sys_platform_setting that ANDs each namespace's readPermission into every non-system read (SettingsService.namespaceReadScope); no file in objectql, runtime or plugin-security is edited. Posture single keeps the default organization's answers (pinned). Stored rows are not rewritten; the H3 reading and the decision it raises went to the seat privately.",
    "tests": "HEAD 61911cf (service-settings rebuilt, dist proven to carry HEAD): service-settings vitest 41 files / 752 passed; settings-organization-isolation.pin 18 passed; settings-read-door.pin 27 passed; dogfood settings-organization-isolation (two organizations, isolated posture, over HTTP) 6 passed, each refusal with its positive control; single-posture HTTP regression (3 existing settings dogfood files) 14 passed; typecheck service-settings and dogfood exit 0; narrowed eslint over the 9 changed .ts files 0/0. Ablations (trap restore from HEAD, blob == HEAD and git diff HEAD empty after each): settings-service.ts at the base commit -> isolation pin 11 red / 5 green (the 5 are the global-row and single-posture guards); namespaceReadScope answering no predicate -> read-door pin 19 red / 8 green; positional preferredRow -> 2 red; dogfood with service and plugin at the base commit, rebuilt, ablation-dist-preflight --absent passed -> 4 of 6 red, restored and rebuilt -> 6 of 6 green; dogfood with only the door predicate disabled, plant proven in dist -> only the read-door case red, restored, rebuilt, --absent passed, tree clean. Gates: dispatch-gates --commands derived 94 at HEAD 61911cf, all run exit 0, plus check:settings-bind-window; --ran: 94 derived, 94 run, 0 NOT-MEASURED, 0 UNRUN.",
    "mcp_calls": "0",
    "api_writes": "3 REST writes, all through scripts/pm via the fleet-write relay: POST /repos/objectstack-ai/objectstack/pulls (pr_create, draft forced; body read back identical), POST /repos//issues/22295/assignees (label-write --assign os-bill; read back matches), POST /repos//issues/22261/comments (this report). Plus git push of the branch (a git op).",
    "open_questions": [
    {
    "question": "H3: how stored rows written before this change are treated under a walled posture. Detail withheld here; the measurement, the options and a four-axis recommendation went to the seat privately.",
    "options": [
    "A keep the ruled reading as implemented",
    "B narrow the walled reading",
    "C attribute stored rows first, then B"
    ],
    "recommendation": "See the private report."
    }
    ],
    "out_of_scope_findings": [
    "carrier: #15207 family (audit ledgers' organization attribution) - the settings-specific audit trail rows carry no organization; noted, not filed"
    ]
    }


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 22261,
    "round": 2,
    "status": "done",
    "branch": "claude/issue-22261-settings-tenant-isolation",
    "pr": "#22295",
    "session": "session_01WkL6Eijt432S1Y7ekb6ovQ (dispatching seat's session; this executor ran as its subagent)",
    "premise_still_valid": true,
    "summary": "R1 resolved: origin/main d1dbe70 merged into the branch with a merge commit (dcbf66b; no rebase, no force-push, no merge-driver override). The only conflict was the census page's derived counts; both rows are kept (18b from this branch, 23d from main) and the counts were regenerated with pnpm gen:system-context-census. check:system-context-census is green: 118 elevation read sites in 20 packages across 55 files. M1 measured with the real tenant wall: under the isolated posture, a non-system find or findOne of either settings store (sys_setting, sys_setting_audit) by another organization's administrator does not return the first organization's organization-stamped row. That holds for both objects. No code change in this round.",
    "tests": "All at HEAD dcbf66b, after a rebuild of the dogfood closure; service-settings dist was confirmed to carry the HEAD source. service-settings vitest: 41 files, 752 passed, including the isolation and read-door pins. Typecheck of service-settings and dogfood: exit 0. Dogfood settings-organization-isolation (two organizations, isolated posture, over HTTP, a positive control per refusal): 6 passed. Single-posture HTTP regression (3 existing settings dogfood files): 14 passed. Gates: dispatch-gates --commands with no paths derived 94 commands at dcbf66b, with no STALE TREE warning. All 94 were run, plus check:settings-bind-window. Two gates (spec check:skill-examples and check:dual-build-cjs-loads) first exited 3, PREREQUISITE NOT MET, because the fresh worktree had no dist for 8 packages. Those packages were built (turbo, 44/44 cached) and both gates re-ran to exit 0. --ran result: 94 derived, 94 run, 0 NOT-MEASURED, 0 UNRUN.",
    "m1": {
    "harness": "Real ObjectQL engine, real SqlDriver (better-sqlite3, in memory) and real SecurityPlugin (its Layer 0 tenant wall) under the isolated posture with the org-scoping declaration. This is the harness shape of plugin-security's tenant-layer0-verdict-end-to-end test. The metadata service (it lists the shipped default permission sets) and the tenancy service are fixtures. The temporary test file was removed after the run; git status was clean and nothing was committed.",
    "sys_setting": "not returned (find and findOne). Positive control: the administrator's own organization row is returned.",
    "sys_setting_audit": "not returned (find and findOne). Positive control: the administrator's own organization row is returned."
    },
    "mcp_calls": "0",
    "api_writes": "1 REST write this round: this comment, POST /repos//issues/22261/comments through scripts/pm post-stamped via the fleet-write relay. Plus git push of the merge commit 61911cf..dcbf66b (a git op). The PR body was not edited; text for the seat to append was handed over privately.",
    "open_questions": [],
    "out_of_scope_findings": []
    }


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    Seat ACCEPT: PR #22295 at dcbf66b41a · seat domain:services#1 (#6021) · session_01WkL6Eijt432S1Y7ekb6ovQ · 2026-10-08T13:28Z

    ⛔ Classes, positions and functions only. Detail stays withheld (the objectstack#21124 precedent).

    Checked against GitHub and the branch, not the reports' prose (os-dev-report 6060247099, round 2 6060893787).

    • Disclosure: both public reports and the PR body stay at class level. The reading of stored rows, and one dormant reader outside this lane, went to the seat privately.
    • Form:
      • The PR is a draft against main. Its first line is Fixes #22261, with a line-start Clause-②: no (narrowing). The PR assignee is os-bill.
      • Net diff: 11 files, +1318/−55. No packages/spec path and the arm is no, so no contract review is owed. check-governed-merges --pr 22295: NOT governed, 1373 changed lines.
      • It merges clean with main (git merge-tree).
      • Round 1's scope beyond the claim is appended to the claim (6058798876): the plugin wiring, census row 18b, the dogfood file, two fixtures, and sys_platform_setting in the read door as an in-place adjacent fix.
    • Diff, read by the seat:
      • SettingsService carries the caller's organization in tenant and user row identity and writes.
      • Its loads filter in the query by the caller's organization plus organization-less rows, and preferredRow makes the cascade and the lock pre-flight take the organization's own row first.
      • Under a walled posture, an organization-less tenant write or reset is refused whole: SETTINGS_VALIDATION 400, with invalid_value per field.
      • The posture is read only for a caller naming no organization below the global rung, and a posture source that throws is not guessed past.
      • settings-read-door.ts ANDs each namespace's read capability into every non-system find / findOne / count / aggregate on the three settings stores. It is a filter, so pages and counts agree. A read with no query is refused rather than served unscoped. The deny baseline is $in: [].
    • Acceptance, all five, over HTTP with a positive control per refusal:
      • each organization keeps its own value through a write and a reset;
      • a global value is read by both;
      • the organization-less write under the wall answers 400;
      • the data-API read hides a namespace from a principal lacking its read capability (0 rows, 404 by id), while a holder reads it.
    • Evidence:
      • Ablations: the service at base gives 11 red; the door predicate off gives 19 red; a positional preferredRow gives 2 red; the dogfood leg at base gives 4 of 6 red; the door-only dogfood leg gives 1 of 6 red. Each was restored to HEAD.
      • Round 2 at dcbf66b41a: service-settings 752 passed, dogfood 6 plus 14, typechecks 0.
      • Gates: 94 of 94 derived gates, no stale tree, --ran a derived zero.
    • Round 2 (REWORK, mechanical) is resolved:
      • The merge of main is a merge commit. Census rows 18b and 23d are both present, the counts are regenerated, and check:system-context-census is green.
      • The round-2 markdown is appended to the PR body as the seat's append.
    • M1 (measured, real tenant wall, isolated posture): another organization's administrator's non-system find / findOne on sys_setting and sys_setting_audit does not return the first organization's stamped row. The positive control reads its own. Nothing to file.
    • The reading of rows stored before this change (the dev's open question): ruled by the seat, not escalated, under ADR-0131 D10.
    • Out of scope, dispositions:
    • Landing to-do:
      • Every check on dcbf66b41a must be green or an expected skip.
      • Then ready and auto-merge through the relay, the merge-queue check, and the close-out.
  6. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · domain:services seat 1 (#6021) · session_01WkL6Eijt432S1Y7ekb6ovQ · 2026-10-08T14:06Z.

    ⛔ Classes, positions and functions only.

    PR #22295 merged through the merge queue as 79c35d45. On origin/main, @objectstack/service-settings (minor, BREAKING accept-set narrowing):

    • tenant and user settings rows carry the caller's organization in their identity, their writes and their reads, and the cascade prefers the organization's own row;
    • an organization-less tenant write under a walled posture is refused (SETTINGS_VALIDATION, 400);
    • the generic data-API read of the three settings stores applies each namespace's read capability.

    The PR's Fixes line closed the card completed. This note also removes pm:dispatched and the assignee.

  7. added a commit that references this issue on Oct 9, 2026
    79c35d4
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 guardsbugSomething isn't workingdomain:servicespriority:p1High: required for production / M2security

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions