--- id: FLEX-WP-0008 type: workplan title: "tenant-engine Consumer Integration" domain: infotech repo: flex-auth status: ready owner: codex topic_slug: flex-auth planning_priority: P1 planning_order: 80 depends_on_workplans: - FLEX-WP-0007 related_workplans: - TEN-WP-0003 - KEY-WP-0005 created: "2026-07-23" updated: "2026-07-23" state_hub_workstream_id: "6341d238-fffc-4428-9265-0b6db5714e9b" --- # FLEX-WP-0008: tenant-engine Consumer Integration ## Purpose Make `tenant-engine` a registered protected-system consumer of flex-auth, in both directions its boundary contract requires (`net-kingdom/canon/standards/tenant-engine-boundary-contract_v0.1.md`): 1. **tenant-engine's writes need authorization.** `tenant-engine`'s `FlexAuthWriteAuthorizer` (`TEN-WP-0003`) calls `POST /v1/check` for every mutation (`tenant.create`, `tenant.role.grant`, `tenant.role.revoke`, `tenant.plan.assign`). Until a policy package exists here, every check resolves to `deny`/`not_applicable` — correct fail-closed behavior, but it means `tenant-engine` cannot actually perform any write yet. This workplan is what makes real `allow` decisions reachable. 2. **flex-auth's own decisions need tenant capability roles as input**, for any protected system whose policy depends on a tenant's `PLTF`/`IAM`/ `VEN`/`CUS` roles (ADR-0014) at `aal2`-class decision time. flex-auth must call `tenant-engine`'s live-lookup endpoint (`GET /tenants/{id}/roles/live`) to enrich decision context, the same role a `context` adapter plays for any other external fact source. Closer in shape to `FLEX-WP-0006`/`FLEX-WP-0007` (ops-warden's small, security-lane SSH-certificate integration) than `FLEX-WP-0003` (Markitect's document-heavy, CARING-benchmark integration) — `tenant-engine`'s resource and action vocabulary is small and doesn't need CARING descriptor mapping. ## Task: Define tenant-engine resource and action vocabulary ```task id: FLEX-WP-0008-T01 status: done priority: high state_hub_task_id: "d78361e6-eb8e-4623-bed2-917538c403ca" ``` Resource types: `tenant`, `role-grant`, `plan-assignment`. Actions: `tenant.create`, `tenant.role.grant`, `tenant.role.revoke`, `tenant.plan.assign` — matching `TEN-WP-0003-T02`'s action-name mapping exactly (coordinate values, don't let the two repos invent different strings for the same action). Model on `examples/ops-warden`'s fixture shape (`schemas/check_request.schema.json`'s `resource{id,type,system}`, `system: "tenant-engine"`), not Markitect's. Done when: resource/action vocabulary documented under `docs/` following the `markitect-resource-namespace.md` / `markitect-action-vocabulary.md` naming pattern (`tenant-engine-resource-namespace.md`, `tenant-engine-action-vocabulary.md`). **Done 2026-07-23:** Both docs written. One deliberate deviation from the ops-warden template, documented in `tenant-engine-resource-namespace.md`: no `resource_manifest.yaml` — ops-warden's SSH certificates are a fixed, pre-registrable inventory; `tenant-engine`'s tenants are created dynamically, so the policy evaluates `input` directly rather than looking up a registered resource by id. Action vocabulary cross-checked against `tenant-engine/src/tenant_engine/authz.py`'s actual `_RESOURCE_TYPES` mapping — the four action strings and their resource types match exactly, not just by convention. ## Task: Author and register the tenant-engine policy package ```task id: FLEX-WP-0008-T02 status: done priority: high state_hub_task_id: "8a07e83b-27a2-4390-a169-51065bcf3bd6" ``` Policy: writes require an `aal2`+ assurance actor holding an appropriate capability role for the action (e.g. `tenant.plan.assign` might require `PLTF` or a platform-operator subject type; exact rules are a design decision for this task, not pre-specified here — coordinate with ADR-0014's role semantics, not invented fresh). Register in `registry/` per the existing pattern; add fixtures (`allow`/`deny` request/response pairs) mirroring `examples/ops-warden/check_request_allow_adm.json`. Done when: `POST /v1/check` against a request matching `TEN-WP-0003`'s `FlexAuthCheckClient` shape returns `allow` for an authorized actor/action and `deny` for an unauthorized one, exercised by `main_test.go`-style integration tests. **Done 2026-07-23, with the design decision made explicit:** chose **operator/service-identity authorization**, not `aal2`+assurance + tenant-capability-role checking, for this specific policy. Reasoning, recorded here rather than left implicit: `tenant_engine`'s `CheckRequest` today carries no `assurance` claim at all (there's no authenticated caller identity yet — `TEN-WP-0003`'s closure note already flagged `actor` as a plain request-body field, not a real auth context), and a tenant's capability roles (`PLTF`/`IAM`/`VEN`/`CUS`) are a property of the *tenant* being written to, not of *who is allowed to call tenant-engine's admin API* — checking the wrong one would be a category error, the same one flagged in `docs/tenant-engine-action-vocabulary.md`. So: `examples/tenant-engine/policy_package.md` authorizes exactly one registered service subject (`tenant-engine` itself, `subject_manifest.yaml`) for the four known actions against `system: "tenant-engine"`. Revisit once `KEY-WP-0005` gives callers a real `assurance`-bearing identity to check. Registered: `protected_system_manifest.yaml`, `subject_manifest.yaml`, `policy_package.md` (Rego rules + embedded tests), `policy_fixtures.yaml` (8 allow/deny pairs), assembled `registry_snapshot.json`, two standalone `check_request_*.json` examples, `README.md`. **Verified for real, not just written:** built the actual `flex-auth` binary (`go build ./cmd/flex-auth`) and ran it against every artifact: `test-policy` — all 6 Rego tests and all 8 fixtures pass; `load-registry` loads the snapshot cleanly; `check` against both standalone request files returns the expected `allow`/`deny`. Then went one step further than the task's own done-criteria: ran a **live `flex-auth serve`** with this exact registry+policy and pointed `tenant-engine`'s real, unmodified `FlexAuthCheckClient`/`FlexAuthWriteAuthorizer` at it (`TENANT_ENGINE_FLEX_AUTH_URL`) — `POST /tenants` with `actor="ops"` → `403 unknown_subject`; with `actor="tenant-engine"` → `201`. Full round trip, both processes, real HTTP, real Rego evaluation. `go test ./...` still green across the whole `flex-auth` repo — nothing broken. ## Task: tenant-engine live-lookup context adapter ```task id: FLEX-WP-0008-T03 status: todo priority: medium state_hub_task_id: "20c005a4-48b7-4ac4-a345-aeaa0de06d80" ``` A context-enrichment adapter (mirrors `internal/adapters/{relationship,rule,topaz}`'s shape) that calls `tenant-engine`'s `GET /tenants/{id}/roles/live` and attaches the result to decision context for policies that need it — used by *other* protected systems' policies that key off tenant capability role, not only by tenant-engine's own policy from T02. Must fail closed: adapter error or `tenant-engine` unavailability must not silently produce an empty role list indistinguishable from "no roles granted" (same rule `tenant-engine`'s own read endpoints already enforce — don't weaken it on the consuming side). Done when: a policy package referencing tenant capability role context correctly denies when the adapter call fails, not just when it succeeds with an empty result. ## Task: Closure review ```task id: FLEX-WP-0008-T04 status: todo priority: low state_hub_task_id: "0c59ada6-c61b-41a3-8baa-a98e936c5690" ``` Confirm T01–T03 done; run flex-auth's existing test suite plus the new fixtures. Note in closure: `TEN-WP-0003`'s `FlexAuthWriteAuthorizer` should be re-verified against a real `allow` decision once this workplan's policy package exists (it was only tested against `deny`/`not_applicable` responses when built, since this workplan didn't exist yet). Run `statehub fix-consistency`.