Repository navigation
Commit aaee084
fix(plugin-security): the controlled_by_parent master-edit leg asks the master's own write-door question (#25008)
Fixes #24986
Clause-②: no
## What this changes
ADR-0055 §3: "editing/deleting/creating a detail requires **edit**
access to its master". The object-level leg of the
`controlled_by_parent` detail write gate (leg 1 of
`assertMasterRowEditable`) asked the permission evaluator for an
object-level `update` grant on the master, and nothing else. For an
`update` on the master, the master's own write door asks three things:
- the master's posture must resolve (#3545);
- the capabilities the master requires for `update` must be held
(ADR-0066 D3/⑤);
- the grant is asked under the master's posture (ADR-0066 D2).
The leg now asks those three. Each input is read from
`getObjectSecurityMeta`, the source every door reads it from. No rule is
re-derived: each check is the evaluator's answer or the posture's own,
asked the way the door asks it.
- **One region.** In `security-plugin.ts` the diff is leg 1 of
`assertMasterRowEditable` and nothing else. Legs 2 and 3 (the master's
write RLS and record sharing), the chain walk and every other region are
untouched.
- **Every door that asks the leg follows.** That is an insert, a by-id
update and a by-id delete through the engine. It is also
`checkControlledByParentWrite`, which the comment and attachment parent
gates ask. The record-grained `security/explain` of a by-id write asks
the same master gate.
- **No widening.** The change adds refusals ahead of the existing grant
check. The posture option only removes coverage, and never adds a grant.
## Reach
Stated abstractly, per the ruling recorded on #9387. Measured with
throwaway probes (not committed) on a real `ObjectQL` + `SqlDriver` +
`SecurityPlugin`, at `e4d604efd3`, which includes #24985.
- **Reach is shown.** Some principals are refused by the master's own
write door at its object gate when they update the master. They can
still read that master, and the detail's own door admits them. On `main`
this leg was the deciding check for them, and it admitted them. An
insert, a by-id update and a by-id delete of a detail under such a
master all landed, and `checkControlledByParentWrite` answered `allow`.
Two inputs produce this class on their own: the master's posture, and a
capability the master requires for `update`. The grant half has a named
real producer. With a non-empty master write filter, the leg still
admitted rows inside the filter.
- **Other gates stop the rest.** If a principal cannot read the master,
the read door stops it before this leg. On an insert that is the
reference check. On a by-id update or delete it is the read scope (since
#24985). The on-behalf-of half is stopped the same way when the agent's
ceiling does not cover the master. The delegator's half is the direct
case.
- **Other paths.** Readings for paths this change does not touch are
held privately by the seat; see Acceptance notes.
The concrete probes, grant shapes and served values are held privately
by the seat.
## Scope, beyond the card's lead
The card's lead names one input: the posture. The capability
prerequisite and the unresolvable posture are the same defect: this leg
asks a narrower question than the master's door. Both are handled here
under the bounded in-place rule, and all four of its conditions hold:
1. it is the same defect class;
2. the fix is mechanical, in the shape the door already uses;
3. no other claim holds this region (#24988 touched
`reportNameFoldCollisions`, which is disjoint, and has landed);
4. it is the same gate family, with the same new test file.
How the four axes read:
- **Real need.** The capability case has measured reach at every write
door above. The unresolvable case had no reach through an engine write,
which already refused. It did have reach through
`checkControlledByParentWrite`, which answered `allow`.
- **Long-term fit.** The leg and the door now ask one question. Each
door pairs the posture with its unresolvable case, because an unresolved
posture reads as not private.
- **AI authoring.** A master's declared `requiredPermissions.update` is
now enforced for its details too. Declared is enforced.
- **Startup scope.** No new capability and no new gate.
The seat may veto either part. Each part is one `if` block with its own
pin.
### Envelopes that move (declared)
- Some principals are refused here and also cannot read the master,
including on an on-behalf-of write. An insert by such a principal now
answers `PERMISSION_DENIED` (403). Before, it reached the reference
check's `VALIDATION_FAILED`. An existing master id and an id that exists
nowhere answer the same 403 (pinned), so there is no existence oracle.
- An insert under a master object that does not resolve now answers
`PERMISSION_DENIED` (403). Before, it answered `OBJECT_NOT_FOUND` (404).
## Tests
New
`packages/plugins/plugin-security/src/controlled-by-parent-master-object-write.test.ts`:
13 tests on the real stack.
- **Refusals.** Three refused groups, one per input. Each pins the
precondition where it applies (the master's own door refuses the update)
and the insert, by-id update and by-id delete envelopes
(`PERMISSION_DENIED` / 403). Each also pins that nothing was written,
and that `checkControlledByParentWrite` answers `{ outcome: 'deny', leg:
'object_permission' }`.
- **Controls.** These keep landing: an object-level edit grant, a
super-user grant, the required capability held, a master with none of
the three inputs, and a system context.
**Ablation.** One leg per pinned behaviour. Each leg was run after the
fix was committed (`38081b6a05`), with `scripts/ablation-replace.mjs` in
wrap mode under a trap-armed driver.
- **Posture** (the option deleted): 3 failed, 10 passed.
- **Capability** (the check disabled): 1 failed, 12 passed.
- **Unresolvable posture** (the check disabled): 2 failed, 11 passed.
Ablated, the insert answered `OBJECT_NOT_FOUND`/404 and the check
answered `allow`.
Every restore was proven: the blob equals HEAD (`7dbb423417f0`) and `git
diff HEAD` is empty. The suite imports `./security-plugin.js` from
source, so no `dist/` sits in the ablation path.
## Verification
Every run below went under `scripts/pm/os-verify-lock.sh`. The final
head is `85f0113f1e`: the fix, a merge of `main` at `7532ea3c08` (which
brings in #24988, a disjoint region), and a changeset-only commit. The
gate union was run at `85f0113f1e`. The build, test and typecheck were
run at `b24e619107`, whose code is byte-identical to the final head (the
last commit edits only `.changeset/`).
- **Closure build** `pnpm --filter '@objectstack/plugin-security^...'
build`: exit 0.
- **`pnpm --filter @objectstack/plugin-security test`**: 199 files
passed; 4114 tests passed, 45 skipped.
- **`pnpm --filter @objectstack/plugin-security typecheck`** (`tsc` ×2 +
`check:test-typecheck`): exit 0.
- **`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack`** at `85f0113f1e`: 69 derived commands, each
run with its exit captured before any pipe, all 69 exit 0. The `--ran`
reconciliation answers "69 derived, 69 run, 0 NOT-MEASURED, 0 UNRUN".
The union at `b24e619107` read the same.
- **Before the merge** (`ac70f18533`): test 4113 passed, typecheck exit
0, gates 67 exit 0. `check:dual-build-cjs-loads` and `check:i18n` exited
3 (PREREQUISITE NOT MET) there; both are green at the final head.
## Acceptance notes
- **A finding outside this leg.** One write path that never asks this
leg was measured too. It is reported to the seat privately as a separate
finding, under the same disclosure ruling. This PR's pins do not touch
that path.
- **The leg vocabulary's contract text.**
`ControlledByParentWriteDenialLeg`'s TSDoc in `@objectstack/spec` reads
`object_permission` as "no object-level `update` grant on a master".
That leg now also carries the capability and unresolvable-posture
refusals, which are the door's object gate. The spec is out of this
card's scope, so its text is unchanged.
- `test:repo` (the vitest `repo` project) was not run locally. The diff
does not touch it, and CI's `Test Core` runs it.
---
_Generated by [Claude
Code](https://claude.ai/code/session_011TdZ8GjixNZFLz1xJD9zzK)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 62bc129 commit aaee084
3 files changed
Lines changed: 422 additions & 1 deletion
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 | + | |
| 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 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
| 374 | + | |
| 375 | + | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
| 379 | + | |
| 380 | + | |
0 commit comments