Repository navigation
Commit b3fddd1
fix(plugin-approvals): an out-of-office delegation applies only in its own organization (#25439)
<!-- claude-projects-attribution: {"github_login":"os-sales"} -->
_Requested by **Sales** · [project
thread](https://claude.ai/code/project/chan_01D5bWFY3rFzgJcytD5gD4Uu?thread=cmsg_01D5bWFY3rFzgJcytD5gD4UuGEYT5aVm7ttZRBJJNvF8Bj)_
Fixes #25280
Clause-②: no (narrowing)
Metadata-protocol: none
## What changed
`ApprovalService.lookupActiveDelegation` now reads a
`sys_approval_delegation` row the way `grantAppliesInTenant`
(`@objectstack/core`, `resolve-authz-context.ts`) reads a stored
position or permission-set grant: the row reroutes an approver only when
its `organization_id` is the request's organization, and both must name
one.
| Delegation row | Request | Before | After |
|---|---|---|---|
| org A | org A | reroutes | reroutes |
| org A | org B | no | no |
| no organization (`NULL`, absent, blank) | any | **reroutes in every
organization** | no, and a `warn` names the row |
| any | no organization | **takes every active row** | no |
- `organizationNamed()` normalizes `NULL`, absent and `''` to "none", as
core's module-private `grantOrganizationOf` does. It is applied to both
sides of the comparison.
- An active organization-less row that the lookup skips is logged at
`warn`. The structured fields are `{ delegationId, delegatorId,
delegateId, requestOrganizationId }`, and the message gives the remedy.
Its author still sees the row in Setup, and nothing else says it has
stopped applying.
- The rule is the same in every posture (ADR-0131 D11). There is no
posture branch.
- `sys_approval_delegation.organization_id` help, which said "null =
applies across tenants for this delegator", now says what the code does:
- `en` is regenerated with `node scripts/check-i18n-bundles.mjs
--write`.
- `zh-CN`, `es-ES` and `ja-JP` are translated by hand. The `es-ES` and
`ja-JP` leaves were byte copies of the old English, so their source-hash
entries drop on extract.
- The object docblock and `content/docs/automation/approvals.mdx`
(Out-of-office delegation) state the organization scope.
## Who is affected
**Under a wall (`group`/`isolated`), only stored state is affected.** No
current write produces an organization-less delegation:
- A session insert is stamped with the active organization
(`injectTenantOnInsert` in `sql-driver.ts`).
- An elevated insert with no organization is refused
(`resolveSystemInsertOrganization`, D9).
- An update that empties the column is refused
(`denyOrganizationRemoval`, `security-plugin-write-tenant-wall.ts`).
What remains are rows stored before 18.0. The D10 ceremony has no anchor
for this table under a wall (inventory fate 3), so it reports such a row
and leaves it `NULL`. Before this change, that row rerouted every
organization's approvals for its delegator, onto a delegate who may hold
no membership there.
**Under `single`, a delegation written on 18.x applies exactly as
before.** D9 stamps the Default Organization on both the delegation and
the record, and the request takes its record's organization. Only a
pre-ceremony organization-less row stops applying, and `os migrate
organization-ownership --apply` attributes it.
The changeset declares the break (`**BREAKING**`, ADR-0087 `not-required
(no-migration-prescription)` marker) and gives the operator remedy for
each posture. Precedent:
`.changeset/24583-7a2-organization-less-grant-readers-retired.md`.
## Tests
New file `ooo-delegation-organization.test.ts` (18 cases). Each walled
case runs under both `isolated` and `group`:
- A's delegation does not reroute an approval in B, and the request
lands in B.
- The same-organization delegation still applies, with the audit comment
unchanged.
- The request's organization is its record's: a member signed in to A,
opening on B's record, gets no reroute from A's delegation.
- An organization-less row reroutes nothing in either organization and
is reported once per lookup, with the named fields.
- A blank or absent organization counts as organization-less.
- Beside an organization-less row, the same-organization row is the one
that applies.
- A chain stops at an organization-less hop.
- A request with no organization takes no delegation.
- Under `single`, a Default-Organization delegation reroutes, and an
organization-less row does not and is reported.
`approval-service.test.ts`: the pin "a null-org rule applies across
tenants" is replaced by its inverse.
Fixtures that wrote an organization-less delegation and expected a
reroute now write it in the request's organization:
- `admin-exemption-retired.test.ts`: `seedDelegation` writes `'t1'`.
- `admin-rescue-named-slate.integration.test.ts`: `dlg_admin` writes
`ORG`, and `sys_approval_delegation` leaves the `relaxTenantColumns`
list.
- `approval-notification-body.integration.test.ts`: `dlg_1` and `dlg_2`
write `orgOfDeal(...)`, and `sys_approval_delegation` leaves the
`relaxTenantColumns` list.
The three `installDefaultOrganization` suites that insert a delegation
(`actor-lookups-hold-ids`, `approval-notification-keyed-text`,
`approval-requested-notification`) pass unchanged. D9 stamps the Default
Organization on both sides there.
`scripts/engine-double-contract.pinned.json` records the new test file's
double (`check-engine-double-contract --write`). Its `update`/`delete`
open with `assertEngineUpdateDispatch`/`assertEngineDeleteDispatch`, and
its `find` holds the caller's `limit`.
## Verification
- `pnpm --filter @objectstack/plugin-approvals exec vitest run`: 95
files, 1232 tests, all passed. `pnpm --filter
@objectstack/plugin-approvals typecheck` passed, including
`check:test-typecheck`.
- Ablation (`scripts/ablation-replace.mjs`): the old filter was restored
in `approval-service.ts`, then the new file, `approval-service.test.ts`
and the three re-stamped fixtures were run.
- 10 tests went red: the organization-less, blank or absent, chain,
no-organization-request and `single` organization-less cases, plus the
replaced pin.
- The cross-organization and same-organization cases stay green under
the old filter, as expected: the old filter already kept a scoped row in
its own organization.
- After the restore, the file matched HEAD and `git diff HEAD` was
empty.
- `node scripts/check-i18n-bundles.mjs`: OK, all bundles in sync.
- `node scripts/check-adr-0087-registration.mjs --base origin/main`:
exit 0.
- Local gates (`dispatch-gates --commands`): on the first pass 95 of 99
exited 0. The other four were handled as follows:
- `check:engine-double-contract` and `check:objectql-double-limit`:
fixed (see above).
- `check:dual-build-cjs-loads` and `check:skill-examples`: exit 3
because their prerequisite dists were missing. Those packages are now
built.
- The full re-run of the gates on the final head and the full dogfood
battery (the `(narrowing)` arm) are running now. Their counts go in the
`os-dev-report` on #25280.
## Acceptance notes
- The branch is the harness-assigned `claude/project-thread-4a6wrl`, not
`claude/issue-25280-…`.
- `content/docs/automation/approvals.mdx` gains one paragraph on the
organization scope, so the guide states the behaviour this PR changes.
- #25342 (delegator prefill, in flight) edits the same object file and
bundles, but only the `delegator_id` default and help. Whichever lands
second merges main and regenerates the bundles.
- The remedy for a walled organization-less row is in the warning and
the changeset: recreate the delegation while signed in to the right
organization, then delete the old row. No automated rewrite is offered,
because under a wall the row names nothing to attribute it by.
---
_Generated by [Claude
Code](https://claude.ai/code/session_012KEX9o2giH3Wzt4jgeNTAK)_
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_012KEX9o2giH3Wzt4jgeNTAK
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 17ef032 commit b3fddd1
16 files changed
Lines changed: 389 additions & 20 deletions
File tree
- .changeset
- content/docs/automation
- packages/plugins/plugin-approvals/src
- translations
- scripts
| 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 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
900 | 900 | | |
901 | 901 | | |
902 | 902 | | |
| 903 | + | |
| 904 | + | |
| 905 | + | |
| 906 | + | |
| 907 | + | |
| 908 | + | |
| 909 | + | |
903 | 910 | | |
904 | 911 | | |
905 | 912 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
202 | 202 | | |
203 | 203 | | |
204 | 204 | | |
205 | | - | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
206 | 209 | | |
207 | 210 | | |
208 | 211 | | |
209 | | - | |
| 212 | + | |
210 | 213 | | |
211 | 214 | | |
212 | 215 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
188 | 188 | | |
189 | 189 | | |
190 | 190 | | |
191 | | - | |
| 191 | + | |
192 | 192 | | |
193 | 193 | | |
194 | 194 | | |
| |||
292 | 292 | | |
293 | 293 | | |
294 | 294 | | |
295 | | - | |
| 295 | + | |
296 | 296 | | |
297 | 297 | | |
298 | 298 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
216 | 216 | | |
217 | 217 | | |
218 | 218 | | |
219 | | - | |
| 219 | + | |
220 | 220 | | |
221 | 221 | | |
222 | 222 | | |
| |||
263 | 263 | | |
264 | 264 | | |
265 | 265 | | |
| 266 | + | |
266 | 267 | | |
267 | | - | |
| 268 | + | |
268 | 269 | | |
269 | 270 | | |
270 | 271 | | |
| |||
273 | 274 | | |
274 | 275 | | |
275 | 276 | | |
276 | | - | |
| 277 | + | |
277 | 278 | | |
278 | 279 | | |
279 | 280 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
3134 | 3134 | | |
3135 | 3135 | | |
3136 | 3136 | | |
3137 | | - | |
| 3137 | + | |
| 3138 | + | |
| 3139 | + | |
3138 | 3140 | | |
3139 | 3141 | | |
3140 | | - | |
| 3142 | + | |
| 3143 | + | |
3141 | 3144 | | |
3142 | 3145 | | |
3143 | 3146 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1222 | 1222 | | |
1223 | 1223 | | |
1224 | 1224 | | |
| 1225 | + | |
| 1226 | + | |
| 1227 | + | |
| 1228 | + | |
| 1229 | + | |
| 1230 | + | |
| 1231 | + | |
| 1232 | + | |
| 1233 | + | |
| 1234 | + | |
| 1235 | + | |
| 1236 | + | |
| 1237 | + | |
1225 | 1238 | | |
1226 | 1239 | | |
1227 | 1240 | | |
| |||
3227 | 3240 | | |
3228 | 3241 | | |
3229 | 3242 | | |
| 3243 | + | |
| 3244 | + | |
| 3245 | + | |
| 3246 | + | |
| 3247 | + | |
| 3248 | + | |
| 3249 | + | |
| 3250 | + | |
| 3251 | + | |
| 3252 | + | |
| 3253 | + | |
| 3254 | + | |
| 3255 | + | |
| 3256 | + | |
| 3257 | + | |
| 3258 | + | |
| 3259 | + | |
| 3260 | + | |
| 3261 | + | |
| 3262 | + | |
| 3263 | + | |
| 3264 | + | |
| 3265 | + | |
| 3266 | + | |
| 3267 | + | |
| 3268 | + | |
3230 | 3269 | | |
3231 | 3270 | | |
3232 | 3271 | | |
| |||
3243 | 3282 | | |
3244 | 3283 | | |
3245 | 3284 | | |
3246 | | - | |
3247 | | - | |
3248 | | - | |
3249 | | - | |
| 3285 | + | |
| 3286 | + | |
| 3287 | + | |
| 3288 | + | |
| 3289 | + | |
| 3290 | + | |
| 3291 | + | |
| 3292 | + | |
| 3293 | + | |
| 3294 | + | |
| 3295 | + | |
| 3296 | + | |
| 3297 | + | |
| 3298 | + | |
| 3299 | + | |
| 3300 | + | |
| 3301 | + | |
| 3302 | + | |
3250 | 3303 | | |
3251 | 3304 | | |
3252 | 3305 | | |
| |||
0 commit comments