Updated JWT authentication to use configuration values instead of hardcoded secrets, improving security and flexibility. Enhanced CORS policy to conditionally allow origins based on configuration settings, with logging for permissive defaults. Updated README to reflect project renaming and clarify service context.
21 KiB
Solution — Azaion.Missions
Status: derived-from-code (autodev
/documentStep 5, 2026-05-14). Mode: retrospective synthesis from the verified_docs/02_document/set. Forward-looking caveat: this document describes the post-rename, post-GPS-Denied-removal target the documentation already reflects. Today's source still usesAzaion.Flights.*,Aircraft*/Flight*/Orthophoto*/GpsCorrection*filenames, and[Route("aircrafts"|"flights")]. The implementation gap is tracked under Jira AZ-EPIC (AZ-539) children B4–B12; the doc-vs-code reconciliation table lives in_docs/02_document/04_verification_log.md§ 0. References to "the implemented solution" in this document mean the code as it exists today plus the deltas closed by B4–B12.
1. Product Solution Description
missions is the edge-tier .NET 10 REST service that owns the mission domain of an Azaion deployment — vehicle inventory (Plane / Copter / UGV / GuidedMissile), mission plans, ordered waypoints, and the cross-service cascade-delete that keeps the rest of the edge stack consistent when missions or waypoints are removed.
Runtime topology: exactly one container per device (Jetson Orin / OrangePI / operator-PC), co-located with annotations, the detection pipeline, autopilot, gps-denied, and the React ui. All edge services share one local PostgreSQL on the device; each migrates and writes only the tables it owns. JWTs are minted by the central admin service and validated locally with a shared HMAC secret — missions never calls back.
Component interaction (high-level)
flowchart LR
ui[[Operator UI]]
admin[[admin service<br/>JWT issuer]]
autopilot[[autopilot]]
annotations[[annotations]]
detection[[detection pipeline]]
gps[[gps-denied service]]
subgraph missions["missions (this service, one .NET process)"]
direction TB
h07[07_host<br/>Program.cs / DI / startup]
c01[01_vehicle_catalog]
c02[02_mission_planning]
p04[04_persistence<br/>AppDataConnection + Migrator]
i05[05_identity<br/>JWT bearer + FL policy]
h06[06_http_conventions<br/>error envelope + pagination]
end
pg[(postgres-local<br/>shared per device)]
ui -- "REST + JWT" --> c01
ui -- "REST + JWT" --> c02
admin -. "shared HMAC secret (token only)" .-> i05
c01 --> p04
c02 --> p04
c02 -. "cross-service cascade delete" .-> annotations
c02 -. "cross-service cascade delete" .-> detection
autopilot <-- "DB read missions/waypoints<br/>DB write map_objects" --> pg
p04 --> pg
annotations <--> pg
detection <--> pg
gps -. "no runtime coupling<br/>(GUID refs only)" .-> pg
h07 --> c01
h07 --> c02
h07 --> p04
h07 --> i05
h07 --> h06
The HTTP surface is summarised in _docs/02_document/system-flows.md (7 flows, F1–F7); the per-component HTTP routes are listed in _docs/02_document/components/0[1,2]_*/description.md.
2. Architecture (as implemented)
The dominant pattern is thin ASP.NET Core controller → service class → linq2db active-record over a per-request scoped DataConnection, with no repository abstraction and no in-process message queue / event bus. ADR rationale (ADR-001 .. ADR-008) is in _docs/02_document/architecture.md § 8.
2.1 Per-component solution table
| # | Component | Solution (what it does) | Tools / libs | Advantages | Limitations | Requirements satisfied | Security | Cost | Fit |
|---|---|---|---|---|---|---|---|---|---|
| 01 | 01_vehicle_catalog |
Vehicle CRUD + is_default exclusivity. Controller [Authorize(Policy="FL")] → VehicleService → ITable<Vehicle> |
ASP.NET Core, linq2db ITable<Vehicle>, [Authorize] |
Single owner of the inventory abstraction; same exact pattern as 02_mission_planning so engineers context-switch cheaply |
"Exactly one default" is enforced by clear-then-set without a transaction → race window (B12 decision pending); no input validation on Name/BatteryCapacity (carry-forward) |
Spec § 6.1 (Vehicle Catalog), suite roles FL |
[Authorize(Policy="FL")] on every action; no per-method authz |
One service file + one controller (~190 LoC together) | Good — matches operator-paced load, vertical scale only |
| 02 | 02_mission_planning |
Mission + Waypoint CRUD + the cross-service cascade-delete walk. Existence-checks vehicle_id on create/update; paginates GET /missions (the only paginated endpoint). |
ASP.NET Core, linq2db, PaginatedResponse<T> (06_http_conventions) |
One canonical place that knows the full mission ownership graph; cascade walks map_objects → media → annotations → detection → waypoints → missions in FK order |
Cascade is NOT transaction-wrapped (ADR-006) → partial failure leaves orphans; UpdateWaypoint is a full overwrite even though DTO looks partial; vehicle_id missing returns 400 (spec wants 404); LinqToDB does not eager-load [Association] so Vehicle and Waypoints serialize null/empty |
Spec § 6.2 (Mission Planning + Waypoints), spec § cascade contract | [Authorize(Policy="FL")] on every action; no audit log, no correlation id |
Two service files + one controller (~370 LoC together); sequential I/O (4–7 round-trips per cascade) — single-digit ms typical against local Postgres | Acceptable today; will need transaction wrap (one-line) before SLO commitments |
| 04 | 04_persistence |
AppDataConnection : DataConnection exposes ITable<T> for every persisted entity (4 owned post-B7+B9 + 3 borrowed read-only stubs). DatabaseMigrator runs CREATE TABLE IF NOT EXISTS + CREATE INDEX IF NOT EXISTS at startup; B9 adds a one-shot DROP TABLE IF EXISTS orthophotos / gps_corrections for fielded devices |
linq2db 6.2.0, Npgsql 10.0.2, raw Execute for DDL |
Lightweight; no migration tool dependency; idempotent every restart; ITable<T> lets cross-component reads/cascades stay typed |
No schema versioning; column drops / type changes need manual SQL or a future migration tool; no connection-pool tuning beyond Npgsql defaults | Spec § database schema, suite ER diagram (post-B7) | DB credentials are env-driven (DATABASE_URL); no column-level encryption; relies on PG-level access control |
One file for the connection (~70 LoC) + one for the migrator (~120 LoC post-B9) | Good for current schema scale (4 owned tables); will become limiting when schema starts evolving frequently |
| 05 | 05_identity |
JwtExtensions.AddJwtAuth registers JwtBearer with HMAC-SHA256 + the named policy "FL" (1-min clock skew). Validation is local; this service never calls admin |
Microsoft.AspNetCore.Authentication.JwtBearer 10.0.5, SymmetricSecurityKey |
admin outage does NOT take this service down (until tokens expire); zero-trip auth = lowest possible auth latency |
iss / aud validation disabled (CMMC L2 row 3, AZ-487 / AZ-494 — suite-tracked, NOT in this Epic); the policy code "FL" retains the legacy "Flight" wording even after the service rename (fleet-wide auth change deferred); user-id claim is parsed but not consumed anywhere (no per-user audit) |
Spec § auth, ../../suite/_docs/00_roles_permissions.md |
Shared HMAC secret (JWT_SECRET); rotation requires coordinated re-deploy across every backend that shares the secret |
One file (~60 LoC) | Good for the deployment shape (closed edge network behind a reverse proxy); iss/aud gap is a documented and tracked finding |
| 06 | 06_http_conventions |
ErrorHandlingMiddleware (global exception → JSON envelope) + PaginatedResponse<T> + the dead ErrorResponse DTO |
ASP.NET Core middleware, System.Text.Json (defaults) |
Single chokepoint for HTTP wire shape — error mapping is uniform across components | Two divergences from the suite spec carry forward (ADR-002): entity/DTO bodies are PascalCase (no JsonNamingPolicy.CamelCase); error envelope misses spec's errors field. The error envelope IS already camelCase by accidental match (anonymous-object literal). The ErrorResponse DTO is dead on the wire and has the wrong shape (List<string>? instead of spec's object? keyed by field name) |
Spec § Error Response Format, § Pagination | LogError(ex, ...) only — no PII redaction (none in payload today); fallback 500 body shows the generic message, NOT the stack trace (logged only) |
One middleware file + two DTO files (~80 LoC together) | Acceptable until the suite-wide camelCase migration; cutover is all-or-nothing because UI + autopilot consume PascalCase today |
| 07 | 07_host |
Program.cs composition root: env → connection string adapter, JWT registration, scoped DI for AppDataConnection + service classes, run migrator at startup, mount middleware in correct order, MapGet("/health"), mount Swagger |
ASP.NET Core minimal host APIs | One file you can read top-to-bottom in one sitting; environment-fallback adapter (ConvertPostgresUrl) makes dotnet run zero-config in dev |
Swagger UI + dev fallbacks are NOT gated on IsDevelopment() (ADR-005) — a misconfigured production deploy silently boots with JWT_SECRET=development-secret-key-min-32-chars!!; CORS is AllowAnyOrigin/Method/Header in every environment (assumed safe behind suite reverse proxy) |
Spec § service composition; container EXPOSE 8080; Watchtower restart contract |
Hardcoded dev fallbacks for JWT_SECRET / DATABASE_URL (security finding tracked at suite level) |
One file (~150 LoC) | Acceptable in the closed edge environment; the unconditional Swagger + dev fallbacks are debt that should be paid when the service moves to a less trusted network |
2.2 Cross-cutting design choices
| Choice | Rationale | Status |
|---|---|---|
| One PostgreSQL per device, shared by all edge services (ADR-001) | 6× operational overhead saved per device; cross-service cascade is physically possible in one DB connection | Implemented |
Manual cascade-delete in code, NOT ON DELETE CASCADE (ADR-003) |
Schema-level cascade would couple annotations / detection schemas to this service's lifecycle |
Implemented (transaction-wrap missing — ADR-006 carry-forward) |
CREATE TABLE IF NOT EXISTS schema bootstrap (ADR-004), no migration tool |
4-table schema; no column drops or type changes; restart-driven deploy via Watchtower | Implemented (B9 adds the one explicit DROP TABLE IF EXISTS block for fielded devices) |
Local JWT validation, no callback to admin (ADR-005, F5) |
Zero auth-related coupling at runtime; admin outage doesn't take this service down |
Implemented (iss/aud validation disabled — suite-tracked) |
| One csproj, one root namespace (ADR-008); layering by convention not by compiler | Service is small enough that 6 csprojs add more navigation cost than safety value | Implemented (post-B5); enforcement via module-layout.md § Allowed Dependencies + /code-review Phase 7 |
| GPS-Denied moved to a sibling service (ADR-007, B7+B9) | Different scaling + deployment cadence; GPS-Denied owns its tables and lifecycle | Doc-only today; B7 (code) + B9 (DB migration) close the gap |
2.3 Implementation order (relative to other components)
The 6 components have no circular dependencies. Implementation/refactor order (lowest layer first), per _docs/02_document/module-layout.md:
04_persistence(Layer 1) — depends only on linq2db + Npgsql.05_identity,06_http_conventions(Layer 2) — depend on ASP.NET Core only.01_vehicle_catalog(Layer 3) — depends on04_persistence,05_identity.02_mission_planning(Layer 4) — depends on01_vehicle_catalog(vehicle existence check),04_persistence,05_identity,06_http_conventions(paginated envelope).07_host(Layer 5) — depends on every other component (composition root).
Cross-component reads happen via the shared AppDataConnection (e.g. 02_mission_planning reads vehicles to existence-check vehicle_id); the codebase does not wrap that lookup behind an interface in 01_vehicle_catalog. This is intentional (one csproj, one DB) — see ADR-008.
3. Testing Strategy (as observed)
Status (today): NO automated tests are present in this codebase. The Tech Stack table in
_docs/02_document/architecture.md§ 2 says "Tests: None present";_docs/02_document/00_discovery.md§ Repository Layout confirms there is notests/directory and the csproj has no test project sibling. The autodevexisting-codeflow's Phase A Steps 3 → 7 (Test Spec → Decompose Tests → Implement Tests → Run Tests) is the planned path to close this gap.
3.1 What exists today
| Layer | Coverage | Where it is |
|---|---|---|
| Unit | None | — |
| Integration / functional | None | — |
| Non-functional (perf, load, security) | None | — |
| Health probe | One endpoint (GET /health returns { status: "healthy" }) |
Program.cs MapGet("/health") |
| Schema sanity | Indirect — DatabaseMigrator runs at every startup; if a column/table is missing the process crashes |
Database/DatabaseMigrator.cs |
| Wire-shape verification | Manual diff against ../../suite/_docs/00_top_level_architecture.md § Error Response Format + § Pagination |
Code review |
3.2 What the autodev existing-code flow will produce
- Step 3 (Test Spec) →
_docs/02_document/tests/traceability-matrix.md+ per-flow scenario files for F1–F7. The 8 ADRs and 7 carry-forward concerns fromarchitecture.mdare the seed set for test scenarios. - Step 4 (Code Testability Revision) → minimal, surgical fixes if the codebase blocks tests from running (env-driven
DATABASE_URLalready lands here; hardcoded dev fallbacks inProgram.csare the prime candidate). Scope: smallest set of changes; deeper refactors deferred to Step 8. - Step 5 (Decompose Tests) → per-test task files in
_docs/02_tasks/todo/, plus_test_infrastructure.md. - Step 6 (Implement Tests) →
tests/Azaion.Missions.Tests/sibling project (xUnit is the suite-standard choice; percoderule.mdc"follow the established directory structure", nosrc/layer). - Step 7 (Run Tests) → green test suite forms the safety net for Step 8 (Refactor) and every Phase B feature cycle thereafter.
3.3 Scenarios likely to land first (anticipated, not yet specified)
These are obvious test seams given the F1–F7 flows and the 7 carry-forward concerns; the actual scenario set is produced by Step 3.
| Priority | Scenario family | Why it lands first |
|---|---|---|
| 1 | MissionService.DeleteMission — full cascade in dependency order |
Critical-flow F3, NOT transaction-wrapped today; tests would catch any future regressions in the cascade chain immediately |
| 1 | WaypointService.DeleteWaypoint — scoped cascade variant |
Same reason as F3; same NO-transaction caveat |
| 2 | MissionService.CreateMission / UpdateMission — vehicle_id existence check + spec-vs-code 400 vs 404 divergence |
Locks in the current behaviour so the spec-conformance fix is intentional, not accidental |
| 2 | VehicleService.SetDefault / Create / Update — "exactly one default" race |
B12 decision (spec-vs-code stricter behaviour) — tests pin whichever resolution the user picks |
| 2 | ErrorHandlingMiddleware mapping (KeyNotFoundException → 404, ArgumentException → 400, InvalidOperationException → 409, fallthrough → 500) |
Wire-shape contract used by every flow |
| 3 | JWT validation — accept valid HS256 / reject invalid signature / reject expired (with 1-min skew) / reject missing-FL claim |
F5 cross-cutting; pins the local-validation contract |
| 3 | DatabaseMigrator.Migrate — idempotent on a fresh DB, idempotent on already-migrated DB, B9 DROP on a fielded-legacy DB |
F6; tests guard the only explicit destructive step |
4. Non-functional behaviour (observed)
| Concern | Observed behaviour | Where it is set | Notes |
|---|---|---|---|
| Latency | Single-digit ms typical; cascade delete = 4–7 sequential round-trips against local Postgres | Database/AppDataConnection.cs (per-request scope), MissionService.DeleteMission |
No SLO in spec; observed under operator-paced load |
| Throughput | Operator-paced (~1 op/s peak); not load-tested | — | Edge deployment shape; not a hot path |
| Availability | Best-effort per device; Watchtower restarts on crash; flight-gate prevents restart mid-mission |
Dockerfile + ../../suite/_infra/_compose/, suite arch doc |
No multi-instance HA per device by design |
| Recovery | RTO ≈ container restart time (~10s); RPO = device-local backup cadence (suite concern) | Watchtower + suite-level backup | — |
| Cascade atomicity | Currently violated (ADR-006); one-line fix queued | Services/MissionService.cs, Services/WaypointService.cs |
Recommended to land with B6 |
| Wire-shape conformance | Currently divergent on entity/DTO case + error envelope's missing errors field (ADR-002) |
Program.cs (no JsonNamingPolicy.CamelCase); Middleware/ErrorHandlingMiddleware.cs (anonymous object envelope) |
Cutover is suite-wide; out of this Epic |
| Health endpoint | < 10 ms typical (no DB ping); used by Watchtower + reverse proxy |
Program.cs MapGet("/health") |
Future improvement: gate on DB ping |
| Resource limits | None in code; container-level limits set by edge compose | Dockerfile (no --memory / cpu limits inside) |
Suite-level concern |
5. References
5.1 Source artefacts (this repo)
| Concern | File |
|---|---|
| Web host composition | Program.cs |
| Vehicle catalog | Controllers/AircraftsController.cs (post-B6/B8: Controllers/VehiclesController.cs), Services/AircraftService.cs (post-B6: VehicleService.cs) |
| Mission planning | Controllers/FlightsController.cs (post-B6/B8: Controllers/MissionsController.cs), Services/FlightService.cs (post-B6: MissionService.cs), Services/WaypointService.cs |
| Persistence | Database/AppDataConnection.cs, Database/DatabaseMigrator.cs, Database/Entities/*.cs |
| Identity | Auth/JwtExtensions.cs |
| HTTP conventions | Middleware/ErrorHandlingMiddleware.cs, DTOs/PaginatedResponse.cs, DTOs/ErrorResponse.cs |
| Container | Dockerfile |
| CI | .woodpecker/build-arm.yml |
| Project | Azaion.Flights.csproj (post-B5: Azaion.Missions.csproj) |
5.2 Generated documentation (this repo)
| Doc | Path |
|---|---|
| Discovery | _docs/02_document/00_discovery.md |
| Per-module docs (12 modules) | _docs/02_document/modules/*.md |
| Per-component descriptions (6 components) | _docs/02_document/components/*/description.md |
| Module layout (file ownership + layering) | _docs/02_document/module-layout.md |
| Architecture (this solution's source-of-truth) | _docs/02_document/architecture.md |
| System flows (F1–F7) | _docs/02_document/system-flows.md + _docs/02_document/diagrams/flows/*.md |
| Data model | _docs/02_document/data_model.md |
| Glossary (confirmed by user) | _docs/02_document/glossary.md |
| Verification log (drift mapping) | _docs/02_document/04_verification_log.md |
| Deployment notes | _docs/02_document/deployment/{containerization,ci_cd_pipeline,environment_strategy,observability}.md |
5.3 Suite-level cross-references
| Concern | File |
|---|---|
| Primary spec for this service | ../../suite/_docs/02_missions.md |
| Top-level architecture (error envelope, pagination, topology) | ../../suite/_docs/00_top_level_architecture.md |
| Authoritative ER diagram (shared edge Postgres) | ../../suite/_docs/00_database_schema.md |
Roles & permissions (FL permission origin) |
../../suite/_docs/00_roles_permissions.md |
| GPS-Denied (separate service after B7) | ../../suite/_docs/11_gps_denied.md |
CMMC L2 scorecard (JWT iss/aud finding) |
../../suite/_docs/05_security/cmmc_l2_scorecard.md |
| Repo-config (post-rename) | ../../suite/_docs/_repo-config.yaml |
5.4 Tracker (Jira AZ project)
| Plan ID | Jira | Type | SP | Status |
|---|---|---|---|---|
| Epic | AZ-539 | Epic | — | To Do |
| B1 (local docs) | AZ-540 | Task | 3 | Done |
| B2 (suite docs) | AZ-541 | Task | 3 | Done |
| B3 (state bookkeeping) | AZ-542 | Task | 3 | Done |
| B4 (repo rename) | AZ-543 | Task | 3 | To Do |
| B5 (csproj + namespace) | AZ-544 | Story | 3 | To Do |
| B6 (domain rename) | AZ-545 | Story | 5 | To Do |
| B7 (drop GPS-Denied) | AZ-546 | Story | 3 | To Do |
| B8 (HTTP routes) | AZ-547 | Story | 3 | To Do |
| B9 (DB migration) | AZ-548 | Story | 5 | To Do |
| B10 (Dockerfile + image tag) | AZ-549 | Task | 2 | To Do |
| B11 (consumer cutover) | AZ-550 | Story | 5 | To Do |
| B12 (default vehicle rule decision) | AZ-551 | Task | 2 | To Do |
Leftover index: _docs/_process_leftovers/2026-05-14_rename-flights-to-missions.md.