Skip to content

Commit 91177a2

Browse files
committed
test(plugin-security): S10 refusal pins; changeset for the position namespace narrowing
Claude-Session: https://claude.ai/code/session_013j5gkUCpqQiti4GgPqqmnt Co-authored-by: Claude <noreply@anthropic.com>
1 parent 1804e29 commit 91177a2

2 files changed

Lines changed: 26 additions & 3 deletions

File tree

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
'@objectstack/plugin-security': minor
3+
---
4+
5+
feat(plugin-security)!: under `single`, a Setup create of a position, or a rename into a name, that a package or a built-in already holds is refused with the metadata door's own refusal (ADR-0048 addendum N.2/N.3)
6+
7+
Clause-②: no (narrowing)
8+
9+
<!-- adr-0087: not-required (no-migration-prescription) No metadata moves: no spec key, authorable spelling, export, type or stored shape is added, removed, renamed or re-shaped, so there is nothing for `objectstack migrate meta` to rewrite. What narrows is a runtime write door: under the single posture, a sys_position create, or a rename into a name, that a package or a built-in holds is now refused at the data door, as the metadata door already refuses the same name, and the remedy is a different data value (the position's name), not a rewrite of anyone's code or metadata. The other categories are closed on facts: the package publishes (not unpublished); no ADR-0087 id is named or touched (not registered or already-registered); and no TypeScript declaration moves (not runtime-interface-only or type-surface-only). -->
10+
11+
**BREAKING** (an accept-set narrowing), shipped as `minor` under the launch-window convention for breaking changes.
12+
13+
Positions hold one name per deployment (ADR-0048 addendum, N.2). A position a package declares, and the platform's built-in positions, are managed items: the metadata door already refused an environment save under their names (`PUT /api/v1/meta/position/:name` answers `403 NOT_OVERRIDABLE`). The Setup data door did not.
14+
15+
**FROM.** Under `single`, a non-system `POST /api/v1/data/sys_position` whose `name` a package or a built-in holds answered `201`. The new row sat beside the package's (or the platform's) position under the same name. A `PATCH` that renamed an administrator's position into such a name answered `200`, and it also deleted that position's own environment definition. Measured on the showcase: a create of `manager` (package-declared), `everyone` or `guest` (built-in audience anchors) answered `201`, and a rename into `exec` answered `200`.
16+
17+
**TO.** Under `single`, the same create, or a rename into such a name, is refused with the metadata door's own refusal, relayed as the door builds it: `403 NOT_OVERRIDABLE`. No row is kept, a renamed row keeps its old name, and its definition is untouched. The door's own verdict is asked; nothing is written to environment metadata for such a name.
18+
19+
- **Unchanged.** The engine's own refusals answer first, as before: a reserved built-in identity name (`platform_admin`, `org_owner`, `org_admin`, `org_member`) stays `400 VALIDATION_FAILED`, and a name already held in the same organization stays `409 UNIQUE_VIOLATION`. An edit that keeps such a name, and a delete, behave as before. System writes, walled postures and kernels without a metadata door behave as before. Under a walled posture, each organization already holds its own rows of the declared and built-in names, so the same create answers `409 UNIQUE_VIOLATION`, as before.
20+
- **Remedy.** Rename the position: give it a name no package and no built-in holds (for example `regional_manager` instead of `manager`), then point any assignment at the new name. `GET /api/v1/meta/position` lists the names already held.

‎packages/plugins/plugin-security/src/position-write-through.test.ts‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -508,10 +508,13 @@ describe('[C2 stage S10, ADR-0048 addendum N.2/N.3] one namespace — a create o
508508
expect(await b.envRows(held)).toEqual([]);
509509
});
510510

511-
it('control: the engine\'s own refusals keep their answer — a reserved identity name stays 400 VALIDATION_FAILED', async () => {
511+
it('control: the engine\'s own refusals keep their answer — a reserved identity name stays VALIDATION_FAILED', async () => {
512512
const b = await boot();
513-
const refusal = await refusalOf(create(b, { name: 'org_admin', label: 'Mine' }));
514-
expect(envelope(refusal)).toEqual({ code: 'VALIDATION_FAILED', status: 400 });
513+
// The platform holds `org_admin` too, but the engine's rule validator refuses
514+
// the row before the door is asked (the REST layer answers it 400).
515+
const refusal: any = await refusalOf(create(b, { name: 'org_admin', label: 'Mine' }));
516+
expect({ name: refusal?.name, code: refusal?.code }).toEqual({ name: 'ValidationError', code: 'VALIDATION_FAILED' });
517+
expect(await b.rows('org_admin')).toEqual([]);
515518
});
516519

517520
it('control: an edit that keeps a held name stays a row write, with no definition and no refusal', async () => {

0 commit comments

Comments
 (0)