mirror of
https://github.com/azaion/admin.git
synced 2026-06-21 14:21:09 +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:
@@ -13,31 +13,54 @@ Custom exception type for domain-level errors, paired with an `ExceptionEnum` ca
|
||||
| `GetMessage` | `static string GetMessage(ExceptionEnum exEnum)` | Looks up human-readable message for an error code |
|
||||
|
||||
### ExceptionEnum
|
||||
| Value | Code | Description |
|
||||
|-------|------|-------------|
|
||||
| `NoEmailFound` | 10 | No such email found |
|
||||
| `EmailExists` | 20 | Email already exists |
|
||||
| `WrongPassword` | 30 | Passwords do not match |
|
||||
| `PasswordLengthIncorrect` | 32 | Password should be at least 12 characters (description text — actual validator threshold is 8 chars per `RegisterUserValidator`) |
|
||||
| `EmailLengthIncorrect` | 35 | Email is empty or invalid |
|
||||
| `WrongEmail` | 37 | (no description attribute) |
|
||||
| `UserDisabled` | 38 | User account is disabled |
|
||||
| `NoFileProvided` | 60 | No file provided |
|
||||
| Value | Code | Description | HTTP Status |
|
||||
|-------|------|-------------|-------------|
|
||||
| `NoEmailFound` | 10 | No such email found | 409 |
|
||||
| `EmailExists` | 20 | Email already exists | 409 |
|
||||
| `WrongPassword` | 30 | Passwords do not match | 409 |
|
||||
| `PasswordLengthIncorrect` | 32 | Password should be at least 12 characters | 409 |
|
||||
| `EmailLengthIncorrect` | 35 | Email is empty or invalid | 409 |
|
||||
| `WrongEmail` | 37 | (no description attribute) | 409 |
|
||||
| `UserDisabled` | 38 | User account is disabled | 409 |
|
||||
| `AccountLocked` | 50 | AZ-537 — account temporarily locked due to too many failed login attempts (carries `RetryAfterSeconds`) | **423 Locked** |
|
||||
| `LoginRateLimited` | 51 | AZ-537 — too many login attempts per account; try again later (carries `RetryAfterSeconds`) | **429 Too Many Requests** |
|
||||
| `InvalidRefreshToken` | 52 | AZ-531 — refresh token invalid / expired / revoked / reuse-detected | **401 Unauthorized** |
|
||||
| `SessionNotFound` | 53 | AZ-535 — admin tried to revoke a non-existent session | **404 Not Found** |
|
||||
| `InvalidMissionRequest` | 54 | AZ-533 — mission_id pattern fail or planned_duration_h out of bounds | **400 Bad Request** |
|
||||
| `AircraftNotFound` | 55 | AZ-533 — aircraft id missing or not a `CompanionPC` user | **400 Bad Request** |
|
||||
| `MfaAlreadyEnabled` | 56 | AZ-534 — `/users/me/mfa/enroll` called for a user that already has MFA on | **409 Conflict** |
|
||||
| `MfaNotEnrolling` | 57 | AZ-534 — confirm called without a prior enroll | **409 Conflict** |
|
||||
| `MfaNotEnabled` | 58 | AZ-534 — disable / verify-for-login called for a user without MFA | **409 Conflict** |
|
||||
| `InvalidMfaCode` | 59 | AZ-534 — TOTP code (and recovery code) failed to verify | **401 Unauthorized** |
|
||||
| `NoFileProvided` | 60 | No file provided | 409 |
|
||||
| `InvalidMfaToken` | 61 | AZ-534 — step-1 MFA token failed to validate (signature / audience / expiry) | **401 Unauthorized** |
|
||||
|
||||
> **Cycle 1 (2026-05-13) note** — `HardwareIdMismatch = 40` and `BadHardware = 45` were removed by AZ-197 (admin-side hardware-binding cleanup). Codes 40 and 45 should NOT be reused for a different meaning — older clients may still surface "Hardware mismatch" UX strings keyed on the integer. `UserDisabled = 38` was added earlier (still part of the baseline). See `_docs/03_implementation/batch_06_report.md`.
|
||||
### RetryAfterSeconds
|
||||
|
||||
| Member | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| Constructor | `BusinessException(ExceptionEnum exEnum, int retryAfterSeconds)` | Cycle 2 (AZ-537) — sets `RetryAfterSeconds`, surfaced by `BusinessExceptionHandler` as a `Retry-After` response header. Used by `AccountLocked` (returns remaining lockout seconds) and `LoginRateLimited` (returns the window seconds). |
|
||||
| `RetryAfterSeconds` | `int?` | Optional cooldown hint; null when the exception was constructed without a window. |
|
||||
|
||||
> **Cycle 1 (2026-05-13) note** — `HardwareIdMismatch = 40` and `BadHardware = 45` were removed by AZ-197. Codes 40 and 45 should NOT be reused.
|
||||
>
|
||||
> **Cycle 2 (2026-05-14) note** — `WrongResourceName = 50` was removed along with the `GetResourceRequest` validator (the only consumer). Code 50 should NOT be reused — gap kept per the cycle-1 lesson on retired numeric codes.
|
||||
> **Cycle 2 (2026-05-14) note** — `WrongResourceName = 50` was removed early in the cycle along with the `GetResourceRequest` validator. The integer 50 has since been **reused for `AccountLocked`** as part of AZ-537 (since the previous user-facing string "Wrong resource name" is no longer surfaced anywhere). This is the one deliberate exception to the "gap kept" lesson — the old code had no remaining client surface and the auth modernization wanted a tightly-clustered range of new codes.
|
||||
|
||||
## Internal Logic
|
||||
Static constructor eagerly loads all `ExceptionEnum` descriptions into a dictionary via `EnumExtensions.GetDescriptions<ExceptionEnum>()`. Messages are retrieved by dictionary lookup with fallback to `ToString()`.
|
||||
Static constructor eagerly loads all `ExceptionEnum` descriptions into a dictionary via `EnumExtensions.GetDescriptions<ExceptionEnum>()`. Messages are retrieved by dictionary lookup with fallback to `ToString()`. The two-arg constructor sets `RetryAfterSeconds` for the lockout / rate-limit paths.
|
||||
|
||||
## Dependencies
|
||||
- `EnumExtensions` — for `GetDescriptions<T>()`
|
||||
|
||||
## Consumers
|
||||
- `BusinessExceptionHandler` — catches and serializes to HTTP 409 response
|
||||
- `UserService` — throws for email/password validation failures (`NoEmailFound`, `WrongPassword`, `EmailExists`, `UserDisabled`)
|
||||
- `BusinessExceptionHandler` — catches and maps via `MapStatusCode`. The default mapping is 409; cycle 2 codes use a per-enum status map (`AccountLocked` → 423, `LoginRateLimited` → 429, refresh/MFA validation failures → 401, `SessionNotFound` → 404, mission validation failures → 400, MFA conflict states → 409). When `RetryAfterSeconds > 0` the handler also stamps a `Retry-After` response header.
|
||||
- `UserService` — throws for the auth path (`NoEmailFound`, `WrongPassword`, `EmailExists`, `UserDisabled`, `AccountLocked`, `LoginRateLimited`)
|
||||
- `RefreshTokenService` — throws `InvalidRefreshToken` on bad/expired/reuse-detected
|
||||
- `SessionService` — throws `SessionNotFound` for admin-revoke of missing sids
|
||||
- `MissionTokenService` — throws `InvalidMissionRequest`, `AircraftNotFound`
|
||||
- `MfaService` — throws `MfaAlreadyEnabled`, `MfaNotEnrolling`, `MfaNotEnabled`, `InvalidMfaCode`, `InvalidMfaToken`, `NoEmailFound`, `WrongPassword`
|
||||
- `ResourcesService` — throws `NoFileProvided` for missing file uploads
|
||||
- `Program.cs` `ParseSidClaim` / `ParseUserIdClaim` helpers — throw `InvalidRefreshToken` (401) on missing or malformed claims
|
||||
- FluentValidation validators — reference `ExceptionEnum` codes in `.WithErrorCode()`
|
||||
|
||||
## Data Models
|
||||
@@ -50,7 +73,7 @@ None.
|
||||
None.
|
||||
|
||||
## Security
|
||||
Error codes are returned to the client via `BusinessExceptionHandler`. Codes are numeric and messages are user-facing.
|
||||
Error codes are returned to the client via `BusinessExceptionHandler` along with the per-enum HTTP status. The `Retry-After` header on lockout / rate-limit responses lets well-behaved clients back off without blind retries.
|
||||
|
||||
## Tests
|
||||
None.
|
||||
|
||||
Reference in New Issue
Block a user