Dev build 2026-05-28 16:17

This commit is contained in:
Ivo Oskamp 2026-05-28 16:17:22 +02:00
parent 98f5d740f0
commit bd8e2268db
2 changed files with 2600 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,177 @@
# Clearview — Authentication Design
**Date:** 2026-05-28
**Status:** Approved (brainstorming phase)
**Scope:** Add an authentication layer to the existing Clearview FastAPI + static-frontend application.
---
## 1. Goal
Restrict access to the Clearview UI and API to a small group of named administrators. No self-service registration, no public exposure of endpoints, no multi-tenant user isolation.
## 2. Requirements
| # | Requirement |
|---|---|
| R1 | All existing API routes (`/api/tenants/*`, `/api/jobs/*`, `/api/onboarding/*`) require an authenticated session. |
| R2 | Two roles: `admin` and `user`. Only `admin` can manage users and view the audit log. Both can use the scanner UI. |
| R3 | Server-side sessions using an opaque session ID stored in an HttpOnly cookie. |
| R4 | First admin is created via a one-time **initial-setup** page that is reachable only while the `users` table is empty. No env-var fallback. |
| R5 | Password hashing with **Argon2id** (default parameters via `argon2-cffi`). |
| R6 | Password policy: minimum 12 characters, at least one letter and one digit. Validated server-side. |
| R7 | Session TTL: **8 hours sliding** by default; **30 days fixed** when "remember me" is checked at login. |
| R8 | Audit log persisted in DB and viewable in the UI by admins. |
| R9 | Existing changelog convention applies: append entries to `changelog-develop.md` per change. |
Explicitly **out of scope** for v1: rate limiting / login lockout, password reset via email, MFA, SSO, fine-grained permissions beyond `admin` / `user`.
## 3. Architecture
### 3.1 Backend module layout
A new package `clearview_app/auth/` with focused modules:
| File | Purpose |
|---|---|
| `auth/__init__.py` | Package marker. |
| `auth/models.py` | SQLAlchemy models: `User`, `Session`, `AuthAudit`. |
| `auth/security.py` | Argon2id hashing, password policy validation, session-ID generation. |
| `auth/sessions.py` | Create / lookup / refresh / revoke session records; expiry handling. |
| `auth/audit.py` | Single `record(event, user_id, ip, detail)` helper. |
| `auth/dependencies.py` | FastAPI dependencies: `current_session`, `require_user`, `require_admin`. |
| `auth/router.py` | `POST /api/auth/login`, `POST /api/auth/logout`, `GET /api/auth/me`, `GET /api/auth/setup-required`, `POST /api/auth/setup`. |
| `auth/users_router.py` | Admin-only: `GET/POST/PATCH/DELETE /api/users`, `POST /api/users/{id}/reset-password`, `GET /api/audit`. |
`main.py` wires the new routers **before** the static-files mount and applies `Depends(require_user)` to the three existing routers (`tenants`, `jobs`, `onboarding`).
### 3.2 Database
Three new tables, added via a new Alembic migration in `clearview_app/migrations/versions/`.
```text
users
id int pk
username text unique not null
password_hash text not null -- Argon2id encoded string
role text not null -- 'admin' | 'user'
is_active bool not null default true
created_at timestamptz not null default now()
updated_at timestamptz not null default now()
sessions
id uuid pk -- opaque session id, stored in cookie
user_id int not null fk -> users.id on delete cascade
created_at timestamptz not null default now()
expires_at timestamptz not null
last_seen_at timestamptz not null default now()
ip text
user_agent text
remember bool not null default false
auth_audit
id bigserial pk
ts timestamptz not null default now()
user_id int null fk -> users.id on delete set null
event text not null -- see event list below
ip text
detail jsonb -- e.g. {"username": "alice"} on login_fail
```
Audit events: `login_ok`, `login_fail`, `logout`, `user_create`, `user_update`, `user_delete`, `password_reset`, `setup`.
### 3.3 Session handling
- Cookie name: `clearview_session`. Flags: `HttpOnly`, `SameSite=Lax`, `Secure` (set when request is HTTPS; configurable for local-dev HTTP).
- Cookie value: the `sessions.id` UUIDv4. No data is encoded in the cookie itself.
- On every authenticated request:
1. Read cookie → look up row in `sessions`.
2. If missing or `expires_at <= now()` → 401, delete cookie.
3. If `remember = false` → extend `expires_at = now() + 8h` (sliding).
4. If `remember = true` → leave `expires_at` untouched (fixed 30 days from creation).
5. Update `last_seen_at`.
- Logout: delete the session row and clear the cookie.
- Cleanup: a lightweight purge of expired sessions runs at login time and is also exposed as a periodic task in the existing worker (cheap query: `DELETE FROM sessions WHERE expires_at < now()`).
### 3.4 Initial-setup flow
- `GET /api/auth/setup-required` returns `{"setup_required": true}` iff `COUNT(*) FROM users = 0`.
- `POST /api/auth/setup` accepts `{username, password}`, succeeds **only** when the table is empty, creates the user with `role='admin'`, immediately establishes a session (sets cookie), and writes a `setup` audit row.
- Once any user exists, both endpoints return 409 / `setup_required=false`.
### 3.5 Password policy
- Server-side check before hashing or updating: `len(pw) >= 12 and any(c.isalpha() for c in pw) and any(c.isdigit() for c in pw)`.
- Hashing: `argon2.PasswordHasher()` with library defaults; the encoded string includes salt + parameters, so future tuning is non-breaking.
## 4. Frontend
The frontend is the existing static `site/` (`index.html`, `app.js`, `styles.css`). Changes:
- **New `login.html`** — standalone page, no app-shell. Form with username, password, "remember me" checkbox. Posts to `/api/auth/login`, then redirects to `/`.
- **New `setup.html`** — same shell as login, used when `/api/auth/setup-required` returns true. Posts to `/api/auth/setup`.
- **`app.js`**:
- On boot, call `GET /api/auth/me`. On 401, redirect to `/login` (or `/setup` if `setup-required`).
- Wrap the `fetch` helper so any 401 response triggers a redirect to `/login`.
- Header shows `username (role)` + a **Logout** button that calls `POST /api/auth/logout` then redirects to `/login`.
- **New "Users" sidebar tab** (admin-only): list / add / edit (username, role, active) / delete users, and a **Reset password** action that opens a modal.
- **New "Audit" sub-view under Users** (admin-only): paged table of `auth_audit` rows, newest first, with event-type filter.
- Non-admin users do not see the Users tab; the backend also enforces this independently (defence in depth).
## 5. Data flow — successful login
1. Browser `POST /api/auth/login` with `{username, password, remember}`.
2. Server fetches user by username. If not found or `is_active=false` → 401 + audit `login_fail` with the supplied username.
3. Verify password with Argon2id. On mismatch → 401 + audit `login_fail`.
4. Insert `sessions` row (`remember` flag determines TTL: 8h or 30d).
5. `Set-Cookie: clearview_session=<uuid>; HttpOnly; SameSite=Lax; Secure?; Path=/; Max-Age=<ttl>`.
6. Audit `login_ok`. Return `{username, role}`.
7. Browser then calls `GET /api/auth/me` and renders the app.
## 6. Error handling
| Situation | Response |
|---|---|
| Missing / invalid / expired session cookie on protected endpoint | `401 Unauthorized`, cookie cleared. |
| Authenticated `user` hitting an admin-only endpoint | `403 Forbidden`. |
| Setup endpoint called when users already exist | `409 Conflict`. |
| Password policy violation | `400 Bad Request` with a single human-readable message. |
| Argon2 verification raising any exception | Treated as failed login (no info leak). |
## 7. Testing
Pytest cases (added in the existing test layout, mirroring current patterns):
- Password hashing round-trip; policy validator edge cases.
- Login success, wrong password, unknown user, inactive user.
- Session expiry: sliding 8h refresh vs fixed 30d remember.
- Logout invalidates the session.
- `require_user` and `require_admin` reject correctly.
- Setup endpoint: succeeds when empty, 409 when not empty.
- User CRUD endpoints: admin allowed, `user` role gets 403.
- Audit rows are written for each event.
## 8. Migration & compatibility
- Single forward Alembic migration adds the three tables. No changes to existing tables.
- First deploy on an existing install: the `users` table is empty → users are redirected to `/setup` on first visit.
- No env vars are introduced for credentials. (An optional `CLEARVIEW_COOKIE_SECURE` toggle may be added so local-dev HTTP still works; default `true`.)
## 9. Work breakdown (units of work)
1. DB models + Alembic migration.
2. Auth core: hashing, policy, session create/lookup/refresh/revoke, audit helper.
3. Auth router: login / logout / me / setup-required / setup.
4. Users router + audit endpoint.
5. Apply `require_user` to existing routers; `require_admin` to user/audit routes.
6. Frontend: `login.html`, `setup.html`, fetch-wrapper 401 handling, header user-badge + logout.
7. Frontend: Users tab (CRUD + reset password) and Audit sub-view.
8. Tests for items 25.
9. Append entries to `changelog-develop.md` per change.
## 10. Open items deferred to v2
- Rate limiting / brute-force lockout.
- Email-based password reset.
- MFA / SSO via Microsoft Entra (would reuse existing tenant app-registration plumbing).
- Per-tenant data scoping for non-admin users.