[AZ-529] [AZ-530] Cycle-2 documentation refresh

Refreshes _docs/02_document/ to reflect the cycle-2 auth-modernization
+ CMMC hardening landings (AZ-531..AZ-538). Authoritative source for
the ripple set is ripple_log_cycle2.md.

Covered:
- architecture.md (section 1 rewritten, ADRs 6-9 added)
- data_model.md (sessions, audit_events, user columns, migrations)
- system-flows.md (F1 rewritten; F11-F17 added; F2/F7/F9 minor)
- module-layout.md (cycle-2 sub-component table)
- diagrams/flows/flow_login.md (dual-token + MFA)
- components/{01_data_layer,03_auth_and_security,05_admin_api}
- modules/ (12 new, 8 modified — full Argon2id/ES256/MFA/refresh
  /mission/session/audit/jwks rollup)
- tests/{blackbox,security,traceability-matrix}

Step 13 (Update Docs) output for cycle 2.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-14 09:22:53 +03:00
parent c2c659ef62
commit a77b3f8a59
35 changed files with 3624 additions and 468 deletions
@@ -29,43 +29,66 @@
### Entities
> **Cycle 1 (2026-05-13) note** — `DetectionClass` (AZ-513) entity was added. `Resource` (AZ-183) was added then removed in the same cycle (post-cycle-1 revert; security audit F-1 + the OTA delivery model itself was deemed obsolete). The `User.Hardware` column is left in place as a tombstone (nullable, unused) per AZ-197. A UNIQUE INDEX `users_email_uidx` was added on `users.email` (security audit F-3, `env/db/06_users_email_unique.sql`).
> **Cycle 1 (2026-05-13) note** — `DetectionClass` (AZ-513) added; `Resource` (AZ-183) added then reverted same cycle. `User.Hardware` left as a tombstone (AZ-197). UNIQUE INDEX `users_email_uidx` added on `users.email` (security audit F-3, `env/db/06_users_email_unique.sql`).
>
> **Cycle 2 (2026-05-14) note** — `ResourcesConfig.SuiteInstallerFolder` and `SuiteStageInstallerFolder` were removed along with the installer endpoints (`GET /resources/get-installer[/stage]`); the POCO is now a single-property class (`ResourcesFolder`).
> **Cycle 2 — early (2026-05-14)** — `ResourcesConfig.SuiteInstallerFolder` / `SuiteStageInstallerFolder` removed with the installer endpoints; `ResourcesConfig` is now `ResourcesFolder`-only.
>
> **Cycle 2 — Auth Modernization (2026-05-14)** — significant data-layer changes:
> - **`User`** gained `FailedLoginCount`, `LockoutUntil` (AZ-537) and `MfaEnabled`, `MfaSecret` (DataProtection-encrypted), `MfaRecoveryCodes` (jsonb of Argon2id-hashed codes), `MfaEnrolledAt`, `MfaLastUsedWindow` (AZ-534). `PasswordHash` column unchanged in shape but now contains Argon2id PHC strings; legacy SHA-384 base64 values are accepted by `Security.VerifyPassword` and lazily upgraded on next login (AZ-536).
> - **New table `public.sessions`** (AZ-531 / AZ-535) — refresh-token rotation + revocation, mapped via `Common/Entities/Session`.
> - **New table `public.audit_events`** (AZ-537 + AZ-534) — append-only login + MFA event log, mapped via `Common/Entities/AuditEvent`.
> - **New `RoleEnum.Service = 60`** (AZ-535) — verifier-fleet identity used by the `revocationReaderPolicy`.
> - **New configs**: `AuthConfig` (rate limit + lockout + Argon2id parameters), `SessionConfig` (refresh sliding + absolute lifetimes). `JwtConfig` rebuilt around ES256 (`KeysFolder`, `ActiveKid`, `AccessTokenLifetimeMinutes`, `MfaStepTokenLifetimeMinutes`); the legacy `Secret` and `TokenLifetimeHours` fields are no longer read.
> - **Migrations** added: `07_auth_lockout_and_audit.sql`, `08_sessions.sql`, `09_sessions_logout_and_mission.sql`, `10_users_mfa.sql`.
```
User:
Id: Guid (PK)
Email: string (required)
PasswordHash: string (required)
Hardware: string? (optional — TOMBSTONED by AZ-197; nullable, unused; no application code reads or writes)
PasswordHash: string (required, Argon2id PHC; legacy SHA-384 base64 accepted on read, rehashed on next login — AZ-536)
Hardware: string? (TOMBSTONED AZ-197)
Role: RoleEnum (required)
CreatedAt: DateTime (required)
LastLogin: DateTime? (optional)
UserConfig: UserConfig? (optional, JSON-serialized)
IsEnabled: bool (required)
UserConfig:
QueueOffsets: UserQueueOffsets? (optional)
UserQueueOffsets:
AnnotationsOffset: ulong
AnnotationsConfirmOffset: ulong
AnnotationsCommandsOffset: ulong
DetectionClass (AZ-513):
Id: int (PK, DB-assigned identity)
Name, ShortName, Color: string
MaxSizeM: double
PhotoMode: string?
CreatedAt: DateTime
LastLogin: DateTime?
UserConfig: UserConfig?
IsEnabled: bool
FailedLoginCount: int (AZ-537 — reset on successful login)
LockoutUntil: DateTime? (AZ-537 — UTC; "now < LockoutUntil" → AccountLocked)
MfaEnabled: bool (AZ-534)
MfaSecret: string? (AZ-534 — IDataProtector-encrypted base32 TOTP secret)
MfaRecoveryCodes: List<string>? (AZ-534 — jsonb of Argon2id-hashed single-use codes)
MfaEnrolledAt: DateTime?
MfaLastUsedWindow: long? (AZ-534 — anti-replay; last consumed TOTP step)
// Resource entity — REMOVED post-cycle-1 (AZ-183 reverted). The `resources`
// table no longer exists; see env/db/ for the current migration set.
Session (AZ-531 / AZ-535):
Id: Guid (PK — used as the JWT `sid` claim)
UserId: Guid (FK to users)
Class: string ("interactive" | "mission")
RefreshTokenHash: byte[]? (SHA-256 of opaque refresh; null for mission sessions)
RotatedFromTokenId: Guid? (chain pointer for reuse detection)
IssuedAt: DateTime
ExpiresAt: DateTime (sliding for interactive, absolute for mission)
RevokedAt: DateTime?
RevokedReason: string? (one of SessionRevokedReasons)
RevokedByUserId: Guid?
Ip: string?
UserAgent: string?
MfaAuthenticated: bool (AZ-534 — pinned at issue, inherited by rotations)
AircraftId: Guid? (mission-only)
MissionId: string? (mission-only)
RoleEnum: None=0, Operator=10, Validator=20, CompanionPC=30, Admin=40, ResourceUploader=50, ApiAdmin=1000
// ResourceUploader is now data-only — no endpoint policy references it
// after AZ-183 was reverted.
AuditEvent (AZ-537 + AZ-534):
Id: long (PK identity)
EventType: string (one of AuditEventTypes — login_failed/success/lockout, mfa_*)
Email: string (lowercase normalised)
Ip: string?
OccurredAt: DateTime (UTC)
DetectionClass (AZ-513): unchanged
RoleEnum: None=0, Operator=10, Validator=20, CompanionPC=30, Admin=40, ResourceUploader=50, Service=60 (AZ-535), ApiAdmin=1000
// ResourceUploader is data-only since AZ-183 revert.
// Service is the verifier-fleet identity used by revocationReaderPolicy.
```
### Configuration POCOs
@@ -75,16 +98,33 @@ ConnectionStrings:
AzaionDb: string — read-only connection string
AzaionDbAdmin: string — admin (read/write) connection string
JwtConfig:
JwtConfig (AZ-532):
Issuer: string
Audience: string
Secret: string
TokenLifetimeHours: double
KeysFolder: string — directory containing one PEM per kid
ActiveKid: string — selects the signing key
AccessTokenLifetimeMinutes: int — default 15
MfaStepTokenLifetimeMinutes: int — default 5 (AZ-534)
# Secret + TokenLifetimeHours: no longer read; kept only for back-compat deserialisation
SessionConfig (AZ-531):
RefreshSlidingHours: int — sliding window per rotate
RefreshAbsoluteHours: int — hard cap (no rotation past this)
RevokedSnapshotMinutes: int — verifier-poll grace window for /sessions/revoked
AuthConfig (AZ-536 + AZ-537):
PasswordHashing: { TimeCost, MemoryCostKiB, Parallelism } — Argon2id parameters
RateLimit:
PerIpPermitLimit: int
PerIpWindowSeconds: int
PerAccountWindowSeconds: int
PerAccountFailedThreshold: int
Lockout:
ConsecutiveFailureThreshold: int
LockoutSeconds: int
ResourcesConfig:
ResourcesFolder: string
# SuiteInstallerFolder / SuiteStageInstallerFolder removed in cycle 2 with the installer endpoints.
# EncryptionMasterKey was added by AZ-183 and removed in the post-cycle-1 revert.
```
## 3. External API Specification
@@ -97,25 +137,34 @@ N/A — internal component.
| Query | Frequency | Hot Path | Index Needed |
|-------|-----------|----------|--------------|
| `SELECT * FROM users WHERE email = ?` | High | Yes | Yes — UNIQUE INDEX `users_email_uidx` on `email` (security audit F-3, `env/db/06_users_email_unique.sql`) |
| `SELECT * FROM users WHERE email = ?` | High | Yes | Yes — UNIQUE INDEX `users_email_uidx` on `email` |
| `SELECT * FROM users` with optional filters | Medium | No | No |
| `UPDATE users SET ... WHERE email = ?` | Medium | No | No |
| `INSERT INTO users` | Low | No | No (UNIQUE INDEX above also enforces single-row-per-email atomically) |
| `INSERT INTO users` | Low | No | UNIQUE INDEX above |
| `DELETE FROM users WHERE email = ?` | Low | No | No |
| `SELECT * FROM sessions WHERE refresh_token_hash = ?` (AZ-531) | High | Yes | Yes — UNIQUE INDEX on `refresh_token_hash` (`08_sessions.sql`) |
| `UPDATE sessions SET revoked_at..., revoked_reason... WHERE id = ?` (AZ-535) | Medium | No | PK |
| `UPDATE sessions SET revoked_... WHERE user_id = ? AND revoked_at IS NULL` (AZ-535 logout/all) | Low | No | INDEX on `(user_id, revoked_at)` |
| `UPDATE sessions SET revoked_... WHERE aircraft_id = ? AND class='mission' AND revoked_at IS NULL` (AZ-533) | Low | No | INDEX on `(aircraft_id, class, revoked_at)` |
| `SELECT ... FROM sessions WHERE revoked_at >= ? AND expires_at > now()` (AZ-535 verifier poll) | High | Yes | INDEX on `revoked_at` |
| `SELECT count(*) FROM audit_events WHERE event_type='login_failed' AND email=? AND occurred_at >= ?` (AZ-537) | High | Yes | INDEX on `(email, event_type, occurred_at)` |
| `INSERT INTO audit_events (...)` (AZ-537 / AZ-534) | High | Yes | n/a |
### Caching Strategy
| Data | Cache Type | TTL | Invalidation |
|------|-----------|-----|-------------|
| User by email | In-memory (LazyCache) | 4 hours | On `UpdateQueueOffsets` (post-AZ-197 — hardware paths gone) |
| User by email | In-memory (LazyCache) | 4 hours | On `UpdateQueueOffsets`, on lazy-rehash (AZ-536), on MFA enroll/confirm/disable (AZ-534), on user enable/disable, on lockout state changes (AZ-537) |
> The `Resources.Latest.{arch}.{stage}` cache key (added by AZ-183) was removed in the post-cycle-1 revert.
> Refresh tokens, sessions, and audit events are NOT cached — they are read directly from Postgres on every request. The verifier-poll snapshot (`/sessions/revoked`) is the only "edge" cache and lives in the verifier process, not in this component.
### Storage Estimates
| Table | Est. Row Count (1yr) | Row Size | Total Size | Growth Rate |
|-------|---------------------|----------|------------|-------------|
| `users` | 1001000 web users + 200010000 CompanionPC device users (AZ-196 grows this) | ~500 bytes | ~5 MB | Medium (device fleet) |
| `users` | 1001000 web users + 200010000 CompanionPC device users | ~700 bytes (post-MFA columns) | ~7 MB | Medium |
| `sessions` (AZ-531) | 30 d retention (`RefreshAbsoluteHours`) × N active sessions per user × pruning job | ~400 bytes | ~50 MB ceiling | High during active fleet ops; bounded by retention |
| `audit_events` (AZ-537) | ~50 events/user/day × ~5000 users × 365 d | ~150 bytes | ~14 GB/yr | High — partition or archive after 90 d (operational follow-up) |
| `detection_classes` (AZ-513) | 10200 | ~250 bytes | ~50 KB | Low |
### Data Management
@@ -182,12 +231,15 @@ N/A — internal component.
## Modules Covered
- `Common/Configs/ConnectionStrings`
- `Common/Configs/JwtConfig`
- `Common/Configs/JwtConfig` *(AZ-532 — ES256 + session config)*
- `Common/Configs/AuthConfig` *(new in cycle 2 — AZ-536 + AZ-537)*
- `Common/Configs/ResourcesConfig`
- `Common/Entities/User`
- `Common/Entities/RoleEnum`
- `Common/Entities/User` *(extended in cycle 2 — AZ-537 + AZ-534)*
- `Common/Entities/RoleEnum` *(extended in cycle 2 — AZ-535 added `Service`)*
- `Common/Entities/Session` *(new in cycle 2 — AZ-531 + AZ-535)*
- `Common/Entities/AuditEvent` *(new in cycle 2 — AZ-537)*
- `Common/Entities/DetectionClass` *(added cycle 1, AZ-513)*
- `Common/Database/AzaionDb` (now also holds the `DetectionClasses` table; the `Resources` ITable added by AZ-183 was removed in the post-cycle-1 revert)
- `Common/Database/AzaionDbSchemaHolder`
- `Common/Database/AzaionDb` (`Sessions` and `AuditEvents` ITables added in cycle 2)
- `Common/Database/AzaionDbSchemaHolder` (Session + AuditEvent mappings, jsonb for `MfaRecoveryCodes`)
- `Common/Database/DbFactory`
- `Services/Cache`