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>
9.4 KiB
| id | type | title | domain | repo | status | owner | topic_slug | created | updated | state_hub_workstream_id |
|---|---|---|---|---|---|---|---|---|---|---|
| TEN-WP-0003 | workplan | flex-auth-backed WriteAuthorizer | infotech | tenant-engine | finished | codex | netkingdom | 2026-07-23 | 2026-07-23 | 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-capedoes not currently emit any IAM Profile core claims (tenant,principal_type,groups,roles,assurance) — confirmed by readingkey-cape/src/internal/server/oidc/token.go, which has no such claim assembly today. Addingtenant_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 iskey-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-authhas a real, workingPOST /v1/check(verified againstcmd/flex-auth/main.goandschemas/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 realallowdecisions 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 workFLEX-WP-0003did for Markitect (its own multi-task workplan, not a config tweak). That isflex-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
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:DecisionEnvelopewitheffectinallow | 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
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
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
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.