flex-auth/workplans/FLEX-WP-0008-tenant-engine-consumer-integration.md
tegwick 49de2f40ff
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 3s
FLEX-WP-0008 T01-T02: tenant-engine resource/action vocabulary + policy package
docs/tenant-engine-{resource-namespace,action-vocabulary}.md: four actions
(tenant.create, tenant.role.grant, tenant.role.revoke, tenant.plan.assign)
matching tenant_engine/src/tenant_engine/authz.py's _RESOURCE_TYPES mapping
exactly. No resource_manifest.yaml -- tenant-engine's resources are created
dynamically, unlike ops-warden's fixed SSH-certificate inventory; documented
as a deliberate deviation.

examples/tenant-engine/: protected_system_manifest.yaml, subject_manifest.yaml
(one registered caller: tenant-engine's own service identity),
policy_package.md (Rego rules + embedded tests), policy_fixtures.yaml (8
allow/deny pairs), assembled registry_snapshot.json, two standalone
check_request examples, README.

Design decision made explicit in the workplan: operator/service-identity
authorization (one known subject, four known actions), not aal2+assurance +
tenant-capability-role checking -- CheckRequest carries no assurance claim
yet (TEN-WP-0003 already flagged actor as a plain request-body field, not a
real auth context), and a tenant's capability roles are the tenant's own
state, not a property of who may call tenant-engine's admin API. Revisit
once KEY-WP-0005 gives callers a real assurance-bearing identity.

Verified for real: built ./cmd/flex-auth, ran test-policy (6 tests + 8
fixtures, all pass), load-registry, check against both example requests.
Then ran a live flex-auth serve with this exact registry+policy and pointed
tenant-engine's real, unmodified FlexAuthCheckClient/FlexAuthWriteAuthorizer
at it over real HTTP: unknown subject -> 403, tenant-engine -> 201. go test
./... still green across the whole repo.

T03 (live-lookup context adapter for other protected systems) stays open --
new Go adapter package, tracked separately.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 23:05:57 +02:00

174 lines
7.8 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: 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 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`.