State tenant-engine's tenant relation in the write-api package, v3.
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
Build and Publish Container Image / build-and-push (push) Successful in 1m24s

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:
tegwick 2026-09-21 07:39:57 +02:00
parent bd3d270531
commit 3081067325
17 changed files with 783 additions and 150 deletions

View file

@ -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.

View file

@ -21,7 +21,7 @@ Shared identity for every request:
| `policy-nexus` | other | Update `source-inventory.config.json` remote URL; re-ingest same publication lineage | pending |
| `user-engine` | documentation | Update `wiki/ArchitectureBlueprint.md` absolute source path; adapter/runtime vocabulary stays `flex-auth` | pending |
| `net-kingdom` | deployment | Verify three live `flex-auth-*` Deployments and `sso-mfa/k8s/**`; no runtime rename | pending |
| `tenant-engine` | consumer | Verify docs/client config keep the retained product/runtime contract | pending |
| `tenant-engine` | consumer | Verify docs/client config keep the retained product/runtime contract | `TEN-IN-0004` / intake `01a0c14b-b2f7-78f1-8f6c-e36c952fe004` |
| `sbom-nexus` | sbom | Re-ingest new canonical checkout; snapshots remain related to the UUID above | pending |
| `repo-manager` | other | Reconcile new canonical path; do not rewrite archived UUID-migration evidence | pending |
| `railiance-platform`, `markitect-tool`, `gate-house`, `approval-engine`, `secrets-engine`, `zone-engine` | consumer | Confirm no live repository URL/path remains; retain product/runtime terminology | pending |
@ -81,3 +81,12 @@ Still pending, and T05 stays blocked on them: `railiance-fabric`, `ops-warden`,
`policy-nexus`, `user-engine`, `net-kingdom`, `tenant-engine`, `sbom-nexus`,
`repo-manager`, and the named semantic-consumer verifiers.
2026-09-21 — two more owner records returned, acknowledged, not closed from here.
| Owner | Record | Reply message | What stays open on their side |
| --- | --- | --- | --- |
| `tenant-engine` | `TEN-IN-0004` (`intakes/intakes.md`, commit `d132db0`; hub intake `01a0c14b-b2f7-78f1-8f6c-e36c952fe004`), `open` | `588df1c4-28d9-4887-807c-4950590a2359` | Runtime contract verified retained (cluster DNS `flex-auth-tenant-engine.flex-auth.svc`, audience `flex-auth`, NetworkPolicy, `railiance/app.toml`). Five repository-path cross-references in `docs/flex-auth-integration.md` are repointed after T06 lands; `flex-auth` owes them a "rename landed" notice. |
| `secrets-engine` | `SECRETS-IN-0002` (`intakes/intakes.md`, commit `ba73dba`), `open` | `8539206e-4443-4cec-8736-0efb6d45ef67` | One live repository path, `docs/approval-service-auth.md` line 56 (`--flex-auth-source /home/worsch/flex-auth`), held until the rename lands; every other `flex-auth` string is retained runtime/contract vocabulary. `flex-auth` owes them a "rename landed" notice. |
Both record ids were confirmed present in the owners' committed intake files.

View file

@ -10,10 +10,10 @@ protected-system consumer, gating its own write API
| --- | --- |
| `protected_system_manifest.yaml` | Resource types (`tenant`, `role-grant`, `plan-assignment`, `guardrail`) and the nine actions: the original four, the `FLEX-WP-0010` lifecycle trio, and the `FLEX-WP-0014` guardrail pair `tenant.guardrail.read` / `tenant.guardrail.set` |
| `subject_manifest.yaml` | Two registered callers: `tenant-engine` (all nine actions) and `flex-auth` (read-only on `tenant.guardrail.read`) |
| `policy_package.md` | Rego rules + embedded tests gating the write API |
| `policy_fixtures.yaml` | Allow/deny request/decision pairs, referenced by `policy_package.md`'s frontmatter |
| `policy_package.md` | Rego rules + embedded tests gating the write API. v3 states the tenant relation (`TEN-DEC-2026-002`, `FLEX-DEC-2026-016`): `tenant` is the target record and must equal `resource.id` (`tenant_not_target` otherwise); the scope is cross-tenant by design |
| `policy_fixtures.yaml` | Allow/deny request/decision pairs, referenced by `policy_package.md`'s frontmatter. `tenant` varies across four tenants so the suite reports on the field |
| `registry_snapshot.json` | Merged `systems`/`subjects`/`groups` snapshot assembled from the two manifests above, loadable by `flex-auth serve`/`check`/`load-registry` |
| `check_request_allow_create.json`, `check_request_deny_unknown_subject.json`, `check_request_allow_retire.json`, `check_request_deny_misspelled_lifecycle.json`, `check_request_allow_guardrail_read.json`, `check_request_allow_guardrail_set.json`, `check_request_deny_guardrail_set_as_reader.json`, `check_request_deny_misspelled_guardrail.json` | Standalone example requests for `flex-auth check` |
| `check_request_allow_create.json`, `check_request_deny_unknown_subject.json`, `check_request_allow_retire.json`, `check_request_deny_misspelled_lifecycle.json`, `check_request_allow_guardrail_read.json`, `check_request_allow_guardrail_set.json`, `check_request_deny_guardrail_set_as_reader.json`, `check_request_deny_misspelled_guardrail.json`, `check_request_deny_tenant_not_target.json` | Standalone example requests for `flex-auth check` |
**No `resource_manifest.yaml`** — unlike ops-warden's fixed SSH-certificate
inventory, `tenant-engine`'s resources (tenants) are created dynamically.

View file

@ -7,7 +7,7 @@
},
"action": "tenant.create",
"resource": {
"id": "t-1",
"id": "tenant:friendly:binky",
"type": "tenant",
"system": "tenant-engine"
},

View file

@ -7,7 +7,7 @@
},
"action": "tenant.guardrail.read",
"resource": {
"id": "t-1",
"id": "tenant:friendly:binky",
"type": "guardrail",
"system": "tenant-engine"
},

View file

@ -7,7 +7,7 @@
},
"action": "tenant.guardrail.set",
"resource": {
"id": "t-1",
"id": "tenant:friendly:binky",
"type": "guardrail",
"system": "tenant-engine"
},

View file

@ -7,7 +7,7 @@
},
"action": "tenant.retire",
"resource": {
"id": "t-1",
"id": "tenant:friendly:binky",
"type": "tenant",
"system": "tenant-engine"
},

View file

@ -7,7 +7,7 @@
},
"action": "tenant.guardrail.set",
"resource": {
"id": "t-1",
"id": "tenant:friendly:binky",
"type": "guardrail",
"system": "tenant-engine"
},

View file

@ -7,7 +7,7 @@
},
"action": "tenant.guardrail.get",
"resource": {
"id": "t-1",
"id": "tenant:friendly:binky",
"type": "guardrail",
"system": "tenant-engine"
},

View file

@ -7,7 +7,7 @@
},
"action": "tenant.retired",
"resource": {
"id": "t-1",
"id": "tenant:friendly:binky",
"type": "tenant",
"system": "tenant-engine"
},

View file

@ -0,0 +1,15 @@
{
"id": "check:tenant-engine-tenant-not-target",
"tenant": "tenant:friendly:binky",
"subject": {
"id": "tenant-engine",
"type": "service"
},
"action": "tenant.create",
"resource": {
"id": "tenant:acme:prod",
"type": "tenant",
"system": "tenant-engine"
},
"context": {}
}

View file

@ -7,7 +7,7 @@
},
"action": "tenant.create",
"resource": {
"id": "t-1",
"id": "tenant:friendly:binky",
"type": "tenant",
"system": "tenant-engine"
},

View file

@ -4,276 +4,506 @@
"request": {
"id": "check:tenant-engine-create-t1",
"tenant": "tenant:friendly:binky",
"subject": {"id": "tenant-engine", "type": "service"},
"subject": {
"id": "tenant-engine",
"type": "service"
},
"action": "tenant.create",
"resource": {"id": "t-1", "type": "tenant", "system": "tenant-engine"},
"resource": {
"id": "tenant:friendly:binky",
"type": "tenant",
"system": "tenant-engine"
},
"context": {}
},
"expect": {"effect": "allow", "reason": "write_api_policy_matched"}
"expect": {
"effect": "allow",
"reason": "write_api_policy_matched"
}
},
{
"id": "fixture:tenant-engine-role-grant-allow",
"request": {
"id": "check:tenant-engine-role-grant-t1",
"tenant": "tenant:friendly:binky",
"subject": {"id": "tenant-engine", "type": "service"},
"tenant": "tenant:acme:prod",
"subject": {
"id": "tenant-engine",
"type": "service"
},
"action": "tenant.role.grant",
"resource": {"id": "t-1", "type": "role-grant", "system": "tenant-engine"},
"resource": {
"id": "tenant:acme:prod",
"type": "role-grant",
"system": "tenant-engine"
},
"context": {}
},
"expect": {"effect": "allow", "reason": "write_api_policy_matched"}
"expect": {
"effect": "allow",
"reason": "write_api_policy_matched"
}
},
{
"id": "fixture:tenant-engine-role-revoke-allow",
"request": {
"id": "check:tenant-engine-role-revoke-t1",
"tenant": "tenant:friendly:binky",
"subject": {"id": "tenant-engine", "type": "service"},
"tenant": "tenant:platform",
"subject": {
"id": "tenant-engine",
"type": "service"
},
"action": "tenant.role.revoke",
"resource": {"id": "t-1", "type": "role-grant", "system": "tenant-engine"},
"resource": {
"id": "tenant:platform",
"type": "role-grant",
"system": "tenant-engine"
},
"context": {}
},
"expect": {"effect": "allow", "reason": "write_api_policy_matched"}
"expect": {
"effect": "allow",
"reason": "write_api_policy_matched"
}
},
{
"id": "fixture:tenant-engine-plan-assign-allow",
"request": {
"id": "check:tenant-engine-plan-assign-t1",
"tenant": "tenant:friendly:binky",
"subject": {"id": "tenant-engine", "type": "service"},
"tenant": "tenant:trial:demo-company",
"subject": {
"id": "tenant-engine",
"type": "service"
},
"action": "tenant.plan.assign",
"resource": {"id": "t-1", "type": "plan-assignment", "system": "tenant-engine"},
"resource": {
"id": "tenant:trial:demo-company",
"type": "plan-assignment",
"system": "tenant-engine"
},
"context": {}
},
"expect": {"effect": "allow", "reason": "write_api_policy_matched"}
"expect": {
"effect": "allow",
"reason": "write_api_policy_matched"
}
},
{
"id": "fixture:tenant-engine-update-allow",
"request": {
"id": "check:tenant-engine-update-t1",
"tenant": "tenant:friendly:binky",
"subject": {"id": "tenant-engine", "type": "service"},
"subject": {
"id": "tenant-engine",
"type": "service"
},
"action": "tenant.update",
"resource": {"id": "t-1", "type": "tenant", "system": "tenant-engine"},
"resource": {
"id": "tenant:friendly:binky",
"type": "tenant",
"system": "tenant-engine"
},
"context": {}
},
"expect": {"effect": "allow", "reason": "write_api_policy_matched"}
"expect": {
"effect": "allow",
"reason": "write_api_policy_matched"
}
},
{
"id": "fixture:tenant-engine-retire-allow",
"request": {
"id": "check:tenant-engine-retire-t1",
"tenant": "tenant:friendly:binky",
"subject": {"id": "tenant-engine", "type": "service"},
"tenant": "tenant:acme:prod",
"subject": {
"id": "tenant-engine",
"type": "service"
},
"action": "tenant.retire",
"resource": {"id": "t-1", "type": "tenant", "system": "tenant-engine"},
"resource": {
"id": "tenant:acme:prod",
"type": "tenant",
"system": "tenant-engine"
},
"context": {}
},
"expect": {"effect": "allow", "reason": "write_api_policy_matched"}
"expect": {
"effect": "allow",
"reason": "write_api_policy_matched"
}
},
{
"id": "fixture:tenant-engine-reactivate-allow",
"request": {
"id": "check:tenant-engine-reactivate-t1",
"tenant": "tenant:friendly:binky",
"subject": {"id": "tenant-engine", "type": "service"},
"tenant": "tenant:platform",
"subject": {
"id": "tenant-engine",
"type": "service"
},
"action": "tenant.reactivate",
"resource": {"id": "t-1", "type": "tenant", "system": "tenant-engine"},
"resource": {
"id": "tenant:platform",
"type": "tenant",
"system": "tenant-engine"
},
"context": {}
},
"expect": {"effect": "allow", "reason": "write_api_policy_matched"}
"expect": {
"effect": "allow",
"reason": "write_api_policy_matched"
}
},
{
"id": "fixture:tenant-engine-update-unknown-subject-deny",
"request": {
"id": "check:tenant-engine-update-t1",
"tenant": "tenant:friendly:binky",
"subject": {"id": "some-other-service", "type": "service"},
"tenant": "tenant:trial:demo-company",
"subject": {
"id": "some-other-service",
"type": "service"
},
"action": "tenant.update",
"resource": {"id": "t-1", "type": "tenant", "system": "tenant-engine"},
"resource": {
"id": "tenant:trial:demo-company",
"type": "tenant",
"system": "tenant-engine"
},
"context": {}
},
"expect": {"effect": "deny", "reason": "unknown_subject"}
"expect": {
"effect": "deny",
"reason": "unknown_subject"
}
},
{
"id": "fixture:tenant-engine-retire-unknown-subject-deny",
"request": {
"id": "check:tenant-engine-retire-t1",
"tenant": "tenant:friendly:binky",
"subject": {"id": "some-other-service", "type": "service"},
"subject": {
"id": "some-other-service",
"type": "service"
},
"action": "tenant.retire",
"resource": {"id": "t-1", "type": "tenant", "system": "tenant-engine"},
"resource": {
"id": "tenant:friendly:binky",
"type": "tenant",
"system": "tenant-engine"
},
"context": {}
},
"expect": {"effect": "deny", "reason": "unknown_subject"}
"expect": {
"effect": "deny",
"reason": "unknown_subject"
}
},
{
"id": "fixture:tenant-engine-reactivate-unknown-subject-deny",
"request": {
"id": "check:tenant-engine-reactivate-t1",
"tenant": "tenant:friendly:binky",
"subject": {"id": "some-other-service", "type": "service"},
"tenant": "tenant:acme:prod",
"subject": {
"id": "some-other-service",
"type": "service"
},
"action": "tenant.reactivate",
"resource": {"id": "t-1", "type": "tenant", "system": "tenant-engine"},
"resource": {
"id": "tenant:acme:prod",
"type": "tenant",
"system": "tenant-engine"
},
"context": {}
},
"expect": {"effect": "deny", "reason": "unknown_subject"}
"expect": {
"effect": "deny",
"reason": "unknown_subject"
}
},
{
"id": "fixture:tenant-engine-misspelled-lifecycle-action-deny",
"request": {
"id": "check:tenant-engine-retired-t1",
"tenant": "tenant:friendly:binky",
"subject": {"id": "tenant-engine", "type": "service"},
"tenant": "tenant:platform",
"subject": {
"id": "tenant-engine",
"type": "service"
},
"action": "tenant.retired",
"resource": {"id": "t-1", "type": "tenant", "system": "tenant-engine"},
"resource": {
"id": "tenant:platform",
"type": "tenant",
"system": "tenant-engine"
},
"context": {}
},
"expect": {"effect": "deny", "reason": "unknown_action"}
"expect": {
"effect": "deny",
"reason": "unknown_action"
}
},
{
"id": "fixture:tenant-engine-lifecycle-underscore-action-deny",
"request": {
"id": "check:tenant-engine-underscore-t1",
"tenant": "tenant:friendly:binky",
"subject": {"id": "tenant-engine", "type": "service"},
"tenant": "tenant:trial:demo-company",
"subject": {
"id": "tenant-engine",
"type": "service"
},
"action": "tenant_update",
"resource": {"id": "t-1", "type": "tenant", "system": "tenant-engine"},
"resource": {
"id": "tenant:trial:demo-company",
"type": "tenant",
"system": "tenant-engine"
},
"context": {}
},
"expect": {"effect": "deny", "reason": "unknown_action"}
"expect": {
"effect": "deny",
"reason": "unknown_action"
}
},
{
"id": "fixture:tenant-engine-unknown-subject-deny",
"request": {
"id": "check:tenant-engine-create-t1",
"tenant": "tenant:friendly:binky",
"subject": {"id": "some-other-service", "type": "service"},
"subject": {
"id": "some-other-service",
"type": "service"
},
"action": "tenant.create",
"resource": {"id": "t-1", "type": "tenant", "system": "tenant-engine"},
"resource": {
"id": "tenant:friendly:binky",
"type": "tenant",
"system": "tenant-engine"
},
"context": {}
},
"expect": {"effect": "deny", "reason": "unknown_subject"}
"expect": {
"effect": "deny",
"reason": "unknown_subject"
}
},
{
"id": "fixture:tenant-engine-wrong-system-deny",
"request": {
"id": "check:tenant-engine-create-t1",
"tenant": "tenant:friendly:binky",
"subject": {"id": "tenant-engine", "type": "service"},
"tenant": "tenant:acme:prod",
"subject": {
"id": "tenant-engine",
"type": "service"
},
"action": "tenant.create",
"resource": {"id": "t-1", "type": "tenant", "system": "some-other-system"},
"resource": {
"id": "tenant:acme:prod",
"type": "tenant",
"system": "some-other-system"
},
"context": {}
},
"expect": {"effect": "deny", "reason": "wrong_system"}
"expect": {
"effect": "deny",
"reason": "wrong_system"
}
},
{
"id": "fixture:tenant-engine-unknown-action-deny",
"request": {
"id": "check:tenant-engine-delete-t1",
"tenant": "tenant:friendly:binky",
"subject": {"id": "tenant-engine", "type": "service"},
"tenant": "tenant:platform",
"subject": {
"id": "tenant-engine",
"type": "service"
},
"action": "tenant.delete",
"resource": {"id": "t-1", "type": "tenant", "system": "tenant-engine"},
"resource": {
"id": "tenant:platform",
"type": "tenant",
"system": "tenant-engine"
},
"context": {}
},
"expect": {"effect": "deny", "reason": "unknown_action"}
"expect": {
"effect": "deny",
"reason": "unknown_action"
}
},
{
"id": "fixture:tenant-engine-wrong-subject-type-deny",
"request": {
"id": "check:tenant-engine-create-t1",
"tenant": "tenant:friendly:binky",
"subject": {"id": "tenant-engine", "type": "human"},
"tenant": "tenant:trial:demo-company",
"subject": {
"id": "tenant-engine",
"type": "human"
},
"action": "tenant.create",
"resource": {"id": "t-1", "type": "tenant", "system": "tenant-engine"},
"resource": {
"id": "tenant:trial:demo-company",
"type": "tenant",
"system": "tenant-engine"
},
"context": {}
},
"expect": {"effect": "deny", "reason": "wrong_subject_type"}
"expect": {
"effect": "deny",
"reason": "wrong_subject_type"
}
},
{
"id": "fixture:tenant-engine-guardrail-read-pdp-allow",
"request": {
"id": "check:tenant-engine-guardrail-read-pdp",
"tenant": "tenant:friendly:binky",
"subject": {"id": "flex-auth", "type": "service"},
"subject": {
"id": "flex-auth",
"type": "service"
},
"action": "tenant.guardrail.read",
"resource": {"id": "t-1", "type": "guardrail", "system": "tenant-engine"},
"resource": {
"id": "tenant:friendly:binky",
"type": "guardrail",
"system": "tenant-engine"
},
"context": {}
},
"expect": {"effect": "allow", "reason": "write_api_policy_matched"}
"expect": {
"effect": "allow",
"reason": "write_api_policy_matched"
}
},
{
"id": "fixture:tenant-engine-guardrail-read-writer-allow",
"request": {
"id": "check:tenant-engine-guardrail-read-writer",
"tenant": "tenant:friendly:binky",
"subject": {"id": "tenant-engine", "type": "service"},
"tenant": "tenant:acme:prod",
"subject": {
"id": "tenant-engine",
"type": "service"
},
"action": "tenant.guardrail.read",
"resource": {"id": "t-1", "type": "guardrail", "system": "tenant-engine"},
"resource": {
"id": "tenant:acme:prod",
"type": "guardrail",
"system": "tenant-engine"
},
"context": {}
},
"expect": {"effect": "allow", "reason": "write_api_policy_matched"}
"expect": {
"effect": "allow",
"reason": "write_api_policy_matched"
}
},
{
"id": "fixture:tenant-engine-guardrail-set-writer-allow",
"request": {
"id": "check:tenant-engine-guardrail-set-writer",
"tenant": "tenant:friendly:binky",
"subject": {"id": "tenant-engine", "type": "service"},
"tenant": "tenant:platform",
"subject": {
"id": "tenant-engine",
"type": "service"
},
"action": "tenant.guardrail.set",
"resource": {"id": "t-1", "type": "guardrail", "system": "tenant-engine"},
"resource": {
"id": "tenant:platform",
"type": "guardrail",
"system": "tenant-engine"
},
"context": {}
},
"expect": {"effect": "allow", "reason": "write_api_policy_matched"}
"expect": {
"effect": "allow",
"reason": "write_api_policy_matched"
}
},
{
"id": "fixture:tenant-engine-guardrail-set-pdp-deny",
"request": {
"id": "check:tenant-engine-guardrail-set-pdp",
"tenant": "tenant:friendly:binky",
"subject": {"id": "flex-auth", "type": "service"},
"tenant": "tenant:trial:demo-company",
"subject": {
"id": "flex-auth",
"type": "service"
},
"action": "tenant.guardrail.set",
"resource": {"id": "t-1", "type": "guardrail", "system": "tenant-engine"},
"resource": {
"id": "tenant:trial:demo-company",
"type": "guardrail",
"system": "tenant-engine"
},
"context": {}
},
"expect": {"effect": "deny", "reason": "action_not_granted"}
"expect": {
"effect": "deny",
"reason": "action_not_granted"
}
},
{
"id": "fixture:tenant-engine-guardrail-read-unknown-subject-deny",
"request": {
"id": "check:tenant-engine-guardrail-read-ops",
"tenant": "tenant:friendly:binky",
"subject": {"id": "ops", "type": "service"},
"subject": {
"id": "ops",
"type": "service"
},
"action": "tenant.guardrail.read",
"resource": {"id": "t-1", "type": "guardrail", "system": "tenant-engine"},
"resource": {
"id": "tenant:friendly:binky",
"type": "guardrail",
"system": "tenant-engine"
},
"context": {}
},
"expect": {"effect": "deny", "reason": "unknown_subject"}
"expect": {
"effect": "deny",
"reason": "unknown_subject"
}
},
{
"id": "fixture:tenant-engine-guardrail-set-unknown-subject-deny",
"request": {
"id": "check:tenant-engine-guardrail-set-ops",
"tenant": "tenant:friendly:binky",
"subject": {"id": "ops", "type": "service"},
"tenant": "tenant:acme:prod",
"subject": {
"id": "ops",
"type": "service"
},
"action": "tenant.guardrail.set",
"resource": {"id": "t-1", "type": "guardrail", "system": "tenant-engine"},
"resource": {
"id": "tenant:acme:prod",
"type": "guardrail",
"system": "tenant-engine"
},
"context": {}
},
"expect": {"effect": "deny", "reason": "unknown_subject"}
"expect": {
"effect": "deny",
"reason": "unknown_subject"
}
},
{
"id": "fixture:tenant-engine-misspelled-guardrail-action-deny",
"request": {
"id": "check:tenant-engine-guardrail-get",
"tenant": "tenant:friendly:binky",
"subject": {"id": "flex-auth", "type": "service"},
"tenant": "tenant:platform",
"subject": {
"id": "flex-auth",
"type": "service"
},
"action": "tenant.guardrail.get",
"resource": {"id": "t-1", "type": "guardrail", "system": "tenant-engine"},
"resource": {
"id": "tenant:platform",
"type": "guardrail",
"system": "tenant-engine"
},
"context": {}
},
"expect": {"effect": "deny", "reason": "unknown_action"}
"expect": {
"effect": "deny",
"reason": "unknown_action"
}
},
{
"id": "fixture:tenant-engine-portal-create",
@ -538,5 +768,158 @@
"effect": "deny",
"reason": "action_not_granted"
}
},
{
"id": "fixture:tenant-engine-guardrail-read-pdp-cross-tenant-platform-allow",
"request": {
"id": "check:guardrail-read-pdp-tenant:platform",
"tenant": "tenant:platform",
"subject": {
"id": "flex-auth",
"type": "service"
},
"action": "tenant.guardrail.read",
"resource": {
"id": "tenant:platform",
"type": "guardrail",
"system": "tenant-engine"
},
"context": {}
},
"expect": {
"effect": "allow",
"reason": "write_api_policy_matched"
}
},
{
"id": "fixture:tenant-engine-guardrail-read-pdp-cross-tenant-acme-prod-allow",
"request": {
"id": "check:guardrail-read-pdp-tenant:acme:prod",
"tenant": "tenant:acme:prod",
"subject": {
"id": "flex-auth",
"type": "service"
},
"action": "tenant.guardrail.read",
"resource": {
"id": "tenant:acme:prod",
"type": "guardrail",
"system": "tenant-engine"
},
"context": {}
},
"expect": {
"effect": "allow",
"reason": "write_api_policy_matched"
}
},
{
"id": "fixture:tenant-engine-guardrail-read-pdp-cross-tenant-trial-demo-company-allow",
"request": {
"id": "check:guardrail-read-pdp-tenant:trial:demo-company",
"tenant": "tenant:trial:demo-company",
"subject": {
"id": "flex-auth",
"type": "service"
},
"action": "tenant.guardrail.read",
"resource": {
"id": "tenant:trial:demo-company",
"type": "guardrail",
"system": "tenant-engine"
},
"context": {}
},
"expect": {
"effect": "allow",
"reason": "write_api_policy_matched"
}
},
{
"id": "fixture:tenant-engine-create-cross-tenant-platform-allow",
"request": {
"id": "check:create-tenant:platform",
"tenant": "tenant:platform",
"subject": {
"id": "tenant-engine",
"type": "service"
},
"action": "tenant.create",
"resource": {
"id": "tenant:platform",
"type": "tenant",
"system": "tenant-engine"
},
"context": {}
},
"expect": {
"effect": "allow",
"reason": "write_api_policy_matched"
}
},
{
"id": "fixture:tenant-engine-create-cross-tenant-acme-prod-allow",
"request": {
"id": "check:create-tenant:acme:prod",
"tenant": "tenant:acme:prod",
"subject": {
"id": "tenant-engine",
"type": "service"
},
"action": "tenant.create",
"resource": {
"id": "tenant:acme:prod",
"type": "tenant",
"system": "tenant-engine"
},
"context": {}
},
"expect": {
"effect": "allow",
"reason": "write_api_policy_matched"
}
},
{
"id": "fixture:tenant-engine-tenant-not-target-deny",
"request": {
"id": "check:tenant-not-target",
"tenant": "tenant:friendly:binky",
"subject": {
"id": "tenant-engine",
"type": "service"
},
"action": "tenant.create",
"resource": {
"id": "tenant:acme:prod",
"type": "tenant",
"system": "tenant-engine"
},
"context": {}
},
"expect": {
"effect": "deny",
"reason": "tenant_not_target"
}
},
{
"id": "fixture:tenant-engine-tenant-absent-deny",
"request": {
"id": "check:tenant-absent",
"subject": {
"id": "tenant-engine",
"type": "service"
},
"action": "tenant.create",
"resource": {
"id": "tenant:acme:prod",
"type": "tenant",
"system": "tenant-engine"
},
"context": {}
},
"expect": {
"effect": "deny",
"reason": "tenant_not_target"
}
}
]

View file

@ -2,7 +2,7 @@
id: tenant-engine.write-api.mutate
name: tenant-engine Write API authorization
namespace: tenant-engine:tenant
version: v2
version: v3
status: ready
package: flexauth.tenant_engine.write_api
actions:
@ -178,6 +178,45 @@ The earlier single-write-subject decisions above describe their dated baseline;
this explicit minimal onboarding grant supersedes that baseline only for these
operations and this existing service integration.
## Tenant relation (FLEX-WP-0022-T02, TEN-DEC-2026-002)
**Version v3.** v2 had no tenant rule and every fixture carried the same
tenant, so a deliberate cross-tenant scope and an omitted rule were
indistinguishable — the `FLEX-DEC-2026-008` shape. `tenant-engine` has now
named the relation in its own record (`TEN-DEC-2026-002`,
`tenant-engine/docs/flex-auth-integration.md`); this section states it so a
reviewer can check it, and `FLEX-DEC-2026-016` records the version change.
1. **`tenant` denotes the target tenant record**, not the caller's tenant.
tenant-engine verifies no inbound token and holds no caller tenant to send.
On the guardrail actions the "tenant the guardrail applies to" is the same
record.
2. **Invariant — encoded as a rule.** On every check tenant-engine sends,
`tenant` equals `resource.id` (`authz.FlexAuthWriteAuthorizer` copies one
`tenant_id` onto both). A check where they differ, or where `tenant` is
absent, did not come from this engine and is denied `tenant_not_target`.
This is fail-closed and denies nothing tenant-engine sends.
3. **Scope — deliberately cross-tenant, stated here.** No action in
`valid_actions` is refused on the relationship between `subject` and
`tenant`. The caller administers tenants: its subjects are platform service
identities and the targets are arbitrary tenant records, and `tenant.create`
has no existing target at check time. Authorization is a service-identity
question over `(subject.id, action)`. **`tenant.guardrail.read` does not
differ and must not**: flex-auth calls it while deciding about arbitrary
tenants. No rule below compares `tenant` to the subject; the embedded test
`test_tenant_never_changes_effect` quantifies that over every action and
subject, and the fixtures vary `tenant` so the suite reports on the field.
4. **The one target-dependent rule is not a tenant relation.** The
`user-engine` onboarding grant (NK-WP-0036) excludes the fixed record
`tenant:platform`. That depends on *which* record is targeted, not on any
relation between the subject and the target, so it is outside the scope
statement above and is tested separately
(`test_portal_tenant_changes_effect_only_on_platform_record`).
**Revisit when** tenant-engine gains a verified inbound identity
(`tenancy.yaml` gap I): the caller's tenant becomes knowable, and it arrives as
a new field — this one keeps its meaning.
## Rules
```rego
@ -208,27 +247,41 @@ read_subjects := {"tenant-engine", "flex-auth"}
mutate_subjects := {"tenant-engine"}
# TEN-DEC-2026-002: tenant is the target record and always equals resource.id.
# No rule compares tenant to the subject: the scope is cross-tenant by design.
request_tenant := object.get(input, "tenant", "")
tenant_is_target if {
request_tenant != ""
request_tenant == input.resource.id
}
allowed if {
tenant_is_target
granted
}
decision := {"effect": "allow", "reason": "write_api_policy_matched"} if {
allowed
} else := {"effect": "deny", "reason": first_denial} if {
true
}
allowed if {
granted if {
input.resource.system == "tenant-engine"
input.action in read_actions
input.subject.type == "service"
input.subject.id in read_subjects
}
allowed if {
granted if {
input.resource.system == "tenant-engine"
input.action in mutate_actions
input.subject.type == "service"
input.subject.id in mutate_subjects
}
allowed if {
granted if {
input.resource.system == "tenant-engine"
input.resource.type == "tenant"
input.resource.id != "tenant:platform"
@ -237,7 +290,7 @@ allowed if {
input.subject.id == "user-engine"
}
allowed if {
granted if {
input.resource.system == "tenant-engine"
input.resource.type == "tenant"
input.action == "tenant.read"
@ -249,6 +302,8 @@ default first_denial := "no_matching_rule"
first_denial := "wrong_system" if {
input.resource.system != "tenant-engine"
} else := "tenant_not_target" if {
not tenant_is_target
} else := "unknown_action" if {
not input.action in valid_actions
} else := "wrong_subject_type" if {
@ -265,7 +320,9 @@ first_denial := "wrong_system" if {
```rego test
package flexauth.tenant_engine.write_api_test
import future.keywords.every
import future.keywords.if
import future.keywords.in
import data.flexauth.tenant_engine.write_api
base_request := {
@ -273,7 +330,7 @@ base_request := {
"tenant": "tenant:friendly:binky",
"subject": {"id": "tenant-engine", "type": "service"},
"action": "tenant.create",
"resource": {"id": "t-1", "type": "tenant", "system": "tenant-engine"}
"resource": {"id": "tenant:friendly:binky", "type": "tenant", "system": "tenant-engine"}
}
test_known_operator_create_allowed if {
@ -281,170 +338,245 @@ test_known_operator_create_allowed if {
}
test_role_grant_allowed if {
write_api.decision.effect == "allow" with input as {
write_api.decision.effect == "allow" with input as { "tenant": "tenant:friendly:binky",
"subject": {"id": "tenant-engine", "type": "service"},
"action": "tenant.role.grant",
"resource": {"id": "t-1", "type": "role-grant", "system": "tenant-engine"}
"resource": {"id": "tenant:friendly:binky", "type": "role-grant", "system": "tenant-engine"}
}
}
test_tenant_update_allowed if {
write_api.decision.effect == "allow" with input as {
write_api.decision.effect == "allow" with input as { "tenant": "tenant:friendly:binky",
"subject": {"id": "tenant-engine", "type": "service"},
"action": "tenant.update",
"resource": {"id": "t-1", "type": "tenant", "system": "tenant-engine"}
"resource": {"id": "tenant:friendly:binky", "type": "tenant", "system": "tenant-engine"}
}
}
test_tenant_retire_allowed if {
write_api.decision.effect == "allow" with input as {
write_api.decision.effect == "allow" with input as { "tenant": "tenant:friendly:binky",
"subject": {"id": "tenant-engine", "type": "service"},
"action": "tenant.retire",
"resource": {"id": "t-1", "type": "tenant", "system": "tenant-engine"}
"resource": {"id": "tenant:friendly:binky", "type": "tenant", "system": "tenant-engine"}
}
}
test_tenant_reactivate_allowed if {
write_api.decision.effect == "allow" with input as {
write_api.decision.effect == "allow" with input as { "tenant": "tenant:friendly:binky",
"subject": {"id": "tenant-engine", "type": "service"},
"action": "tenant.reactivate",
"resource": {"id": "t-1", "type": "tenant", "system": "tenant-engine"}
"resource": {"id": "tenant:friendly:binky", "type": "tenant", "system": "tenant-engine"}
}
}
test_misspelled_lifecycle_action_denied if {
write_api.decision.reason == "unknown_action" with input as {
write_api.decision.reason == "unknown_action" with input as { "tenant": "tenant:friendly:binky",
"subject": {"id": "tenant-engine", "type": "service"},
"action": "tenant.retired",
"resource": {"id": "t-1", "type": "tenant", "system": "tenant-engine"}
"resource": {"id": "tenant:friendly:binky", "type": "tenant", "system": "tenant-engine"}
}
}
test_unknown_subject_retire_denied if {
write_api.decision.reason == "unknown_subject" with input as {
write_api.decision.reason == "unknown_subject" with input as { "tenant": "tenant:friendly:binky",
"subject": {"id": "some-other-service", "type": "service"},
"action": "tenant.retire",
"resource": {"id": "t-1", "type": "tenant", "system": "tenant-engine"}
"resource": {"id": "tenant:friendly:binky", "type": "tenant", "system": "tenant-engine"}
}
}
test_unknown_subject_denied if {
write_api.decision.reason == "unknown_subject" with input as {
write_api.decision.reason == "unknown_subject" with input as { "tenant": "tenant:friendly:binky",
"subject": {"id": "some-other-service", "type": "service"},
"action": "tenant.create",
"resource": {"id": "t-1", "type": "tenant", "system": "tenant-engine"}
"resource": {"id": "tenant:friendly:binky", "type": "tenant", "system": "tenant-engine"}
}
}
test_wrong_system_denied if {
write_api.decision.reason == "wrong_system" with input as {
write_api.decision.reason == "wrong_system" with input as { "tenant": "tenant:friendly:binky",
"subject": {"id": "tenant-engine", "type": "service"},
"action": "tenant.create",
"resource": {"id": "t-1", "type": "tenant", "system": "some-other-system"}
"resource": {"id": "tenant:friendly:binky", "type": "tenant", "system": "some-other-system"}
}
}
test_unknown_action_denied if {
write_api.decision.reason == "unknown_action" with input as {
write_api.decision.reason == "unknown_action" with input as { "tenant": "tenant:friendly:binky",
"subject": {"id": "tenant-engine", "type": "service"},
"action": "tenant.delete",
"resource": {"id": "t-1", "type": "tenant", "system": "tenant-engine"}
"resource": {"id": "tenant:friendly:binky", "type": "tenant", "system": "tenant-engine"}
}
}
test_wrong_subject_type_denied if {
write_api.decision.reason == "wrong_subject_type" with input as {
write_api.decision.reason == "wrong_subject_type" with input as { "tenant": "tenant:friendly:binky",
"subject": {"id": "tenant-engine", "type": "human"},
"action": "tenant.create",
"resource": {"id": "t-1", "type": "tenant", "system": "tenant-engine"}
"resource": {"id": "tenant:friendly:binky", "type": "tenant", "system": "tenant-engine"}
}
}
test_guardrail_read_by_pdp_allowed if {
write_api.decision.effect == "allow" with input as {
write_api.decision.effect == "allow" with input as { "tenant": "tenant:friendly:binky",
"subject": {"id": "flex-auth", "type": "service"},
"action": "tenant.guardrail.read",
"resource": {"id": "t-1", "type": "guardrail", "system": "tenant-engine"}
"resource": {"id": "tenant:friendly:binky", "type": "guardrail", "system": "tenant-engine"}
}
}
test_guardrail_read_by_writer_allowed if {
write_api.decision.effect == "allow" with input as {
write_api.decision.effect == "allow" with input as { "tenant": "tenant:friendly:binky",
"subject": {"id": "tenant-engine", "type": "service"},
"action": "tenant.guardrail.read",
"resource": {"id": "t-1", "type": "guardrail", "system": "tenant-engine"}
"resource": {"id": "tenant:friendly:binky", "type": "guardrail", "system": "tenant-engine"}
}
}
test_guardrail_set_by_writer_allowed if {
write_api.decision.effect == "allow" with input as {
write_api.decision.effect == "allow" with input as { "tenant": "tenant:friendly:binky",
"subject": {"id": "tenant-engine", "type": "service"},
"action": "tenant.guardrail.set",
"resource": {"id": "t-1", "type": "guardrail", "system": "tenant-engine"}
"resource": {"id": "tenant:friendly:binky", "type": "guardrail", "system": "tenant-engine"}
}
}
test_guardrail_set_by_pdp_denied if {
write_api.decision.reason == "action_not_granted" with input as {
write_api.decision.reason == "action_not_granted" with input as { "tenant": "tenant:friendly:binky",
"subject": {"id": "flex-auth", "type": "service"},
"action": "tenant.guardrail.set",
"resource": {"id": "t-1", "type": "guardrail", "system": "tenant-engine"}
"resource": {"id": "tenant:friendly:binky", "type": "guardrail", "system": "tenant-engine"}
}
}
test_guardrail_read_unknown_subject_denied if {
write_api.decision.reason == "unknown_subject" with input as {
write_api.decision.reason == "unknown_subject" with input as { "tenant": "tenant:friendly:binky",
"subject": {"id": "ops", "type": "service"},
"action": "tenant.guardrail.read",
"resource": {"id": "t-1", "type": "guardrail", "system": "tenant-engine"}
"resource": {"id": "tenant:friendly:binky", "type": "guardrail", "system": "tenant-engine"}
}
}
test_misspelled_guardrail_action_denied if {
write_api.decision.reason == "unknown_action" with input as {
write_api.decision.reason == "unknown_action" with input as { "tenant": "tenant:friendly:binky",
"subject": {"id": "flex-auth", "type": "service"},
"action": "tenant.guardrail.get",
"resource": {"id": "t-1", "type": "guardrail", "system": "tenant-engine"}
"resource": {"id": "tenant:friendly:binky", "type": "guardrail", "system": "tenant-engine"}
}
}
test_portal_create if {
write_api.decision.effect == "allow" with input as {"subject": {"id": "user-engine", "type": "service"}, "action": "tenant.create", "resource": {"id": "tenant:trial:demo-company", "type": "tenant", "system": "tenant-engine"}}
write_api.decision.effect == "allow" with input as { "tenant": "tenant:trial:demo-company","subject": {"id": "user-engine", "type": "service"}, "action": "tenant.create", "resource": {"id": "tenant:trial:demo-company", "type": "tenant", "system": "tenant-engine"}}
}
test_portal_read if {
write_api.decision.effect == "allow" with input as {"subject": {"id": "user-engine", "type": "service"}, "action": "tenant.read", "resource": {"id": "tenant:trial:demo-company", "type": "tenant", "system": "tenant-engine"}}
write_api.decision.effect == "allow" with input as { "tenant": "tenant:trial:demo-company","subject": {"id": "user-engine", "type": "service"}, "action": "tenant.read", "resource": {"id": "tenant:trial:demo-company", "type": "tenant", "system": "tenant-engine"}}
}
test_owner_read if {
write_api.decision.effect == "allow" with input as {"subject": {"id": "tenant-engine", "type": "service"}, "action": "tenant.read", "resource": {"id": "tenant:trial:demo-company", "type": "tenant", "system": "tenant-engine"}}
write_api.decision.effect == "allow" with input as { "tenant": "tenant:trial:demo-company","subject": {"id": "tenant-engine", "type": "service"}, "action": "tenant.read", "resource": {"id": "tenant:trial:demo-company", "type": "tenant", "system": "tenant-engine"}}
}
test_portal_platform if {
write_api.decision.effect == "deny" with input as {"subject": {"id": "user-engine", "type": "service"}, "action": "tenant.create", "resource": {"id": "tenant:platform", "type": "tenant", "system": "tenant-engine"}}
write_api.decision.effect == "deny" with input as { "tenant": "tenant:platform","subject": {"id": "user-engine", "type": "service"}, "action": "tenant.create", "resource": {"id": "tenant:platform", "type": "tenant", "system": "tenant-engine"}}
}
test_portal_wrong_type if {
write_api.decision.effect == "deny" with input as {"subject": {"id": "user-engine", "type": "service"}, "action": "tenant.create", "resource": {"id": "tenant:trial:demo-company", "type": "guardrail", "system": "tenant-engine"}}
write_api.decision.effect == "deny" with input as { "tenant": "tenant:trial:demo-company","subject": {"id": "user-engine", "type": "service"}, "action": "tenant.create", "resource": {"id": "tenant:trial:demo-company", "type": "guardrail", "system": "tenant-engine"}}
}
test_portal_no_grants if {
write_api.decision.effect == "deny" with input as {"subject": {"id": "user-engine", "type": "service"}, "action": "tenant.role.grant", "resource": {"id": "tenant:trial:demo-company", "type": "role-grant", "system": "tenant-engine"}}
write_api.decision.effect == "deny" with input as { "tenant": "tenant:trial:demo-company","subject": {"id": "user-engine", "type": "service"}, "action": "tenant.role.grant", "resource": {"id": "tenant:trial:demo-company", "type": "role-grant", "system": "tenant-engine"}}
}
test_portal_no_retirement if {
write_api.decision.effect == "deny" with input as {"subject": {"id": "user-engine", "type": "service"}, "action": "tenant.retire", "resource": {"id": "tenant:trial:demo-company", "type": "tenant", "system": "tenant-engine"}}
write_api.decision.effect == "deny" with input as { "tenant": "tenant:trial:demo-company","subject": {"id": "user-engine", "type": "service"}, "action": "tenant.retire", "resource": {"id": "tenant:trial:demo-company", "type": "tenant", "system": "tenant-engine"}}
}
test_portal_no_guardrail if {
write_api.decision.effect == "deny" with input as {"subject": {"id": "user-engine", "type": "service"}, "action": "tenant.guardrail.set", "resource": {"id": "tenant:trial:demo-company", "type": "guardrail", "system": "tenant-engine"}}
write_api.decision.effect == "deny" with input as { "tenant": "tenant:trial:demo-company","subject": {"id": "user-engine", "type": "service"}, "action": "tenant.guardrail.set", "resource": {"id": "tenant:trial:demo-company", "type": "guardrail", "system": "tenant-engine"}}
}
test_pdp_no_tenant_read if {
write_api.decision.effect == "deny" with input as {"subject": {"id": "flex-auth", "type": "service"}, "action": "tenant.read", "resource": {"id": "tenant:trial:demo-company", "type": "tenant", "system": "tenant-engine"}}
write_api.decision.effect == "deny" with input as { "tenant": "tenant:trial:demo-company","subject": {"id": "flex-auth", "type": "service"}, "action": "tenant.read", "resource": {"id": "tenant:trial:demo-company", "type": "tenant", "system": "tenant-engine"}}
}
test_unknown_no_read if {
write_api.decision.effect == "deny" with input as {"subject": {"id": "unknown", "type": "service"}, "action": "tenant.read", "resource": {"id": "tenant:trial:demo-company", "type": "tenant", "system": "tenant-engine"}}
write_api.decision.effect == "deny" with input as { "tenant": "tenant:trial:demo-company","subject": {"id": "unknown", "type": "service"}, "action": "tenant.read", "resource": {"id": "tenant:trial:demo-company", "type": "tenant", "system": "tenant-engine"}}
}
# --- Tenant relation (TEN-DEC-2026-002, FLEX-WP-0022-T02) ---
scope_tenants := {"tenant:platform", "tenant:friendly:binky", "tenant:acme:prod", "tenant:trial:demo-company"}
resource_type_for := {
"tenant.read": "tenant",
"tenant.create": "tenant",
"tenant.role.grant": "role-grant",
"tenant.role.revoke": "role-grant",
"tenant.plan.assign": "plan-assignment",
"tenant.update": "tenant",
"tenant.retire": "tenant",
"tenant.reactivate": "tenant",
"tenant.guardrail.read": "guardrail",
"tenant.guardrail.set": "guardrail",
}
scoped_request(subject, action, t) := {
"tenant": t,
"subject": {"id": subject, "type": "service"},
"action": action,
"resource": {"id": t, "type": resource_type_for[action], "system": "tenant-engine"},
}
effects_for(subject, action, tenants) := {e |
some t in tenants
e := write_api.decision.effect with input as scoped_request(subject, action, t)
}
# Scope: varying the target tenant never changes the effect.
test_tenant_never_changes_effect if {
every subject in {"tenant-engine", "flex-auth", "some-other-service"} {
every action in write_api.valid_actions {
count(effects_for(subject, action, scope_tenants)) == 1
}
}
}
# The quantified test above must not pass by denying everything.
test_cross_tenant_writes_allowed if {
every action in write_api.mutate_actions {
effects_for("tenant-engine", action, scope_tenants) == {"allow"}
}
}
test_pdp_guardrail_read_is_cross_tenant if {
effects_for("flex-auth", "tenant.guardrail.read", scope_tenants) == {"allow"}
}
# user-engine's grant excludes one fixed record; that is not a tenant relation.
test_portal_tenant_changes_effect_only_on_platform_record if {
every action in write_api.valid_actions {
count(effects_for("user-engine", action, scope_tenants - {"tenant:platform"})) == 1
}
effects_for("user-engine", "tenant.create", {"tenant:platform"}) == {"deny"}
}
# Invariant: tenant must equal resource.id.
test_tenant_not_target_denied if {
write_api.decision.reason == "tenant_not_target" with input as {
"tenant": "tenant:friendly:binky",
"subject": {"id": "tenant-engine", "type": "service"},
"action": "tenant.create",
"resource": {"id": "tenant:acme:prod", "type": "tenant", "system": "tenant-engine"}
}
}
test_tenant_absent_denied if {
write_api.decision.reason == "tenant_not_target" with input as {
"subject": {"id": "tenant-engine", "type": "service"},
"action": "tenant.create",
"resource": {"id": "tenant:acme:prod", "type": "tenant", "system": "tenant-engine"}
}
}
```

View file

@ -266,6 +266,14 @@ and `flex-auth` owes them a ping on message thread
`82bfe60f-1258-4d5d-9b81-9596b5fedd9e` once `access-engine` serves
`/raw/main/registry/indexes/capabilities.yaml`. Nine owners remain pending.
2026-09-21: two more owner records acknowledged. `tenant-engine` returned
`TEN-IN-0004` (hub intake `01a0c14b-b2f7-78f1-8f6c-e36c952fe004`) with the
retained runtime contract verified; `secrets-engine` returned `SECRETS-IN-0002`
with one repository path held until the rename lands. Both are `open` by
design and close on their side after T06; `flex-auth` owes each a
"rename landed" notice. Recorded in the evidence file. Seven owners remain
pending.
Reviewed inventory baseline:
| Owner | Required source/verification surface |

View file

@ -4,7 +4,7 @@ type: workplan
title: "Tenant scoping is unstated in tenant-engine and untested in two more packages"
domain: infotech
repo: flex-auth
status: active
status: finished
flavor: implementation
depends_on:
- FLEX-WP-0021
@ -18,7 +18,7 @@ related_workplans:
- FLEX-WP-0010
- FLEX-WP-0014
created: "2026-09-06"
updated: "2026-09-15"
updated: "2026-09-21"
state_hub_workstream_id: "804c588c-f47a-50c4-bdd7-51b24bbf9539"
---
@ -60,7 +60,7 @@ look identical in the artifact. That is the same publishing-shape argument
```task
id: FLEX-WP-0022-T01
status: progress
status: done
priority: high
state_hub_task_id: "a84dcee5-9426-5b30-8dd3-f4c076d00174"
```
@ -86,11 +86,19 @@ policy rule inferred from reading their code would make `flex-auth` the author
of their tenancy model, which is the boundary `FLEX-WP-0021-T01` exists to hold.
T02 stays `wait` rather than being guessed forward.
2026-09-21: answered by tenant-engine in its own record, `TEN-DEC-2026-002`
(hub decision `ecaebbb7-fe90-4235-a79f-23457e9727bc`) and
`docs/flex-auth-integration.md`, commit `d132db0`; message `3d3de8bc`. Read
from the committed record, not the message. `tenant` is the **target tenant
record**; it always equals `resource.id`; none of the nine write actions is
refused cross-tenant, deliberately; `tenant.guardrail.read` does not differ and
must not. Gate met: the relation is named by tenant-engine.
## 2. Encode the relation, or record that there is none
```task
id: FLEX-WP-0022-T02
status: wait
status: done
priority: high
state_hub_task_id: "712ac826-845f-54bd-8446-f11258d21eb4"
```
@ -107,6 +115,20 @@ tenants" is a rule a reviewer can check. Silence is not.
Either way the fixtures must vary `tenant`, so the suite reports on the field.
2026-09-21: done as `tenant-engine.write-api.mutate` **v3**
(`FLEX-DEC-2026-016`). Both of tenant-engine's commitments are encoded:
`allowed` requires `tenant_is_target` (non-empty `tenant` equal to
`resource.id`; otherwise `deny` / `tenant_not_target`), and the package states
the cross-tenant scope in prose and quantifies it in
`test_tenant_never_changes_effect` (every action, three subjects, four
tenants). Fixtures now rotate `tenant` across `tenant:friendly:binky`,
`tenant:acme:prod`, `tenant:platform`, `tenant:trial:demo-company`, with five
cross-tenant allows and two `tenant_not_target` denies added: 42 fixtures and
33 embedded tests pass, `go test ./...` green. One boundary reported back to
tenant-engine rather than papered over: `user-engine`'s grant excludes the
fixed record `tenant:platform`, which is target-dependent but not a
subject/tenant relation.
## 3. Vary tenant in the two suites that hold it constant
```task

View file

@ -275,6 +275,17 @@ A13 notes it. B2: `gate-house` declared; `key-cape`, `ops-mason` and
`net-kingdom` have not answered. Task stays `progress` on B2 alone — not closed
by silence. Consequences are T06T08.
2026-09-21 (acknowledgements): `kings-guard`'s reply to the B1 correction
(`4c328bf6`) predates `GH-DEC-2026-017`; it held its two forms rather than
guessing, recorded `KG-IN-0007`, and has since applied the ruling and closed
that intake (`kings-guard` `5120adf`, value unchanged, `standard_version`
removed). Its observation — that off-vocabulary values are a third class a
precedence ruling alone leaves untouched — is answered by the post-ruling
survey (`docs/evidence/2026-09-21-layer-declaration-survey-after-ghdec017.json`):
`off_vocab` is empty once `Taxonomy` is admitted. `approval-engine`,
`maturity-engine`, `zone-engine`, `secrets-engine` and `railiance-master` also
confirmed the ruling applied. None of these bears on B2.
## Out of scope
- Bumping flex-auth to declare v0.8. v0.8 is `status: proposed`; T01 removes the