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.
15 KiB
Drift Findings — Targeted Verification Re-run, 2026-05-14
Status: discovery complete; doc revisions and test-spec re-issues PENDING (next session).
Scope: targeted re-verification of Auth/JwtExtensions.cs, Program.cs, Infrastructure/*.cs, Services/*.cs, Database/DatabaseMigrator.cs, Middleware/ErrorHandlingMiddleware.cs, Controllers/FlightsController.cs, Controllers/AircraftsController.cs against the corresponding _docs/ artifacts.
Trigger: while preparing autodev Step 4 (Code Testability Revision), ran a code-level cross-check that contradicts the _docs/02_document/04_verification_log.md § 3 "All flow claims reconcile" verdict for F5 (JWT) + F6 (Startup) + AC-9 (Authz).
Root cause (likely): the prior verification did doc-vs-doc consistency checks for these areas instead of opening the actual .cs files. The docs are internally consistent describing a HS256+shared-secret+permissive-CORS+dev-fallback world that no longer exists in the code.
Decision (user, this turn): Option A — re-run /document Step 4 targeted at the drifted areas, then re-issue test-spec for the affected ACs.
Drift NOT covered by the B-ticket rename mapping
These are real findings. Each item is actual today-code state, NOT a "post-rename target". The _docs/ and the test specs in _docs/02_document/tests/ need updates to match.
D-JWT — Auth/JwtExtensions.cs (MAJOR)
| # | Aspect | Doc claim (AC-5.*, modules/auth.md, components/05_identity, architecture.md § 7) | Code today (Auth/JwtExtensions.cs) |
|---|---|---|---|
| J1 | Algorithm | HS256 (SymmetricSecurityKey(UTF-8(JWT_SECRET))) |
ECDSA-SHA256 (ValidAlgorithms = [SecurityAlgorithms.EcdsaSha256]) with JWKS keys |
| J2 | Key material | Shared HMAC secret in JWT_SECRET env var |
JWKS retrieved from admin via ConfigurationManager<JsonWebKeySet> + JwksRetriever + HttpDocumentRetriever { RequireHttps = true } |
| J3 | Env var contract | JWT_SECRET (single var) |
Three vars: JWT_ISSUER, JWT_AUDIENCE, JWT_JWKS_URL (NO JWT_SECRET) |
| J4 | ValidateIssuer |
false |
true + ValidIssuer = <JWT_ISSUER> |
| J5 | ValidateAudience |
false |
true + ValidAudience = <JWT_AUDIENCE> |
| J6 | ClockSkew |
1 minute |
30 seconds |
| J7 | Pinned algorithms | not mentioned | ValidAlgorithms = [EcdsaSha256] (forces algo to prevent HS256-confusion attack) |
| J8 | RequireSignedTokens / RequireExpirationTime |
not explicitly mentioned | both true |
| J9 | Coupling to admin |
"Local validation; this service never calls back to admin" |
Calls admin for JWKS at startup + on ConfigurationManager refresh schedule |
| J10 | Rotation model | "Token signed with old JWT_SECRET → 401 across the entire device until coordinated re-deploy" |
JWKS rotation on admin + auto-refresh; no coordinated re-deploy needed |
| J11 | Dev fallback | JWT_SECRET=development-secret-key-min-32-chars!! if env unset (ADR-005 carry-forward) |
No fallback; ConfigurationResolver.ResolveRequiredOrThrow throws at startup if any of JWT_ISSUER/JWT_AUDIENCE/JWT_JWKS_URL is unset |
| J12 | Authz policies | Single "FL" policy; "GPS" is the post-B7 target (existing-code has both today) |
Today has both "FL" AND "GPS" — matches what the verification log already says, kept here for completeness |
ACs / NFTs to revise: AC-5.1, AC-5.2, AC-5.3, AC-5.4, AC-5.5, AC-5.6, AC-5.7, AC-5.9; NFT-SEC-01–09; NFT-RES-07; FT-N-08; results_report.md AC-5 entire group; environment.md JWT mock spec; test-data.md JWT mint section.
D-CONFIG — Program.cs + Infrastructure/ConfigurationResolver.cs (MAJOR)
| # | Aspect | Doc claim | Code today |
|---|---|---|---|
| C1 | Required env vars | Two: DATABASE_URL, JWT_SECRET (E1) |
Four: DATABASE_URL, JWT_ISSUER, JWT_AUDIENCE, JWT_JWKS_URL |
| C2 | Configuration source order | IConfiguration → Environment.GetEnvironmentVariable → fallback |
Env var first (Environment.GetEnvironmentVariable), then IConfiguration key (e.g. Database:Url, Jwt:Issuer), then throw (no fallback) |
| C3 | Dev fallbacks for JWT_SECRET / DATABASE_URL |
"ungated by IsDevelopment(); production deploy without env vars silently boots with the dev secret" (ADR-005 carry-forward) |
No fallbacks at all; production deploy without env vars now throws InvalidOperationException at startup. ADR-005 is OBSOLETE for this aspect |
| C4 | Database:Url config-key alternative |
not mentioned in docs (env-only) | Code reads Database:Url as fallback to DATABASE_URL env var |
| C5 | Jwt:Issuer / Jwt:Audience / Jwt:JwksUrl config-key alternatives |
not mentioned | Code reads each as fallback to its env var |
ACs / NFTs to revise: AC-6.1, AC-6.2, E1, E3, E4 (no shared secret anymore); NFT-RES-05 (still tests DB-down crash, but the failure mode is more direct now); environment.md "Test Execution" env var list; test-data.md env vars; results_report.md AC-6 group + E3 lock test.
D-CORS — Infrastructure/CorsConfigurationValidator.cs (MAJOR)
| # | Aspect | Doc claim (E9) | Code today |
|---|---|---|---|
| O1 | Permissive policy scope | "AllowAnyOrigin / AllowAnyMethod / AllowAnyHeader in all environments (assumed safe behind suite reverse proxy)" |
Conditionally permissive: EnsureSafeForEnvironment THROWS in Production if CorsConfig:AllowedOrigins is empty AND CorsConfig:AllowAnyOrigin != true. Permissive only when explicit opt-in OR non-Production |
| O2 | Config keys | not mentioned | New keys: CorsConfig:AllowedOrigins (string array) and CorsConfig:AllowAnyOrigin (bool) |
| O3 | Warning behavior | not mentioned | Logs PermissiveDefaultWarning at startup when implicit-permissive applies (origins empty + AllowAnyOrigin=false + non-Production) |
ACs / NFTs to revise: E9 restriction; results_report.md E9 lock test (currently doesn't exist; was deferred to follow-up); ADR-002 in architecture.md only if it discusses CORS.
D-DBSCHEMA — Database/DatabaseMigrator.cs (SMALL)
| # | Aspect | Doc claim (data_parameters.md § 3) | Code today |
|---|---|---|---|
| S1 | created_date / first_seen_at / last_seen_at column type |
TIMESTAMPTZ |
TIMESTAMP (no timezone) — affects how DateTime kinds round-trip |
| S2 | Foreign-key declarations | "logical FK ... no DB-level FK constraint declared in migrator" (data_parameters.md § 3.2 + § 3.3 note) | REFERENCES <parent>(id) declared on every FK in the migrator (flights.aircraft_id, waypoints.flight_id, orthophotos.flight_id, gps_corrections.flight_id + gps_corrections.waypoint_id, map_objects.flight_id) |
| S3 | Default values | not detailed | Migrator sets DEFAULT 0 / DEFAULT FALSE / DEFAULT NOW() / DEFAULT '' on most non-nullable columns |
ACs / NFTs to revise: data_parameters.md § 3 schema tables (TIMESTAMPTZ → TIMESTAMP, add REFERENCES notes, add DEFAULT notes); AC-2.8 (TOCTOU on FK) — actually now PARTLY mitigated by DB-level FK (insert would fail at DB layer with PG error 23503, not just app-layer); modules/database.md § Internal Logic.
D-FILTER — Services/AircraftService.cs + FlightService.cs (SMALL)
| # | Aspect | Doc claim (AC-1.6) | Code today |
|---|---|---|---|
| F1 | Vehicle (today: Aircraft) name filter case sensitivity |
"case-sensitive contains on Name" (AC-1.6, data_parameters.md § 2.1) |
a.Name.ToLower().Contains(query.Name.ToLower()) — case-INSENSITIVE contains |
| F2 | Mission (today: Flight) name filter case sensitivity |
not specified in AC-2.3 | case-INSENSITIVE contains (same ToLower().Contains(ToLower()) pattern) |
| F3 | Mission list ordering | AC-2.3 doesn't specify | OrderByDescending(f => f.CreatedDate) — newest first |
| F4 | Vehicle list ordering | AC-1.5 doesn't specify | OrderBy(a => a.Name) — alphabetical ASC |
ACs / NFTs to revise: AC-1.6 (case-INSENSITIVE); FT-N-01 (current test asserts "case mismatch returns 0 rows" which is WRONG against today's code — case is ignored, so a name=br query against BR-01 actually returns 1 row, not 0); add ordering specs to AC-1.5 and AC-2.3; FT-P-04 + FT-P-08 should assert ordering.
D-WP-NEST-CHECK — Services/WaypointService.cs (TINY)
| # | Aspect | Doc claim (AC-4.2) | Code today |
|---|---|---|---|
| W1 | Parent-mission existence check | "Parent mission missing → 404" | Code's CreateWaypoint checks db.Flights.AnyAsync(f => f.Id == flightId) and throws KeyNotFoundException ✓; UpdateWaypoint and DeleteWaypoint use a composite WHERE w.FlightId == flightId && w.Id == waypointId and throw KeyNotFoundException if no match — meaning the test for "parent missing" returns 404 BUT the doc-implied "parent missing first, then waypoint missing" two-step check is collapsed into one. |
ACs / NFTs to revise: minor — clarify in AC-4.2 that the check is "matching (flightId, waypointId) returns no row → 404", which collapses two error cases into one.
D-VERIFICATION-LOG — _docs/02_document/04_verification_log.md (META)
The verification log itself is wrong:
- § 3 row F5 (JWT validation): says "JwtExtensions matches exactly" — wrong, see D-JWT above.
- § 3 row F6 (Startup + migration): says "matches exactly" but the docs claim hardcoded fallbacks while code has
ResolveRequiredOrThrow— wrong, see D-CONFIG. - § 4.1 D6 (modules/middleware.md correction): correctly identifies the camelCase envelope, ✓.
- § 4.2 F3 (carry-forward Swagger / CORS unconditional): "CORS unconditional" is wrong — code is gated. Swagger is still unconditional ✓.
Action: re-issue § 3 rows F5, F6 with the new evidence; demote § 4.2 F3 (CORS unconditional) into the corrected list.
Recommended re-verification + revision plan (next session)
Phase 1 — /document re-run in task mode, scope = drifted files
Inputs: this drift findings report.
Skills: .cursor/skills/document/SKILL.md in Task mode.
Files to update (estimate 10–12 doc files):
| File | Sections to revise |
|---|---|
_docs/02_document/architecture.md |
§ 7 Cross-cutting (auth subsection: ECDSA+JWKS+iss/aud), § 7 (CORS subsection: gated), ADR-005 (mark obsolete or rewrite "no dev fallback" + "Swagger still ungated"), ADR-002 (no change — wire shape unaffected) |
_docs/02_document/components/05_identity/description.md |
full rewrite of "Mechanism" + "Caveats" (ECDSA, JWKS, iss/aud, calls admin) |
_docs/02_document/components/07_host/description.md |
Program.cs section (ConfigurationResolver, CorsConfigurationValidator); ADR-005 cross-ref |
_docs/02_document/modules/auth.md |
full rewrite |
_docs/02_document/modules/program.md |
rewrite startup section: env var contract, no fallback, CORS gating |
_docs/02_document/modules/database.md |
TIMESTAMP (not TIMESTAMPTZ), REFERENCES declared, DEFAULT clauses |
_docs/02_document/data_model.md |
§ 11 schema table column types + FK note |
_docs/02_document/04_verification_log.md |
re-issue § 3 F5+F6 rows; correct § 4.2 F3 |
_docs/02_document/state.json |
append decomposition_revised entry recording the verification re-run; update last_updated |
_docs/00_problem/problem.md |
review for any auth-shape claims |
_docs/00_problem/acceptance_criteria.md |
AC-1.6 (case), AC-1.5 + AC-2.3 (ordering), AC-5.1–5.7, AC-5.9, AC-9.1 (today both FL+GPS), AC-6.1, AC-6.2 |
_docs/00_problem/restrictions.md |
E1 (4 env vars), E3 (no fallback today), E4 (no shared secret), E9 (gated CORS), S6 (Swagger still ungated ✓ — no change) |
_docs/00_problem/security_approach.md |
JWT validation, CORS gating, no dev secret |
_docs/00_problem/input_data/data_parameters.md |
§ 1 env vars (4 vars now), § 3 schema (TIMESTAMP, REFERENCES, DEFAULT) |
_docs/01_solution/solution.md |
per-component table for 05 + 07 |
Phase 2 — /test-spec re-issue in scoped mode
Inputs: revised docs from Phase 1.
Skill: .cursor/skills/test-spec/SKILL.md in cycle-update mode (NOT full re-run; scoped to AC-5, AC-6.1/6.2, AC-9.1, AC-1.5, AC-1.6, AC-2.3, E1, E3, E4, E9 + the 4 NFT families that those ACs feed).
Files to revise:
| File | Scope |
|---|---|
_docs/00_problem/input_data/expected_results/results_report.md |
re-issue AC-1 row 1.6, AC-5 entire group, AC-6 rows 6.1–6.2, AC-9 row 9.1, AC-1 ordering rows, AC-2 ordering rows; add E3+E9 lock rows |
_docs/02_document/tests/environment.md |
replace "in-process JWT mint with HS256 shared secret" with "in-process ECDSA keypair + ephemeral JWKS HTTP service mock" (e.g. WireMock.NET serves /.well-known/jwks.json); add JWT_ISSUER + JWT_AUDIENCE + JWT_JWKS_URL env vars; remove JWT_SECRET |
_docs/02_document/tests/test-data.md |
rewrite "External Dependency Mocks" — admin JWKS mock; rewrite Data Validation Rules JWT rows |
_docs/02_document/tests/blackbox-tests.md |
revise FT-N-01 (case-insensitive); add ordering assertions to FT-P-04 + FT-P-08 |
_docs/02_document/tests/security-tests.md |
full revision of NFT-SEC-01 through NFT-SEC-09 (ECDSA, iss/aud, JWKS rotation, missing JWT_ISSUER startup throw) |
_docs/02_document/tests/resilience-tests.md |
revise NFT-RES-05 (ResolveRequiredOrThrow failure modes — add scenarios for missing each of the 4 env vars); revise NFT-RES-07 (JWKS rotation, not shared-secret rotation) |
_docs/02_document/tests/traceability-matrix.md |
re-trace AC-5, AC-6, AC-9, AC-1.5, AC-1.6, AC-2.3, E-rows |
docker-compose.test.yml |
replace JWT_SECRET with JWT_ISSUER + JWT_AUDIENCE + JWT_JWKS_URL; add a jwks-mock service (e.g. WireMock or a small Kestrel test server) |
Phase 3 — Resume autodev Step 4 (Code Testability Revision)
After Phase 1+2: re-enter existing-code Step 4 with the revised docs + test specs. The original Step 4 analysis result ("code is largely testable as-is") still holds — the JWT/CORS/Config drift didn't introduce hardcoded paths or singletons, it just made the code MORE testable than the docs described.
Expected outcome: Step 4 → "all scenarios testable as-is" → Step 5 (Decompose Tests, session boundary).
Cross-cutting acknowledgements
- The B-ticket plan (B5–B12) is unaffected. None of the drift overlaps with the rename/GPS-Denied work — the JWT/CORS/Config evolution happened independently.
- The
_docs/_process_leftovers/2026-05-14_rename-flights-to-missions.mdleftover stays as-is. - The suite docs (
../suite/_docs/00_roles_permissions.md,../suite/_docs/05_identity*, etc.) likely have correlated drift on the JWT model. Out of scope for this repo's/autodev; flag at suite-level next time/autodevruns in the suite workspace.