mirror of
https://github.com/azaion/ui.git
synced 2026-06-21 13:01:10 +00:00
[AZ-447] autodev Steps 1-4 baseline: docs, tests, refactor specs
Captures the full output of autodev existing-code Phase A through Step 4 (Code Testability Revision) for the Azaion UI workspace: - Step 1 Document: _docs/02_document/ (FINAL_report, architecture, glossary, components/, modules/, diagrams/, system-flows, module-layout) plus _docs/00_problem/ + _docs/01_solution/ + _docs/legacy/ + _docs/how_to_test + README. - Step 2 Architecture Baseline: architecture_compliance_baseline.md. - Step 3 Test Spec: _docs/02_document/tests/ (environment, test-data, blackbox/performance/resilience/security/ resource-limit tests, traceability-matrix), enum_spec_snapshot, expected_results/results_report.md (98 rows), plus the run-tests.sh + run-performance-tests.sh runners. - Step 4 Code Testability Revision: 01-testability-refactoring/ run dir (list-of-changes C01-C07, deferred_to_refactor, analysis/research_findings + refactoring_roadmap) and the 7 child task specs AZ-448..AZ-454 under _docs/02_tasks/todo/ plus _dependencies_table.md. - _docs/_autodev_state.md pins the cursor at Step 4 / refactor Phase 4 entry so /autodev resumes cleanly. Epic AZ-447 (UI testability gates) tracks the 7 child tasks that will land in subsequent commits. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,324 @@
|
||||
# Security Approach — Azaion UI
|
||||
|
||||
> Output of `/document` Step 6e. Retrospective security view of the SPA
|
||||
> grounded in code (`src/auth/AuthContext.tsx`, `src/api/client.ts`,
|
||||
> `src/api/sse.ts`), config (`nginx.conf`, `Dockerfile`,
|
||||
> `.woodpecker/build-arm.yml`), and the verified architecture
|
||||
> (`_docs/02_document/architecture.md` § 7). Every claim cites its evidence.
|
||||
|
||||
**Status**: synthesised-from-verified-docs (Step 6e — `/document`)
|
||||
**Date**: 2026-05-10
|
||||
|
||||
---
|
||||
|
||||
## Threat model summary
|
||||
|
||||
The UI is **operator-internal**, not public. The trust model is:
|
||||
|
||||
- **Trusted**: the suite services (reached via nginx reverse-proxy on the
|
||||
same origin); the suite's identity provider (`admin/`); the operator's
|
||||
authenticated browser session.
|
||||
- **Untrusted**: the browser itself (XSS-resistant design — bearer in
|
||||
memory only); operator network if not on the suite VPN; OpenWeatherMap
|
||||
(currently exfiltrated to via a hardcoded key — finding); OSM tile
|
||||
servers (read-only third-party).
|
||||
|
||||
Primary threats considered: **token theft via XSS**; **CSRF via cookie
|
||||
auto-attach**; **bearer leakage via SSE query string**; **secret leakage in
|
||||
bundle**; **privilege escalation via missing client-side route gates**;
|
||||
**clickjacking / framing**.
|
||||
|
||||
---
|
||||
|
||||
## 1. Authentication
|
||||
|
||||
### Login
|
||||
|
||||
- `POST /api/admin/auth/login` with `{ email, password }`.
|
||||
- `admin/` service responds with:
|
||||
- **Bearer JWT** in the response body — held in `AuthContext` memory
|
||||
only (never written to `localStorage` / `sessionStorage`, P3).
|
||||
- **`Secure HttpOnly SameSite=Strict` refresh cookie** — issued by
|
||||
server, scoped to the suite origin.
|
||||
|
||||
Source: `src/auth/AuthContext.tsx`; `architecture.md` § 7.
|
||||
|
||||
### Session bootstrap (cold load)
|
||||
|
||||
- On mount, `AuthContext` attempts `GET /api/admin/auth/refresh` to obtain
|
||||
a new bearer.
|
||||
- **Bug**: this call is missing `credentials:'include'` — the HttpOnly
|
||||
refresh cookie is NOT sent → cold-load refresh fails → user is
|
||||
redirected to `/login` even with a valid cookie. **Step 4 fix
|
||||
candidate**.
|
||||
|
||||
Source: `src/auth/AuthContext.tsx:24`; `04_verification_log.md` F2.
|
||||
|
||||
### 401-retry path
|
||||
|
||||
- The `01_api-transport` `client.ts` wraps every authenticated `fetch`.
|
||||
On 401 it issues `POST /api/admin/auth/refresh` **with**
|
||||
`credentials:'include'`, replaces the bearer in `AuthContext`, and
|
||||
retries the original request.
|
||||
- This path is correct and is the working refresh mechanism today.
|
||||
|
||||
Source: `src/api/client.ts:44`; `04_verification_log.md` F2.
|
||||
|
||||
### Logout
|
||||
|
||||
- `POST /api/admin/auth/logout` — clears the bearer in memory; server
|
||||
invalidates the refresh cookie.
|
||||
|
||||
Source: `src/auth/AuthContext.tsx`.
|
||||
|
||||
### Pre-port (legacy WPF)
|
||||
|
||||
- The WPF-era encrypted-creds command-line handoff (binary-split key
|
||||
fragments + DPAPI) is **intentionally not ported** — the browser cannot
|
||||
participate in that handoff and the suite identity infrastructure now
|
||||
lives server-side. P8.
|
||||
|
||||
Source: `_docs/legacy/wpf-era.md` §11.
|
||||
|
||||
---
|
||||
|
||||
## 2. Authorization
|
||||
|
||||
- RBAC is **server-enforced** — every authenticated endpoint validates
|
||||
`User.role` + `permissions[]` server-side.
|
||||
- The UI inspects `AuthUser.role` to render or hide nav links and pages,
|
||||
but does **NOT** treat the result as a security gate.
|
||||
- Browser is treated as untrusted; every action confirms with the server.
|
||||
|
||||
### Findings
|
||||
|
||||
- **`/admin` route lacks a client-side role-gate** (PRIORITY — security
|
||||
finding, AC-22). Server-side 403 IS the authoritative gate, but a
|
||||
non-admin user navigating to `/admin` today sees the broken admin UI
|
||||
flicker before the server rejects requests. **Step 4 / Step 8 fix.**
|
||||
- **`/settings` route gate is more nuanced** — there is no explicit
|
||||
`SETTINGS` permission code in the suite spec; gating relies on
|
||||
server-side 403. Treat as a soft gate (don't expose the link in the
|
||||
Header for non-admins) rather than a hard redirect.
|
||||
|
||||
Source: `architecture.md` § 7 Authorization; `App.tsx`.
|
||||
|
||||
---
|
||||
|
||||
## 3. Token handling
|
||||
|
||||
| Token | Lifetime | Where it lives | Where it appears on the wire |
|
||||
|-------|----------|----------------|------------------------------|
|
||||
| Bearer JWT | Short (server-issued; refreshed on 401) | `AuthContext` React state — **memory only** | `Authorization: Bearer ${token}` header on every authenticated `fetch`; `?token=${bearer}` query string on SSE (`ADR-008`) |
|
||||
| Refresh token | Long (server-issued) | **`Secure HttpOnly SameSite=Strict` cookie** — never accessible to JS | Cookie header on `POST /api/admin/auth/refresh` (and the broken bootstrap GET — Step 4 fix) |
|
||||
| `X-Refresh-Token` header | Per-request (long-running video detect) | passed in by `01_api-transport` for endpoints that need it | `X-Refresh-Token: ${value}` per `_docs/10_auth.md`. **Currently NOT sent on `POST /api/detect/${mediaId}` for video** — long videos can blow the access-token TTL → silent failure. Step 4 fix candidate (finding #29). |
|
||||
|
||||
### Key invariants (P3)
|
||||
|
||||
- Bearer is **never** written to `localStorage` / `sessionStorage` / IndexedDB.
|
||||
- Refresh token is **never** read from JS — `HttpOnly` enforces this.
|
||||
- Code-search regression test: zero matches for `localStorage|sessionStorage`
|
||||
touching the bearer token in `src/`.
|
||||
|
||||
---
|
||||
|
||||
## 4. SSE bearer-in-query-string
|
||||
|
||||
`EventSource` cannot send arbitrary headers, so `src/api/sse.ts` passes the
|
||||
bearer in the URL: `?token=${bearer}`.
|
||||
|
||||
### Trade-offs
|
||||
|
||||
- **Bearer is short-lived** — minimises window of compromise.
|
||||
- **HTTPS encrypts the URL on the wire** — but the URL still appears in:
|
||||
- **nginx access logs** (mitigation: log redaction at the nginx layer —
|
||||
Step 6 surface; not configured today).
|
||||
- **Browser history** (low risk for SSE URLs, but document).
|
||||
- **Refresh-rotation breaks open SSE connections** — the URL was created
|
||||
with the **old** bearer; no reconnect logic exists today (Step 8
|
||||
hardening — AC-24).
|
||||
|
||||
Source: `src/api/sse.ts`; `ADR-008`; `architecture.md` § 7.
|
||||
|
||||
---
|
||||
|
||||
## 5. Secrets management
|
||||
|
||||
### Hardcoded OpenWeatherMap API key — P10 violation
|
||||
|
||||
- **File**: `mission-planner/src/utils/flightPlanUtils.ts:60`
|
||||
- **Value**: a 32-char hex key shipped in the production bundle.
|
||||
- **Risk**: anyone with access to the bundle can extract and reuse the
|
||||
key (rate-limit theft; provider account abuse). The key is committed to
|
||||
git history.
|
||||
- **Fix sequence (Step 4 / Phase B)**:
|
||||
1. **Rotate** the key at OpenWeatherMap (out-of-band, user action).
|
||||
2. **Move to env** — `import.meta.env.VITE_OPENWEATHERMAP_API_KEY`
|
||||
read at build time (interim).
|
||||
3. **Proxy via suite** — long-term, route the wind compute through
|
||||
`flights/` so no key ever reaches the browser (preferred; per
|
||||
`architecture.md` § Architecture Vision Open Questions item 8).
|
||||
|
||||
### Other secrets
|
||||
|
||||
- **No other hardcoded keys** in `src/` per Grep audit at Step 4.
|
||||
- Suite service URLs are not secrets (they are docker-network hostnames).
|
||||
- The bearer is the only sensitive value in browser memory, and it is
|
||||
short-lived.
|
||||
|
||||
Source: P10; `architecture.md` § Architecture Vision; finding (security).
|
||||
|
||||
---
|
||||
|
||||
## 6. CORS, cookie scope, CSRF
|
||||
|
||||
- **Same-origin via nginx**: the SPA is served by the same nginx that
|
||||
reverse-proxies `/api/<service>/`. The browser sees a single origin →
|
||||
cookies scope cleanly; CORS preflight is unnecessary for the suite
|
||||
endpoints.
|
||||
- **`credentials:'include'`** is required on every authenticated `fetch`
|
||||
for the HttpOnly refresh cookie to flow. The 401-retry path
|
||||
(`api/client.ts:44`) is correct; the bootstrap refresh
|
||||
(`AuthContext.tsx:24`) is **broken**.
|
||||
- **CSRF**: `SameSite=Strict` on the refresh cookie + bearer-in-header on
|
||||
authenticated requests. The bearer header cannot be auto-attached by a
|
||||
cross-origin form submit. **No additional CSRF token** is used today —
|
||||
the architecture pattern (header-based bearer + SameSite=Strict cookie)
|
||||
obviates it.
|
||||
|
||||
Source: `src/api/client.ts`; `nginx.conf`; `architecture.md` § 7.
|
||||
|
||||
---
|
||||
|
||||
## 7. Input validation
|
||||
|
||||
- **Server is authoritative.** The UI does not duplicate validation logic
|
||||
it cannot guarantee.
|
||||
- **Numeric inputs in `09_settings`** use `parseInt(v) || 0` — clearing a
|
||||
field silently writes `0` (finding B4, AC-26). Step 4 fix.
|
||||
- **File upload**: `react-dropzone` filters by MIME / extension client-side;
|
||||
the server is authoritative on virus scanning and size enforcement
|
||||
(`client_max_body_size 500M`).
|
||||
- **Annotation save** body must include `Source`, `WaypointId`, `videoTime`
|
||||
(currently incomplete — finding #32). The wire format is validated by
|
||||
the `annotations/` service.
|
||||
|
||||
Source: `09_settings/SettingsPage.tsx`; `06_annotations/MediaList.tsx`;
|
||||
`nginx.conf`; finding B4 / #32.
|
||||
|
||||
---
|
||||
|
||||
## 8. Output encoding / XSS surface
|
||||
|
||||
- React 19 escapes JSX text by default — string content is safe.
|
||||
- **`dangerouslySetInnerHTML`** is **not used** in `src/` (Grep audit).
|
||||
- **`HelpModal`** ships hardcoded English strings inline — XSS-safe but
|
||||
P6 violation (i18n).
|
||||
- **Tainted-canvas** risk on annotation download (`AnnotationsPage.handleDownload`
|
||||
finding) — cross-origin image data may taint the canvas; the download
|
||||
silently fails. Pure UX bug, not a security defect, but flagged.
|
||||
|
||||
Source: `06_annotations/AnnotationsPage.tsx`; `HelpModal.tsx`.
|
||||
|
||||
---
|
||||
|
||||
## 9. Headers / hardening at the nginx layer
|
||||
|
||||
### Currently configured
|
||||
|
||||
- nginx serves `dist/` and reverse-proxies `/api/<service>/` to suite
|
||||
services.
|
||||
- `client_max_body_size 500M`.
|
||||
|
||||
### Currently MISSING (Step 6 surface)
|
||||
|
||||
- **`Content-Security-Policy`** — no CSP header. Recommended starting
|
||||
point: `default-src 'self'; img-src 'self' https: data:; connect-src
|
||||
'self' https://api.openweathermap.org/ https://*.tile.openstreetmap.org/;
|
||||
frame-ancestors 'none'; object-src 'none'`.
|
||||
- **`X-Frame-Options: DENY`** (or covered by CSP `frame-ancestors`) —
|
||||
clickjacking protection.
|
||||
- **`Referrer-Policy: strict-origin-when-cross-origin`**.
|
||||
- **`Strict-Transport-Security`** — depends on suite ingress; document the
|
||||
expected value.
|
||||
- **`X-Content-Type-Options: nosniff`**.
|
||||
- **Bearer-redaction** in nginx access logs for SSE URLs.
|
||||
|
||||
These are nginx config additions (server-side), not SPA changes — but the
|
||||
SPA depends on them for hardening. Track at suite level.
|
||||
|
||||
Source: `nginx.conf`; `architecture.md` § 7 row "Cross-site / clickjack".
|
||||
|
||||
---
|
||||
|
||||
## 10. Audit logging
|
||||
|
||||
- **Server-side concern** — the `admin/`, `flights/`, `annotations/`, etc.
|
||||
services are responsible for audit-event emission.
|
||||
- The SPA does **not** emit audit events directly. It does not maintain
|
||||
any client-side audit log.
|
||||
- The browser console is the only client-side log surface today; no
|
||||
centralized client telemetry (Step 6 surface — `_docs/02_document/deployment/observability.md`).
|
||||
|
||||
Source: `architecture.md` § 7 Audit logging.
|
||||
|
||||
---
|
||||
|
||||
## 11. Image / supply-chain
|
||||
|
||||
### Currently in pipeline
|
||||
|
||||
- Multi-stage Dockerfile: `oven/bun:1.3.11-alpine` (build) →
|
||||
`nginx:alpine` (runtime).
|
||||
- `bun install --frozen-lockfile` enforces lockfile fidelity.
|
||||
- `AZAION_REVISION=$CI_COMMIT_SHA` and OCI labels stamped at push time.
|
||||
|
||||
### Currently MISSING (Step 6 surface)
|
||||
|
||||
- **No vulnerability scan** (Trivy / Grype) on the produced image.
|
||||
- **No SBOM emission** (Syft / cyclonedx).
|
||||
- **No image signing** (cosign).
|
||||
- **No dependency audit step** in CI (`bun audit` equivalent — Bun does
|
||||
not yet have a first-party audit; `npm audit --omit=dev` against the
|
||||
lockfile is a reasonable substitute).
|
||||
|
||||
Source: `.woodpecker/build-arm.yml`; `architecture.md` § 3 "Missing from the
|
||||
pipeline today".
|
||||
|
||||
---
|
||||
|
||||
## 12. Findings → Fix Map
|
||||
|
||||
| Finding | AC | Fix step |
|
||||
|---------|----|----------|
|
||||
| Bootstrap refresh missing `credentials:'include'` (F2) | AC-01 | Step 4 (Code Testability Revision) |
|
||||
| Bearer-in-query SSE — refresh-rotation breaks subscription | AC-24 | Step 8 (Refactor — optional) or Phase B |
|
||||
| Hardcoded OpenWeatherMap key (P10) | AC-20 | Step 4 (env move); Phase B (suite proxy) |
|
||||
| `/admin` route lacks role-gate | AC-22 | Step 4 |
|
||||
| `09_settings` numeric input writes `0` on empty | AC-26 | Step 4 |
|
||||
| `09_settings` save handlers leak `saving:true` on PUT failure | AC-27 | Step 4 |
|
||||
| `AdminPage.handleDeleteClass` lacks ConfirmDialog | AC-30 | Step 4 |
|
||||
| `MediaList` uses `alert()` | AC-14 | Step 4 |
|
||||
| `ConfirmDialog` lacks `aria-modal/role=dialog` | AC-15 | Step 4 / Step 8 |
|
||||
| Header dropdown lacks combobox/expanded/Esc/focus-trap | AC-16 | Step 4 / Step 8 |
|
||||
| Annotation save body missing `Source`, `WaypointId`, wrong `time` field | AC-05 | Step 4 |
|
||||
| `X-Refresh-Token` not sent on long-video detect (#29) | — | Step 4 |
|
||||
| Numeric enum drift (`AnnotationStatus`, `MediaStatus`, `Affiliation`, `CombatReadiness`) | AC-04 | Step 4 (P9 alignment) |
|
||||
| No CSP / hardening headers in `nginx.conf` | — | Step 6 — track at suite level |
|
||||
| No vulnerability scan / SBOM / image signing in CI | — | Phase B |
|
||||
|
||||
---
|
||||
|
||||
## 13. Compliance / standards
|
||||
|
||||
The UI does NOT claim conformance to any specific standard today:
|
||||
|
||||
- **No WCAG-level declaration** (multiple a11y findings recorded).
|
||||
- **No SOC2 / ISO27001 controls** are implemented at the SPA layer
|
||||
(server-side concern of the suite).
|
||||
- **No FIPS / specific crypto-mode requirements** at the SPA layer (TLS
|
||||
is terminated server-side; bearer JWT signing is server-side).
|
||||
|
||||
These are recorded as anti-criteria (AC-N4) — the UI is **internal**,
|
||||
**operator-only**, and **trusts the suite** for compliance enforcement.
|
||||
Phase B may revisit if a regulated deployment surface emerges.
|
||||
Reference in New Issue
Block a user