Repository navigation
Commit 669a8aa
fix(plugin-security)!: a delegated business-unit walk never starts at a unit outside the caller's organization (#31442)
Clause-②: no (narrowing) — the delegated-administration gate holds the
root of every delegated business-unit walk to the same organization as
its descendants, so the range a self-delegation's anchor is judged
against holds units of the caller's organization only. Nothing is
widened: no export, error code or status is added.
Metadata-protocol: none
Skills-docs: none — a runtime authorization bound inside
`@objectstack/plugin-security`; no authorable key, value or metadata
type changes, and no `skills/**` or docs page teaches the
self-delegation anchor range's organization bound.
## 维护者速读
- 改了什么:委派管理按业务单元圈定的范围,其起点(根单元)与下级单元同一条规则:只能是调用者所在组织的单元。
- 为什么改:ADR-0105 D6 红线 2 ——
一切业务单元机制只在一个组织内运作,没有跨组织的树;委派范围里的每个单元(含根)都属于调用者的组织。
- 风险与回滚:只动 plugin-security
的委派管理闸门;组织内范围里的锚点、不带锚点的职务代理照旧通过,拒绝的答复与范围外锚点一致。按先例 c1e08fe
声明为收窄(BREAKING,patch 级):锚在本组织之外单元的自我委派从此被拒。回滚 = revert 本 PR。
- 先看:`packages/plugins/plugin-security/src/delegated-admin-gate.ts` 的
`resolveSubtreeById`(根判定那一段);再看两个测试文件末尾新增的 describe。
- 两分钟验证:`pnpm --filter @objectstack/plugin-security exec vitest run
--maxWorkers=2 src/delegated-admin-own-name-set.posture.test.ts -t
"never starts at a unit outside"`,预期 `Tests 15 passed`。
- 需要你做的:无,席位落地。
## What changes
**Rule** (ADR-0105 D6, red line 2: *"Every BU mechanism —
`unit_and_subordinates` sharing, `adminScope` delegation, depth scopes —
operates within one organization. There is no cross-org tree."*): a
delegated business-unit walk never starts at a unit outside the caller's
organization.
- `DelegatedAdminGate.resolveSubtreeById` reads the walk's root under
the bound organization's own scoped context and applies to it the same
row predicate the walk already applies to every descendant. A root that
is not a unit of that organization covers nothing.
- Every subtree a self-delegation's anchor range is built from (ADR-0091
D3, rule 4b) therefore holds units of the caller's organization only. An
anchor outside the range is refused with `403 PERMISSION_DENIED`, the
same answer whichever organization the unit belongs to.
- `resolveSubtree` (a scope's by-name anchor) keeps its answers: its
root is already resolved inside the organization, so the new predicate
holds for it by construction. It gains one by-id read per resolved
scope.
- No refusal text, error code, schema or export changes.
## Tests
- `delegated-admin-own-name-set.posture.test.ts`, new describe *a
delegated business-unit walk never starts at a unit outside the caller's
organization*, on a real `ObjectQL` engine and `SqlDriver` with the real
`SecurityPlugin`, in `single`, `group` and `isolated` (5 tests per
posture, 15 in all):
- an unanchored holding's range is the delegator's member units of its
own organization; an anchor at a unit of another organization gets the
same answer as an anchor outside the range;
- a holding anchored at a unit of another organization makes no unit an
admitted anchor;
- CONTROL: a holding anchored at a unit of the caller's organization
keeps its subtree as the range;
- the data door stores no delegation anchored at a unit of another
organization, and stores one inside the range;
- CONTROLS throughout: an in-organization anchor inside the range is
admitted, and so is an unanchored delegation.
- `delegated-admin-gate.test.ts`, new describe on a double that scopes
nothing, so the row predicate is the only thing standing: 3 tests, one
of them a CONTROL, including an organization-less unit.
- The posture rig's `boot` takes optional extra catalog entries, so the
new describe's delegatable position does not change what the other
describes enumerate.
## Acceptance notes
- **Not added:** a second anchor check in the self-delegation rule. Once
every root is held to the organization predicate it is redundant, and a
refusal reason of its own would answer ids that are units of the
caller's organization differently from other ids.
- **Untouched, as ordered:** the by-name root for a context that names
no organization, with its docblocks and pins. The existing pins on the
subtree staying inside one organization, with or without an organization
on the context, hold unchanged.
## Verification
Readings at `3de01b6fb` unless stated. The final head `b731e114b`
changes only the changeset (the narrowing declaration, its BREAKING
paragraph and the ADR-0087 marker).
- Dogfood battery at `b731e114b`: lock `VERDICT command-exit 0`, `Test
Files 438 passed | 3 skipped (441)`, `Tests 3622 passed | 41 skipped
(3663)`, no failures.
<details><summary>Package suite and typecheck</summary>
- `pnpm --filter @objectstack/plugin-security test`: `Test Files 249
passed (249)`, `Tests 5886 passed | 45 skipped (5931)`, lock `VERDICT
command-exit 0`.
- `pnpm --filter @objectstack/plugin-security typecheck`: exit 0 (`tsc`
src and scripts, plus `check:test-typecheck` OK).
- These two ran at `cf5c2456c`. The commit after it changes only the
changeset's declaration line.
</details>
<details><summary>Ablation (one-time, nothing left in the
tree)</summary>
- Mutation: the root predicate is removed on disk through
`scripts/ablation-replace.mjs` (anchor hits 1 → 0, blob `4cd4659e28e4` →
`e0c1deb04f78`). The subject resolves through relative `src` imports, so
no `dist` is in the path.
- With it removed, the two files report `Tests 11 failed | 242 passed
(253)`: the 9 posture legs (3 per posture) and the 2 fake-engine pins
that are not controls. Every CONTROL stayed green.
- Restored: the blob equals the one at `HEAD` (`4cd4659e28e4`) and `git
diff HEAD` is empty. The same files then report `Tests 253 passed
(253)`.
</details>
<details><summary>Derived gates</summary>
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` at `3de01b6fb`: 66 commands. Each ran from
the worktree, with its exit code captured before any pipe: 66 of 66 exit
0.
- `--ran` reconciliation: `66 derived, 66 run, 0 NOT-MEASURED, 0 UNRUN`
(a derived zero: every line records its exit code).
- At `cf5c2456c`, the first sweep had `check:dual-build-cjs-loads` and
`check:i18n` refuse their prerequisite (exit 3, missing `dist`). After
building the closure that `check:i18n` names, both measure green on the
head re-run: 122 require entry points across 72 packages load, and 10
packages' bundles are in sync.
- Changeset gates on the final changeset: `check-adr-0087-registration`,
`check-changeset-no-major` and `check-empty-changeset` (all `--base
origin/main`) exit 0, and so does `check:nul-bytes`.
- Every family CI runs is read from this PR's check-runs. None of them
is re-run locally.
</details>
<!-- signed: security#1 · sunhaolin · session_017xRBcT1w3zpekijraHJE4f
-->
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent a40eb02 commit 669a8aa
4 files changed
Lines changed: 261 additions & 6 deletions
File tree
- .changeset
- packages/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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
741 | 741 | | |
742 | 742 | | |
743 | 743 | | |
| 744 | + | |
| 745 | + | |
| 746 | + | |
| 747 | + | |
| 748 | + | |
| 749 | + | |
| 750 | + | |
| 751 | + | |
| 752 | + | |
| 753 | + | |
| 754 | + | |
| 755 | + | |
| 756 | + | |
| 757 | + | |
| 758 | + | |
| 759 | + | |
| 760 | + | |
| 761 | + | |
| 762 | + | |
| 763 | + | |
| 764 | + | |
| 765 | + | |
| 766 | + | |
| 767 | + | |
| 768 | + | |
| 769 | + | |
| 770 | + | |
| 771 | + | |
| 772 | + | |
| 773 | + | |
| 774 | + | |
| 775 | + | |
| 776 | + | |
| 777 | + | |
| 778 | + | |
| 779 | + | |
| 780 | + | |
| 781 | + | |
| 782 | + | |
| 783 | + | |
| 784 | + | |
| 785 | + | |
| 786 | + | |
| 787 | + | |
| 788 | + | |
| 789 | + | |
| 790 | + | |
| 791 | + | |
| 792 | + | |
| 793 | + | |
| 794 | + | |
| 795 | + | |
| 796 | + | |
| 797 | + | |
| 798 | + | |
| 799 | + | |
| 800 | + | |
| 801 | + | |
| 802 | + | |
| 803 | + | |
| 804 | + | |
| 805 | + | |
| 806 | + | |
| 807 | + | |
| 808 | + | |
| 809 | + | |
| 810 | + | |
744 | 811 | | |
745 | 812 | | |
746 | 813 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1752 | 1752 | | |
1753 | 1753 | | |
1754 | 1754 | | |
1755 | | - | |
| 1755 | + | |
| 1756 | + | |
| 1757 | + | |
| 1758 | + | |
| 1759 | + | |
| 1760 | + | |
| 1761 | + | |
| 1762 | + | |
1756 | 1763 | | |
1757 | 1764 | | |
1758 | 1765 | | |
1759 | 1766 | | |
| 1767 | + | |
| 1768 | + | |
| 1769 | + | |
| 1770 | + | |
| 1771 | + | |
| 1772 | + | |
| 1773 | + | |
| 1774 | + | |
| 1775 | + | |
| 1776 | + | |
| 1777 | + | |
| 1778 | + | |
| 1779 | + | |
1760 | 1780 | | |
1761 | 1781 | | |
1762 | 1782 | | |
| |||
1788 | 1808 | | |
1789 | 1809 | | |
1790 | 1810 | | |
1791 | | - | |
| 1811 | + | |
| 1812 | + | |
| 1813 | + | |
| 1814 | + | |
| 1815 | + | |
1792 | 1816 | | |
1793 | 1817 | | |
1794 | 1818 | | |
| |||
0 commit comments