tenant-engine/workplans/TEN-WP-0003-flex-auth-write-authorizer.md
tegwick 5d57c7d488 TEN-WP-0003: FlexAuthWriteAuthorizer -- gate writes through flex-auth
flex_auth.py: CheckRequest + FlexAuthCheckClient against flex-auth's real
POST /v1/check contract (schemas/check_request.schema.json,
decision_envelope.schema.json, read directly from the flex-auth repo, not
guessed). Fail-closed by construction: only effect=="allow" authorizes;
every other effect, non-200, malformed body, or transport failure resolves
to deny, nothing raises past is_allowed().

authz.FlexAuthWriteAuthorizer implements the existing WriteAuthorizer
Protocol. Action -> resource-type mapping coordinated with FLEX-WP-0008's
planned vocabulary (both repos reference the same table).
DefaultDenyWriteAuthorizer stays the fallback when no flex-auth URL is
configured.

config.py: Settings.from_env(), mirroring qonto-assistant's pattern.
docs/flex-auth-integration.md documents the contract, fail-closed rule,
and current real state (denies everything until FLEX-WP-0008 lands).

60 tests passing. Verified live twice over real HTTP between separate
processes (not just MockTransport): a deny-returning flex-auth double
produces 403 from POST /tenants, an allow-returning one produces 201.

Also registered (not implemented) the two workplans this depends on for a
complete picture: flex-auth/FLEX-WP-0008 (protected-system registration --
what makes allow reachable) and key-cape/KEY-WP-0005 (discovered key-cape
emits none of iam-profile_v0.3.md's core claims yet, not just missing
tenant_roles -- a bigger, security-sensitive gap flagged rather than
quietly worked around).

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

203 lines
9.4 KiB
Markdown
Raw Permalink 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: TEN-WP-0003
type: workplan
title: "flex-auth-backed WriteAuthorizer"
domain: infotech
repo: tenant-engine
status: finished
owner: codex
topic_slug: netkingdom
created: "2026-07-23"
updated: "2026-07-23"
state_hub_workstream_id: "9191e4e7-9716-4f9a-8e48-fa52b643b1c1"
---
# flex-auth-backed WriteAuthorizer
Replaces `DefaultDenyWriteAuthorizer` with a real HTTP client against
flex-auth's `POST /v1/check` endpoint — the first of two integrations
needed to close ADR-0014's follow-ups (`key-cape` and `flex-auth` wiring).
This is tenant-engine's own side of that work: a Python HTTP client, fully
implementable and testable here.
**Scope note, discovered while planning this workplan:** the other two
integration directions are each their own dedicated workplan in a different
repo, not small follow-on tasks bundled into this one:
- `key-cape` does not currently emit **any** IAM Profile core claims
(`tenant`, `principal_type`, `groups`, `roles`, `assurance`) — confirmed by
reading `key-cape/src/internal/server/oidc/token.go`, which has no such
claim assembly today. Adding `tenant_roles` (IAM Profile v0.3) is not a
small addition on top of existing tenant support — it's part of bringing
token issuance up to core profile conformance for the first time. That is
`key-cape`'s own, larger workplan — not drafted here, not attempted here
(security-sensitive Go token-issuance code deserves dedicated context, not
a rushed edit from an unfamiliar codebase).
- `flex-auth` has a real, working `POST /v1/check` (verified against
`cmd/flex-auth/main.go` and `schemas/check_request.schema.json` /
`decision_envelope.schema.json`), and ops-warden already uses it in
production (`examples/ops-warden/check_request_allow_adm.json`). But
getting real `allow` decisions for tenant-engine's actions requires
registering tenant-engine as a protected system with a resource/action
vocabulary and an authored policy package — the same shape of work
`FLEX-WP-0003` did for Markitect (its own multi-task workplan, not a
config tweak). That is `flex-auth`'s own workplan too.
This workplan builds tenant-engine's client against the real, documented
`/v1/check` contract regardless of whether a tenant-engine policy package
exists yet in flex-auth — until it does, every check will resolve to
`deny`/`not_applicable`, which is the correct fail-closed behavior, not a
bug to work around.
**Depends on:** `TEN-WP-0002` (done). **Related, not depended on:**
`key-cape`'s claim-conformance workplan and `flex-auth`'s tenant-engine
protected-system workplan — both drafted alongside this one, tracked
separately.
## Task: FlexAuthCheckClient
```task
id: TEN-WP-0003-T01
status: done
priority: high
state_hub_task_id: "54cf7e29-5f21-4310-bc42-9e133b353da4"
```
HTTP client for `POST {base_url}/v1/check`, built against the real request/
response contract:
- Request shape from `flex-auth/schemas/check_request.schema.json`:
`id`, `tenant`, `subject{id,type}`, `action`, `resource{id,type,system}`,
`context`.
- Response shape from `flex-auth/schemas/decision_envelope.schema.json`:
`DecisionEnvelope` with `effect` in `allow | deny | redact | audit_only |
not_applicable`.
- Only `effect == "allow"` counts as authorized. Every other effect,
including a request/connection failure, timeout, or non-2xx response,
must resolve to deny — this client has the same fail-closed obligation
the boundary contract already places on the live-lookup read path.
- Bounded timeout (short — this sits on a synchronous write path), no
retries that could turn a slow deny into a slower deny that looks like a
hang to the caller.
Done when: unit tests cover an `allow` response, every non-allow `effect`
value, a malformed/non-2xx response, and a connection failure/timeout — all
resolving to deny, none raising an unhandled exception past the client
boundary.
**Done 2026-07-23:** `flex_auth.py`'s `CheckRequest` + `FlexAuthCheckClient`,
built against the real `check_request.schema.json`/`decision_envelope.schema.json`
contracts (read directly, not guessed). `httpx.Client` with an injectable
`transport`, mirroring `qonto-assistant`'s `QontoClient` testability pattern.
`tests/test_flex_auth.py` covers `allow`, all four non-allow effects
(parametrized), non-200 status, malformed body, non-object body, connection
error, and timeout — all resolve to `False`, none raise past `is_allowed()`.
Also asserts the outgoing request body actually matches the schema shape,
not just that responses are handled correctly.
## Task: FlexAuthWriteAuthorizer
```task
id: TEN-WP-0003-T02
status: done
priority: high
state_hub_task_id: "aa22bbb1-0df0-47a3-8f86-d654a6ff691e"
```
Implements the existing `WriteAuthorizer` Protocol (`authz.py`) using
`FlexAuthCheckClient`. Maps tenant-engine's write actions
(`tenant.create`, `tenant.role.grant`, `tenant.role.revoke`,
`tenant.plan.assign`) to `CheckRequest.action` values, and `tenant_id` to
`CheckRequest.tenant` and a `resource` reference — following the same
mapping style as `flex-auth/examples/ops-warden`'s fixtures, tenant-engine's
closest real precedent (a small, security-lane protected system, not a
document-heavy one like Markitect).
`DefaultDenyWriteAuthorizer` stays exported and becomes the fallback when no
flex-auth base URL is configured (mirrors `qonto-assistant`'s
`QONTO_FIXTURE_DIR`-gates-real-network-calls pattern) — local/test runs
never need a reachable flex-auth.
Done when: `create_app()` selects `FlexAuthWriteAuthorizer` when a
`TENANT_ENGINE_FLEX_AUTH_URL`-equivalent setting is present, else
`DefaultDenyWriteAuthorizer`; integration test proves a write is denied
against a `flex-auth` double returning `deny`/`not_applicable`, and (if a
local `flex-auth` fixture registry can be stood up without needing the full
protected-system workplan) that an `allow` decision actually permits the
write through the existing `_AllowAllAuthorizer`-style seam.
**Done 2026-07-23:** `authz.FlexAuthWriteAuthorizer` implements
`WriteAuthorizer` using `FlexAuthCheckClient`; action → resource-type
mapping matches `FLEX-WP-0008-T01`'s planned vocabulary exactly (both repos
now reference the same table). `tests/test_authz_flex.py` covers
`create_app()`'s selection logic (no URL → `DefaultDenyWriteAuthorizer`,
URL present → `FlexAuthWriteAuthorizer`), deny on `deny` and
`not_applicable` effects (the realistic state until `FLEX-WP-0008` lands),
allow on `allow`, and a full HTTP-level lifecycle test through
`create_app()`'s own wiring. Went beyond `MockTransport`-level testing:
verified live with a real local HTTP double standing in for flex-auth
(`http.server`, two separate OS processes, real sockets) — `deny``403`,
`allow``201`, both over the wire, not just in-process mocking.
## Task: Config + docs
```task
id: TEN-WP-0003-T03
status: done
priority: medium
state_hub_task_id: "2a2bec93-2408-4597-8d40-84df50f2cc8c"
```
Add `config.py` (tenant-engine doesn't have one yet — `create_app()` takes
constructor args only today) for the flex-auth base URL and timeout,
following `qonto-assistant/src/qonto_assistant/config.py`'s
`Settings.from_env()` pattern. Document the integration in
`docs/flex-auth-integration.md`: what tenant-engine sends, what it expects
back, the fail-closed rule, and a pointer to the still-open flex-auth-side
protected-system registration this client depends on for real `allow`
decisions.
Done when: `make run` picks up `TENANT_ENGINE_FLEX_AUTH_URL` from the
environment; doc exists and is linked from `INTENT.md`/`SCOPE.md`.
**Done 2026-07-23:** `config.py`'s `Settings.from_env()` reads
`TENANT_ENGINE_FLEX_AUTH_URL`/`TENANT_ENGINE_FLEX_AUTH_TIMEOUT_SECONDS`/
`TENANT_ENGINE_HOST`/`TENANT_ENGINE_PORT`, wired through `main.py` and
`app.create_app()`. `docs/flex-auth-integration.md` documents the request/
response contracts, the fail-closed rule, current real state (denies
everything until `FLEX-WP-0008` lands), and configuration — linked from
both `INTENT.md`'s Related section and `SCOPE.md`'s Getting Oriented
section.
## Task: Closure review
```task
id: TEN-WP-0003-T04
status: done
priority: low
state_hub_task_id: "225207ab-6dca-4df6-afec-af83f41bcee1"
```
Confirm T01T03 done; `pytest`/`compileall` clean. Note in closure: this
does not make writes actually succeed against a real deployment yet —
that needs both the `flex-auth` protected-system workplan (policy package
authored, `allow` becomes reachable) and, for the cache-read/live-lookup
direction, `key-cape`'s claim-conformance workplan. Run `statehub
fix-consistency`.
**Closed 2026-07-23.** T01T03 done. `PYTHONPATH=src pytest``60 passed`;
`python -m compileall src tests` clean. Verified live twice, over real
HTTP between separate processes, not just in-process test doubles: a
`deny`-returning flex-auth double produces `403` from `POST /tenants`; an
`allow`-returning one produces `201`.
**Confirmed not done, as expected going in:** no write can succeed against
a real `flex-auth` deployment yet — `FLEX-WP-0008` (registered, `flex-auth`
repo) still needs to author and register tenant-engine's policy package
before `allow` is reachable outside a test double. The cache-read/
`tenant_roles` direction is entirely separate and unaffected by this
workplan — that's `KEY-WP-0005` (registered, `key-cape` repo), which found
`key-cape` doesn't emit any IAM Profile core claims yet, a materially
bigger gap than "add one claim." Both are real, scoped, registered
workplans — neither implemented in this pass, both flagged clearly rather
than quietly skipped.