00e39a3f85
Phase 1 complete — Operator Foundation: - Postgres 16 in Docker-in-LXC (asyncpg pool, 5-table schema, PgStore, migrations) - Operator auth (argon2id, signed stateless cookies, slowapi 5/min rate limit) - VC issuer key migration SQLite→Postgres (archive-before-active, R-VC-MIG-01) - Operator bootstrap CLI (create-operator.py, idempotent) - Backup cron script + G-008 restore drill - Graceful degradation (server starts without Postgres) - 272 tests pass, 33 skip (Postgres-requiring), 0 fail ---ci--- project: praxis phase: 1 milestone: v0.4 status: complete requirements: covered: [REQ-MT-01, REQ-AUTH-01, REQ-NFR-AUTH-01, REQ-NFR-MT-01, REQ-MT-02] partial: [] ---/ci---
68 lines
2.5 KiB
Python
68 lines
2.5 KiB
Python
"""Signed cookie configuration (TASK-03-02, D-041, D-056, R-AUTH-01, G-031).
|
|
|
|
Returns kwargs for Starlette SessionMiddleware (itsdangerous HMAC-SHA256
|
|
signed cookies — D-056, stateless, no sessions table). The cookie name is
|
|
`praxis_op` (distinct from any future learner cookie).
|
|
|
|
R-AUTH-01 / G-031 reframe: the PRIMARY mitigation for a sniffed operator
|
|
cookie is the k-anonymity defense-in-depth — the cohort dashboard reads
|
|
only k-anonymized aggregates, so a sniffed cookie leaks NO learner PII.
|
|
The `PRAXIS_COOKIE_SECURE` flag is the SECONDARY mitigation (operational
|
|
convenience for when TLS arrives). It defaults to true; the HTTP pilot
|
|
(LXC, no TLS — D-030) sets it to false with a logged WARNING.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import secrets
|
|
|
|
from loguru import logger
|
|
|
|
_COOKIE_MAX_AGE_S = 28800 # 8h (D-041)
|
|
|
|
|
|
def _env_bool(key: str, default: bool) -> bool:
|
|
raw = os.environ.get(key, "").strip().lower()
|
|
if raw in ("true", "1", "yes", "on"):
|
|
return True
|
|
if raw in ("false", "0", "no", "off"):
|
|
return False
|
|
return default
|
|
|
|
|
|
def get_session_middleware_kwargs() -> dict:
|
|
"""Return kwargs for Starlette SessionMiddleware.
|
|
|
|
If PRAXIS_COOKIE_SECRET is unset, generate an ephemeral random secret
|
|
and log a WARNING (dev only — sessions won't survive a restart and this
|
|
MUST NOT be used in pilot/production).
|
|
"""
|
|
secret = os.environ.get("PRAXIS_COOKIE_SECRET", "").strip()
|
|
if not secret:
|
|
secret = secrets.token_urlsafe(48)
|
|
logger.warning(
|
|
"PRAXIS_COOKIE_SECRET not set — generated an ephemeral random secret. "
|
|
"Sessions will NOT survive a server restart. This is dev-only; set "
|
|
"PRAXIS_COOKIE_SECRET (>=32 bytes) for pilot/production."
|
|
)
|
|
secure = _env_bool("PRAXIS_COOKIE_SECURE", True)
|
|
if not secure:
|
|
logger.warning(
|
|
"Cookie Secure flag disabled (PRAXIS_COOKIE_SECURE=false) — HTTP pilot "
|
|
"mode (R-AUTH-01). Do not use in production. NOTE (G-031): the primary "
|
|
"R-AUTH-01 mitigation is k-anon defense-in-depth (cohort dashboard reads "
|
|
"only k-anonymized aggregates → sniffed cookie leaks no PII); this flag "
|
|
"is the secondary mitigation."
|
|
)
|
|
return {
|
|
"secret_key": secret,
|
|
"session_cookie": "praxis_op",
|
|
"max_age": _COOKIE_MAX_AGE_S,
|
|
"https_only": secure,
|
|
"same_site": "strict",
|
|
"path": "/",
|
|
}
|
|
|
|
|
|
__all__ = ["get_session_middleware_kwargs"] |