TEN-WP-0002 T04-T07: cache-read, live-lookup (fail-closed), write API, close
- authz.py: WriteAuthorizer Protocol + DefaultDenyWriteAuthorizer. Every
write endpoint calls it before touching the store; denial maps to
403 write_denied via an exception handler.
- app.py: GET /tenants/{id}/roles (cache-read, key-cape) and
GET /tenants/{id}/roles/live (live-lookup, flex-auth) share one handler
that fails closed (503) on StoreUnavailableError -- deliberately made
identical rather than giving cache-read weaker guarantees than the task
strictly required. POST /tenants, /roles/grant, /roles/revoke, /plan --
all four gated by the WriteAuthorizer seam, domain/store errors mapped to
400/404/409 after authorization passes.
- store.py: new StoreUnavailableError for the fail-closed test double.
43 tests passing: default-deny on every write endpoint, an
_AllowAllAuthorizer test double proving the seam actually gates (full
create->grant->read->revoke->read->assign-plan lifecycle over real HTTP),
and a _BrokenStore double proving outage never looks like "zero roles".
Verified live over real HTTP, not just TestClient.
TEN-WP-0002 closed: all 7 tasks done, boundary-contract ownership checked
against the implementation with no drift found. Follow-ups recorded in the
closure note (real flex-auth WriteAuthorizer, key-cape wiring, guardrail
policy design, Binky as first real tenant record, durable persistence).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
934a2f7c35
commit
adb74d2443
6 changed files with 505 additions and 8 deletions
|
|
@ -4,7 +4,7 @@ type: workplan
|
|||
title: "Service skeleton, domain model, and the three boundary-contract APIs"
|
||||
domain: infotech
|
||||
repo: tenant-engine
|
||||
status: ready
|
||||
status: finished
|
||||
owner: codex
|
||||
topic_slug: netkingdom
|
||||
created: "2026-07-23"
|
||||
|
|
@ -135,7 +135,7 @@ sequence for a full mutation chain. `python -m compileall src tests` clean.
|
|||
|
||||
```task
|
||||
id: TEN-WP-0002-T04
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "425cab98-ab7f-47fb-a8b6-ec0f52d62cd5"
|
||||
```
|
||||
|
|
@ -150,11 +150,20 @@ fast, per the boundary contract's performance model.
|
|||
Done when: integration test hits the endpoint against the in-memory store
|
||||
and returns the expected role set for a seeded tenant.
|
||||
|
||||
**Done 2026-07-23:** Implemented in `app.py`. Returns
|
||||
`{"tenant_id": ..., "roles": [...]}`, sorted role-value strings; 404 for an
|
||||
unknown tenant. Extended beyond the task's minimum: also fails closed (503)
|
||||
on store unavailability (see T05) — there was no good reason for the two
|
||||
read endpoints to behave differently on that axis, and keeping them
|
||||
identical avoids a second, subtly-different error-handling path to drift
|
||||
later. `tests/test_api_reads.py` covers the happy path and the 404 case;
|
||||
verified live over real HTTP against the running service.
|
||||
|
||||
## Task: Live-lookup API (for flex-auth) — fail closed
|
||||
|
||||
```task
|
||||
id: TEN-WP-0002-T05
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "2a04ce4c-c967-4540-8873-70ac419378e6"
|
||||
```
|
||||
|
|
@ -173,11 +182,21 @@ role list indistinguishable from "no roles granted".
|
|||
Done when: a test simulates store unavailability and asserts the endpoint
|
||||
signals failure distinctly from "zero roles", not silently as 200 + `[]`.
|
||||
|
||||
**Done 2026-07-23:** New `store.StoreUnavailableError`; both read endpoints
|
||||
convert it to `503 {"detail": "tenant_roles_unavailable"}`. A `_BrokenStore`
|
||||
test double (`tests/test_api_reads.py`) always raises it from
|
||||
`active_roles()`, simulating an outage; the test asserts `503` and
|
||||
explicitly asserts the response body is *not*
|
||||
`{"tenant_id": ..., "roles": []}` — the exact ambiguity the task exists to
|
||||
prevent. `TenantNotFoundError` stays a distinct `404`, so "tenant doesn't
|
||||
exist," "store is down," and "tenant exists with zero roles" are three
|
||||
different, distinguishable responses, never collapsed into one shape.
|
||||
|
||||
## Task: Write API — grant, revoke, assign-plan
|
||||
|
||||
```task
|
||||
id: TEN-WP-0002-T06
|
||||
status: todo
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "3a2d1ee7-6e66-4080-9345-32ba457acf5f"
|
||||
```
|
||||
|
|
@ -197,11 +216,30 @@ Done when: unit tests confirm every write endpoint calls the
|
|||
test-only authorizer override proves the seam actually gates the mutation
|
||||
when swapped.
|
||||
|
||||
**Done 2026-07-23:** `authz.py`'s `WriteAuthorizer` Protocol +
|
||||
`DefaultDenyWriteAuthorizer`; every write endpoint in `app.py` calls
|
||||
`authorizer.authorize(...)` before touching the store, and a
|
||||
`WriteAuthorizationDeniedError` exception handler maps denial to
|
||||
`403 {"error_code": "write_denied", ...}`. `tests/test_api_writes.py`
|
||||
asserts all four write endpoints are `403` under the default authorizer,
|
||||
then swaps in an `_AllowAllAuthorizer` test double and exercises the full
|
||||
create → grant → read → revoke → read → assign-plan lifecycle over real
|
||||
HTTP requests (`TestClient`), confirming the seam actually gates rather
|
||||
than just existing decoratively. Domain/store errors surfacing after
|
||||
authorization passes get separate status codes: `400` invalid
|
||||
identifier/grant, `404` unknown tenant, `409` duplicate tenant. Verified
|
||||
live: unauthenticated `POST /tenants` over real HTTP returns `403` with the
|
||||
expected body, matching the test suite. One known simplification, not
|
||||
resolved here: `actor` is a request-body field rather than extracted from a
|
||||
real auth context — there is no real auth context yet, since wiring one in
|
||||
is exactly what a `flex-auth`-backed `WriteAuthorizer` will do; noted for
|
||||
whoever picks up that follow-up.
|
||||
|
||||
## Task: Closure review
|
||||
|
||||
```task
|
||||
id: TEN-WP-0002-T07
|
||||
status: todo
|
||||
status: done
|
||||
priority: low
|
||||
state_hub_task_id: "eecf4bc8-d20b-4b18-986a-6518f2f74d7b"
|
||||
```
|
||||
|
|
@ -213,3 +251,31 @@ integration, `key-cape` wiring to actually call the cache-read endpoint at
|
|||
issuance, guardrail/quota policy design (ADR-0014's reserved item), and
|
||||
Binky Hedgehog GmbH as the first real tenant record once `key-cape`'s
|
||||
`KEY-WP-0004` reaches that point. Run `statehub fix-consistency`.
|
||||
|
||||
**Closed 2026-07-23.** T01–T06 all done. `PYTHONPATH=src pytest` → `43
|
||||
passed`; `python -m compileall src tests` clean; `ruff` unavailable in this
|
||||
workstation's shared venv (same environment gap noted in `qonto-assistant`'s
|
||||
own workplans) — `compileall` substituted, `make lint` untested against a
|
||||
real `ruff` install. Verified live over real HTTP, not just `TestClient`:
|
||||
`/health` 200, unauthenticated `POST /tenants` 403 with the expected error
|
||||
body, `GET` on an unknown tenant 404.
|
||||
|
||||
**Ownership check against the boundary contract:** all three API surfaces
|
||||
present (cache-read, live-lookup, write); every write routes through the
|
||||
`WriteAuthorizer` seam, never self-authorized; every mutation emits a
|
||||
`DomainEvent`; `plan_id` is stored and returned as an opaque string, never
|
||||
resolved against `adaptive-pricing` locally; nothing here stores user data,
|
||||
issues tokens, or makes an authorization decision. No drift found.
|
||||
|
||||
**Follow-ups, not started:**
|
||||
- Real `flex-auth`-backed `WriteAuthorizer` (replaces the default-deny
|
||||
stub) — also where a real `actor` identity would come from instead of a
|
||||
request-body field.
|
||||
- `key-cape` wiring to actually call the cache-read endpoint at token
|
||||
issuance.
|
||||
- Guardrail/quota policy design (ADR-0014's reserved item — spend limits,
|
||||
entity/action counts).
|
||||
- Binky Hedgehog GmbH as the first real tenant record, once `key-cape`'s
|
||||
`KEY-WP-0004` reaches that point.
|
||||
- Persistence beyond in-memory (`TenantStore` is already a swappable seam
|
||||
for this).
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue