T10.8 D13/D14 - documentation matches what the code now does
DEPLOY-login-portal.md was the most wrong and is rewritten. It described taking a username/password portal live - bcrypt, and a first admin created with `create-admin --password`. Every command in it now fails. It keeps its filename and carries a note saying what it replaced, because an admin holding the old copy needs to know why the steps stopped working rather than concluding the deploy is broken. New content leads with the warning that there is no break-glass, and puts verification BEFORE announcing the deploy - the log line, the certificate check that binds nothing, then a real sign-in. DEPLOYMENT.md: AUTH_RESET_* replaced with the LDAP variables; the users table row no longer claims a password_hash column; "Self-service password reset" replaced by a section saying there isn't one and pointing at Okta. New "Domain authentication" section covering the three things that are not obvious - why prime.local and never a DC or an IP, why the CA bundle is not a certificate issued to this app (with the thumbprints and a Get-ChildItem line to rebuild it), and why the outbound network stopped being optional - plus the lockout arithmetic written out so the next person to raise AUTH_MAX_ATTEMPTS sees the constraint rather than a magic 2. server/README.md: endpoint table drops /api/auth/password and gains the role route; the login-portal section becomes domain authentication; create-admin becomes the two-step bootstrap (sign in, then promote). CLAUDE.md: a new "authentication rules" section beside the token rule, for the same reason that one exists - four things that look like tidying-up if you do not know why. The empty-password guard that must run before bind(), CERT_REQUIRED with an explicit CA file, AUTH_MAX_ATTEMPTS being arithmetic rather than taste, and connecting to the domain name rather than a DC. Plus: no break-glass, and roles are local - never read a role from AD. Closed three done-when boxes that were open rather than ticked: T10.8 all of them T10.9 promote/demote verified against a real bind (Aug 24), not a stub T10.3 the Postgres round trip, on postgres:16-alpine Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
29
CLAUDE.md
29
CLAUDE.md
@@ -70,6 +70,31 @@ Adding a raw hex value to a page stylesheet is a defect regardless of what the t
|
||||
for. Four parallel token systems is what produced S5, and the `.field-hint` comment at
|
||||
`work-package-suite-styles.css:336` is the bug that resulted. Do not recreate it.
|
||||
|
||||
## The authentication rules
|
||||
|
||||
Sign-in is an LDAPS bind against the domain (`D13`, `docs/waves/decisions-2026-08-21.md`).
|
||||
Four things about it are load-bearing and look like tidying-up if you do not know why:
|
||||
|
||||
- **The empty-password guard in `ldap_auth.verify` runs before `bind()`.** An LDAP
|
||||
simple bind with an empty password is an *anonymous* bind and it SUCCEEDS. Remove
|
||||
that check and a blank password authenticates as any username submitted. It looks
|
||||
redundant because `login()` checks too. Both stay.
|
||||
- **`validate=ssl.CERT_REQUIRED` with an explicit CA file.** Never `CERT_NONE`, never
|
||||
the system trust store (which trusts five other self-signed CAs on this estate).
|
||||
`CERT_NONE` still encrypts, so it fails silently - what it loses is the ability to
|
||||
tell a real DC from someone harvesting domain passwords.
|
||||
- **`AUTH_MAX_ATTEMPTS` is 2, and that is arithmetic, not taste.** Failures are real
|
||||
domain binds counting against the AD lockout policy (5 here), and 2 workers double
|
||||
it: 2 x 2 = 4 < 5. Raising it, or adding a worker, makes `/api/auth/login` a way to
|
||||
lock colleagues out of Windows.
|
||||
- **Connect to `prime.local`, never a DC name or an IP.** Every DC certificate carries
|
||||
the domain name in its SAN; an IP fails hostname validation, and the only way to
|
||||
force it is to disable the check above.
|
||||
|
||||
There is **no break-glass account** - a misconfiguration locks out everyone including
|
||||
admins. And roles are LOCAL: the directory supplies identity, this app supplies
|
||||
authorization. Never read a role from AD.
|
||||
|
||||
## Accessibility is in scope
|
||||
|
||||
Approved Aug 14, 2026 (C1). Any component you rebuild ships accessible or it is not done:
|
||||
@@ -90,10 +115,12 @@ A task is not done because the code is written. Every task file lists its own do
|
||||
checks. In addition, for any task touching the frontend:
|
||||
|
||||
1. Run the app locally: `uvicorn server.app:app` against a throwaway SQLite database.
|
||||
Signing in needs a domain credential now (D13) — a local run reaches `prime.local`
|
||||
from the host with no extra configuration. A container needs the `outbound` network.
|
||||
2. Exercise the affected flow at **390px** and at **1440px**. Field View at 390px is the
|
||||
gloved-hands surface and is where the worst rendering was found.
|
||||
3. Capture before and after screenshots into the PR.
|
||||
4. Run the existing smoke test. It signs in, and so does `server/seed_demo.py` (S13, fixed at T1.6 - this line said otherwise until Aug 20 2026, a stale record).
|
||||
4. Run the existing smoke test. It signs in, and so does `server/seed_demo.py` (S13, fixed at T1.6 - this line said otherwise until Aug 20 2026, a stale record). Since D13 both need a **domain** credential, and `WP_SMOKE_PASSWORD` is now a real Windows password - never put one on a command line.
|
||||
|
||||
If a done-when check cannot be verified, do not mark the task complete. Say which check
|
||||
failed and why.
|
||||
|
||||
Reference in New Issue
Block a user