From 3081067325652e898087478ff5398f468dcc5ee1 Mon Sep 17 00:00:00 2001 From: tegwick Date: Mon, 21 Sep 2026 07:39:57 +0200 Subject: [PATCH] State tenant-engine's tenant relation in the write-api package, v3. tenant-engine named the relation in TEN-DEC-2026-002: `tenant` is the target tenant record and always equals `resource.id`; the write API is cross-tenant by design and `tenant.guardrail.read` does not differ. v2 carried no tenant rule and a constant fixture tenant, so the deliberate scope and an omitted rule were indistinguishable. tenant-engine.write-api.mutate v3 (FLEX-DEC-2026-016): - allowed requires tenant_is_target; a mismatch or absent tenant is denied tenant_not_target (object.get, so an absent key names the right cause). - the cross-tenant scope is stated in the package and quantified by test_tenant_never_changes_effect over every action, three subjects and four tenants, with guards against passing by denying everything. - fixtures rotate tenant across four tenants; five cross-tenant allows and two tenant_not_target denies added (42 fixtures, 33 tests, all pass). - user-engine's tenant:platform exclusion is named as a fixed-record rule, not a subject/tenant relation, and tested separately. Closes FLEX-WP-0022 (T01, T02 done). Also records TEN-IN-0004 and SECRETS-IN-0002 on FLEX-WP-0020 and acknowledges the GH-DEC-2026-017 replies on FLEX-WP-0030-T04. Co-Authored-By: Claude Opus 5 Assistant: claude-code Assistant-Model: opus Assistant-Process: 63291@bnt-lap001 Assistant-Session: 8bd77868-ca68-4f49-bb1e-d539ecc0d703 --- decisions/decisions.md | 53 ++ .../2026-09-15-repository-rename-handoffs.md | 11 +- examples/tenant-engine/README.md | 6 +- .../check_request_allow_create.json | 2 +- .../check_request_allow_guardrail_read.json | 2 +- .../check_request_allow_guardrail_set.json | 2 +- .../check_request_allow_retire.json | 2 +- ..._request_deny_guardrail_set_as_reader.json | 2 +- ...eck_request_deny_misspelled_guardrail.json | 2 +- ...eck_request_deny_misspelled_lifecycle.json | 2 +- .../check_request_deny_tenant_not_target.json | 15 + .../check_request_deny_unknown_subject.json | 2 +- examples/tenant-engine/policy_fixtures.yaml | 555 +++++++++++++++--- examples/tenant-engine/policy_package.md | 228 +++++-- ...X-WP-0020-repository-identity-migration.md | 8 + .../FLEX-WP-0022-tenant-scope-coverage.md | 30 +- ...EX-WP-0030-boundary-declaration-cleanup.md | 11 + 17 files changed, 783 insertions(+), 150 deletions(-) create mode 100644 examples/tenant-engine/check_request_deny_tenant_not_target.json diff --git a/decisions/decisions.md b/decisions/decisions.md index 49f32ca..c8ed6fb 100644 --- a/decisions/decisions.md +++ b/decisions/decisions.md @@ -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. diff --git a/docs/evidence/2026-09-15-repository-rename-handoffs.md b/docs/evidence/2026-09-15-repository-rename-handoffs.md index cf981d2..e9e8956 100644 --- a/docs/evidence/2026-09-15-repository-rename-handoffs.md +++ b/docs/evidence/2026-09-15-repository-rename-handoffs.md @@ -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. + diff --git a/examples/tenant-engine/README.md b/examples/tenant-engine/README.md index d823748..7eb9a75 100644 --- a/examples/tenant-engine/README.md +++ b/examples/tenant-engine/README.md @@ -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. diff --git a/examples/tenant-engine/check_request_allow_create.json b/examples/tenant-engine/check_request_allow_create.json index 1438a79..b02d797 100644 --- a/examples/tenant-engine/check_request_allow_create.json +++ b/examples/tenant-engine/check_request_allow_create.json @@ -7,7 +7,7 @@ }, "action": "tenant.create", "resource": { - "id": "t-1", + "id": "tenant:friendly:binky", "type": "tenant", "system": "tenant-engine" }, diff --git a/examples/tenant-engine/check_request_allow_guardrail_read.json b/examples/tenant-engine/check_request_allow_guardrail_read.json index b622b71..5f9b7bf 100644 --- a/examples/tenant-engine/check_request_allow_guardrail_read.json +++ b/examples/tenant-engine/check_request_allow_guardrail_read.json @@ -7,7 +7,7 @@ }, "action": "tenant.guardrail.read", "resource": { - "id": "t-1", + "id": "tenant:friendly:binky", "type": "guardrail", "system": "tenant-engine" }, diff --git a/examples/tenant-engine/check_request_allow_guardrail_set.json b/examples/tenant-engine/check_request_allow_guardrail_set.json index fd15aba..7d1688a 100644 --- a/examples/tenant-engine/check_request_allow_guardrail_set.json +++ b/examples/tenant-engine/check_request_allow_guardrail_set.json @@ -7,7 +7,7 @@ }, "action": "tenant.guardrail.set", "resource": { - "id": "t-1", + "id": "tenant:friendly:binky", "type": "guardrail", "system": "tenant-engine" }, diff --git a/examples/tenant-engine/check_request_allow_retire.json b/examples/tenant-engine/check_request_allow_retire.json index af7fca8..9b2914b 100644 --- a/examples/tenant-engine/check_request_allow_retire.json +++ b/examples/tenant-engine/check_request_allow_retire.json @@ -7,7 +7,7 @@ }, "action": "tenant.retire", "resource": { - "id": "t-1", + "id": "tenant:friendly:binky", "type": "tenant", "system": "tenant-engine" }, diff --git a/examples/tenant-engine/check_request_deny_guardrail_set_as_reader.json b/examples/tenant-engine/check_request_deny_guardrail_set_as_reader.json index 0b5316e..ed1aa9a 100644 --- a/examples/tenant-engine/check_request_deny_guardrail_set_as_reader.json +++ b/examples/tenant-engine/check_request_deny_guardrail_set_as_reader.json @@ -7,7 +7,7 @@ }, "action": "tenant.guardrail.set", "resource": { - "id": "t-1", + "id": "tenant:friendly:binky", "type": "guardrail", "system": "tenant-engine" }, diff --git a/examples/tenant-engine/check_request_deny_misspelled_guardrail.json b/examples/tenant-engine/check_request_deny_misspelled_guardrail.json index 9beaffa..6502133 100644 --- a/examples/tenant-engine/check_request_deny_misspelled_guardrail.json +++ b/examples/tenant-engine/check_request_deny_misspelled_guardrail.json @@ -7,7 +7,7 @@ }, "action": "tenant.guardrail.get", "resource": { - "id": "t-1", + "id": "tenant:friendly:binky", "type": "guardrail", "system": "tenant-engine" }, diff --git a/examples/tenant-engine/check_request_deny_misspelled_lifecycle.json b/examples/tenant-engine/check_request_deny_misspelled_lifecycle.json index 988fa63..b2808c5 100644 --- a/examples/tenant-engine/check_request_deny_misspelled_lifecycle.json +++ b/examples/tenant-engine/check_request_deny_misspelled_lifecycle.json @@ -7,7 +7,7 @@ }, "action": "tenant.retired", "resource": { - "id": "t-1", + "id": "tenant:friendly:binky", "type": "tenant", "system": "tenant-engine" }, diff --git a/examples/tenant-engine/check_request_deny_tenant_not_target.json b/examples/tenant-engine/check_request_deny_tenant_not_target.json new file mode 100644 index 0000000..c68d5f2 --- /dev/null +++ b/examples/tenant-engine/check_request_deny_tenant_not_target.json @@ -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": {} +} diff --git a/examples/tenant-engine/check_request_deny_unknown_subject.json b/examples/tenant-engine/check_request_deny_unknown_subject.json index e711730..a5e368a 100644 --- a/examples/tenant-engine/check_request_deny_unknown_subject.json +++ b/examples/tenant-engine/check_request_deny_unknown_subject.json @@ -7,7 +7,7 @@ }, "action": "tenant.create", "resource": { - "id": "t-1", + "id": "tenant:friendly:binky", "type": "tenant", "system": "tenant-engine" }, diff --git a/examples/tenant-engine/policy_fixtures.yaml b/examples/tenant-engine/policy_fixtures.yaml index f686e55..4e3c7f9 100644 --- a/examples/tenant-engine/policy_fixtures.yaml +++ b/examples/tenant-engine/policy_fixtures.yaml @@ -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" + } } ] diff --git a/examples/tenant-engine/policy_package.md b/examples/tenant-engine/policy_package.md index e5a8b39..e287e2e 100644 --- a/examples/tenant-engine/policy_package.md +++ b/examples/tenant-engine/policy_package.md @@ -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"} + } } ``` diff --git a/workplans/FLEX-WP-0020-repository-identity-migration.md b/workplans/FLEX-WP-0020-repository-identity-migration.md index b687950..8e85a4c 100644 --- a/workplans/FLEX-WP-0020-repository-identity-migration.md +++ b/workplans/FLEX-WP-0020-repository-identity-migration.md @@ -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 | diff --git a/workplans/FLEX-WP-0022-tenant-scope-coverage.md b/workplans/FLEX-WP-0022-tenant-scope-coverage.md index ee292d4..c7e5f9e 100644 --- a/workplans/FLEX-WP-0022-tenant-scope-coverage.md +++ b/workplans/FLEX-WP-0022-tenant-scope-coverage.md @@ -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 diff --git a/workplans/FLEX-WP-0030-boundary-declaration-cleanup.md b/workplans/FLEX-WP-0030-boundary-declaration-cleanup.md index fdc8289..5cce861 100644 --- a/workplans/FLEX-WP-0030-boundary-declaration-cleanup.md +++ b/workplans/FLEX-WP-0030-boundary-declaration-cleanup.md @@ -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 T06–T08. +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