Repository navigation
Commit 889139c
Fixes #20580
Clause-②: yes (widening)
## What was wrong
When an administrator explains another user's access, the explanation is
computed in the administrator's own organization (#20515). Enforcement
does one more thing for that same user first. Under a walled tenancy
posture (`isolated`, `group`) it vets the organization the user's
session claims: a claim that no current membership backs is dropped, and
the user resolves with no active organization, so only their global
grants apply (#15409, ruling B). The explainer never ran that check. For
a user whose membership in the administrator's organization had ended,
the explanation listed that organization's grants, and the verdicts they
decide, while enforcement applied none of them. Enforcement was correct
throughout; the explanation was wrong.
## What changes
- **`@objectstack/core`:** the session arm's check becomes one exported
function, `vetOrganizationClaim(claimedOrganizationId, accessibleOrgIds,
tenancyPosture)`. It returns the claim while a current membership backs
it, or while no wall is enforced, and `undefined` once the claim is
dropped. `resolveAuthzContext`'s session arm now asks it through the
identical boolean. There is no behaviour change: the 11 existing
session-arm tests in `resolve-authz-context.test.ts` pass unchanged.
- **`@objectstack/plugin-security`:** `explainAccessForCaller` asks
`vetOrganizationClaim` about the explained user (their
`accessible_org_ids`, and the plugin's tenancy posture) and resolves
them in the organization it returns. The explainer spells no membership
rule of its own.
- `buildContextForUser`'s signature is unchanged. Its doc now says who
vets the organization it is handed.
**Landing point:** `security-plugin.ts`, as expected. The check was
reachable only through a new core export. The only other exported path
that runs it, `resolveAuthzContext` itself, would skip the explainer's
ruled grants-cache bypass and would write a false "session claim
dropped" log line.
## Evidence
**Pin:**
`packages/plugins/plugin-security/src/explain-removed-member-principal.test.ts`.
- **Rig:** a real `ObjectQL` on better-sqlite3 and on sqlite-wasm, the
real platform objects and the real `SecurityPlugin`.
- **Two faces:** every case compares the explanation with enforcement's
own answer for the same principal over the same rows. Enforcement's face
is `resolveAuthzContext` with a session that claims `org_alpha`, then
`assembleExecutionContext`, then a `find` through the middleware.
- **Walled (`isolated`, `group`):** the removed member's explanation
lists no `org_alpha`-scoped set, the same sets enforcement resolves for
them. Its `object_crud` verdict is `denies`, where their own read is
refused 403 `PERMISSION_DENIED`.
- **KEEP:** a current member's explanation still lists the set, as
enforcement resolves it, and the read is granted on both faces.
- **CONTROL, `single`:** there is no wall, so the claim stands on both
faces.
- **Result at HEAD:** 22 passed of 22.
**Ablation.** The fix was committed first.
`scripts/ablation-replace.mjs` ran it with EXIT, INT and TERM restore,
plus a shell trap on an absolute path.
- **Mutation:** the vetted organization was replaced by the caller's
organization, unvetted. That is the pre-fix resolution.
- **On disk:** anchor count 1 went to 0, and the marker count was 1
during the run.
- **Result:** 8 failed, 14 passed (of 22). All 8 red cases are the
removed-member pins (2 drivers, 2 walled postures, 2 pins each). KEEP,
the precondition and the `single` control stayed green.
- **Quoted:**
AssertionError: expected [ 'member_default', 'qa_probe_editor' ] to not
include 'qa_probe_editor'
AssertionError: expected 'grants' to be 'denies' // Object.is equality
- **Restore:** blob equals HEAD (`6a86472402fd`), and `git diff HEAD` is
empty.
- **First attempt:** the tool refused it before anything ran. The
replacement text already occurred on disk, so its count could not rise.
Nothing was measured; the rerun used a unique marker.
## Local verification
Recorded at HEAD `bd7b46777`. Core's test and typecheck ran at
`cf18f3e99`; the diff from there to HEAD touches only the
plugin-security pin file.
- `pnpm --filter @objectstack/plugin-security test`: 146 files, 3098
passed, 23 skipped.
- `pnpm --filter @objectstack/plugin-security typecheck`: exit 0.
`check:test-typecheck` OK.
- `pnpm --filter @objectstack/core test`: 56 files, 1522 passed. `vitest
run --project local src/security/resolve-authz-context.test.ts`: 102
passed, including the 4 new `vetOrganizationClaim` contract cases.
- `pnpm --filter @objectstack/core test:repo`: 3 files, 48 passed.
- `pnpm --filter @objectstack/core typecheck`: exit 0.
- **Gates:** `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 67 commands (the 51 derived at
dispatch, plus 16 for the changeset and test-layer kinds). Each exit
code was captured before any pipe, and `--ran` reconciled them: 64 exit
0, 3 NOT MEASURED, 0 unrun.
- **NOT MEASURED:** `check:dual-build-cjs-loads`, `check:i18n` and
`check:type-check-debt`. Each exited 3 with PREREQUISITE NOT MET,
because each needs most of the monorepo built, and the core change
invalidates the build cache for nearly every package downstream of it.
CI builds that closure before these steps.
- **NOT MEASURED:** two runtime suites that exercise the session arm end
to end (`packages-orgless-grants-capability-gate`,
`packages-vetted-org-source`). Runtime's dependency closure was unbuilt
here, so vitest reported "Failed to resolve entry for package" and ran
no test. They are declared to CI.
- **Lint, narrowed and measured:**
- Population: `eslint.config.mjs`'s `files` globs, the ts and js family
minus `NEVER_LINTED`. `--print-config` resolves a config for all 6
touched `.ts` files; the changeset `.md` matches no glob.
- Count: `--no-inline-config --format json` over those 6 files, with 0
errors and 0 warnings.
- Invariance: the config enables no type-aware linting (no
`parserOptions.project`, no `projectService`). So this diff cannot move
a verdict on an untouched file.
- **Additive export:** `vetOrganizationClaim` has no other occurrence in
the repository. The two star re-exporters of core (`runtime`,
`plugin-hono-server`) declare no such name.
## Acceptance notes
- **Surface widening (for the seat to re-judge `Clause-②`).**
`@objectstack/core` gains one export, `vetOrganizationClaim`, with no
behaviour change. The changeset grades core `minor` and plugin-security
`patch`. The line above is copied from the claim as it stands.
- **A separate explain-versus-enforce position, measured and left alone
here.** The explained user's context carries no organization of its own:
`buildContextForUser` returns none, and `explainAccessForCaller` sets
none. Measured at `c96beb27`, better-sqlite3, one current member of the
administrator's organization:
- Under `isolated`, Layer 0 answers deny on a tenant object: the
explanation says `allowed: false` with the fail-closed filter, while
that member's own read is admitted.
- Under every posture, a permission set authored in the database and
scoped to that organization does not load for the explained user, while
enforcement loads it.
- Fixing it (set the explained context's organization to the vetted one,
as `resolveDelegatorContext` does for a delegator) changes a current
member's explanation, which this card's ruled pin keeps unchanged. It is
reported to the seat for #20604, the explain-versus-enforce family card.
- **Observation, not measured.** With no `tenancy` service registered,
admission hands the resolver no posture (no claim is ever dropped),
while plugin-security probes `org-scoping` and can resolve `isolated`.
The explainer reads the plugin's posture. In that composition the two
could disagree. Both read the same `tenancy` service when it is
registered.
## Seat append (domain:services seat,
`session_01XY5uCwTjZj7884yYtyur4H`)
- The `Clause-②` line above was corrected from `no` to `yes (widening)`
by the seat, following the dev's deviation 2. `@objectstack/core` gains
one export, `vetOrganizationClaim`, and the changeset already grades
core `minor`. The claim was corrected in place in the same act.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01XY5uCwTjZj7884yYtyur4H)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent aa23e2c commit 889139c
7 files changed
Lines changed: 432 additions & 6 deletions
File tree
- .changeset
- packages
- core/src/security
- plugins/plugin-security/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
137 | 137 | | |
138 | 138 | | |
139 | 139 | | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
140 | 143 | | |
141 | 144 | | |
142 | 145 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | 3 | | |
4 | | - | |
| 4 | + | |
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
| |||
1832 | 1832 | | |
1833 | 1833 | | |
1834 | 1834 | | |
| 1835 | + | |
| 1836 | + | |
| 1837 | + | |
| 1838 | + | |
| 1839 | + | |
| 1840 | + | |
| 1841 | + | |
| 1842 | + | |
| 1843 | + | |
| 1844 | + | |
| 1845 | + | |
| 1846 | + | |
| 1847 | + | |
| 1848 | + | |
| 1849 | + | |
| 1850 | + | |
| 1851 | + | |
| 1852 | + | |
| 1853 | + | |
| 1854 | + | |
| 1855 | + | |
| 1856 | + | |
| 1857 | + | |
| 1858 | + | |
| 1859 | + | |
| 1860 | + | |
| 1861 | + | |
| 1862 | + | |
| 1863 | + | |
| 1864 | + | |
| 1865 | + | |
| 1866 | + | |
1835 | 1867 | | |
1836 | 1868 | | |
1837 | 1869 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
529 | 529 | | |
530 | 530 | | |
531 | 531 | | |
| 532 | + | |
| 533 | + | |
| 534 | + | |
532 | 535 | | |
533 | 536 | | |
534 | 537 | | |
535 | | - | |
536 | | - | |
537 | | - | |
| 538 | + | |
538 | 539 | | |
539 | 540 | | |
540 | 541 | | |
| |||
565 | 566 | | |
566 | 567 | | |
567 | 568 | | |
| 569 | + | |
| 570 | + | |
| 571 | + | |
| 572 | + | |
| 573 | + | |
| 574 | + | |
| 575 | + | |
| 576 | + | |
| 577 | + | |
| 578 | + | |
| 579 | + | |
| 580 | + | |
| 581 | + | |
| 582 | + | |
| 583 | + | |
| 584 | + | |
| 585 | + | |
| 586 | + | |
| 587 | + | |
| 588 | + | |
| 589 | + | |
| 590 | + | |
| 591 | + | |
| 592 | + | |
| 593 | + | |
| 594 | + | |
| 595 | + | |
| 596 | + | |
568 | 597 | | |
569 | 598 | | |
570 | 599 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
568 | 568 | | |
569 | 569 | | |
570 | 570 | | |
| 571 | + | |
| 572 | + | |
| 573 | + | |
| 574 | + | |
571 | 575 | | |
572 | 576 | | |
573 | 577 | | |
| |||
0 commit comments