mirror of
https://github.com/azaion/admin.git
synced 2026-06-21 15:31: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:
@@ -2,133 +2,201 @@
|
||||
|
||||
## 1. High-Level Overview
|
||||
|
||||
**Purpose**: HTTP API entry point — configures DI, middleware pipeline, authentication, authorization, CORS, Swagger, and defines all REST endpoints using ASP.NET Core Minimal API.
|
||||
**Purpose**: HTTP API entry point — configures DI, middleware pipeline, authentication, authorization, CORS, HSTS, HTTPS redirection, rate limiting, Swagger, DataProtection, and defines all REST endpoints using ASP.NET Core Minimal API.
|
||||
|
||||
**Architectural Pattern**: Composition root + Minimal API endpoints — top-level statements configure the application and map HTTP routes to service methods.
|
||||
**Architectural Pattern**: Composition root + Minimal API endpoints — top-level statements configure the application and map HTTP routes to service methods. A static `IssueDualTokens` helper centralises the access+refresh issuance pattern shared by `/login` (no MFA) and `/login/mfa` (with MFA), and a tiny `ParseSidClaim` / `ParseUserIdClaim` pair extracts session/user identity from the request principal.
|
||||
|
||||
**Upstream dependencies**: User Management (IUserService), Authentication & Security (IAuthService, Security), Resource Management (IResourcesService), Data Layer (IDbFactory, ICache, configs).
|
||||
**Upstream dependencies**: Authentication & Security (AuthService, RefreshTokenService, SessionService, MissionTokenService, MfaService, JwtSigningKeyProvider, AuditLog, Security), User Management (IUserService), Resource Management (IResourcesService), Detection Classes (IDetectionClassService), Data Layer (IDbFactory, ICache, all configs).
|
||||
|
||||
**Downstream consumers**: None (top-level entry point, consumed by HTTP clients).
|
||||
**Downstream consumers**: HTTP clients (admin web UI, verifier services, CompanionPC).
|
||||
|
||||
## 2. Internal Interfaces
|
||||
|
||||
### BusinessExceptionHandler
|
||||
|
||||
| Method | Input | Output | Async | Error Types |
|
||||
|--------|-------|--------|-------|-------------|
|
||||
| `TryHandleAsync` | `HttpContext, Exception, CancellationToken` | `bool` | Yes | None |
|
||||
| Method | Input | Output | Async |
|
||||
|--------|-------|--------|-------|
|
||||
| `TryHandleAsync` | `HttpContext`, `Exception`, `CancellationToken` | `bool` | Yes |
|
||||
|
||||
Converts `BusinessException` to HTTP 409 JSON response: `{ ErrorCode: int, Message: string }`.
|
||||
Cycle 2 (AZ-537 / AZ-531 / AZ-533 / AZ-534 / AZ-535) — the handler now maps `BusinessException` → an exception-specific HTTP status code via a `MapStatusCode` switch, preserves the legacy `409 Conflict` default, and stamps a `Retry-After` response header when `RetryAfterSeconds` is set. It also handles `BadHttpRequestException` → `400 Bad Request` with `{ ErrorCode: 0, Message }` so malformed payloads have a consistent shape with business errors.
|
||||
|
||||
| `ExceptionEnum` | HTTP status |
|
||||
|-----------------|-------------|
|
||||
| `AccountLocked` | `423 Locked` |
|
||||
| `LoginRateLimited` | `429 Too Many Requests` |
|
||||
| `InvalidRefreshToken` / `InvalidMfaCode` / `InvalidMfaToken` | `401 Unauthorized` |
|
||||
| `SessionNotFound` | `404 Not Found` |
|
||||
| `InvalidMissionRequest` / `AircraftNotFound` | `400 Bad Request` |
|
||||
| `MfaAlreadyEnabled` / `MfaNotEnrolling` / `MfaNotEnabled` | `409 Conflict` |
|
||||
| any other | `409 Conflict` (legacy default) |
|
||||
|
||||
### Static helpers in `Program.cs`
|
||||
|
||||
- `IssueDualTokens(user, authService, refreshTokens, sessionService, amr, ct)` — issues a refresh token + an access token, also auto-revokes any open mission sessions if the just-authenticated user is a `CompanionPC` (AZ-533 AC-4).
|
||||
- `ParseSidClaim(ClaimsPrincipal)` / `ParseUserIdClaim(ClaimsPrincipal)` — read `sid` / `nameid` claims; throw `BusinessException(InvalidRefreshToken)` (→ 401) on missing/malformed.
|
||||
|
||||
## 3. External API Specification
|
||||
|
||||
> **Cycle 1 (2026-05-13) note** — endpoints below reflect the post-cycle-1 surface (AZ-513 Detection Classes CRUD, AZ-196 device auto-provisioning, AZ-197 hardware-binding removal). AZ-183 (OTA) shipped in cycle 1 but was reverted later the same day after the security audit (finding F-1) — the OTA delivery model itself was deemed obsolete. For per-endpoint cycle origins see `modules/admin_api_program.md`.
|
||||
> **Cycle 2 (2026-05-14) — auth modernization**: `/login` is now multi-shape (MFA branch); `/login/mfa`, `/token/refresh`, `/logout`, `/logout/all`, `/sessions/*`, `/users/me/mfa/*`, `/.well-known/jwks.json` are all new. The legacy "single JWT" response is preserved as a `Token` getter on `LoginResponse` for compatibility with old clients (= same value as `AccessToken`).
|
||||
|
||||
### Authentication & Sessions
|
||||
|
||||
| Endpoint | Method | Auth | Cycle | Description |
|
||||
|----------|--------|------|-------|-------------|
|
||||
| `/login` | POST | Anonymous | AZ-531/534/537 | Validates credentials. Returns `LoginResponse` (access + refresh + sid) OR `MfaRequiredResponse` (`mfa_required: true`, short-lived `mfa_token`). Per-IP rate limited. |
|
||||
| `/login/mfa` | POST | Anonymous | AZ-534 | Validates the step-1 `mfa_token` + the user's TOTP / recovery code. Returns `LoginResponse`. Per-IP rate limited. |
|
||||
| `/token/refresh` | POST | Anonymous | AZ-531 | Rotates a refresh token. Reuse of a rotated token revokes the entire session family. |
|
||||
| `/logout` | POST | Authenticated | AZ-535 | Revokes the caller's current `sid` (idempotent). |
|
||||
| `/logout/all` | POST | Authenticated | AZ-535 | Revokes every active session for the caller's user. |
|
||||
| `/sessions/{sid:guid}/revoke` | POST | ApiAdmin | AZ-535 | Admin-revoke by session id. |
|
||||
| `/sessions/revoked` | GET | revocationReader (Service or ApiAdmin) | AZ-535 | Verifier-poll snapshot of revoked sessions still within their TTL. `since` is clamped to a 12 h floor to prevent table scans. |
|
||||
| `/sessions/mission` | POST | Authenticated | AZ-533 | Pilot issues a long-lived no-refresh mission token bound to one aircraft + one mission. |
|
||||
| `/.well-known/jwks.json` | GET | Anonymous | AZ-532 | All loaded ES256 public keys (active + retiring). `Cache-Control: public, max-age=3600`. |
|
||||
|
||||
### MFA
|
||||
|
||||
### Authentication
|
||||
| Endpoint | Method | Auth | Description |
|
||||
|----------|--------|------|-------------|
|
||||
| `/login` | POST | Anonymous | Validates credentials, returns JWT |
|
||||
| `/users/me/mfa/enroll` | POST | Authenticated | Starts TOTP enrollment, returns secret + otpauth URL + PNG QR. |
|
||||
| `/users/me/mfa/confirm` | POST | Authenticated | Confirms with a TOTP code. Returns `{ mfaEnabled: true }`. |
|
||||
| `/users/me/mfa/disable` | POST | Authenticated | Requires password + TOTP. Returns `{ mfaEnabled: false }`. |
|
||||
|
||||
### User Management
|
||||
|
||||
| Endpoint | Method | Auth | Description |
|
||||
|----------|--------|------|-------------|
|
||||
| `/users` | POST | ApiAdmin | Creates a new user |
|
||||
| `/devices` | POST | ApiAdmin | **AZ-196**: provisions a CompanionPC device user (returns serial + email + plaintext password once) |
|
||||
| `/users/current` | GET | Authenticated | Returns current user |
|
||||
| `/users` | GET | ApiAdmin | Lists users (optional email/role filters) |
|
||||
| `/users/queue-offsets/set` | PUT | Authenticated | Updates queue offsets |
|
||||
| `/users/{email}/set-role/{role}` | PUT | ApiAdmin | Changes user role |
|
||||
| `/users/{email}/enable` | PUT | ApiAdmin | Enables user |
|
||||
| `/users/{email}/disable` | PUT | ApiAdmin | Disables user |
|
||||
| `/users/{email}` | DELETE | ApiAdmin | Removes user |
|
||||
|
||||
**Removed by AZ-197**: `PUT /users/hardware/set` (Hardware-binding feature deleted)
|
||||
| `/users` | POST | ApiAdmin | Creates a new user (Argon2id-hashed password, AZ-536). |
|
||||
| `/devices` | POST | ApiAdmin | Provisions a CompanionPC device user (returns serial + email + plaintext password once). |
|
||||
| `/users/current` | GET | Authenticated | Returns current user. |
|
||||
| `/users` | GET | ApiAdmin | Lists users (optional email/role filters). |
|
||||
| `/users/queue-offsets/set` | PUT | Authenticated | Updates queue offsets. |
|
||||
| `/users/{email}/set-role/{role}` | PUT | ApiAdmin | Changes user role. |
|
||||
| `/users/{email}/enable` | PUT | ApiAdmin | Enables user. |
|
||||
| `/users/{email}/disable` | PUT | ApiAdmin | Disables user (revokes all active sessions for that user via `SessionService`). |
|
||||
| `/users/{email}` | DELETE | ApiAdmin | Removes user. |
|
||||
|
||||
### Resource Management
|
||||
|
||||
| Endpoint | Method | Auth | Description |
|
||||
|----------|--------|------|-------------|
|
||||
| `/resources/{dataFolder?}` | POST | Authenticated | Uploads a file (up to 200 MB) |
|
||||
| `/resources/list/{dataFolder?}` | GET | Authenticated | Lists files |
|
||||
| `/resources/clear/{dataFolder?}` | POST | ApiAdmin | Clears folder |
|
||||
|
||||
**Removed by AZ-197**: `POST /resources/check` (was the hardware-binding side-effect probe).
|
||||
**Removed in post-cycle-1 revert**: `POST /get-update` and `POST /resources/publish` (AZ-183 reverted — security audit F-1; OTA delivery model itself obsolete).
|
||||
**Removed in cycle 2 (2026-05-14)**: `POST /resources/get/{dataFolder?}`, `GET /resources/get-installer`, `GET /resources/get-installer/stage` — all obsolete; the encrypted-download support stack (`Security.GetApiEncryptionKey` / `EncryptTo` / `DecryptTo`, `ResourcesService.GetEncryptedResource` / `GetInstaller`, `GetResourceRequest`, `WrongResourceName = 50`, `ResourcesConfig.SuiteInstallerFolder` / `SuiteStageInstallerFolder`) was removed with them. ADR-003 retired.
|
||||
| `/resources/{dataFolder?}` | POST | Authenticated | Uploads a file (up to 200 MB). |
|
||||
| `/resources/list/{dataFolder?}` | GET | Authenticated | Lists files. |
|
||||
| `/resources/clear/{dataFolder?}` | POST | ApiAdmin | Clears folder. |
|
||||
|
||||
### Detection Classes
|
||||
|
||||
| Endpoint | Method | Auth | Description |
|
||||
|----------|--------|------|-------------|
|
||||
| `/classes` | POST | ApiAdmin | **AZ-513**: creates a detection class |
|
||||
| `/classes/{id:int}` | PATCH | ApiAdmin | **AZ-513**: partial-merge update of a detection class |
|
||||
| `/classes/{id:int}` | DELETE | ApiAdmin | **AZ-513**: deletes a detection class |
|
||||
| `/classes` | POST | ApiAdmin | Creates a detection class. |
|
||||
| `/classes/{id:int}` | PATCH | ApiAdmin | Partial-merge update. |
|
||||
| `/classes/{id:int}` | DELETE | ApiAdmin | Deletes a detection class. |
|
||||
|
||||
### Health
|
||||
|
||||
| Endpoint | Method | Auth | Description |
|
||||
|----------|--------|------|-------------|
|
||||
| `/health/live` | GET | Anonymous (excluded from Swagger) | Process liveness; never touches DB. |
|
||||
| `/health/ready` | GET | Anonymous (excluded from Swagger) | Pings both DB connections with a 2 s timeout; 503 on failure. |
|
||||
|
||||
### Authorization Policies
|
||||
- **apiAdminPolicy**: requires `ApiAdmin` role (used on most admin endpoints)
|
||||
|
||||
> The `apiUploaderPolicy` was added by AZ-183 and removed in the post-cycle-1 revert along with the OTA endpoints it guarded. `RoleEnum.ResourceUploader` remains as data only.
|
||||
| Policy | Roles | Notes |
|
||||
|--------|-------|-------|
|
||||
| `apiAdminPolicy` | `ApiAdmin` | The "admin endpoints" policy. |
|
||||
| `revocationReaderPolicy` | `Service`, `ApiAdmin` | AZ-535 — verifier services authenticate as `Service`-role identities and are the only callers (besides admin) allowed to read `/sessions/revoked`. |
|
||||
|
||||
### CORS
|
||||
- Allowed origins: `https://admin.azaion.com`, `http://admin.azaion.com`
|
||||
- All methods/headers, credentials allowed
|
||||
> The `apiUploaderPolicy` from AZ-183 was removed in the post-cycle-1 revert. `RoleEnum.ResourceUploader` remains as data only.
|
||||
|
||||
### CORS, HSTS, HTTPS (AZ-538)
|
||||
|
||||
- **CORS** — single origin `https://admin.azaion.com`, `AllowAnyMethod` + `AllowAnyHeader` + `AllowCredentials`. The legacy `http://` origin combined with credentials would have permitted credentialed cleartext traffic; cycle 2 removed it.
|
||||
- **HSTS** — non-Development only: 1 y `MaxAge`, `IncludeSubDomains`, `Preload`.
|
||||
- **HTTPS redirection** — non-Development only. Development skips both so `dotnet watch` on plain HTTP keeps working.
|
||||
|
||||
### Rate limiting (AZ-537)
|
||||
|
||||
- **Per-IP** — ASP.NET Core `RateLimiter` middleware with a `SlidingWindowRateLimiter`. Policy `login-per-ip` is attached to `/login` and `/login/mfa`. Permit limit + window seconds come from `AuthConfig.RateLimit`. Rejection sets `429` and stamps `Retry-After`.
|
||||
- **Per-account** — DB-backed sliding-window check in `UserService.ValidateUser` via `IAuditLog.CountRecentFailedLogins`. Survives process restarts.
|
||||
- **Per-account lockout** — `LockoutOptions` in `AuthConfig`. N consecutive failures → `LockoutUntil`; subsequent logins throw `AccountLocked` with `RetryAfterSeconds`.
|
||||
|
||||
## 4. Data Access Patterns
|
||||
|
||||
No direct data access — delegates to service components.
|
||||
No direct data access — delegates to service components. The composition root also fail-fast checks on missing connection strings (`AzaionDb`, `AzaionDbAdmin`) and missing `JwtConfig` (`Issuer` + `Audience` required).
|
||||
|
||||
## 5. Implementation Details
|
||||
|
||||
**State Management**: Stateless — ASP.NET Core request pipeline.
|
||||
|
||||
**DI registrations added in cycle 2**:
|
||||
- `IJwtSigningKeyProvider` (singleton, eager-built before DI so it's the same instance JwtBearer's `IssuerSigningKeyResolver` uses)
|
||||
- `IRefreshTokenService`, `ISessionService`, `IMissionTokenService`, `IMfaService` (scoped)
|
||||
- `IAuditLog` (scoped)
|
||||
- `IDataProtectionProvider` via `AddDataProtection().SetApplicationName("Azaion.AdminApi")` — production deployments MUST set `DataProtection:KeysFolder` to a persistent volume so encrypted MFA secrets survive restarts.
|
||||
|
||||
**Middleware pipeline (cycle 2 order)**:
|
||||
1. `UseSwagger`/`UseSwaggerUI` (Development only)
|
||||
2. `UseHsts` + `UseHttpsRedirection` (non-Development only)
|
||||
3. `UseCors("AdminCorsPolicy")`
|
||||
4. `UseAuthentication`
|
||||
5. `UseAuthorization`
|
||||
6. `UseRateLimiter`
|
||||
7. `UseRewriter` (root → `/swagger`)
|
||||
8. Endpoint mappings
|
||||
9. `UseExceptionHandler` (registered last so the `BusinessExceptionHandler` exception-handler component runs)
|
||||
|
||||
**JWT Bearer config**:
|
||||
- `ValidAlgorithms = [SecurityAlgorithms.EcdsaSha256]` — pinned to ES256 so a token forged with `alg=HS256` using the public key as the HMAC secret cannot pass validation (AZ-532 AC-5).
|
||||
- `IssuerSigningKeyResolver` consults the same `IJwtSigningKeyProvider` instance the rest of the app uses; if the token has a `kid` it's matched, otherwise all loaded keys are returned.
|
||||
- `ValidateIssuer`, `ValidateAudience`, `ValidateLifetime`, `ValidateIssuerSigningKey` all true.
|
||||
|
||||
**Key Dependencies**:
|
||||
|
||||
| Library | Version | Purpose |
|
||||
|---------|---------|---------|
|
||||
| Swashbuckle.AspNetCore | 10.1.4 | Swagger/OpenAPI documentation |
|
||||
| FluentValidation.AspNetCore | 11.3.0 | Request validation pipeline |
|
||||
| Serilog | 4.1.0 | Structured logging |
|
||||
| Serilog.Sinks.Console | 6.0.0 | Console log output |
|
||||
| Serilog.Sinks.File | 6.0.0 | Rolling file log output |
|
||||
| Library | Purpose |
|
||||
|---------|---------|
|
||||
| Microsoft.AspNetCore.Authentication.JwtBearer | JWT bearer middleware |
|
||||
| Microsoft.AspNetCore.RateLimiting | Per-IP sliding window |
|
||||
| Microsoft.AspNetCore.DataProtection | Encrypt MFA secrets at rest |
|
||||
| Microsoft.AspNetCore.Rewrite | `/` → `/swagger` redirect |
|
||||
| Swashbuckle.AspNetCore | Swagger/OpenAPI |
|
||||
| FluentValidation.AspNetCore | Request validation pipeline |
|
||||
| Serilog | Structured logging (Console + rolling file) |
|
||||
|
||||
**Error Handling Strategy**:
|
||||
- `BusinessException` → `BusinessExceptionHandler` → HTTP 409 with JSON body.
|
||||
- `UnauthorizedAccessException` → thrown in resource endpoints when current user is null.
|
||||
- `FileNotFoundException` → thrown when installer not found.
|
||||
- FluentValidation errors → automatic 400 Bad Request via middleware.
|
||||
- Unhandled exceptions → default ASP.NET Core exception handling.
|
||||
- `BusinessException` → `BusinessExceptionHandler` → per-enum status code (see table above) + optional `Retry-After`.
|
||||
- `BadHttpRequestException` → `400 Bad Request` with `{ ErrorCode: 0, Message }`.
|
||||
- FluentValidation errors → 400 via `Results.ValidationProblem`.
|
||||
- Unhandled → default ASP.NET Core handling.
|
||||
|
||||
## 6. Extensions and Helpers
|
||||
|
||||
None.
|
||||
- `IssueDualTokens` static helper (Program.cs)
|
||||
- `ParseSidClaim` / `ParseUserIdClaim` static helpers (Program.cs)
|
||||
|
||||
## 7. Caveats & Edge Cases
|
||||
|
||||
**Known limitations**:
|
||||
- All endpoints are defined in a single `Program.cs` file — no route grouping or controller separation.
|
||||
- Swagger UI only available in Development environment.
|
||||
- CORS origins are hardcoded (not configurable).
|
||||
- Antiforgery disabled for resource upload endpoint.
|
||||
- Root URL (`/`) redirects to `/swagger`.
|
||||
|
||||
**Performance bottlenecks**:
|
||||
- Kestrel max request body: 200 MB — allows large file uploads but could be a memory concern.
|
||||
- All endpoints are still defined in a single `Program.cs` file — cycle 2 added significantly more endpoints; consider splitting into endpoint groups in a future cycle.
|
||||
- Swagger UI only available in Development.
|
||||
- CORS origins are hardcoded — moving to config is a follow-up.
|
||||
- `BusinessExceptionHandler` lives under namespace `Azaion.Common` despite the file path `Azaion.AdminApi/`. Documented as historical accident; do not "fix" without coordinated rename.
|
||||
- Antiforgery disabled on resource upload.
|
||||
- Kestrel max request body 200 MB.
|
||||
- The eager `JwtSigningKeyProvider` construction means a missing or malformed PEM crashes the app at startup. This is intentional — it's safer than serving requests with no signing key.
|
||||
|
||||
## 8. Dependency Graph
|
||||
|
||||
**Must be implemented after**: All other components (composition root).
|
||||
|
||||
**Can be implemented in parallel with**: Nothing — depends on all services.
|
||||
|
||||
**Blocks**: Nothing.
|
||||
|
||||
## 9. Logging Strategy
|
||||
|
||||
| Log Level | When | Example |
|
||||
|-----------|------|---------|
|
||||
| WARN | Business exception caught | `BusinessExceptionHandler` logs the exception |
|
||||
| INFO | Serilog minimum level | General application events |
|
||||
| Log Level | When | Notes |
|
||||
|-----------|------|-------|
|
||||
| `Warning` | Business exception caught by `BusinessExceptionHandler` | Includes the full exception |
|
||||
| `Warning` | `BadHttpRequestException` caught | |
|
||||
| `Information` | Default for everything else | Serilog minimum level |
|
||||
|
||||
**Log format**: Serilog structured logging with context enrichment.
|
||||
|
||||
**Log storage**: Console + rolling file (`logs/log.txt`, daily rotation).
|
||||
|
||||
## Modules Covered
|
||||
|
||||
Reference in New Issue
Block a user