From 332b74e5dea4cfa3cd002b4af3a1242a76d74d8d Mon Sep 17 00:00:00 2001 From: Cody Schaefer Date: Mon, 24 Aug 2026 10:05:00 -0500 Subject: [PATCH] T10.2 - actually implement the startup line the docs send people to A gap, not a refinement. T10.2's task text said the login change "must add visibility: a startup log line stating whether LDAP is configured", and three documents tell an operator to run docker compose logs api | grep -i "LDAP auth" as the FIRST diagnostic when nobody can sign in. Nothing ever logged it. The grep would have returned silence, during exactly the outage it was written for, and silence reads as "the API never started" rather than "the API is fine and the group is misconfigured". Found while answering a question that the line exists to answer: a required group had been added to .env, sign-in still worked, and there was no way to tell whether the group had been checked or the value had simply not been picked up. (It had not - the file was unsaved. Both readings were correct at the time they were taken.) Implemented as a FastAPI lifespan handler. It reports CONFIGURATION only and opens no connection: startup must not be able to hang on an unreachable domain controller, and a bind at boot would count against the AD lockout policy for whatever account it used. ldap_auth.selftest() remains the reachability check - it validates the certificate without binding, so it cannot contribute to a lockout either. Verified by booting the real app under uvicorn and reading the log: wpsuite.api: LDAP auth enabled - ldaps://prime.local:636, domain prime.local, CA .../prime-ca-chain.pem, required group: CN=Prime Employees,OU=Prime Distribution and Security Groups,DC=prime,DC=local Co-Authored-By: Claude Opus 5 (1M context) --- docs/waves/wave-10.md | 1 + server/app.py | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/docs/waves/wave-10.md b/docs/waves/wave-10.md index d0b0155..f1e72af 100644 --- a/docs/waves/wave-10.md +++ b/docs/waves/wave-10.md @@ -97,6 +97,7 @@ misconfigured deploy is indistinguishable from a forgotten password at the login **Done when:** +- [x] the API logs one line at startup saying whether LDAP is configured and which group gates sign-in — configuration only, so an unreachable DC cannot hang startup. This is what `DEPLOYMENT.md`, `DEPLOY-login-portal.md` and `server/README.md` all send people to first; it was specified in this task on Aug 21 and not actually written until Aug 24. - [x] a correct domain password signs in and sets the session cookie — confirmed against the live domain Aug 24, and in `ldap_auth_check` - [x] a wrong password is refused with the generic message - [x] a blank password is refused (guards `T10.1` from the caller's side too) diff --git a/server/app.py b/server/app.py index f15e3c6..ddeef59 100644 --- a/server/app.py +++ b/server/app.py @@ -10,6 +10,7 @@ Interactive docs: http:///api/docs """ import base64 import logging +from contextlib import asynccontextmanager import os import re import uuid @@ -44,7 +45,30 @@ if engine.dialect.name == "sqlite": # Interactive docs are handy in dev but hand an attacker the full API map in prod, # so enable them only on the SQLite dev fallback (production runs on Postgres). _docs_enabled = engine.dialect.name == "sqlite" + + +@asynccontextmanager +async def _lifespan(_app): + """Say, once, whether anyone can sign in at all. + + D13 removed the local password path and left no break-glass, so a broken LDAP + configuration and a forgotten password look identical at the login box. This + line is what tells an operator which one they have, and DEPLOYMENT.md, + DEPLOY-login-portal.md and server/README.md all send people here first: + + docker compose logs api | grep -i "LDAP auth" + + Configuration only — it opens no connection and binds nothing, so startup stays + fast and cannot be made to hang by an unreachable domain controller. Use + `ldap_auth.selftest()` for a reachability check; it validates the certificate + without binding, so it cannot contribute to a lockout either. + """ + log.info("%s", ldap_auth.describe()) + yield + + app = FastAPI( + lifespan=_lifespan, title="Work Package Suite API", docs_url="/api/docs" if _docs_enabled else None, redoc_url=None,