State tenant-engine's tenant relation in the write-api package, v3.
tenant-engine named the relation in TEN-DEC-2026-002: `tenant` is the target tenant record and always equals `resource.id`; the write API is cross-tenant by design and `tenant.guardrail.read` does not differ. v2 carried no tenant rule and a constant fixture tenant, so the deliberate scope and an omitted rule were indistinguishable. tenant-engine.write-api.mutate v3 (FLEX-DEC-2026-016): - allowed requires tenant_is_target; a mismatch or absent tenant is denied tenant_not_target (object.get, so an absent key names the right cause). - the cross-tenant scope is stated in the package and quantified by test_tenant_never_changes_effect over every action, three subjects and four tenants, with guards against passing by denying everything. - fixtures rotate tenant across four tenants; five cross-tenant allows and two tenant_not_target denies added (42 fixtures, 33 tests, all pass). - user-engine's tenant:platform exclusion is named as a fixed-record rule, not a subject/tenant relation, and tested separately. Closes FLEX-WP-0022 (T01, T02 done). Also records TEN-IN-0004 and SECRETS-IN-0002 on FLEX-WP-0020 and acknowledges the GH-DEC-2026-017 replies on FLEX-WP-0030-T04. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 63291@bnt-lap001 Assistant-Session: 8bd77868-ca68-4f49-bb1e-d539ecc0d703
This commit is contained in:
parent
bd3d270531
commit
3081067325
17 changed files with 783 additions and 150 deletions
|
|
@ -1891,3 +1891,56 @@ applies there identically: the repository rename answers nothing about it.
|
||||||
**Reversal condition.** Reverses only by a runtime-rename decision that
|
**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.
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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": {}
|
||||||
|
}
|
||||||
|
|
@ -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"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -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"}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -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 |
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 T06–T08.
|
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
|
## 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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue