diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index f097acc..86ae657 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -16,8 +16,7 @@ | workplan | FLEX-WP-0006 | finished | — | workplans/FLEX-WP-0006-ops-warden-ssh-signing-policy-gate.md | | workplan | FLEX-WP-0007 | finished | — | workplans/FLEX-WP-0007-ops-warden-policy-gate-production-deployment.md | | workplan | FLEX-WP-0008 | finished | — | workplans/FLEX-WP-0008-tenant-engine-consumer-integration.md | -| workplan | FLEX-WP-0009 | active | — | workplans/FLEX-WP-0009-user-engine-production-policy-service.md | -| workplan | FLEX-WP-0010 | finished | — | workplans/FLEX-WP-0010-tenant-lifecycle-policy-actions.md | +| workplan | FLEX-WP-0009 | ready | — | workplans/FLEX-WP-0009-user-engine-production-policy-service.md | | task | FLEX-WP-0001-T001 | done | — | workplans/FLEX-WP-0001-repo-intent-and-architecture-baseline.md | | task | FLEX-WP-0001-T002 | done | — | workplans/FLEX-WP-0001-repo-intent-and-architecture-baseline.md | | task | FLEX-WP-0001-T003 | done | — | workplans/FLEX-WP-0001-repo-intent-and-architecture-baseline.md | @@ -62,11 +61,7 @@ | task | FLEX-WP-0008-T02 | done | — | workplans/FLEX-WP-0008-tenant-engine-consumer-integration.md | | task | FLEX-WP-0008-T03 | done | — | workplans/FLEX-WP-0008-tenant-engine-consumer-integration.md | | task | FLEX-WP-0008-T04 | done | — | workplans/FLEX-WP-0008-tenant-engine-consumer-integration.md | -| task | FLEX-WP-0009-T01 | done | — | workplans/FLEX-WP-0009-user-engine-production-policy-service.md | -| task | FLEX-WP-0009-T02 | done | — | workplans/FLEX-WP-0009-user-engine-production-policy-service.md | -| task | FLEX-WP-0009-T03 | done | — | workplans/FLEX-WP-0009-user-engine-production-policy-service.md | -| task | FLEX-WP-0009-T04 | progress | — | workplans/FLEX-WP-0009-user-engine-production-policy-service.md | -| task | FLEX-WP-0010-T01 | done | — | workplans/FLEX-WP-0010-tenant-lifecycle-policy-actions.md | -| task | FLEX-WP-0010-T02 | done | — | workplans/FLEX-WP-0010-tenant-lifecycle-policy-actions.md | -| task | FLEX-WP-0010-T03 | done | — | workplans/FLEX-WP-0010-tenant-lifecycle-policy-actions.md | -| task | FLEX-WP-0010-T04 | done | — | workplans/FLEX-WP-0010-tenant-lifecycle-policy-actions.md | +| task | FLEX-WP-0009-T01 | todo | — | workplans/FLEX-WP-0009-user-engine-production-policy-service.md | +| task | FLEX-WP-0009-T02 | todo | — | workplans/FLEX-WP-0009-user-engine-production-policy-service.md | +| task | FLEX-WP-0009-T03 | todo | — | workplans/FLEX-WP-0009-user-engine-production-policy-service.md | +| task | FLEX-WP-0009-T04 | todo | — | workplans/FLEX-WP-0009-user-engine-production-policy-service.md | diff --git a/docs/tenant-engine-action-vocabulary.md b/docs/tenant-engine-action-vocabulary.md index 0b35bea..2882f8e 100644 --- a/docs/tenant-engine-action-vocabulary.md +++ b/docs/tenant-engine-action-vocabulary.md @@ -12,19 +12,8 @@ repos, not independently invented. | `tenant.role.grant` | `POST /tenants/{id}/roles/grant` | `role-grant` | `allow`, `deny` | | `tenant.role.revoke` | `POST /tenants/{id}/roles/revoke` | `role-grant` | `allow`, `deny` | | `tenant.plan.assign` | `POST /tenants/{id}/plan` | `plan-assignment` | `allow`, `deny` | -| `tenant.update` | `PATCH /tenants/{id}` | `tenant` | `allow`, `deny` | -| `tenant.retire` | `POST /tenants/{id}/retire` | `tenant` | `allow`, `deny` | -| `tenant.reactivate` | `POST /tenants/{id}/reactivate` | `tenant` | `allow`, `deny` | -The last three are the tenant lifecycle actions added by `FLEX-WP-0010` for -`TEN-WP-0005`. They are three actions rather than one `tenant.write` because -tenant-engine separated them so policy *can* permit renaming a tenant without -thereby permitting retiring it. Under the current single-operator model all -three resolve identically — the separation is a seam for later, and the -reasoning for not yet treating `tenant.retire` more strictly is recorded in -`examples/tenant-engine/policy_package.md`. - -These are the only seven actions `tenant-engine` sends to `POST /v1/check` — +These are the only four actions `tenant-engine` sends to `POST /v1/check` — its `FlexAuthCheckClient` (`TEN-WP-0003`) never sends anything else, and `tenant-engine`'s reads (`/roles`, `/roles/live`) are never gated through flex-auth at all; they're `tenant-engine`'s own cache-read and live-lookup @@ -35,10 +24,7 @@ endpoints, consumed *by* `key-cape` and `flex-auth` respectively. `tenant-engine`'s capability roles (`PLTF`/`IAM`/`VEN`/`CUS`, ADR-0014) are **tenant** state — what a tenant is allowed to do on the platform. This vocabulary governs a different question: which **operator/service -subjects** are allowed to call `tenant-engine`'s admin API at all. The -lifecycle actions do not change that boundary: `tenant.retire` asks whether -*this caller* may retire a tenant, not what a *retired tenant* may still do — -the latter is tenant state that tenant-engine enforces itself. The two +subjects** are allowed to call `tenant-engine`'s admin API at all. The two must not be conflated — a policy package that checked a *caller's* action against a *tenant's* capability roles would be checking the wrong thing. See `examples/tenant-engine/policy_package.md`'s Rules section for how the diff --git a/examples/tenant-engine/README.md b/examples/tenant-engine/README.md index 7a4b43c..db0a493 100644 --- a/examples/tenant-engine/README.md +++ b/examples/tenant-engine/README.md @@ -8,12 +8,12 @@ protected-system consumer, gating its own write API | File | Purpose | | --- | --- | -| `protected_system_manifest.yaml` | Resource types (`tenant`, `role-grant`, `plan-assignment`) and the seven actions: `tenant.create`, `tenant.role.grant`, `tenant.role.revoke`, `tenant.plan.assign`, plus the `FLEX-WP-0010` lifecycle actions `tenant.update`, `tenant.retire`, `tenant.reactivate` | +| `protected_system_manifest.yaml` | Resource types (`tenant`, `role-grant`, `plan-assignment`) and actions (`tenant.create`, `tenant.role.grant`, `tenant.role.revoke`, `tenant.plan.assign`) | | `subject_manifest.yaml` | The one registered caller: `tenant-engine`'s own service identity | | `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 | | `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` | Standalone example requests for `flex-auth check` | +| `check_request_allow_create.json`, `check_request_deny_unknown_subject.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. @@ -49,37 +49,6 @@ bin/flex-auth serve -addr 127.0.0.1:9098 \ TENANT_ENGINE_FLEX_AUTH_URL=http://127.0.0.1:9098 make run ``` -## Verified — lifecycle actions (FLEX-WP-0010-T03, 2026-08-10) - -`test-policy` reports **11/11 Rego tests and 16/16 fixtures passing**, the -registry loads, and `go test ./...` / `gofmt` / `go vet` are clean. - -End-to-end over real HTTP: a live `flex-auth serve` on `127.0.0.1:9098` -loaded with this registry and policy, and a real `tenant-engine` -(`TENANT_ENGINE_FLEX_AUTH_URL=http://127.0.0.1:9098`) driven through its -unmodified `FlexAuthWriteAuthorizer` with `Idempotency-Key` and an `If-Match` -echoed from a prior `GET`: - -| Call | tenant-engine | flex-auth decision | -| --- | --- | --- | -| `POST /tenants` | 201 | `decision:174dc9ecb03ed9e5` allow `write_api_policy_matched` | -| `PATCH /tenants/t-e2e-1` | 200 `active` | `decision:6176c39c2f4d7b15` allow `write_api_policy_matched` | -| `POST /tenants/t-e2e-1/retire` | 200 `retired` | `decision:8a801b8ee8455080` allow `write_api_policy_matched` | -| `POST /tenants/t-e2e-1/reactivate` | 200 `active` | `decision:c64cf3713cecd970` allow `write_api_policy_matched` | -| `POST .../retire` as `actor: ops` | 403 `write_denied` | `decision:59d3e99c6416be89` deny `unknown_subject` | - -The misspelled-action guard (`tenant.retired` → deny `unknown_action`) is -covered by fixture and by -`check -request check_request_deny_misspelled_lifecycle.json`; it cannot be -driven through the client, which only ever emits the seven registered strings -— which is the property the guard exists to protect. - -**Not yet in production.** The policy package is baked into the container -image, so the deployed `flex-auth-tenant-engine` Deployment still serves the -pre-FLEX-WP-0010 four-action policy — probed 2026-08-10, `tenant.retire` -returns deny `unknown_action` there. A new image built from this commit and -rolled out is the remaining step before a live lifecycle mutation succeeds. - ## Related - `docs/tenant-engine-resource-namespace.md` diff --git a/examples/tenant-engine/check_request_allow_retire.json b/examples/tenant-engine/check_request_allow_retire.json deleted file mode 100644 index af7fca8..0000000 --- a/examples/tenant-engine/check_request_allow_retire.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "id": "check:tenant-engine-retire-t1", - "tenant": "tenant:friendly:binky", - "subject": { - "id": "tenant-engine", - "type": "service" - }, - "action": "tenant.retire", - "resource": { - "id": "t-1", - "type": "tenant", - "system": "tenant-engine" - }, - "context": {} -} diff --git a/examples/tenant-engine/check_request_deny_misspelled_lifecycle.json b/examples/tenant-engine/check_request_deny_misspelled_lifecycle.json deleted file mode 100644 index 988fa63..0000000 --- a/examples/tenant-engine/check_request_deny_misspelled_lifecycle.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "id": "check:tenant-engine-retired-t1", - "tenant": "tenant:friendly:binky", - "subject": { - "id": "tenant-engine", - "type": "service" - }, - "action": "tenant.retired", - "resource": { - "id": "t-1", - "type": "tenant", - "system": "tenant-engine" - }, - "context": {} -} diff --git a/examples/tenant-engine/policy_fixtures.yaml b/examples/tenant-engine/policy_fixtures.yaml index 8cc18ac..37b49c4 100644 --- a/examples/tenant-engine/policy_fixtures.yaml +++ b/examples/tenant-engine/policy_fixtures.yaml @@ -47,102 +47,6 @@ }, "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"}, - "action": "tenant.update", - "resource": {"id": "t-1", "type": "tenant", "system": "tenant-engine"}, - "context": {} - }, - "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"}, - "action": "tenant.retire", - "resource": {"id": "t-1", "type": "tenant", "system": "tenant-engine"}, - "context": {} - }, - "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"}, - "action": "tenant.reactivate", - "resource": {"id": "t-1", "type": "tenant", "system": "tenant-engine"}, - "context": {} - }, - "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"}, - "action": "tenant.update", - "resource": {"id": "t-1", "type": "tenant", "system": "tenant-engine"}, - "context": {} - }, - "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"}, - "action": "tenant.retire", - "resource": {"id": "t-1", "type": "tenant", "system": "tenant-engine"}, - "context": {} - }, - "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"}, - "action": "tenant.reactivate", - "resource": {"id": "t-1", "type": "tenant", "system": "tenant-engine"}, - "context": {} - }, - "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"}, - "action": "tenant.retired", - "resource": {"id": "t-1", "type": "tenant", "system": "tenant-engine"}, - "context": {} - }, - "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"}, - "action": "tenant_update", - "resource": {"id": "t-1", "type": "tenant", "system": "tenant-engine"}, - "context": {} - }, - "expect": {"effect": "deny", "reason": "unknown_action"} - }, { "id": "fixture:tenant-engine-unknown-subject-deny", "request": { diff --git a/examples/tenant-engine/policy_package.md b/examples/tenant-engine/policy_package.md index 99e10c1..0516467 100644 --- a/examples/tenant-engine/policy_package.md +++ b/examples/tenant-engine/policy_package.md @@ -10,9 +10,6 @@ actions: - tenant.role.grant - tenant.role.revoke - tenant.plan.assign - - tenant.update - - tenant.retire - - tenant.reactivate owner: team:platform-security fixtures: - policy_fixtures.yaml @@ -36,9 +33,6 @@ caring: - Grant - Revoke - Bind - - EditAny - - Archive - - Restore - Audit exposure_modes: - Metadata @@ -67,53 +61,6 @@ tenant state a *different* protected system's policy might consult via `tenant-engine`'s live-lookup endpoint (`FLEX-WP-0008-T03`); conflating the two would authorize the wrong thing. -## Lifecycle actions (FLEX-WP-0010) - -`tenant.update`, `tenant.retire`, and `tenant.reactivate` gate -`tenant-engine`'s lifecycle endpoints (`PATCH /tenants/{id}`, -`POST /tenants/{id}/retire`, `POST /tenants/{id}/reactivate`). The action -strings are the exact values `authz.FlexAuthWriteAuthorizer` sends; they are -coordinated between the two repos, not independently chosen here. - -They are three actions rather than one `tenant.write` because tenant-engine -deliberately separated them so policy *can* one day permit renaming a tenant -without permitting retiring it. Under the current model that separation is a -seam, not a difference — see the decision below. - -### Decision: `tenant.retire` does not require stricter authorization yet - -**Decision (FLEX-WP-0010-T02, 2026-08-10): no.** `tenant.retire` is -authorized by the same rule as the other six actions. - -Reasoning: - -1. **There is nothing stricter to check.** The subject set is exactly one - service identity (`tenant-engine`), there is no second operator to - distinguish from, and requests carry no `assurance` claim — the - `CheckRequest` shape from FLEX-WP-0008 has no field that could - differentiate retirement from an update. A "stricter" rule today could - only be stricter in name. -2. **A rule that cannot fail is worse than no rule.** Adding a condition - that the single known caller always satisfies would read, to a later - reviewer, as though retirement were separately controlled when it is - not. That is a false assurance in a policy package whose job is to be - inspectable. -3. **The blast radius is recoverable.** Retirement is reversible by design - via `tenant.reactivate`, and tenant-engine hard-deletes nothing. A - wrongly allowed retirement suspends new grants and plan changes until - reactivated; it does not destroy tenant state. -4. **The seam is already cut.** Because the three actions are distinct - strings in `valid_actions`, tightening `tenant.retire` later is an - additive change to this package — no consumer change, no request-shape - change, no coordination round with tenant-engine. - -**Revisit when** `KEY-WP-0005` gives callers an `assurance`-bearing identity, -**or** when a second operator subject is registered against -`system: "tenant-engine"` — whichever comes first. At that point the honest -options are an assurance floor on `tenant.retire` or a distinct -`group:tenant-engine-lifecycle` membership requirement; both are one rule in -`allowed` plus a `first_denial` branch. - ## Rules ```rego @@ -126,9 +73,6 @@ valid_actions := { "tenant.role.grant", "tenant.role.revoke", "tenant.plan.assign", - "tenant.update", - "tenant.retire", - "tenant.reactivate", } known_operators := {"tenant-engine"} @@ -187,46 +131,6 @@ test_role_grant_allowed if { } } -test_tenant_update_allowed if { - write_api.decision.effect == "allow" with input as { - "subject": {"id": "tenant-engine", "type": "service"}, - "action": "tenant.update", - "resource": {"id": "t-1", "type": "tenant", "system": "tenant-engine"} - } -} - -test_tenant_retire_allowed if { - write_api.decision.effect == "allow" with input as { - "subject": {"id": "tenant-engine", "type": "service"}, - "action": "tenant.retire", - "resource": {"id": "t-1", "type": "tenant", "system": "tenant-engine"} - } -} - -test_tenant_reactivate_allowed if { - write_api.decision.effect == "allow" with input as { - "subject": {"id": "tenant-engine", "type": "service"}, - "action": "tenant.reactivate", - "resource": {"id": "t-1", "type": "tenant", "system": "tenant-engine"} - } -} - -test_misspelled_lifecycle_action_denied if { - write_api.decision.reason == "unknown_action" with input as { - "subject": {"id": "tenant-engine", "type": "service"}, - "action": "tenant.retired", - "resource": {"id": "t-1", "type": "tenant", "system": "tenant-engine"} - } -} - -test_unknown_subject_retire_denied if { - write_api.decision.reason == "unknown_subject" with input as { - "subject": {"id": "some-other-service", "type": "service"}, - "action": "tenant.retire", - "resource": {"id": "t-1", "type": "tenant", "system": "tenant-engine"} - } -} - test_unknown_subject_denied if { write_api.decision.reason == "unknown_subject" with input as { "subject": {"id": "some-other-service", "type": "service"}, diff --git a/examples/tenant-engine/protected_system_manifest.yaml b/examples/tenant-engine/protected_system_manifest.yaml index 6e61a37..c528ed0 100644 --- a/examples/tenant-engine/protected_system_manifest.yaml +++ b/examples/tenant-engine/protected_system_manifest.yaml @@ -70,41 +70,6 @@ actions: - Metadata metadata: required_context: [] - - name: tenant.update - capabilities: - - EditAny - - Audit - planes: - - Identity - - Audit - exposure_modes: - - Metadata - metadata: - required_context: [] - - name: tenant.retire - capabilities: - - Archive - - Audit - planes: - - Identity - - Policy - - Audit - exposure_modes: - - Metadata - metadata: - required_context: [] - - name: tenant.reactivate - capabilities: - - Restore - - Audit - planes: - - Identity - - Policy - - Audit - exposure_modes: - - Metadata - metadata: - required_context: [] caring_profiles: - caring-0.4.0-rc2 metadata: diff --git a/examples/tenant-engine/registry_snapshot.json b/examples/tenant-engine/registry_snapshot.json index fb5033a..e4ce370 100644 --- a/examples/tenant-engine/registry_snapshot.json +++ b/examples/tenant-engine/registry_snapshot.json @@ -109,59 +109,6 @@ "metadata": { "required_context": [] } - }, - { - "name": "tenant.update", - "capabilities": [ - "EditAny", - "Audit" - ], - "planes": [ - "Identity", - "Audit" - ], - "exposure_modes": [ - "Metadata" - ], - "metadata": { - "required_context": [] - } - }, - { - "name": "tenant.retire", - "capabilities": [ - "Archive", - "Audit" - ], - "planes": [ - "Identity", - "Policy", - "Audit" - ], - "exposure_modes": [ - "Metadata" - ], - "metadata": { - "required_context": [] - } - }, - { - "name": "tenant.reactivate", - "capabilities": [ - "Restore", - "Audit" - ], - "planes": [ - "Identity", - "Policy", - "Audit" - ], - "exposure_modes": [ - "Metadata" - ], - "metadata": { - "required_context": [] - } } ], "caring_profiles": [ @@ -194,7 +141,7 @@ ], "tenant": "tenant:platform", "metadata": { - "description": "tenant-engine's own service identity, used for the seven write-API actions it sends to POST /v1/check (authz.FlexAuthWriteAuthorizer): tenant.create, tenant.role.grant, tenant.role.revoke, tenant.plan.assign, and the lifecycle actions tenant.update, tenant.retire, tenant.reactivate." + "description": "tenant-engine's own service identity, used for the tenant.create / tenant.role.grant / tenant.role.revoke / tenant.plan.assign actions it sends to POST /v1/check (authz.FlexAuthWriteAuthorizer)." } } ], diff --git a/examples/tenant-engine/subject_manifest.yaml b/examples/tenant-engine/subject_manifest.yaml index a6f997f..1b8b216 100644 --- a/examples/tenant-engine/subject_manifest.yaml +++ b/examples/tenant-engine/subject_manifest.yaml @@ -14,11 +14,9 @@ subjects: tenant: tenant:platform metadata: description: >- - tenant-engine's own service identity, used for the seven write-API - actions it sends to POST /v1/check - (authz.FlexAuthWriteAuthorizer): tenant.create, tenant.role.grant, - tenant.role.revoke, tenant.plan.assign, and the lifecycle actions - tenant.update, tenant.retire, tenant.reactivate. + tenant-engine's own service identity, used for the tenant.create / + tenant.role.grant / tenant.role.revoke / tenant.plan.assign actions + it sends to POST /v1/check (authz.FlexAuthWriteAuthorizer). groups: - id: group:tenant-engine-writers display_name: tenant-engine Write API Callers diff --git a/workplans/FLEX-WP-0009-user-engine-production-policy-service.md b/workplans/FLEX-WP-0009-user-engine-production-policy-service.md index dbf7cdf..3782eba 100644 --- a/workplans/FLEX-WP-0009-user-engine-production-policy-service.md +++ b/workplans/FLEX-WP-0009-user-engine-production-policy-service.md @@ -103,22 +103,3 @@ availability bypass and the deployed failure matrix remains fail closed. with decision `decision:4bf95ebb989ac628`; the cross-tenant variant returned deny/cross_tenant with decision `decision:bab072ce3ee72d98`. Runtime activation remains gated on the separately owned event and mail receivers. - -2026-08-10 re-probe: all six policy fixtures were replayed against the -**deployed** `flex-auth-user-engine.flex-auth.svc.cluster.local:8080` and -every one matched its expected effect and reason — -`self-allow` `decision:27cbab2e5533508a` allow/`self_service`; -`tenant-admin-allow` `decision:350b46fb42989606` allow/`tenant_admin`; -`platform-allow` `decision:79334537419466bd` allow/`platform_operator`; -`cross-tenant-deny` `decision:b3cc0b7e819aecde` deny/`cross_tenant`; -`missing-role-deny` `decision:818e2249491ef09f` deny/`no_matching_role_or_context`; -`wrong-system-deny` `decision:5e20697e202aa7c6` deny/`wrong_system`. -The deployed failure matrix is therefore confirmed still fail-closed 42h -into the current rollout. - -**Still open, and not flex-auth's to close:** the done-criterion is that -*user-engine* replaces its local authorization bridge without an -availability bypass. That cutover is user-engine's decision and remains -gated on the separately owned event and mail receivers. flex-auth has -delivered and re-verified the service side; T04 stays `progress` rather -than being marked done on evidence that only covers half the criterion. diff --git a/workplans/FLEX-WP-0010-tenant-lifecycle-policy-actions.md b/workplans/FLEX-WP-0010-tenant-lifecycle-policy-actions.md index a937903..b6102f7 100644 --- a/workplans/FLEX-WP-0010-tenant-lifecycle-policy-actions.md +++ b/workplans/FLEX-WP-0010-tenant-lifecycle-policy-actions.md @@ -4,7 +4,7 @@ type: workplan title: "Authorize tenant-engine lifecycle actions" domain: infotech repo: flex-auth -status: finished +status: ready owner: codex topic_slug: netkingdom planning_priority: P1 @@ -16,7 +16,6 @@ related_workplans: - USER-WP-0021 created: "2026-08-10" updated: "2026-08-10" -state_hub_workstream_id: "c159fe8b-d35b-4a74-8a15-c1263f7f6392" --- # FLEX-WP-0010 - Authorize tenant-engine lifecycle actions @@ -69,9 +68,8 @@ yet, and here is why". ```task id: FLEX-WP-0010-T01 -status: done +status: todo priority: high -state_hub_task_id: "e92e45b9-724a-4fbd-8302-75558cf4b6c1" ``` Add `tenant.update`, `tenant.retire`, and `tenant.reactivate` to @@ -95,24 +93,12 @@ Rebuild `examples/tenant-engine/registry_snapshot.json`. Done when `valid_actions` carries all seven actions and the vocabulary doc and subject manifest no longer describe only four. -Done 2026-08-10: all seven actions are in `valid_actions`, the frontmatter -`actions:` list, and the protected-system manifest. CARING capabilities were -extended with `EditAny` (update), `Archive` (retire), and `Restore` -(reactivate) — the existing `Create`/`Grant`/`Revoke`/`Bind` set had no -update or lifecycle shape, and `Archive`/`Restore` name the reversible pair -exactly as tenant-engine implements it. `registry_snapshot.json` was rebuilt -from the two manifests, the subject metadata now enumerates all seven, and -`docs/tenant-engine-action-vocabulary.md` carries the three HTTP surfaces -plus an explicit note that `tenant.retire` asks whether *this caller* may -retire a tenant, not what a *retired tenant* may do. - ## T02 - Decide whether retirement warrants stricter authorization ```task id: FLEX-WP-0010-T02 -status: done +status: todo priority: medium -state_hub_task_id: "5aa9e104-a4b3-40cd-b5ad-4ff56421ce69" ``` `tenant.retire` is the highest-consequence action in the set: it suspends a @@ -133,25 +119,12 @@ allowed retirement is recoverable, which is legitimate input to this decision. Done when the decision and its reasoning are recorded in `policy_package.md`, and implemented if the answer is "yes, stricter". -Done 2026-08-10: **no — `tenant.retire` is authorized by the same rule as the -other six.** Recorded with full reasoning under "Decision: `tenant.retire` -does not require stricter authorization yet" in -`examples/tenant-engine/policy_package.md`. The load-bearing reason is that -there is nothing stricter to check: one service subject, no second operator, -no `assurance` field in the `CheckRequest`. A condition the sole caller -always satisfies would read to a later reviewer as though retirement were -separately controlled when it is not — a false assurance in a package whose -job is inspectability. Retirement being reversible and non-destructive makes -that acceptable. Revisit when `KEY-WP-0005` supplies an assurance-bearing -identity, or when a second `tenant-engine` operator subject is registered. - ## T03 - Fixtures and verification ```task id: FLEX-WP-0010-T03 -status: done +status: todo priority: high -state_hub_task_id: "59856c87-a1b4-4fce-b554-9b13181bb8fd" ``` Add `allow`/`deny` fixture pairs to @@ -178,66 +151,15 @@ Done when all three lifecycle mutations return a real `allow` from the real Rego engine over real HTTP, `go test ./...` is green across the repo, and `gofmt`/`go vet` are clean. -Done 2026-08-10: eight fixture pairs added (allow for each of the three -actions, `unknown_subject` deny for each, and two action-drift denies — -`tenant.retired` and `tenant_update`, both `unknown_action`). Five embedded -Rego tests added. `test-policy` reports 11/11 tests and 16/16 fixtures -passing; `load-registry` loads clean; `check` returns the expected decisions -for the two new standalone request files. - -End-to-end against a live `flex-auth serve` (127.0.0.1:9098) with a real -`tenant-engine` on `TENANT_ENGINE_FLEX_AUTH_URL`, driven through the -unmodified `FlexAuthWriteAuthorizer` with `Idempotency-Key` and an `If-Match` -echoed from a prior `GET`: - -| Call | tenant-engine | flex-auth decision | -| --- | --- | --- | -| `POST /tenants` | 201 | `decision:174dc9ecb03ed9e5` allow | -| `PATCH /tenants/t-e2e-1` | 200 `active` | `decision:6176c39c2f4d7b15` allow | -| `POST .../retire` | 200 `retired` | `decision:8a801b8ee8455080` allow | -| `POST .../reactivate` | 200 `active` | `decision:c64cf3713cecd970` allow | -| `POST .../retire` as `actor: ops` | 403 `write_denied` | `decision:59d3e99c6416be89` deny `unknown_subject` | - -`go test ./...` green across all packages; `gofmt -l` empty; `go vet ./...` -clean. Evidence table mirrored into `examples/tenant-engine/README.md`. - ## T04 - Closure and handoff ```task id: FLEX-WP-0010-T04 -status: done +status: todo priority: low -state_hub_task_id: "04141e54-a250-424e-8dfd-4148875e67da" ``` Confirm T01–T03. Run `statehub fix-consistency`. Notify `tenant-engine` that `TEN-WP-0005-T05` is unblocked, naming the policy revision — T05's own done-criteria require the handoff to name the authorization policy revision alongside the immutable image and API version. - -Done 2026-08-10: T01–T03 confirmed. Policy revision for the handoff is -package `tenant-engine.write-api.mutate` **version `v1`**, status `ready`, -profile `caring-0.4.0-rc2`, now carrying all seven actions — the version -string is unchanged because the package is additive and `v1` remains the -activated revision; the identifying fact for TEN-WP-0005-T05 is that `v1` as -of this commit contains `tenant.update`, `tenant.retire`, and -`tenant.reactivate`. - -**Production is not yet serving this policy — stated plainly because -TEN-WP-0005-T05 depends on it.** The policy package is baked into the -flex-auth container image, not mounted from a ConfigMap: the deployed -`flex-auth-tenant-engine` Deployment in namespace `flex-auth` runs image -`@sha256:c25fc34a6cd7e64d955f8723ec70e176a583d5ae71d76280c4e2d89fba0fe0aa` -with `--policy /opt/flex-auth/examples/tenant-engine/policy_package.md` -from that image. Probed live on 2026-08-10 via port-forward: -`tenant.retire` still returns **deny `unknown_action`**, while -`tenant.create` returns allow — i.e. the running instance is the -pre-FLEX-WP-0010 four-action policy. - -So `TEN-WP-0005-T05` is unblocked **in source** but not **in the cluster** -until a new image is built from commit `8e127e5` and rolled out. That -rollout is a production deploy that this workplan does not scope (contrast -FLEX-WP-0007 and FLEX-WP-0009-T03, which carried explicit deployment -tasks) and it is deliberately left for an owner decision rather than done -implicitly under a policy-authoring workplan. It is the single remaining -step between this commit and a live lifecycle mutation.