mirror of
https://github.com/azaion/missions.git
synced 2026-06-22 09:31:08 +00:00
chore: update configuration and Docker setup for JWT and test results
ci/woodpecker/push/build-arm Pipeline was successful
ci/woodpecker/push/build-arm Pipeline was successful
Enhanced the .gitignore to exclude test results and updated the Dockerfile to include a new entrypoint script for improved container initialization. Refactored JWT configuration to support additional parameters for automatic refresh intervals, ensuring better control over token management. Updated the ConfigurationResolver to enforce required environment variables without hardcoded fallbacks, enhancing security and flexibility.
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
|
||||
> **Status**: confirmed-by-user (autodev `/document` Step 4.5, 2026-05-14). Source-of-truth for "what this service is and why" — downstream skills (`/refactor`, `/decompose`, `/new-task`, `/code-review`) consume this section before reading the lower-level technical sections below.
|
||||
|
||||
`missions` is the **edge-tier .NET 10 REST service** that owns the **mission domain** of each Azaion deployment — vehicle inventory, mission plans, waypoint sequences, and the cross-service cascade-delete that keeps the rest of the edge stack consistent when missions or waypoints are removed. **Exactly one instance runs per device** (Jetson Orin / OrangePI / operator-PC) alongside sibling edge services (`annotations`, detection, `autopilot`, `gps-denied`, `ui`), all sharing **ONE local PostgreSQL with per-service table ownership enforced by convention**. JWTs are minted remotely by the central `admin` service and validated locally with a shared HMAC secret; this service never calls back. The dominant pattern is **thin controller → service → linq2db active-record over a per-request scoped `DataConnection`**, with **no repository abstraction** and **no in-process message queue / event bus**.
|
||||
`missions` is the **edge-tier .NET 10 REST service** that owns the **mission domain** of each Azaion deployment — vehicle inventory, mission plans, waypoint sequences, and the cross-service cascade-delete that keeps the rest of the edge stack consistent when missions or waypoints are removed. **Exactly one instance runs per device** (Jetson Orin / OrangePI / operator-PC) alongside sibling edge services (`annotations`, detection, `autopilot`, `gps-denied`, `ui`), all sharing **ONE local PostgreSQL with per-service table ownership enforced by convention**. JWTs are minted remotely by the central `admin` service using ECDSA-SHA256 and validated locally against `admin`'s JWKS, which this service fetches once at startup and caches; request-path validation is local and does not call `admin`. The dominant pattern is **thin controller → service → linq2db active-record over a per-request scoped `DataConnection`**, with **no repository abstraction** and **no in-process message queue / event bus**.
|
||||
|
||||
### Components & responsibilities (6 logical components, 1 csproj)
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
| 01 | `01_vehicle_catalog` | Vehicle CRUD + "is_default" exclusivity (stricter than spec — B12 decision pending) |
|
||||
| 02 | `02_mission_planning` | Mission + Waypoint CRUD + the cross-service cascade-delete walk (canonical owner of the full mission ownership graph) |
|
||||
| 04 | `04_persistence` | `AppDataConnection` (LinqToDB) + `DatabaseMigrator` (`CREATE TABLE IF NOT EXISTS` for the 4 owned tables post-B7 + B9) |
|
||||
| 05 | `05_identity` | `JwtExtensions`; shared-secret HS256 validation; one `"FL"` policy (post-B7) |
|
||||
| 05 | `05_identity` | `JwtExtensions`; ECDSA-SHA256 validation against admin's JWKS (cached locally); one `"FL"` policy (post-B7) |
|
||||
| 06 | `06_http_conventions` | `ErrorHandlingMiddleware` + `PaginatedResponse<T>` + the unused `ErrorResponse` DTO |
|
||||
| 07 | `07_host` | `Program.cs` composition root; runs migrator at startup; serves on port 8080 |
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
- **F2 Mission create/read/update** — UI → mission service, with vehicle existence check.
|
||||
- **F3 Mission delete + CASCADE** *(critical)* — walks across `annotations` + detection schemas; **not transaction-wrapped today** (ADR-006).
|
||||
- **F4 Waypoint CRUD** — delete is a scoped F3 cascade.
|
||||
- **F5 JWT bearer validation** — every protected request; local HS256, no `iss`/`aud` (CMMC L2 finding, suite-tracked under AZ-487 / AZ-494).
|
||||
- **F5 JWT bearer validation** — every protected request; local ECDSA-SHA256 against admin's JWKS (cached); `iss` and `aud` both validated; `alg` pinned to `EcdsaSha256` (defends against HS256-confusion). The CMMC L2 finding tracked under AZ-487 / AZ-494 is now structurally addressed in this service's code; the suite-level docs still describe the legacy HS256 model and have a sync task pending.
|
||||
- **F6 Startup + schema migration** — `Program → DatabaseMigrator.Migrate → app.Run`.
|
||||
- **F7 Health probe** — anonymous `GET /health`; process-liveness only.
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
|
||||
- **One PostgreSQL per device; per-service table ownership enforced by convention.** *[inferred-from: `../../suite/_docs/00_top_level_architecture.md` § Database Topology, `Database/AppDataConnection.cs`, `Database/DatabaseMigrator.cs`]*
|
||||
- **Manual cascade-delete in code, NOT `ON DELETE CASCADE` in schema.** *[inferred-from: `Database/DatabaseMigrator.cs`, `FlightService.DeleteFlight` (today's `MissionService.DeleteMission`)]*
|
||||
- **JWT validated locally with no callback to `admin`** (HS256 shared-secret). *[inferred-from: `Auth/JwtExtensions.cs`]*
|
||||
- **JWT validated locally against admin's public JWKS** (ECDSA-SHA256). The JWKS is fetched once at startup (via `Microsoft.IdentityModel.Protocols.ConfigurationManager<JsonWebKeySet>`) and refreshed on the default schedule; per-request validation is local. *[inferred-from: `Auth/JwtExtensions.cs`]*
|
||||
- **Forward-only-additive schema bootstrap** (`CREATE TABLE IF NOT EXISTS`); B9's `DROP TABLE IF EXISTS` is the one explicit destructive step. *[inferred-from: `Database/DatabaseMigrator.cs`]*
|
||||
- **Layer-organized layout** (`Controllers/`, `Services/`, `DTOs/`, `Enums/`), NOT feature-folders; one project / one root namespace; layering rules in `module-layout.md` enforced by convention not by the compiler. *[inferred-from: repository tree + `Azaion.Flights.csproj` (today's `Azaion.Missions.csproj`)]*
|
||||
- **`gps-denied` is decoupled by design** — no runtime call in either direction; rows reference `mission_id` / `waypoint_id` as plain GUIDs in `gps-denied`'s own tables. *[inferred-from: ADR-007 + AZ-546 acceptance criteria]*
|
||||
@@ -45,7 +45,7 @@ These divergences from spec or known foot-guns are tracked in `00_discovery.md`
|
||||
|
||||
- PascalCase entity-body wire shape vs spec's camelCase (the *error envelope* is already camelCase by accidental match — see ADR-002).
|
||||
- Cascade-delete is not transaction-wrapped (ADR-006); one-line fix to land opportunistically with B6.
|
||||
- Swagger UI + dev-fallback secrets (`JWT_SECRET`, `DATABASE_URL`) NOT gated on `IsDevelopment()` (ADR-005).
|
||||
- Swagger UI NOT gated on `IsDevelopment()` (ADR-005, scope reduced — the "dev fallback secrets" aspect is now obsolete; see ADR-005 below for details).
|
||||
- `"FL"` policy code retains the legacy "Flight" wording even after the service rename — fleet-wide auth change, not in this Epic.
|
||||
- `Geopoint` stored as 3 flat columns (`lat`, `lon`, `mgrs`) instead of spec's single auto-converting `string GPS`.
|
||||
- F2 returns `400` instead of spec's `404` on a missing `VehicleId` (`ArgumentException` mapping).
|
||||
@@ -64,7 +64,7 @@ These divergences from spec or known foot-guns are tracked in `00_discovery.md`
|
||||
|
||||
| System | Integration Type | Direction | Purpose |
|
||||
|--------|------------------|-----------|---------|
|
||||
| `admin` (.NET, central) | JWT (HMAC shared secret) | Inbound (validation only) | Issues bearer tokens that this service validates locally; no network call back |
|
||||
| `admin` (.NET, central) | JWKS over HTTPS (outbound, startup + refresh) + JWT validation (inbound) | Outbound at startup; inbound on every request | Issues ECDSA-signed bearer tokens; this service fetches admin's public JWKS once at startup, caches it, and validates tokens locally thereafter. No per-request callback. JWKS rotation does not require a coordinated redeploy |
|
||||
| Operator UI (React, edge) | REST (JSON over HTTP) | Inbound | All vehicle / mission / waypoint CRUD |
|
||||
| `autopilot` (edge) | Shared DB (PostgreSQL on the same device) | Bidirectional | `autopilot` writes `map_objects` (this service owns the schema and cascade-deletes them); `autopilot` reads `missions` + `waypoints` to drive the vehicle |
|
||||
| `annotations` (edge) | Shared DB | Outbound delete | `missions` cascade-deletes from `media` + `annotations` on mission/waypoint delete; `annotations` owns the schema |
|
||||
@@ -81,7 +81,7 @@ These divergences from spec or known foot-guns are tracked in `00_discovery.md`
|
||||
| Data access | linq2db | 6.2.0 | Suite-wide ORM choice; explicit SQL escape hatch + attribute mapping; works well with the manual cascade pattern |
|
||||
| Database driver | Npgsql | 10.0.2 | PostgreSQL native protocol driver |
|
||||
| Schema bootstrap | linq2db raw `Execute` (`CREATE TABLE IF NOT EXISTS`) | — | Forward-only-additive; one `DROP TABLE IF EXISTS orthophotos / gps_corrections` block in B9 |
|
||||
| Auth | `Microsoft.AspNetCore.Authentication.JwtBearer` | 10.0.5 | JWT bearer with HS256 (shared secret with `admin`); local validation, no callback to issuer |
|
||||
| Auth | `Microsoft.AspNetCore.Authentication.JwtBearer` + `Microsoft.IdentityModel.Protocols` | 10.0.5 | JWT bearer with ECDSA-SHA256 against admin's JWKS (cached via `ConfigurationManager<JsonWebKeySet>`); `iss`/`aud` validated; algorithm pinned |
|
||||
| API docs | `Swashbuckle.AspNetCore` | 10.1.5 | Swagger UI + JSON spec (mounted unconditionally — see ADR-005) |
|
||||
| HTTP error envelope | Custom `ErrorHandlingMiddleware` | — | Maps `KeyNotFoundException`/`ArgumentException`/`InvalidOperationException` → 404/400/409 (see ADR-002 and component `06_http_conventions` Caveats for divergences from suite spec) |
|
||||
| Container | `mcr.microsoft.com/dotnet/aspnet:10.0` (multi-arch SDK build) | 10.0 | Matches edge target architectures (ARM64 dominant; AMD64 used for operator-PC) |
|
||||
@@ -119,11 +119,15 @@ These divergences from spec or known foot-guns are tracked in `00_discovery.md`
|
||||
|
||||
| Config | Development | Edge production |
|
||||
|--------|-------------|-----------------|
|
||||
| `DATABASE_URL` | `Host=localhost;Database=azaion;Username=postgres;Password=changeme` (hardcoded fallback) | `postgresql://postgres:${PG_LOCAL_PASSWORD}@postgres-local/azaion` (compose env) |
|
||||
| `JWT_SECRET` | `development-secret-key-min-32-chars!!` (hardcoded fallback) | Provisioned secret shared across `admin` + every backend service on the device |
|
||||
| Logging | Console / Debug (ASP.NET Core defaults) | Console only (no Serilog / structured logging configured today) |
|
||||
| `DATABASE_URL` | Operator-supplied env var or `Database:Url` config key (e.g. `Host=localhost;Database=azaion;Username=postgres;Password=changeme`). **No hardcoded fallback** — `ConfigurationResolver.ResolveRequiredOrThrow` aborts startup if unset | `postgresql://postgres:${PG_LOCAL_PASSWORD}@postgres-local/azaion` (compose env) |
|
||||
| `JWT_ISSUER` | Operator-supplied (e.g. `https://admin.azaion.dev/`). **Required at startup** | Set by Edge compose to the central admin issuer |
|
||||
| `JWT_AUDIENCE` | Operator-supplied (e.g. `missions`). **Required at startup** | Set by Edge compose to this service's audience identifier |
|
||||
| `JWT_JWKS_URL` | Operator-supplied HTTPS URL (e.g. `https://admin.azaion.dev/.well-known/jwks.json`). **Required at startup** + must be HTTPS (`HttpDocumentRetriever.RequireHttps = true`) | Set by Edge compose to admin's JWKS endpoint |
|
||||
| `CorsConfig:AllowedOrigins` | Optional; defaults to `[]` (implicit-permissive policy + startup warning) | Required when `CorsConfig:AllowAnyOrigin != true` — startup THROWS in `Production` with empty origins |
|
||||
| `CorsConfig:AllowAnyOrigin` | Optional; defaults to `false` | Optional; explicit opt-in if reverse-proxy already enforces origin checks |
|
||||
| Logging | Console / Debug (ASP.NET Core defaults) + `PermissiveDefaultWarning` when implicit-permissive CORS applies | Console only (no Serilog / structured logging configured today) |
|
||||
| Swagger | enabled | enabled (NOT gated on `IsDevelopment()` — see ADR-005) |
|
||||
| CORS | `AllowAnyOrigin/Method/Header` | `AllowAnyOrigin/Method/Header` (no environment override; assumed safe behind suite reverse proxy) |
|
||||
| CORS | Permissive fallback (with `PermissiveDefaultWarning` startup log) | Explicit allow-list via `CorsConfig:AllowedOrigins`, or explicit `AllowAnyOrigin=true` if reverse-proxy gates origins; implicit-permissive aborts startup |
|
||||
| Migrator | runs at process start | runs at process start (idempotent `IF NOT EXISTS` + the one B9 `DROP TABLE IF EXISTS` block for legacy GPS-Denied tables on previously-deployed devices) |
|
||||
|
||||
For containerization details, CI pipeline structure, and observability, see `_docs/02_document/deployment/`.
|
||||
@@ -210,21 +214,21 @@ This service is a single .NET process. Components communicate via direct C# call
|
||||
|
||||
## 7. Security Architecture
|
||||
|
||||
**Authentication**: JWT bearer (HS256). Tokens are minted by the central `admin` service and validated locally by `05_identity` using a shared HMAC secret (`JWT_SECRET`). This service NEVER calls back to `admin`; rotation of the secret requires coordinated redeploy across every backend service that shares the secret. Per the CMMC L2 scorecard (`../../suite/_docs/05_security/cmmc_l2_scorecard.md` row 3), `iss` / `aud` validation is currently disabled; this is a known finding tracked at the suite level under AZ-487 / AZ-494 (out of this Epic's scope).
|
||||
**Authentication**: JWT bearer with **ECDSA-SHA256** signature validation. Tokens are minted by the central `admin` service (which holds the ECDSA private key) and validated locally by `05_identity` against admin's public JWKS document. The JWKS is fetched once at startup via `Microsoft.IdentityModel.Protocols.ConfigurationManager<JsonWebKeySet>` against `JWT_JWKS_URL` (HTTPS only — `HttpDocumentRetriever.RequireHttps = true`) and refreshed on the manager's default schedule. After the initial fetch, request-path validation is local; no per-request callback to `admin`. Validation enforces `iss == JWT_ISSUER`, `aud == JWT_AUDIENCE`, `exp` (with 30-second clock skew), and pins `alg` to `EcdsaSha256` to defend against the HS256-confusion attack. **JWKS rotation does NOT require a coordinated redeploy** — consumers pick up new keys on the next refresh tick, and old tokens signed with the previous `kid` remain valid until their natural expiry. The CMMC L2 finding (`../../suite/_docs/05_security/cmmc_l2_scorecard.md` row 3) about missing `iss`/`aud` validation is structurally fixed in this service's code; the suite-level docs still describe the legacy HS256 model and have a sync task pending (drift recorded in `_docs/02_document/05_drift_findings_2026-05-14.md`).
|
||||
|
||||
**Authorization**: Single named policy `"FL"`, gated by a `permissions` claim value. Every controller route in `01_vehicle_catalog` and `02_mission_planning` carries `[Authorize(Policy = "FL")]`. The role → permission matrix lives in `../../suite/_docs/00_roles_permissions.md`. Note: the policy code `"FL"` carries the legacy "Flight" name even after the service rename to `missions`; renaming the permission code is a fleet-wide auth change (would invalidate every issued token until new ones are minted) and is **NOT** in this Epic's scope. Tracked as a TODO in `../../suite/_docs/00_roles_permissions.md`.
|
||||
|
||||
**Data protection**:
|
||||
|
||||
- **At rest**: PostgreSQL on-disk encryption is the device-level concern (suite-level, not this service). This service does not encrypt data at the column level.
|
||||
- **In transit**: TLS termination is the reverse proxy's responsibility. This service does NOT enforce HTTPS redirection. The container `EXPOSE 8080` is plain HTTP; the upstream reverse proxy adds TLS.
|
||||
- **Secrets management**: `DATABASE_URL` and `JWT_SECRET` are env vars. The `Program.cs` hardcoded fallbacks (`development-secret-key-min-32-chars!!`, `Password=changeme`) are dev-only and MUST be overridden in production. There is **no runtime gate** that blocks startup with the dev fallback in production — see ADR-005.
|
||||
- **In transit**: TLS termination is the reverse proxy's responsibility. This service does NOT enforce HTTPS redirection. The container `EXPOSE 8080` is plain HTTP; the upstream reverse proxy adds TLS. The JWKS fetch is independently constrained to HTTPS by `HttpDocumentRetriever { RequireHttps = true }`.
|
||||
- **Secrets management**: Four required env vars (`DATABASE_URL`, `JWT_ISSUER`, `JWT_AUDIENCE`, `JWT_JWKS_URL`) plus optional CORS keys flow through `Infrastructure/ConfigurationResolver.cs` → `ResolveRequiredOrThrow`. **There are no hardcoded fallbacks**; a missing required value aborts startup with `InvalidOperationException` before the host is built. A production deploy that forgets `JWT_JWKS_URL` cannot silently accept tokens — it fails fast. The legacy `JWT_SECRET` env var is no longer consulted.
|
||||
|
||||
**Audit logging**: None at the application level. The only structured log emitted by app code is `06_http_conventions`' middleware `LogError(ex, "Unhandled exception")` for unhandled 500s. There is no per-request audit trail, no correlation ID, and no per-user attribution (the JWT's user-id claim is not consumed — see `05_identity` Caveats #2).
|
||||
**Audit logging**: None at the application level. The only structured log emitted by app code is `06_http_conventions`' middleware `LogError(ex, "Unhandled exception")` for unhandled 500s, plus `Program.cs`' `PermissiveDefaultWarning` when implicit-permissive CORS applies. There is no per-request audit trail, no correlation ID, and no per-user attribution (the JWT's user-id claim is not consumed — see `05_identity` Caveats #2).
|
||||
|
||||
**Input validation**: None. No `[Required]` attributes, no range checks. Empty `Name`, negative `BatteryCapacity`, invalid enum int values are accepted on input. Carry-forward improvement; not in this Epic's scope.
|
||||
|
||||
**CORS**: `AllowAnyOrigin/Method/Header` in all environments. Spec does not mandate a CORS policy — likely safe behind the suite's edge reverse proxy (per `../../suite/_docs/00_top_level_architecture.md`) but worth confirming on first production deployment.
|
||||
**CORS**: Gated by `Infrastructure/CorsConfigurationValidator.cs`. In `Production` (case-insensitive match on `ASPNETCORE_ENVIRONMENT`) an empty `CorsConfig:AllowedOrigins` with `CorsConfig:AllowAnyOrigin != true` aborts startup. In non-Production environments, an empty allow-list with `AllowAnyOrigin=false` falls back to permissive (`AllowAnyOrigin/Method/Header`) and emits the `PermissiveDefaultWarning` startup log. Explicit `AllowAnyOrigin=true` always applies permissive without warning. The previous "permissive in all environments" model no longer holds.
|
||||
|
||||
## 8. Key Architectural Decisions
|
||||
|
||||
@@ -297,21 +301,21 @@ This service is a single .NET process. Components communicate via direct C# call
|
||||
- No version table; the migrator is idempotent and runs every startup.
|
||||
- Acceptable today; will become a real problem if the schema starts evolving frequently.
|
||||
|
||||
### ADR-005: Swagger + dev fallbacks not gated on `IsDevelopment()`
|
||||
### ADR-005: Swagger NOT gated on `IsDevelopment()` (scope reduced — dev-fallback secrets obsoleted)
|
||||
|
||||
**Context**: ASP.NET Core's idiomatic pattern gates Swagger UI and dev-only convenience features on `app.Environment.IsDevelopment()`. Today, this service mounts Swagger unconditionally and uses hardcoded dev fallbacks for `JWT_SECRET` / `DATABASE_URL` if env vars are unset.
|
||||
**Context**: ASP.NET Core's idiomatic pattern gates Swagger UI and dev-only convenience features on `app.Environment.IsDevelopment()`. The original form of this ADR also covered hardcoded dev fallbacks for `JWT_SECRET` / `DATABASE_URL`; that aspect is now obsolete after the introduction of `Infrastructure/ConfigurationResolver.cs` (fail-fast `ResolveRequiredOrThrow`). The only remaining gap is Swagger.
|
||||
|
||||
**Decision (current, carry-forward)**: Leave both unconditional today. Swagger UI is useful on edge devices for one-off operator debugging through the local network. The hardcoded dev fallbacks are a known foot-gun (a misconfigured production deploy will silently use the well-known secret) but they are intentional during the rename phase to keep `dotnet run` working zero-config.
|
||||
**Decision (current, carry-forward)**: Leave Swagger UI mounted unconditionally. Swagger UI is useful on edge devices for one-off operator debugging through the local network. There is no hardcoded dev fallback for any secret today.
|
||||
|
||||
**Alternatives considered**:
|
||||
|
||||
1. **Gate both on `IsDevelopment()`** — preferred long-term; out of this Epic.
|
||||
2. **Fail-fast at startup if `JWT_SECRET` is unset** — preferred long-term; out of this Epic.
|
||||
1. **Gate Swagger on `IsDevelopment()` (or on `ASPNETCORE_ENVIRONMENT != "Production"`)** — preferred long-term; out of this Epic.
|
||||
2. **Add a Swagger security scheme so the UI knows how to attach `Authorization: Bearer ...`** — usability improvement; out of this Epic.
|
||||
|
||||
**Consequences**:
|
||||
|
||||
- Swagger UI is exposed on every deployment. The reverse proxy may or may not whitelist it; verify on first production rollout.
|
||||
- A production deployment without `JWT_SECRET` set will silently boot with the well-known dev secret. This is a security finding tracked at the suite level (see CMMC L2 row 3).
|
||||
- The "production silently boots with the dev secret" risk no longer exists: `JWT_ISSUER`, `JWT_AUDIENCE`, `JWT_JWKS_URL`, and `DATABASE_URL` are all required, and `ResolveRequiredOrThrow` aborts startup with `InvalidOperationException` if any is missing. The CMMC L2 row-3 finding (HS256 + missing `iss`/`aud`) is also structurally addressed by the ECDSA + JWKS + iss/aud-validation model — see Section 7 above.
|
||||
|
||||
### ADR-006: Cascade-delete is NOT transaction-wrapped (carry-forward)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user