Initial curated ITWorx_MCP_Hub source from 368fba691ea8d23403b4d56b53b5ec670d7c5020
This commit is contained in:
commit
cb5be898ea
663 files changed
+82674
No files matched your search
@@ -0,0 +1,85 @@
|
||||
# Session store: production decision (WP-230E)
|
||||
|
||||
## Decision
|
||||
|
||||
`control-api`'s management-session store (`services/control_api/auth/bff.py`
|
||||
`SessionStore`) is an **in-memory, single-process store, and this is the
|
||||
accepted v1 production design** -- not a placeholder awaiting a Redis
|
||||
migration. `control-api` MUST run as exactly one instance/replica. This
|
||||
document is the load-bearing artifact for that constraint: code and Compose
|
||||
must stay consistent with it, and `scripts/compose_release_check.py` enforces
|
||||
the Compose side of it statically (see below), not just the docstring.
|
||||
|
||||
## Why this, not a Redis-backed store
|
||||
|
||||
`redis` is already a hard Compose dependency of `control-api`
|
||||
(`docker-compose.yml`'s `control-api.depends_on.redis`), which reads as an
|
||||
implied promise that sessions are Redis-backed. They are not: no
|
||||
`HUB_REDIS_*` setting exists, and no code path talks to Redis for sessions.
|
||||
That gap is exactly what this decision closes.
|
||||
|
||||
A real Redis-backed store was evaluated and deferred rather than built now,
|
||||
for a concrete, checkable reason: it requires a new production runtime
|
||||
dependency (`redis`, or an async client) added to `pyproject.toml` and
|
||||
locked via `uv lock`. At the time of this decision `uv` was not reliably
|
||||
resolvable in the environment this work was executed in, so a lockfile
|
||||
change could not be produced with real confidence -- and a session store
|
||||
touches authentication, the most security-sensitive path in this service.
|
||||
Shipping unlocked or hand-edited dependency pins for that specific subsystem
|
||||
is a worse outcome than shipping the honestly-documented current design.
|
||||
This is a tooling-availability constraint on *when*, not a judgment that
|
||||
Redis-backed sessions are undesirable; re-scoping this work is the correct
|
||||
next step once dependency tooling is confirmed reliable (see "Revisiting
|
||||
this decision" below).
|
||||
|
||||
## What "single instance" actually means here
|
||||
|
||||
- Every session and every CSRF token lives in one process's memory
|
||||
(`SessionStore._records`, a plain `dict` guarded by a `threading.RLock`).
|
||||
- A restart, redeploy, or crash of `control-api` invalidates every active
|
||||
session. Users see this as being signed out and must re-authenticate
|
||||
through Authentik; no data is lost, no state is corrupted, and the BFF's
|
||||
same-origin cookie/CSRF model means there is nothing to reconcile on the
|
||||
client side.
|
||||
- Running two or more `control-api` replicas behind a load balancer would
|
||||
silently split sessions and CSRF tokens across disjoint in-memory stores.
|
||||
A request routed to instance B with a session cookie issued by instance A
|
||||
would see "session expired" even though the session is, from the user's
|
||||
perspective, still valid -- and worse, CSRF token mismatches on that same
|
||||
split would be indistinguishable from a real CSRF attack in the logs.
|
||||
This is a correctness and security-signal problem, not just an HA gap, so
|
||||
it is enforced, not left to operational discipline.
|
||||
|
||||
## Enforcement (not just documentation)
|
||||
|
||||
- `docker-compose.yml` and `docker-compose.prod.yml` do not declare
|
||||
`deploy.replicas` for `control-api` (or anywhere); Compose's default is
|
||||
one container per service, so the constraint already holds today.
|
||||
- `scripts/compose_release_check.py::check_compose` now fails the release
|
||||
gate if `control-api` ever declares `deploy.replicas != 1`. This is a
|
||||
static, automated check (`tests/test_wp150_operations.py` and the new
|
||||
`tests/test_wp230_session_store.py` exercise it), run as part of
|
||||
`python scripts/compose_release_check.py` and the release-readiness gate
|
||||
-- so a future change that tries to scale `control-api` fails loudly
|
||||
before it reaches production, instead of failing silently at 2am when a
|
||||
real user hits the split-brain session bug above.
|
||||
- `SessionStore`'s docstring states the single-instance constraint directly
|
||||
at the point where a future change is most likely to be made, and points
|
||||
back to this document.
|
||||
|
||||
## Revisiting this decision
|
||||
|
||||
Move to a real shared store (Redis-backed, encrypted-at-rest session
|
||||
payloads, TTL-matched key expiry, explicit logout invalidation, and defined
|
||||
behavior for a Redis outage -- fail closed, forcing re-authentication,
|
||||
rather than silently falling back to per-instance memory) when:
|
||||
|
||||
1. `uv lock` can be run and verified in the environment doing the work, so
|
||||
the new dependency's lock entry is trustworthy, and
|
||||
2. there is an actual operational need for more than one `control-api`
|
||||
replica (there is none today -- Tower runs a single instance of every
|
||||
owned service).
|
||||
|
||||
Until then, this document and the `compose_release_check.py` gate are the
|
||||
enforced contract: single instance, in-memory, sessions do not survive a
|
||||
restart, and that is by design.
|
||||
Reference in new issue
Block a user