Finish TEN-WP-0010-T03/T04: audited grouping mutation
All checks were successful
Build and Publish Container Image / build-and-push (push) Successful in 1m5s
All checks were successful
Build and Publish Container Image / build-and-push (push) Successful in 1m5s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
2e11b6a155
commit
b998ca2332
6 changed files with 422 additions and 3 deletions
|
|
@ -167,7 +167,7 @@ this survey only establishes that nothing breaks on day one.
|
|||
|
||||
```task
|
||||
id: TEN-WP-0010-T03
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "b6c0c54f-a961-412b-b2a8-c39e7a6d48c1"
|
||||
```
|
||||
|
|
@ -205,11 +205,43 @@ notion of "loosening" that can drift from the first.
|
|||
Done when the mutation is authorized, versioned, idempotent, correlated, and
|
||||
audited, with the reserved-identifier and retired-tenant cases tested.
|
||||
|
||||
Done 2026-08-17. `Tenant.with_grouping()` plus `POST /tenants/{id}/grouping`,
|
||||
routed through the existing `_lifecycle_mutation` spine — no new store surface,
|
||||
as predicted, because `mutate_tenant()` already takes a callable. 22 new tests,
|
||||
253 total, all passing.
|
||||
|
||||
**Deviation from this task as written: retired tenants cannot be reclassified
|
||||
at all.** The task said to allow tightening and refuse loosening, reusing
|
||||
`guard_guardrail_write`. On implementation that is the wrong call, for two
|
||||
reasons. A retired tenant's guardrails already clamp to the floor, so a
|
||||
reclassification changes nothing until reactivation — there is no operational
|
||||
need being served. And `with_metadata` already refuses outright on a retired
|
||||
tenant, so allowing a *ceiling-moving* change where a rename is refused would
|
||||
be the inconsistency, not the safety. Refusing is the more restrictive
|
||||
behaviour and loses nothing: reactivate, reclassify, and the ordinary rules
|
||||
apply. The reduce-privilege exception stays where it earns its keep — role
|
||||
revocation and guardrail tightening, both of which only ever reduce.
|
||||
|
||||
Other decisions:
|
||||
|
||||
- **`invalid_grouping` is a distinct error code** from `invalid_update`, so a
|
||||
caller can tell "that is not a grouping" from "nothing changed".
|
||||
`InvalidTenantIdentifierError` was previously uncaught in the mutation spine
|
||||
and would have surfaced as a 500.
|
||||
- **Reserved identifiers are refused** in the domain, not the route, so the
|
||||
guarantee holds for any future caller of `with_grouping`.
|
||||
- The audit event is `tenant_grouping_changed` — its own type, not folded into
|
||||
`tenant_updated`, so "why did this ceiling move" is answerable from the event
|
||||
log alone.
|
||||
|
||||
New flex-auth action `tenant.grouping.set` (resource type `tenant`) — takes the
|
||||
package from nine actions to ten. Handoff in T05.
|
||||
|
||||
## T04 - Interactions with guardrails and grants
|
||||
|
||||
```task
|
||||
id: TEN-WP-0010-T04
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "807c34ab-2c04-4276-a872-e89b5b064967"
|
||||
```
|
||||
|
|
@ -237,6 +269,35 @@ refused with a comprehensible error rather than an invariant violation.
|
|||
Done when the interactions are covered in the conformance suites over all
|
||||
backends.
|
||||
|
||||
Done 2026-08-17, in `tests/test_grouping_mutation.py`.
|
||||
|
||||
**Guardrails.** The ceiling follows the new grouping (`small` €250 →
|
||||
`enterprise` €20 000); a per-tenant override still wins, so precedence is
|
||||
unchanged — reclassification moves the grouping layer only; provenance still
|
||||
reads `grouping` after the move. The defect this workplan exists to fix is now
|
||||
covered directly: a `trial` tenant reads a zero ceiling, and reclassifying it
|
||||
to `medium` lifts it to €1 000.
|
||||
|
||||
On the 2am concern — a ceiling that moves with no guardrail write against it —
|
||||
the `tenant_grouping_changed` event carries actor, reason and correlation id,
|
||||
so the move is reconstructible from the event log. Chose *not* to also emit a
|
||||
synthetic guardrail event: it would be a second record of one act, and a
|
||||
guardrail audit trail containing entries that no guardrail write produced is a
|
||||
worse thing to hand an auditor than one that is complete but requires reading
|
||||
two event types.
|
||||
|
||||
**Grants.** Existing `platform_default` grants survive a move off `trial`
|
||||
untouched — the trail is append-only and reclassification is not revocation. A
|
||||
*new* `platform_default` grant afterwards is refused with `InvalidGrantError`
|
||||
carrying the grouping in its message, which is the ADR-0014 invariant doing its
|
||||
job rather than an accident.
|
||||
|
||||
Note these run against the in-memory store rather than the parametrised
|
||||
conformance fixture: grouping lives in the existing `tenants` row and moves
|
||||
through `mutate_tenant()`, which the lifecycle conformance suite already
|
||||
exercises over both backends. There is no new persistence behaviour for a
|
||||
backend to diverge on.
|
||||
|
||||
## T05 - Ship, and tell the consumers
|
||||
|
||||
```task
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue