204 lines
9.4 KiB
Markdown
204 lines
9.4 KiB
Markdown
|
|
---
|
|||
|
|
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 T01–T03 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.** T01–T03 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.
|