178 lines
9.2 KiB
Markdown
178 lines
9.2 KiB
Markdown
# 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 2–5.
|
||
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.
|