Repository navigation
Commit 8f78495
Closes #20896
Clause-②: no (no schema or published-export change; the route and its
closed parameter set are unchanged, as the ruling states)
Implements ruling A on the card (comment 5921162178, its execution
parameters taken whole, per the claim 5921450654): `GET
/api/v1/data/:object/export?template=true` is judged by the import
door's gates, not the export door's. Branch base: `f6ccca4a44`.
## What changed
- `packages/rest/src/rest-server.ts`
- The export route picks its gates by what the caller asked for. A
request whose `template` value reads `true` goes through the new
`enforceImportTemplateGates`. The value is read by the template reader
itself (`readTemplateMode`, given the `template` key alone), so no
second spelling of that reading exists. Every other request goes through
`enforceApiAccess(..., 'export')` and `enforceExportPermission` exactly
as before. The gates still run before the query-string gates, in the
same position, so `template=true&limit=5` from an importer is still
refused `400` with the reason, not `403`.
- `enforceImportTemplateGates`:
1. The object half is the import door's own first gate, the same call
`POST /data/:object/import` makes before it parses:
`enforceApiAccess(..., 'import')` (`404` not exposed, `405` neither
create nor update exposed).
2. The caller half is the create permission: the security service's
`explain({ object, operation: 'create' })`. `allowed: false`, or a
throw, answers `403 PERMISSION_DENIED`. No security service, or one
without `explain`, allows, the stance `enforceExportPermission` takes.
- Docblocks: the `template` paragraph of `DATA_EXPORT_PARAMS` and
`answerImportTemplate` now name the import door's gates.
- `packages/rest/src/import-template-route.test.ts`: the pins below. The
three field-level-security fixtures now grant create through `explain`
instead of `canExport`, which no longer reaches the template.
- `content/docs/permissions/permission-sets.mdx`: the `allowExport`
section says the template is gated by create, never `allowExport`.
- `.changeset/20896-template-import-door.md`: patch, new. The
still-pending `.changeset/18386-export-import-template.md` is NOT
edited: the maintainer answered B on the decision `5922804275` (「20991
20977 都不改」, 2026-10-01), and the one-sentence correction proposed at
`501dca7347` was reverted at `8e2d1fda64` (the file is byte-identical to
the merge base).
## Premise measured: there is no route-level caller create check to
reuse
The dispatch asked to reuse the create check the import door enforces.
Read at `f6ccca4a44`:
- The import door's route-level gates are `enforceApiAccess(...,
'import')` (stage 1) and `enforceApiAccess(..., 'import', { writeMode
})` (stage 2). Both are OBJECT-level (`enable.apiMethods`, `404` /
`405`).
- The caller's create permission is never asked before a write. The
engine's security middleware judges it on each written row, and
`runImport` records the refusal as a failed row (`PERMISSION_DENIED`)
inside a `200` report (the per-row catch in `import-runner.ts`).
So the template branch reuses stage 1 verbatim and asks the security
service for the middleware's create verdict through
`ISecurityService.explain`. That member is not optional. The contract
names it as the composition for an object-level verdict with no
dedicated method, and `mayReadRunState` in
`packages/runtime/src/domains/automation.ts` is the in-repo precedent.
Nothing here is re-derived from permission sets. Stage 2 is not asked:
it needs the write mode a request body names, and a template request
names none.
## Fork clause: not triggered
The template's columns do not depend on a write mode.
`templateColumns(schema, { explicitFields, permitted })` takes none.
`permitted` comes from `ISecurityService.getWritableFields(object,
context)`, whose signature has no operation (plugin-security's
implementation reads the field mask only). The import door's `writeMode`
check is a gate (`404` / `405`) and hands nothing to the column
computation. No mode was picked.
## Pins
In `import-template-route.test.ts`:
- create on the object, no `allowExport`: `200` and the workbook;
`canExport` never asked.
- no create: `403 PERMISSION_DENIED`, empty body, `answerImportTemplate`
never called, `getWritableFields` never asked.
- `allowExport` and no create: `403 PERMISSION_DENIED` on
`template=true`, builder never called, `canExport` never asked.
- a create verdict that throws: `403`, never a grant.
- object half: an object exposing `create` without `list` serves the
template; an object exposing `get` and `list` only answers `405
OBJECT_API_METHOD_NOT_ALLOWED`, builder never called.
- preservation, non-template export: without `allowExport` it is `403
EXPORT_NOT_PERMITTED`, no row read, create verdict never asked. With
`allowExport` and no create it is `200` with the pre-change headers,
text and sha256, create verdict never asked. The existing byte-identity
block is unchanged and green.
Each pin is ablated after this PR opens: a mutation proven on disk, the
run red, the restore proven, the run green. The readings go in the
`os-dev-report` comment on the card, not here.
## Acceptance notes
- One direction is stricter than a write. `explain` denies a caller
whose permission sets resolve empty, where the middleware skips its CRUD
gate. That is reachable only on a deployment with no baseline permission
set, and it is the closed direction. `mayReadRunState` records the same
boundary.
- A caller who holds update but not create is refused the template, even
though they could import in update mode. Ruling A names create.
- The refusal code is `PERMISSION_DENIED`, a standard code, so the
error-code ledger does not change. It is the code the import door's row
report carries for the same caller.
- The objectui#9600 pointer (the button needs create, not export) is the
seat's act when this lands, per the claim.
## Verification at the current head `8e2d1fda64`
- One appended commit restores the 18386 note from the merge base; the
code is byte-identical to `501dca7347`.
- `check-empty-changeset --base origin/main` exit 0;
`check-changeset-no-major` and `check-adr-0087-registration` exit 0.
- rest typecheck exit 0; rest suite 248 files / 4962 passed, exit 0.
- `dispatch-gates`: 93 accounted, 92 run, 1 NOT MEASURED
(`check:dual-build-cjs-loads`; CI builds it). Dev report `5923358262` on
#20896.
## Verification at the prior head `501dca7347`
- The 403 answers `{ success: false, error: { code: PERMISSION_DENIED,
message, details: { object } } }` through `sendError`
(`check:route-envelope` refused the flat form).
- rest typecheck exit 0; rest suite 248 files / 4962 passed, exit 0.
- Ablations A1–A3 and A5 went red as expected and were restored (blob
equal to HEAD).
- `dispatch-gates`: 93 accounted, 92 run, 1 NOT MEASURED
(`check:dual-build-cjs-loads`, which needs a full build; CI builds it).
`check-empty-changeset` was red by design at this head (the
then-proposed correction of the pending #18386 note, since reverted).
## Verification at opening
- `pnpm --filter @objectstack/rest exec vitest run --project local
--maxWorkers=2 src/import-template-route.test.ts`: 37 passed, 0 failed,
at `617255a015`. That is the code commit; the head `3c7d7dd4ef` adds
only the changesets and the doc.
- The package-wide `test` and `typecheck`, the ablations and
`dispatch-gates` finish after opening. Their exit codes are in the
report on the card.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent bb2eccf commit 8f78495
4 files changed
Lines changed: 237 additions & 29 deletions
File tree
- .changeset
- content/docs/permissions
- packages/rest/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 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
95 | 95 | | |
96 | 96 | | |
97 | 97 | | |
98 | | - | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
99 | 101 | | |
100 | 102 | | |
101 | 103 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
8 | 8 | | |
9 | 9 | | |
10 | 10 | | |
11 | | - | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
12 | 14 | | |
13 | 15 | | |
14 | 16 | | |
| |||
104 | 106 | | |
105 | 107 | | |
106 | 108 | | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
107 | 115 | | |
108 | 116 | | |
109 | 117 | | |
| |||
130 | 138 | | |
131 | 139 | | |
132 | 140 | | |
133 | | - | |
| 141 | + | |
134 | 142 | | |
135 | 143 | | |
136 | 144 | | |
| |||
221 | 229 | | |
222 | 230 | | |
223 | 231 | | |
224 | | - | |
| 232 | + | |
225 | 233 | | |
226 | 234 | | |
227 | 235 | | |
| |||
233 | 241 | | |
234 | 242 | | |
235 | 243 | | |
236 | | - | |
| 244 | + | |
237 | 245 | | |
238 | 246 | | |
239 | 247 | | |
| |||
244 | 252 | | |
245 | 253 | | |
246 | 254 | | |
247 | | - | |
| 255 | + | |
248 | 256 | | |
249 | 257 | | |
250 | 258 | | |
| |||
253 | 261 | | |
254 | 262 | | |
255 | 263 | | |
256 | | - | |
257 | | - | |
258 | | - | |
| 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 | + | |
259 | 313 | | |
260 | 314 | | |
261 | | - | |
| 315 | + | |
262 | 316 | | |
263 | 317 | | |
264 | 318 | | |
265 | | - | |
| 319 | + | |
266 | 320 | | |
267 | 321 | | |
268 | | - | |
269 | | - | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
270 | 335 | | |
271 | 336 | | |
272 | | - | |
| 337 | + | |
| 338 | + | |
273 | 339 | | |
274 | 340 | | |
275 | 341 | | |
| 342 | + | |
276 | 343 | | |
277 | 344 | | |
278 | 345 | | |
| |||
556 | 623 | | |
557 | 624 | | |
558 | 625 | | |
559 | | - | |
560 | | - | |
| 626 | + | |
| 627 | + | |
| 628 | + | |
| 629 | + | |
| 630 | + | |
561 | 631 | | |
562 | 632 | | |
563 | 633 | | |
| |||
586 | 656 | | |
587 | 657 | | |
588 | 658 | | |
| 659 | + | |
589 | 660 | | |
590 | 661 | | |
| 662 | + | |
591 | 663 | | |
592 | | - | |
| 664 | + | |
| 665 | + | |
593 | 666 | | |
594 | 667 | | |
595 | | - | |
| 668 | + | |
596 | 669 | | |
597 | 670 | | |
598 | | - | |
| 671 | + | |
599 | 672 | | |
600 | 673 | | |
601 | | - | |
| 674 | + | |
602 | 675 | | |
603 | 676 | | |
604 | 677 | | |
| |||
625 | 698 | | |
626 | 699 | | |
627 | 700 | | |
| 701 | + | |
| 702 | + | |
| 703 | + | |
| 704 | + | |
| 705 | + | |
| 706 | + | |
| 707 | + | |
| 708 | + | |
| 709 | + | |
| 710 | + | |
| 711 | + | |
| 712 | + | |
| 713 | + | |
| 714 | + | |
| 715 | + | |
| 716 | + | |
| 717 | + | |
| 718 | + | |
| 719 | + | |
| 720 | + | |
| 721 | + | |
| 722 | + | |
| 723 | + | |
628 | 724 | | |
0 commit comments