tenant-engine/workplans/TEN-WP-0003-flex-auth-write-authorizer.md

204 lines
9.4 KiB
Markdown
Raw Normal View History

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
---
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.