mirror of
https://github.com/azaion/admin.git
synced 2026-06-21 17:31:10 +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:
@@ -5,28 +5,43 @@ Application entry point: configures DI, middleware, authentication, authorizatio
|
||||
|
||||
## Public Interface (HTTP Endpoints)
|
||||
|
||||
> **Cycle 1 (2026-05-13) note** — endpoint surface changed by AZ-513 (detection-class CRUD), AZ-196 (device auto-registration), AZ-197 (hardware-binding removal). AZ-183 (OTA update check + publish) was reverted later the same day after the security audit (finding F-1) — the OTA delivery model itself was deemed obsolete; see `_docs/05_security/security_report.md` for context. The table reflects the post-cycle-1 state including that revert.
|
||||
> **Cycle 1 (2026-05-13) note** — endpoint surface changed by AZ-513 (detection-class CRUD), AZ-196 (device auto-registration), AZ-197 (hardware-binding removal). AZ-183 (OTA update check + publish) was reverted later the same day after the security audit (finding F-1).
|
||||
>
|
||||
> **Cycle 2 (2026-05-14) note** — three more endpoints were removed as obsolete: `POST /resources/get/{dataFolder?}`, `GET /resources/get-installer`, `GET /resources/get-installer/stage`. The encrypted-download support stack (`Security.GetApiEncryptionKey` / `EncryptTo` / `DecryptTo`, `ResourcesService.GetEncryptedResource` / `GetInstaller`, `GetResourceRequest` DTO, `WrongResourceName = 50` enum value, `ResourcesConfig.SuiteInstallerFolder` / `SuiteStageInstallerFolder`) went with them. ADR-003 in `architecture.md` was retired in the same change.
|
||||
> **Cycle 2 (2026-05-14) note A** — three resource endpoints removed as obsolete: `POST /resources/get/{dataFolder?}`, `GET /resources/get-installer`, `GET /resources/get-installer/stage`. The encrypted-download support stack went with them. ADR-003 in `architecture.md` was retired.
|
||||
>
|
||||
> **Cycle 2 (2026-05-14) note B (auth modernization)** — eight endpoints added or replaced as part of Epic AZ-529 (Auth Modernization) + AZ-530 (CMMC Hardening). The `/login` shape is now dual-token (access + refresh) when MFA is off, or `MfaRequiredResponse` when MFA is enabled. CORS dropped the cleartext origin (AZ-538). HSTS + HTTPS redirection are wired in non-Development environments. Per-IP sliding-window rate limit added to `/login` (and `/login/mfa`). Public-key JWKS feed live at `/.well-known/jwks.json` (AZ-532).
|
||||
|
||||
| Method | Path | Auth | Summary | Cycle 1 origin |
|
||||
|--------|------|------|---------|----------------|
|
||||
| POST | `/login` | Anonymous | Validates credentials, returns JWT token | — |
|
||||
| POST | `/users` | ApiAdmin | Creates a new user | — |
|
||||
| POST | `/devices` | ApiAdmin | Creates a CompanionPC device user (auto serial / email / 32-hex password) | AZ-196 |
|
||||
| GET | `/users/current` | Any authenticated | Returns current user from JWT claims | — |
|
||||
| GET | `/users` | ApiAdmin | Lists users with optional email/role filters | — |
|
||||
| PUT | `/users/queue-offsets/set` | Any authenticated | Updates user's queue offsets | — |
|
||||
| PUT | `/users/{email}/set-role/{role}` | ApiAdmin | Changes a user's role | — |
|
||||
| PUT | `/users/{email}/enable` | ApiAdmin | Enables a user account | — |
|
||||
| PUT | `/users/{email}/disable` | ApiAdmin | Disables a user account | — |
|
||||
| DELETE | `/users/{email}` | ApiAdmin | Removes a user | — |
|
||||
| POST | `/resources/{dataFolder?}` | Any authenticated | Uploads a resource file | — |
|
||||
| GET | `/resources/list/{dataFolder?}` | Any authenticated | Lists files in a resource folder | — |
|
||||
| POST | `/resources/clear/{dataFolder?}` | ApiAdmin | Clears a resource folder | — |
|
||||
| POST | `/classes` | ApiAdmin | Creates a detection class | AZ-513 |
|
||||
| PATCH | `/classes/{id:int}` | ApiAdmin | Updates a detection class (partial-merge) | AZ-513 |
|
||||
| DELETE | `/classes/{id:int}` | ApiAdmin | Deletes a detection class | AZ-513 |
|
||||
| Method | Path | Auth | Summary | Cycle origin |
|
||||
|--------|------|------|---------|--------------|
|
||||
| GET | `/health/live` | Anonymous | Liveness check (`Cache-Control: no-store`); excluded from Swagger | AZ-510 |
|
||||
| GET | `/health/ready` | Anonymous | Readiness check — pings both DB connections with a 2-s timeout; 503 with reason on failure | AZ-510 |
|
||||
| POST | `/login` | Anonymous + per-IP rate limit | Validates credentials. Returns `LoginResponse` (access + refresh) when MFA is off, `MfaRequiredResponse` when MFA is enabled. | AZ-531 / AZ-534 / AZ-537 |
|
||||
| POST | `/login/mfa` | Anonymous + per-IP rate limit | Second-factor verification (TOTP or recovery code). Returns `LoginResponse`. | AZ-534 |
|
||||
| POST | `/token/refresh` | Anonymous (token in body) | Rotates a refresh token; returns a fresh `LoginResponse`. Reuse-detection kills the family. | AZ-531 |
|
||||
| POST | `/logout` | Authenticated | Revokes the caller's current session (idempotent — returns `{ alreadyRevoked }`). | AZ-535 |
|
||||
| POST | `/logout/all` | Authenticated | Revokes every active session for the caller's user (returns `{ revoked: N }`). | AZ-535 |
|
||||
| POST | `/sessions/{sid:guid}/revoke` | ApiAdmin | Admin revoke-by-session-id. | AZ-535 |
|
||||
| GET | `/sessions/revoked` | revocationReader (Service or ApiAdmin) | Verifier-poll snapshot of revoked-but-not-yet-expired sessions. `Cache-Control: no-cache`; `since` clamped to `now - 12h`. | AZ-535 |
|
||||
| POST | `/sessions/mission` | Authenticated | Mints a long-lived no-refresh mission token bound to one aircraft. AZ-533 AC-6 step-up MFA gate is a TODO comment until org-wide MFA adoption. | AZ-533 |
|
||||
| POST | `/users/me/mfa/enroll` | Authenticated | Returns TOTP secret + otpauth URL + QR PNG + 10 recovery codes (ONCE). | AZ-534 |
|
||||
| POST | `/users/me/mfa/confirm` | Authenticated | Validates one TOTP code and flips `mfa_enabled=true`. | AZ-534 |
|
||||
| POST | `/users/me/mfa/disable` | Authenticated | Removes MFA (requires password + valid code). | AZ-534 |
|
||||
| GET | `/.well-known/jwks.json` | Anonymous (excluded from Swagger) | Public JWKS feed for verifiers; `Cache-Control: public, max-age=3600`. | AZ-532 |
|
||||
| POST | `/users` | ApiAdmin | Creates a new user. | — |
|
||||
| POST | `/devices` | ApiAdmin | Creates a CompanionPC device user (auto serial / email / 32-hex password). | AZ-196 |
|
||||
| GET | `/users/current` | Any authenticated | Returns current user from JWT claims. | — |
|
||||
| GET | `/users` | ApiAdmin | Lists users with optional email/role filters. | — |
|
||||
| PUT | `/users/queue-offsets/set` | Any authenticated | Updates user's queue offsets. | — |
|
||||
| PUT | `/users/{email}/set-role/{role}` | ApiAdmin | Changes a user's role. | — |
|
||||
| PUT | `/users/{email}/enable` | ApiAdmin | Enables a user account. | — |
|
||||
| PUT | `/users/{email}/disable` | ApiAdmin | Disables a user account. | — |
|
||||
| DELETE | `/users/{email}` | ApiAdmin | Removes a user. | — |
|
||||
| POST | `/resources/{dataFolder?}` | Any authenticated | Uploads a resource file. | — |
|
||||
| GET | `/resources/list/{dataFolder?}` | Any authenticated | Lists files in a resource folder. | — |
|
||||
| POST | `/resources/clear/{dataFolder?}` | ApiAdmin | Clears a resource folder. | — |
|
||||
| POST | `/classes` | ApiAdmin | Creates a detection class. | AZ-513 |
|
||||
| PATCH | `/classes/{id:int}` | ApiAdmin | Updates a detection class (partial-merge). | AZ-513 |
|
||||
| DELETE | `/classes/{id:int}` | ApiAdmin | Deletes a detection class. | AZ-513 |
|
||||
|
||||
### Removed endpoints
|
||||
|
||||
@@ -34,79 +49,99 @@ The following endpoints have been removed and now return `404`:
|
||||
|
||||
| Method | Path | Removed in | Reason |
|
||||
|--------|------|------------|--------|
|
||||
| PUT | `/users/hardware/set` | cycle 1 (AZ-197) | hardware-binding feature deleted (no fielded clients in target architecture) |
|
||||
| POST | `/resources/check` | cycle 1 (AZ-197) | was the hardware-binding side-effect probe; no remaining purpose |
|
||||
| POST | `/get-update` | post-cycle-1 (AZ-183 reverted) | security audit F-1: endpoint disclosed plaintext per-resource encryption keys to any authenticated caller; the underlying installer-distribution flow is itself obsolete |
|
||||
| POST | `/resources/publish` | post-cycle-1 (AZ-183 reverted) | same revert as `/get-update` — the publish counterpart of the OTA flow |
|
||||
| POST | `/resources/get/{dataFolder?}` | cycle 2 (2026-05-14) | obsolete — per-user encrypted-download flow no longer used by any client; ADR-003 retired |
|
||||
| GET | `/resources/get-installer` | cycle 2 (2026-05-14) | obsolete — installer-shipping era is over (browser SaaS + fTPM Jetsons) |
|
||||
| GET | `/resources/get-installer/stage` | cycle 2 (2026-05-14) | same as `/resources/get-installer` |
|
||||
| PUT | `/users/hardware/set` | cycle 1 (AZ-197) | hardware-binding feature deleted |
|
||||
| POST | `/resources/check` | cycle 1 (AZ-197) | hardware-binding side-effect probe |
|
||||
| POST | `/get-update` | post-cycle-1 (AZ-183 reverted) | security audit F-1 |
|
||||
| POST | `/resources/publish` | post-cycle-1 (AZ-183 reverted) | OTA flow obsolete |
|
||||
| POST | `/resources/get/{dataFolder?}` | cycle 2 | obsolete; ADR-003 retired |
|
||||
| GET | `/resources/get-installer` | cycle 2 | installer-shipping era over |
|
||||
| GET | `/resources/get-installer/stage` | cycle 2 | same as above |
|
||||
|
||||
## Internal Logic
|
||||
|
||||
### DI Registration
|
||||
- `IJwtSigningKeyProvider` → `JwtSigningKeyProvider` (Singleton; eagerly built before `app.Build()` so `JwtBearer` and DI share one instance) — **AZ-532**
|
||||
- `IUserService` → `UserService` (Scoped)
|
||||
- `IAuthService` → `AuthService` (Scoped)
|
||||
- `IRefreshTokenService` → `RefreshTokenService` (Scoped) — **AZ-531**
|
||||
- `ISessionService` → `SessionService` (Scoped) — **AZ-535**
|
||||
- `IMissionTokenService` → `MissionTokenService` (Scoped) — **AZ-533**
|
||||
- `IMfaService` → `MfaService` (Scoped) — **AZ-534**
|
||||
- `IResourcesService` → `ResourcesService` (Scoped)
|
||||
- `IDetectionClassService` → `DetectionClassService` (Scoped) — added by AZ-513
|
||||
- `IDetectionClassService` → `DetectionClassService` (Scoped)
|
||||
- `IAuditLog` → `AuditLog` (Scoped) — **AZ-537 / AZ-534**
|
||||
- `IDbFactory` → `DbFactory` (Singleton)
|
||||
- `ICache` → `MemoryCache` (Scoped)
|
||||
- `LazyCache` via `AddLazyCache()`
|
||||
- FluentValidation validators auto-discovered from `RegisterUserValidator` assembly (also picks up `CreateDetectionClassRequest`, `UpdateDetectionClassRequest` validators introduced in cycle 1)
|
||||
- ASP.NET Core `DataProtection` — `SetApplicationName("Azaion.AdminApi")`; if `DataProtection:KeysFolder` is set, persisted to filesystem (production requirement for MFA-secret durability) — **AZ-534**
|
||||
- FluentValidation validators auto-discovered from `RegisterUserValidator` assembly
|
||||
- `BusinessExceptionHandler` registered as exception handler
|
||||
|
||||
### Middleware Pipeline
|
||||
1. Swagger (dev only)
|
||||
2. CORS (`AdminCorsPolicy`)
|
||||
3. Authentication (JWT Bearer)
|
||||
4. Authorization
|
||||
5. URL rewrite: root `/` → `/swagger`
|
||||
6. Exception handler
|
||||
1. Swagger (Development only)
|
||||
2. **HSTS + HTTPS redirection (non-Development only)** — AZ-538
|
||||
3. CORS (`AdminCorsPolicy`)
|
||||
4. Authentication (JWT Bearer with `ValidAlgorithms = [ES256]` and an `IssuerSigningKeyResolver` that picks by `kid` from `IJwtSigningKeyProvider.All`)
|
||||
5. Authorization
|
||||
6. **Rate limiter (`UseRateLimiter`)** — AZ-537
|
||||
7. URL rewrite: root `/` → `/swagger`
|
||||
8. Exception handler
|
||||
|
||||
### Authorization Policies
|
||||
- `apiAdminPolicy`: requires `RoleEnum.ApiAdmin` role
|
||||
- `revocationReaderPolicy`: requires `RoleEnum.Service` OR `RoleEnum.ApiAdmin` (gates `/sessions/revoked`) — **AZ-535**
|
||||
|
||||
> The `apiUploaderPolicy` (`RoleEnum.ResourceUploader` OR `ApiAdmin`) was added by AZ-183 and removed in the same cycle when the OTA endpoints it guarded were retired (see "Removed in cycle 1" above). `RoleEnum.ResourceUploader` itself remains as a data value (the seed `uploader@azaion.com` still uses it) but is no longer wired to any endpoint policy.
|
||||
### Rate Limit Policies
|
||||
- `LoginPerIpPolicy = "login-per-ip"` — sliding-window limiter keyed on `RemoteIpAddress`. Configured from `AuthConfig.RateLimit.PerIpPermitLimit` / `PerIpWindowSeconds`. On rejection, sets `Retry-After` from the `RetryAfter` lease metadata. Applied to `/login` and `/login/mfa`.
|
||||
|
||||
### Configuration Sections
|
||||
- `JwtConfig` — JWT signing/validation
|
||||
- `JwtConfig` — JWT signing/validation (Issuer, Audience, KeysFolder, ActiveKid, AccessTokenLifetimeMinutes)
|
||||
- `SessionConfig` — refresh-token sliding/absolute window (RefreshSlidingHours, RefreshAbsoluteHours) — **AZ-531**
|
||||
- `AuthConfig` — rate-limit and lockout knobs — **AZ-537**
|
||||
- `ConnectionStrings` — DB connections
|
||||
- `ResourcesConfig` — file storage path (`ResourcesFolder`); the installer subfolders were dropped in cycle 2 along with the installer endpoints
|
||||
- `ResourcesConfig` — file storage path
|
||||
|
||||
### Kestrel
|
||||
- Max request body size: 200 MB (for file uploads)
|
||||
- Max request body size: 200 MB
|
||||
|
||||
### Logging
|
||||
- Serilog: console + rolling file (`logs/log.txt`)
|
||||
|
||||
### CORS
|
||||
- Allowed origins: `https://admin.azaion.com`, `http://admin.azaion.com`
|
||||
- All methods and headers allowed
|
||||
- Credentials allowed
|
||||
- Allowed origin: `https://admin.azaion.com` (the cleartext `http://` origin was dropped by AZ-538)
|
||||
- All methods and headers allowed; credentials allowed
|
||||
|
||||
### Helpers
|
||||
Local static helpers used by logout / mission endpoints:
|
||||
- `ParseSidClaim(ClaimsPrincipal)` — extracts the `sid` claim; throws `InvalidRefreshToken` (401) if missing/malformed.
|
||||
- `ParseUserIdClaim(ClaimsPrincipal)` — extracts `NameIdentifier`; same error semantics.
|
||||
- `IssueDualTokens(...)` — shared by `/login` and `/login/mfa`; calls `IRefreshTokenService.IssueForNewLogin`, `IAuthService.CreateToken`, plus `ISessionService.RevokeMissionsForAircraft` when the caller is `RoleEnum.CompanionPC` (AZ-533 AC-4 reconnect trigger).
|
||||
|
||||
## Dependencies
|
||||
All services, configs, entities, and request types from Azaion.Common and Azaion.Services.
|
||||
All services, configs, entities, and request types from `Azaion.Common` and `Azaion.Services`. New dependencies wired in cycle 2: `Microsoft.AspNetCore.RateLimiting`, `Microsoft.AspNetCore.DataProtection`.
|
||||
|
||||
## Consumers
|
||||
None — this is the application entry point.
|
||||
None — application entry point.
|
||||
|
||||
## Data Models
|
||||
None defined here.
|
||||
|
||||
## Configuration
|
||||
Reads `JwtConfig`, `ConnectionStrings`, `ResourcesConfig` from `IConfiguration`.
|
||||
Reads `JwtConfig`, `SessionConfig`, `AuthConfig`, `ConnectionStrings`, `ResourcesConfig` from `IConfiguration`. Optional `DataProtection:KeysFolder` for MFA-secret durability.
|
||||
|
||||
## External Integrations
|
||||
- PostgreSQL (via DI-registered `DbFactory`)
|
||||
- Local filesystem (via `ResourcesService`)
|
||||
- Local filesystem (via `ResourcesService` and `JwtSigningKeyProvider` for PEM keys)
|
||||
|
||||
## Security
|
||||
- JWT Bearer authentication with full validation (issuer, audience, lifetime, signing key)
|
||||
- Role-based authorization policies
|
||||
- CORS restricted to `admin.azaion.com`
|
||||
- Request body limit of 200 MB
|
||||
- Antiforgery disabled for resource upload endpoint
|
||||
- Password sent via POST body (not URL)
|
||||
- JWT Bearer with full validation: `ValidateIssuer`, `ValidateAudience`, `ValidateLifetime`, `ValidateIssuerSigningKey`, `ValidAlgorithms = [ES256]` (AZ-532 AC-5).
|
||||
- Issuer signing keys resolved per-`kid` via `IJwtSigningKeyProvider`; supports rotation overlap.
|
||||
- Public JWKS endpoint exposes only public components (`x`/`y` for EC); `Cache-Control: public, max-age=3600`.
|
||||
- Per-IP sliding-window rate limit on `/login` and `/login/mfa` (AZ-537).
|
||||
- HSTS (1 year, includeSubDomains, preload) + HTTPS redirect in non-Development envs (AZ-538).
|
||||
- CORS restricted to HTTPS origin only (AZ-538).
|
||||
- DataProtection key folder must be a persistent volume in Production so encrypted MFA secrets survive restarts (AZ-534 known operational requirement; **carry-forward F3** asks for a startup warning when running in Production with the folder unset).
|
||||
- Role-based authorization for admin endpoints; new `Service` role gates the verifier-poll feed.
|
||||
|
||||
## Tests
|
||||
None directly; tested indirectly through integration tests.
|
||||
None directly; tested through `e2e/Azaion.E2E/Tests/` (Login, RefreshToken, RateLimitLockout, Logout, Jwks, MissionToken, MfaEnrollment, MfaLogin, PasswordHashing).
|
||||
|
||||
Reference in New Issue
Block a user