State tenant-engine's tenant relation in the write-api package, v3.
tenant-engine named the relation in TEN-DEC-2026-002: `tenant` is the target tenant record and always equals `resource.id`; the write API is cross-tenant by design and `tenant.guardrail.read` does not differ. v2 carried no tenant rule and a constant fixture tenant, so the deliberate scope and an omitted rule were indistinguishable. tenant-engine.write-api.mutate v3 (FLEX-DEC-2026-016): - allowed requires tenant_is_target; a mismatch or absent tenant is denied tenant_not_target (object.get, so an absent key names the right cause). - the cross-tenant scope is stated in the package and quantified by test_tenant_never_changes_effect over every action, three subjects and four tenants, with guards against passing by denying everything. - fixtures rotate tenant across four tenants; five cross-tenant allows and two tenant_not_target denies added (42 fixtures, 33 tests, all pass). - user-engine's tenant:platform exclusion is named as a fixed-record rule, not a subject/tenant relation, and tested separately. Closes FLEX-WP-0022 (T01, T02 done). Also records TEN-IN-0004 and SECRETS-IN-0002 on FLEX-WP-0020 and acknowledges the GH-DEC-2026-017 replies on FLEX-WP-0030-T04. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 63291@bnt-lap001 Assistant-Session: 8bd77868-ca68-4f49-bb1e-d539ecc0d703
This commit is contained in:
parent
bd3d270531
commit
3081067325
17 changed files with 783 additions and 150 deletions
|
|
@ -1891,3 +1891,56 @@ applies there identically: the repository rename answers nothing about it.
|
|||
**Reversal condition.** Reverses only by a runtime-rename decision that
|
||||
supersedes `FLEX-DEC-2026-013`'s retain row for policy vocabulary — and that
|
||||
decision inherits the PDP-first order above.
|
||||
|
||||
## FLEX-DEC-2026-016 — `tenant-engine.write-api.mutate` v3 states its tenant relation: target record, invariant enforced, cross-tenant by design
|
||||
|
||||
**Date:** 2026-09-21
|
||||
**Status:** accepted
|
||||
**Workplan:** `FLEX-WP-0022-T02`
|
||||
**Answer relied on:** `TEN-DEC-2026-002` (tenant-engine `decisions/decisions.md`,
|
||||
hub decision `ecaebbb7-fe90-4235-a79f-23457e9727bc`) and
|
||||
`tenant-engine/docs/flex-auth-integration.md`, both at tenant-engine `d132db0`
|
||||
|
||||
**Context.** v2 had no tenant rule and every fixture carried
|
||||
`tenant:friendly:binky`, so a deliberate cross-tenant scope and an omitted rule
|
||||
looked identical — the `FLEX-DEC-2026-008` shape. `flex-auth` declined to infer
|
||||
the relation from `FlexAuthWriteAuthorizer.authorize`. tenant-engine has now
|
||||
named it in its own record: `tenant` is the target tenant record; it always
|
||||
equals `resource.id`; no action is refused on the subject/tenant relationship;
|
||||
`tenant.guardrail.read` does not differ and must not.
|
||||
|
||||
**Decision.** v3 encodes both commitments tenant-engine offered, as rules a
|
||||
reviewer can check rather than as an absence:
|
||||
|
||||
1. **Invariant, enforced.** `allowed` now requires `tenant_is_target`
|
||||
(`object.get(input, "tenant", "") == input.resource.id`, non-empty). A
|
||||
mismatch or an absent `tenant` is denied `tenant_not_target`, second rung
|
||||
of the ladder after `wrong_system` — both mean "this check did not come
|
||||
from tenant-engine". `object.get` is used so an absent key names the right
|
||||
cause (`FLEX-DEC-2026-008`'s lesson). tenant-engine offered to leave this as
|
||||
a documented expectation; we enforce it because it is fail-closed, verified
|
||||
against `authz.py` (one `tenant_id` copied onto both fields), and denies
|
||||
nothing tenant-engine sends.
|
||||
2. **Scope, stated and quantified.** The package prose says the write API is
|
||||
deliberately cross-tenant and why. The embedded test
|
||||
`test_tenant_never_changes_effect` evaluates every action for three
|
||||
subjects across four target tenants (including `tenant:platform`) and
|
||||
requires one effect per pair; `test_cross_tenant_writes_allowed` and
|
||||
`test_pdp_guardrail_read_is_cross_tenant` stop that passing by denying
|
||||
everything. A future same-tenant rule fails the suite.
|
||||
3. **One boundary named rather than hidden.** `user-engine`'s onboarding grant
|
||||
(NK-WP-0036) excludes the fixed record `tenant:platform`, so for that one
|
||||
subject varying `tenant` *does* change the effect. That is a fixed-record
|
||||
exclusion, not a subject/tenant relation, and it is tested separately; it
|
||||
is reported back to tenant-engine because its commitment (b) reads as
|
||||
literally unconditional.
|
||||
|
||||
**Fixtures.** Every tenant-engine-subject fixture now carries `tenant` equal to
|
||||
`resource.id`, rotated across four tenants; five cross-tenant allow fixtures
|
||||
(three `flex-auth` guardrail reads, two creates) and two `tenant_not_target`
|
||||
denies (mismatch, absent) were added. 42 fixtures, 33 embedded tests, all pass.
|
||||
|
||||
**Version.** v2 → v3. The invariant is a new deny, so this is a visible change
|
||||
of behaviour for any caller that is not tenant-engine; per
|
||||
`FLEX-DEC-2026-008` it is a version change, not a silent edit. tenant-engine
|
||||
asked for no notice period.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue