Dev build 2026-05-28 16:17
This commit is contained in:
parent
98f5d740f0
commit
bd8e2268db
2423
docs/superpowers/plans/2026-05-28-authentication.md
Normal file
2423
docs/superpowers/plans/2026-05-28-authentication.md
Normal file
File diff suppressed because it is too large
Load Diff
177
docs/superpowers/specs/2026-05-28-authentication-design.md
Normal file
177
docs/superpowers/specs/2026-05-28-authentication-design.md
Normal 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 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.
|
||||||
Loading…
Reference in New Issue
Block a user