mirror of
https://github.com/azaion/admin.git
synced 2026-06-21 21:11:08 +00:00
[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:
@@ -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` | 100–1000 web users + 2000–10000 CompanionPC device users (AZ-196 grows this) | ~500 bytes | ~5 MB | Medium (device fleet) |
|
||||
| `users` | 100–1000 web users + 2000–10000 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) | 10–200 | ~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`
|
||||
|
||||
Reference in New Issue
Block a user