flex-auth/workplans/FLEX-WP-0008-tenant-engine-consumer-integration.md
codex 1d90aac30f
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
fix(workplans): adopt ADR-007 derived identifiers for unregistered records
These workplans exist only in the retired local hub. Their random pre-ADR-007
identifiers are refused by C-06 as stale references, so they cannot be
registered. Deriving from the canonical record id takes no identity from
anything: central does not hold them and the old ids die with the cache.

Records central already holds were deliberately left untouched.

Refs CUST-WP-0068-T06

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 2583210@bnt-lap001
Assistant-Session: f2bff2d5-e9b2-4338-92ca-10282a927006
2026-08-25 20:10:35 +02:00

234 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
id: FLEX-WP-0008
type: workplan
title: "tenant-engine Consumer Integration"
domain: infotech
repo: flex-auth
status: finished
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: "1358db95-967c-5a03-8b8c-4816dc106594"
---
# 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: "5606408f-f94c-5d79-ba41-ed23fadac480"
```
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: "bbe1a8f4-dcd9-58bb-8d82-386ee0c11a51"
```
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: done
priority: medium
state_hub_task_id: "e27d24b3-0aef-5bd4-b7e3-81532cd7aa9c"
```
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.
**Done 2026-07-23, one architectural finding recorded first:** confirmed by
reading `internal/decision/engine.go` that `Check()` has no context-adapter
hook at all, and confirmed via `cmd/flex-auth/main.go` that none of the
existing `topaz`/`relationship`/`rule` adapters are wired into the shipped
binary either — they're standalone Go packages for downstream composition,
not auto-invoked plugins. This adapter follows the same shape: Rego
evaluation is stateless and can't make an HTTP call mid-evaluation, so
`internal/adapters/tenantengine` is a **request-preparation helper** a
protected system's own request-building code calls before submitting to
`POST /v1/check` — not an engine-internal hook.
`HTTPClient.LiveRoles(ctx, tenantID) (LiveRolesResult, error)` calls
`GET {base}/tenants/{id}/roles/live`. `LiveRolesResult.Available` is the
load-bearing field: `false` on *any* failure (transport error, non-200,
malformed body) — never inferred as zero roles.
`AttachToContext(context, result)` writes `tenant_roles` **and**
`tenant_roles_available` into a `CheckRequest.Context` map; the
`Available: false` / `tenant_roles_available: false` pairing is what a
consuming policy must check before trusting `tenant_roles` at all —
documented in the package doc comment as the required Rego precondition.
9 Go tests (`http_client_test.go`, `context_test.go`): success, non-200,
malformed body, connection failure, context-timeout, empty base URL
rejected, context attachment (including nil-context and
non-clobbering-existing-fields cases). `gofmt`/`go vet`/`go build ./...`
clean; `go test ./...` still green across the whole repo.
**Verified as a real three-service chain, not a mock:** ran a live
`flex-auth serve` (T02's registry+policy) and a live `tenant-engine`
pointed at it (`TENANT_ENGINE_FLEX_AUTH_URL`), created a tenant and granted
it a `CUS` role through the real, `flex-auth`-gated write path, then called
this new Go adapter (via a throwaway `cmd/` harness, removed after use, not
committed) against the running `tenant-engine`: `roles=[CUS]
available=true` for the known tenant, `roles=[] available=false
err="status 404"` for an unknown one — the exact fail-closed distinction
this task exists to guarantee, proven end-to-end across Go → Python → Go
(via HTTP), not asserted in isolation.
## Task: Closure review
```task
id: FLEX-WP-0008-T04
status: done
priority: low
state_hub_task_id: "0f319a2f-e4bb-5ba8-92de-b693ae9a2aa3"
```
Confirm T01T03 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`.
**Closed 2026-07-23.** T01T03 done. `go test ./...` green across the whole
repo (including the new `internal/adapters/tenantengine` package);
`gofmt`/`go vet` clean.
The re-verification this task asked for happened twice, for real: in T02's
closure (a live `flex-auth serve` + `tenant-engine`'s unmodified
`FlexAuthWriteAuthorizer`, `allow``201`) and again in T03's closure (the
same live pair, plus a write through it, plus the new Go adapter reading
the result back) — `FlexAuthWriteAuthorizer` was never re-tested against a
mock standing in for `allow`; every `allow` assertion in this workplan came
from the real Rego engine.
**Full picture after this workplan:** `tenant-engine`'s write path is real
end-to-end (`tenant-engine` → real `flex-auth` → real `allow`/`deny`), and
any *other* protected system can now pull live tenant capability-role
context via `internal/adapters/tenantengine`, fail-closed. What's still
open, unchanged from `TEN-WP-0003`'s own closure: `KEY-WP-0005` (`key-cape`
doesn't emit IAM Profile core claims yet — the cache-read/`tenant_roles`
direction depends on it, not this workplan). This workplan's own T02 policy
(operator/service identity, not `aal2`+role) should be revisited once
`KEY-WP-0005` gives real caller assurance to check.