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 **Reversal condition.** Reverses only by a runtime-rename decision that
supersedes `FLEX-DEC-2026-013`'s retain row for policy vocabulary — and that supersedes `FLEX-DEC-2026-013`'s retain row for policy vocabulary — and that
decision inherits the PDP-first order above. 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 | | `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 | | `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 | | `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 | | `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 | | `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 | | `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`, `policy-nexus`, `user-engine`, `net-kingdom`, `tenant-engine`, `sbom-nexus`,
`repo-manager`, and the named semantic-consumer verifiers. `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` | | `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`) | | `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_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 | | `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` | | `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 **No `resource_manifest.yaml`** — unlike ops-warden's fixed SSH-certificate
inventory, `tenant-engine`'s resources (tenants) are created dynamically. inventory, `tenant-engine`'s resources (tenants) are created dynamically.

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -7,7 +7,7 @@
}, },
"action": "tenant.retired", "action": "tenant.retired",
"resource": { "resource": {
"id": "t-1", "id": "tenant:friendly:binky",
"type": "tenant", "type": "tenant",
"system": "tenant-engine" "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", "action": "tenant.create",
"resource": { "resource": {
"id": "t-1", "id": "tenant:friendly:binky",
"type": "tenant", "type": "tenant",
"system": "tenant-engine" "system": "tenant-engine"
}, },

View file

@ -4,276 +4,506 @@
"request": { "request": {
"id": "check:tenant-engine-create-t1", "id": "check:tenant-engine-create-t1",
"tenant": "tenant:friendly:binky", "tenant": "tenant:friendly:binky",
"subject": {"id": "tenant-engine", "type": "service"}, "subject": {
"id": "tenant-engine",
"type": "service"
},
"action": "tenant.create", "action": "tenant.create",
"resource": {"id": "t-1", "type": "tenant", "system": "tenant-engine"}, "resource": {
"id": "tenant:friendly:binky",
"type": "tenant",
"system": "tenant-engine"
},
"context": {} "context": {}
}, },
"expect": {"effect": "allow", "reason": "write_api_policy_matched"} "expect": {
"effect": "allow",
"reason": "write_api_policy_matched"
}
}, },
{ {
"id": "fixture:tenant-engine-role-grant-allow", "id": "fixture:tenant-engine-role-grant-allow",
"request": { "request": {
"id": "check:tenant-engine-role-grant-t1", "id": "check:tenant-engine-role-grant-t1",
"tenant": "tenant:friendly:binky", "tenant": "tenant:acme:prod",
"subject": {"id": "tenant-engine", "type": "service"}, "subject": {
"id": "tenant-engine",
"type": "service"
},
"action": "tenant.role.grant", "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": {} "context": {}
}, },
"expect": {"effect": "allow", "reason": "write_api_policy_matched"} "expect": {
"effect": "allow",
"reason": "write_api_policy_matched"
}
}, },
{ {
"id": "fixture:tenant-engine-role-revoke-allow", "id": "fixture:tenant-engine-role-revoke-allow",
"request": { "request": {
"id": "check:tenant-engine-role-revoke-t1", "id": "check:tenant-engine-role-revoke-t1",
"tenant": "tenant:friendly:binky", "tenant": "tenant:platform",
"subject": {"id": "tenant-engine", "type": "service"}, "subject": {
"id": "tenant-engine",
"type": "service"
},
"action": "tenant.role.revoke", "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": {} "context": {}
}, },
"expect": {"effect": "allow", "reason": "write_api_policy_matched"} "expect": {
"effect": "allow",
"reason": "write_api_policy_matched"
}
}, },
{ {
"id": "fixture:tenant-engine-plan-assign-allow", "id": "fixture:tenant-engine-plan-assign-allow",
"request": { "request": {
"id": "check:tenant-engine-plan-assign-t1", "id": "check:tenant-engine-plan-assign-t1",
"tenant": "tenant:friendly:binky", "tenant": "tenant:trial:demo-company",
"subject": {"id": "tenant-engine", "type": "service"}, "subject": {
"id": "tenant-engine",
"type": "service"
},
"action": "tenant.plan.assign", "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": {} "context": {}
}, },
"expect": {"effect": "allow", "reason": "write_api_policy_matched"} "expect": {
"effect": "allow",
"reason": "write_api_policy_matched"
}
}, },
{ {
"id": "fixture:tenant-engine-update-allow", "id": "fixture:tenant-engine-update-allow",
"request": { "request": {
"id": "check:tenant-engine-update-t1", "id": "check:tenant-engine-update-t1",
"tenant": "tenant:friendly:binky", "tenant": "tenant:friendly:binky",
"subject": {"id": "tenant-engine", "type": "service"}, "subject": {
"id": "tenant-engine",
"type": "service"
},
"action": "tenant.update", "action": "tenant.update",
"resource": {"id": "t-1", "type": "tenant", "system": "tenant-engine"}, "resource": {
"id": "tenant:friendly:binky",
"type": "tenant",
"system": "tenant-engine"
},
"context": {} "context": {}
}, },
"expect": {"effect": "allow", "reason": "write_api_policy_matched"} "expect": {
"effect": "allow",
"reason": "write_api_policy_matched"
}
}, },
{ {
"id": "fixture:tenant-engine-retire-allow", "id": "fixture:tenant-engine-retire-allow",
"request": { "request": {
"id": "check:tenant-engine-retire-t1", "id": "check:tenant-engine-retire-t1",
"tenant": "tenant:friendly:binky", "tenant": "tenant:acme:prod",
"subject": {"id": "tenant-engine", "type": "service"}, "subject": {
"id": "tenant-engine",
"type": "service"
},
"action": "tenant.retire", "action": "tenant.retire",
"resource": {"id": "t-1", "type": "tenant", "system": "tenant-engine"}, "resource": {
"id": "tenant:acme:prod",
"type": "tenant",
"system": "tenant-engine"
},
"context": {} "context": {}
}, },
"expect": {"effect": "allow", "reason": "write_api_policy_matched"} "expect": {
"effect": "allow",
"reason": "write_api_policy_matched"
}
}, },
{ {
"id": "fixture:tenant-engine-reactivate-allow", "id": "fixture:tenant-engine-reactivate-allow",
"request": { "request": {
"id": "check:tenant-engine-reactivate-t1", "id": "check:tenant-engine-reactivate-t1",
"tenant": "tenant:friendly:binky", "tenant": "tenant:platform",
"subject": {"id": "tenant-engine", "type": "service"}, "subject": {
"id": "tenant-engine",
"type": "service"
},
"action": "tenant.reactivate", "action": "tenant.reactivate",
"resource": {"id": "t-1", "type": "tenant", "system": "tenant-engine"}, "resource": {
"id": "tenant:platform",
"type": "tenant",
"system": "tenant-engine"
},
"context": {} "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", "id": "fixture:tenant-engine-update-unknown-subject-deny",
"request": { "request": {
"id": "check:tenant-engine-update-t1", "id": "check:tenant-engine-update-t1",
"tenant": "tenant:friendly:binky", "tenant": "tenant:trial:demo-company",
"subject": {"id": "some-other-service", "type": "service"}, "subject": {
"id": "some-other-service",
"type": "service"
},
"action": "tenant.update", "action": "tenant.update",
"resource": {"id": "t-1", "type": "tenant", "system": "tenant-engine"}, "resource": {
"id": "tenant:trial:demo-company",
"type": "tenant",
"system": "tenant-engine"
},
"context": {} "context": {}
}, },
"expect": {"effect": "deny", "reason": "unknown_subject"} "expect": {
"effect": "deny",
"reason": "unknown_subject"
}
}, },
{ {
"id": "fixture:tenant-engine-retire-unknown-subject-deny", "id": "fixture:tenant-engine-retire-unknown-subject-deny",
"request": { "request": {
"id": "check:tenant-engine-retire-t1", "id": "check:tenant-engine-retire-t1",
"tenant": "tenant:friendly:binky", "tenant": "tenant:friendly:binky",
"subject": {"id": "some-other-service", "type": "service"}, "subject": {
"id": "some-other-service",
"type": "service"
},
"action": "tenant.retire", "action": "tenant.retire",
"resource": {"id": "t-1", "type": "tenant", "system": "tenant-engine"}, "resource": {
"id": "tenant:friendly:binky",
"type": "tenant",
"system": "tenant-engine"
},
"context": {} "context": {}
}, },
"expect": {"effect": "deny", "reason": "unknown_subject"} "expect": {
"effect": "deny",
"reason": "unknown_subject"
}
}, },
{ {
"id": "fixture:tenant-engine-reactivate-unknown-subject-deny", "id": "fixture:tenant-engine-reactivate-unknown-subject-deny",
"request": { "request": {
"id": "check:tenant-engine-reactivate-t1", "id": "check:tenant-engine-reactivate-t1",
"tenant": "tenant:friendly:binky", "tenant": "tenant:acme:prod",
"subject": {"id": "some-other-service", "type": "service"}, "subject": {
"id": "some-other-service",
"type": "service"
},
"action": "tenant.reactivate", "action": "tenant.reactivate",
"resource": {"id": "t-1", "type": "tenant", "system": "tenant-engine"}, "resource": {
"id": "tenant:acme:prod",
"type": "tenant",
"system": "tenant-engine"
},
"context": {} "context": {}
}, },
"expect": {"effect": "deny", "reason": "unknown_subject"} "expect": {
"effect": "deny",
"reason": "unknown_subject"
}
}, },
{ {
"id": "fixture:tenant-engine-misspelled-lifecycle-action-deny", "id": "fixture:tenant-engine-misspelled-lifecycle-action-deny",
"request": { "request": {
"id": "check:tenant-engine-retired-t1", "id": "check:tenant-engine-retired-t1",
"tenant": "tenant:friendly:binky", "tenant": "tenant:platform",
"subject": {"id": "tenant-engine", "type": "service"}, "subject": {
"id": "tenant-engine",
"type": "service"
},
"action": "tenant.retired", "action": "tenant.retired",
"resource": {"id": "t-1", "type": "tenant", "system": "tenant-engine"}, "resource": {
"id": "tenant:platform",
"type": "tenant",
"system": "tenant-engine"
},
"context": {} "context": {}
}, },
"expect": {"effect": "deny", "reason": "unknown_action"} "expect": {
"effect": "deny",
"reason": "unknown_action"
}
}, },
{ {
"id": "fixture:tenant-engine-lifecycle-underscore-action-deny", "id": "fixture:tenant-engine-lifecycle-underscore-action-deny",
"request": { "request": {
"id": "check:tenant-engine-underscore-t1", "id": "check:tenant-engine-underscore-t1",
"tenant": "tenant:friendly:binky", "tenant": "tenant:trial:demo-company",
"subject": {"id": "tenant-engine", "type": "service"}, "subject": {
"id": "tenant-engine",
"type": "service"
},
"action": "tenant_update", "action": "tenant_update",
"resource": {"id": "t-1", "type": "tenant", "system": "tenant-engine"}, "resource": {
"id": "tenant:trial:demo-company",
"type": "tenant",
"system": "tenant-engine"
},
"context": {} "context": {}
}, },
"expect": {"effect": "deny", "reason": "unknown_action"} "expect": {
"effect": "deny",
"reason": "unknown_action"
}
}, },
{ {
"id": "fixture:tenant-engine-unknown-subject-deny", "id": "fixture:tenant-engine-unknown-subject-deny",
"request": { "request": {
"id": "check:tenant-engine-create-t1", "id": "check:tenant-engine-create-t1",
"tenant": "tenant:friendly:binky", "tenant": "tenant:friendly:binky",
"subject": {"id": "some-other-service", "type": "service"}, "subject": {
"id": "some-other-service",
"type": "service"
},
"action": "tenant.create", "action": "tenant.create",
"resource": {"id": "t-1", "type": "tenant", "system": "tenant-engine"}, "resource": {
"id": "tenant:friendly:binky",
"type": "tenant",
"system": "tenant-engine"
},
"context": {} "context": {}
}, },
"expect": {"effect": "deny", "reason": "unknown_subject"} "expect": {
"effect": "deny",
"reason": "unknown_subject"
}
}, },
{ {
"id": "fixture:tenant-engine-wrong-system-deny", "id": "fixture:tenant-engine-wrong-system-deny",
"request": { "request": {
"id": "check:tenant-engine-create-t1", "id": "check:tenant-engine-create-t1",
"tenant": "tenant:friendly:binky", "tenant": "tenant:acme:prod",
"subject": {"id": "tenant-engine", "type": "service"}, "subject": {
"id": "tenant-engine",
"type": "service"
},
"action": "tenant.create", "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": {} "context": {}
}, },
"expect": {"effect": "deny", "reason": "wrong_system"} "expect": {
"effect": "deny",
"reason": "wrong_system"
}
}, },
{ {
"id": "fixture:tenant-engine-unknown-action-deny", "id": "fixture:tenant-engine-unknown-action-deny",
"request": { "request": {
"id": "check:tenant-engine-delete-t1", "id": "check:tenant-engine-delete-t1",
"tenant": "tenant:friendly:binky", "tenant": "tenant:platform",
"subject": {"id": "tenant-engine", "type": "service"}, "subject": {
"id": "tenant-engine",
"type": "service"
},
"action": "tenant.delete", "action": "tenant.delete",
"resource": {"id": "t-1", "type": "tenant", "system": "tenant-engine"}, "resource": {
"id": "tenant:platform",
"type": "tenant",
"system": "tenant-engine"
},
"context": {} "context": {}
}, },
"expect": {"effect": "deny", "reason": "unknown_action"} "expect": {
"effect": "deny",
"reason": "unknown_action"
}
}, },
{ {
"id": "fixture:tenant-engine-wrong-subject-type-deny", "id": "fixture:tenant-engine-wrong-subject-type-deny",
"request": { "request": {
"id": "check:tenant-engine-create-t1", "id": "check:tenant-engine-create-t1",
"tenant": "tenant:friendly:binky", "tenant": "tenant:trial:demo-company",
"subject": {"id": "tenant-engine", "type": "human"}, "subject": {
"id": "tenant-engine",
"type": "human"
},
"action": "tenant.create", "action": "tenant.create",
"resource": {"id": "t-1", "type": "tenant", "system": "tenant-engine"}, "resource": {
"id": "tenant:trial:demo-company",
"type": "tenant",
"system": "tenant-engine"
},
"context": {} "context": {}
}, },
"expect": {"effect": "deny", "reason": "wrong_subject_type"} "expect": {
"effect": "deny",
"reason": "wrong_subject_type"
}
}, },
{ {
"id": "fixture:tenant-engine-guardrail-read-pdp-allow", "id": "fixture:tenant-engine-guardrail-read-pdp-allow",
"request": { "request": {
"id": "check:tenant-engine-guardrail-read-pdp", "id": "check:tenant-engine-guardrail-read-pdp",
"tenant": "tenant:friendly:binky", "tenant": "tenant:friendly:binky",
"subject": {"id": "flex-auth", "type": "service"}, "subject": {
"id": "flex-auth",
"type": "service"
},
"action": "tenant.guardrail.read", "action": "tenant.guardrail.read",
"resource": {"id": "t-1", "type": "guardrail", "system": "tenant-engine"}, "resource": {
"id": "tenant:friendly:binky",
"type": "guardrail",
"system": "tenant-engine"
},
"context": {} "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", "id": "fixture:tenant-engine-guardrail-read-writer-allow",
"request": { "request": {
"id": "check:tenant-engine-guardrail-read-writer", "id": "check:tenant-engine-guardrail-read-writer",
"tenant": "tenant:friendly:binky", "tenant": "tenant:acme:prod",
"subject": {"id": "tenant-engine", "type": "service"}, "subject": {
"id": "tenant-engine",
"type": "service"
},
"action": "tenant.guardrail.read", "action": "tenant.guardrail.read",
"resource": {"id": "t-1", "type": "guardrail", "system": "tenant-engine"}, "resource": {
"id": "tenant:acme:prod",
"type": "guardrail",
"system": "tenant-engine"
},
"context": {} "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", "id": "fixture:tenant-engine-guardrail-set-writer-allow",
"request": { "request": {
"id": "check:tenant-engine-guardrail-set-writer", "id": "check:tenant-engine-guardrail-set-writer",
"tenant": "tenant:friendly:binky", "tenant": "tenant:platform",
"subject": {"id": "tenant-engine", "type": "service"}, "subject": {
"id": "tenant-engine",
"type": "service"
},
"action": "tenant.guardrail.set", "action": "tenant.guardrail.set",
"resource": {"id": "t-1", "type": "guardrail", "system": "tenant-engine"}, "resource": {
"id": "tenant:platform",
"type": "guardrail",
"system": "tenant-engine"
},
"context": {} "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", "id": "fixture:tenant-engine-guardrail-set-pdp-deny",
"request": { "request": {
"id": "check:tenant-engine-guardrail-set-pdp", "id": "check:tenant-engine-guardrail-set-pdp",
"tenant": "tenant:friendly:binky", "tenant": "tenant:trial:demo-company",
"subject": {"id": "flex-auth", "type": "service"}, "subject": {
"id": "flex-auth",
"type": "service"
},
"action": "tenant.guardrail.set", "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": {} "context": {}
}, },
"expect": {"effect": "deny", "reason": "action_not_granted"} "expect": {
"effect": "deny",
"reason": "action_not_granted"
}
}, },
{ {
"id": "fixture:tenant-engine-guardrail-read-unknown-subject-deny", "id": "fixture:tenant-engine-guardrail-read-unknown-subject-deny",
"request": { "request": {
"id": "check:tenant-engine-guardrail-read-ops", "id": "check:tenant-engine-guardrail-read-ops",
"tenant": "tenant:friendly:binky", "tenant": "tenant:friendly:binky",
"subject": {"id": "ops", "type": "service"}, "subject": {
"id": "ops",
"type": "service"
},
"action": "tenant.guardrail.read", "action": "tenant.guardrail.read",
"resource": {"id": "t-1", "type": "guardrail", "system": "tenant-engine"}, "resource": {
"id": "tenant:friendly:binky",
"type": "guardrail",
"system": "tenant-engine"
},
"context": {} "context": {}
}, },
"expect": {"effect": "deny", "reason": "unknown_subject"} "expect": {
"effect": "deny",
"reason": "unknown_subject"
}
}, },
{ {
"id": "fixture:tenant-engine-guardrail-set-unknown-subject-deny", "id": "fixture:tenant-engine-guardrail-set-unknown-subject-deny",
"request": { "request": {
"id": "check:tenant-engine-guardrail-set-ops", "id": "check:tenant-engine-guardrail-set-ops",
"tenant": "tenant:friendly:binky", "tenant": "tenant:acme:prod",
"subject": {"id": "ops", "type": "service"}, "subject": {
"id": "ops",
"type": "service"
},
"action": "tenant.guardrail.set", "action": "tenant.guardrail.set",
"resource": {"id": "t-1", "type": "guardrail", "system": "tenant-engine"}, "resource": {
"id": "tenant:acme:prod",
"type": "guardrail",
"system": "tenant-engine"
},
"context": {} "context": {}
}, },
"expect": {"effect": "deny", "reason": "unknown_subject"} "expect": {
"effect": "deny",
"reason": "unknown_subject"
}
}, },
{ {
"id": "fixture:tenant-engine-misspelled-guardrail-action-deny", "id": "fixture:tenant-engine-misspelled-guardrail-action-deny",
"request": { "request": {
"id": "check:tenant-engine-guardrail-get", "id": "check:tenant-engine-guardrail-get",
"tenant": "tenant:friendly:binky", "tenant": "tenant:platform",
"subject": {"id": "flex-auth", "type": "service"}, "subject": {
"id": "flex-auth",
"type": "service"
},
"action": "tenant.guardrail.get", "action": "tenant.guardrail.get",
"resource": {"id": "t-1", "type": "guardrail", "system": "tenant-engine"}, "resource": {
"id": "tenant:platform",
"type": "guardrail",
"system": "tenant-engine"
},
"context": {} "context": {}
}, },
"expect": {"effect": "deny", "reason": "unknown_action"} "expect": {
"effect": "deny",
"reason": "unknown_action"
}
}, },
{ {
"id": "fixture:tenant-engine-portal-create", "id": "fixture:tenant-engine-portal-create",
@ -538,5 +768,158 @@
"effect": "deny", "effect": "deny",
"reason": "action_not_granted" "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 id: tenant-engine.write-api.mutate
name: tenant-engine Write API authorization name: tenant-engine Write API authorization
namespace: tenant-engine:tenant namespace: tenant-engine:tenant
version: v2 version: v3
status: ready status: ready
package: flexauth.tenant_engine.write_api package: flexauth.tenant_engine.write_api
actions: 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 this explicit minimal onboarding grant supersedes that baseline only for these
operations and this existing service integration. 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 ## Rules
```rego ```rego
@ -208,27 +247,41 @@ read_subjects := {"tenant-engine", "flex-auth"}
mutate_subjects := {"tenant-engine"} 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 { decision := {"effect": "allow", "reason": "write_api_policy_matched"} if {
allowed allowed
} else := {"effect": "deny", "reason": first_denial} if { } else := {"effect": "deny", "reason": first_denial} if {
true true
} }
allowed if { granted if {
input.resource.system == "tenant-engine" input.resource.system == "tenant-engine"
input.action in read_actions input.action in read_actions
input.subject.type == "service" input.subject.type == "service"
input.subject.id in read_subjects input.subject.id in read_subjects
} }
allowed if { granted if {
input.resource.system == "tenant-engine" input.resource.system == "tenant-engine"
input.action in mutate_actions input.action in mutate_actions
input.subject.type == "service" input.subject.type == "service"
input.subject.id in mutate_subjects input.subject.id in mutate_subjects
} }
allowed if { granted if {
input.resource.system == "tenant-engine" input.resource.system == "tenant-engine"
input.resource.type == "tenant" input.resource.type == "tenant"
input.resource.id != "tenant:platform" input.resource.id != "tenant:platform"
@ -237,7 +290,7 @@ allowed if {
input.subject.id == "user-engine" input.subject.id == "user-engine"
} }
allowed if { granted if {
input.resource.system == "tenant-engine" input.resource.system == "tenant-engine"
input.resource.type == "tenant" input.resource.type == "tenant"
input.action == "tenant.read" input.action == "tenant.read"
@ -249,6 +302,8 @@ default first_denial := "no_matching_rule"
first_denial := "wrong_system" if { first_denial := "wrong_system" if {
input.resource.system != "tenant-engine" input.resource.system != "tenant-engine"
} else := "tenant_not_target" if {
not tenant_is_target
} else := "unknown_action" if { } else := "unknown_action" if {
not input.action in valid_actions not input.action in valid_actions
} else := "wrong_subject_type" if { } else := "wrong_subject_type" if {
@ -265,7 +320,9 @@ first_denial := "wrong_system" if {
```rego test ```rego test
package flexauth.tenant_engine.write_api_test package flexauth.tenant_engine.write_api_test
import future.keywords.every
import future.keywords.if import future.keywords.if
import future.keywords.in
import data.flexauth.tenant_engine.write_api import data.flexauth.tenant_engine.write_api
base_request := { base_request := {
@ -273,7 +330,7 @@ base_request := {
"tenant": "tenant:friendly:binky", "tenant": "tenant:friendly:binky",
"subject": {"id": "tenant-engine", "type": "service"}, "subject": {"id": "tenant-engine", "type": "service"},
"action": "tenant.create", "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 { test_known_operator_create_allowed if {
@ -281,170 +338,245 @@ test_known_operator_create_allowed if {
} }
test_role_grant_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"}, "subject": {"id": "tenant-engine", "type": "service"},
"action": "tenant.role.grant", "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 { 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"}, "subject": {"id": "tenant-engine", "type": "service"},
"action": "tenant.update", "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 { 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"}, "subject": {"id": "tenant-engine", "type": "service"},
"action": "tenant.retire", "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 { 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"}, "subject": {"id": "tenant-engine", "type": "service"},
"action": "tenant.reactivate", "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 { 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"}, "subject": {"id": "tenant-engine", "type": "service"},
"action": "tenant.retired", "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 { 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"}, "subject": {"id": "some-other-service", "type": "service"},
"action": "tenant.retire", "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 { 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"}, "subject": {"id": "some-other-service", "type": "service"},
"action": "tenant.create", "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 { 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"}, "subject": {"id": "tenant-engine", "type": "service"},
"action": "tenant.create", "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 { 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"}, "subject": {"id": "tenant-engine", "type": "service"},
"action": "tenant.delete", "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 { 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"}, "subject": {"id": "tenant-engine", "type": "human"},
"action": "tenant.create", "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 { 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"}, "subject": {"id": "flex-auth", "type": "service"},
"action": "tenant.guardrail.read", "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 { 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"}, "subject": {"id": "tenant-engine", "type": "service"},
"action": "tenant.guardrail.read", "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 { 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"}, "subject": {"id": "tenant-engine", "type": "service"},
"action": "tenant.guardrail.set", "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 { 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"}, "subject": {"id": "flex-auth", "type": "service"},
"action": "tenant.guardrail.set", "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 { 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"}, "subject": {"id": "ops", "type": "service"},
"action": "tenant.guardrail.read", "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 { 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"}, "subject": {"id": "flex-auth", "type": "service"},
"action": "tenant.guardrail.get", "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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 `82bfe60f-1258-4d5d-9b81-9596b5fedd9e` once `access-engine` serves
`/raw/main/registry/indexes/capabilities.yaml`. Nine owners remain pending. `/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: Reviewed inventory baseline:
| Owner | Required source/verification surface | | 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" title: "Tenant scoping is unstated in tenant-engine and untested in two more packages"
domain: infotech domain: infotech
repo: flex-auth repo: flex-auth
status: active status: finished
flavor: implementation flavor: implementation
depends_on: depends_on:
- FLEX-WP-0021 - FLEX-WP-0021
@ -18,7 +18,7 @@ related_workplans:
- FLEX-WP-0010 - FLEX-WP-0010
- FLEX-WP-0014 - FLEX-WP-0014
created: "2026-09-06" created: "2026-09-06"
updated: "2026-09-15" updated: "2026-09-21"
state_hub_workstream_id: "804c588c-f47a-50c4-bdd7-51b24bbf9539" 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 ```task
id: FLEX-WP-0022-T01 id: FLEX-WP-0022-T01
status: progress status: done
priority: high priority: high
state_hub_task_id: "a84dcee5-9426-5b30-8dd3-f4c076d00174" 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. of their tenancy model, which is the boundary `FLEX-WP-0021-T01` exists to hold.
T02 stays `wait` rather than being guessed forward. 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 ## 2. Encode the relation, or record that there is none
```task ```task
id: FLEX-WP-0022-T02 id: FLEX-WP-0022-T02
status: wait status: done
priority: high priority: high
state_hub_task_id: "712ac826-845f-54bd-8446-f11258d21eb4" 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. 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 ## 3. Vary tenant in the two suites that hold it constant
```task ```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 `net-kingdom` have not answered. Task stays `progress` on B2 alone — not closed
by silence. Consequences are T06T08. 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 ## Out of scope
- Bumping flex-auth to declare v0.8. v0.8 is `status: proposed`; T01 removes the - Bumping flex-auth to declare v0.8. v0.8 is `status: proposed`; T01 removes the