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

9.4 KiB
Raw Permalink Blame History

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

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

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) — deny403, allow201, 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 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 pytest60 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.