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.
11 KiB
Module: Program (composition root) + GlobalUsings
Files (2): Program.cs, GlobalUsings.cs
NOTE (forward-looking): post-rename. Today's source has
Azaion.Flightsnamespace anddotnet Azaion.Flights.dllentrypoint. Renames + DLL/image changes tracked under Jira AZ-EPIC children B5 (namespace), B7 (drop GPS policy), B10 (Dockerfile + docker image rename).
Purpose
Top-level statements that build the ASP.NET Core web host: read environment, register DI services (DB connection, services, auth, CORS, MVC, Swagger), run the migrator once, then app.Run().
GlobalUsings.cs adds three project-wide global using directives so individual files don't need to repeat them:
global using LinqToDB;
global using LinqToDB.Async;
global using LinqToDB.Data;
Public Interface
Program.cs is a top-level program -- it is not a class with a public surface. Its observable contract is the resulting HTTP server:
- Listens on the Kestrel-default URL (typically
http://0.0.0.0:8080in container per DockerfileEXPOSE 8080). - Exposes routes mapped by
MapControllers(seecontroller_vehicles.md,controller_missions.md) plusGET /health. - Serves Swagger UI at the default
/swaggerroute (not gated on environment).
Internal Logic
1. WebApplicationBuilder = WebApplication.CreateBuilder(args)
2. Resolve DATABASE_URL via ConfigurationResolver.ResolveRequiredOrThrow
(env var DATABASE_URL -> config key Database:Url -> THROW).
If it begins with "postgresql://" -> ConvertPostgresUrl() to Npgsql key=value form.
3. Register services (scoped where applicable):
- AppDataConnection <- scoped, built via new DataOptions().UsePostgreSQL(connectionString)
- MissionService, WaypointService, VehicleService <- scoped
- AddJwtAuth(builder.Configuration) -> resolves JWT_ISSUER + JWT_AUDIENCE + JWT_JWKS_URL
(each via ResolveRequiredOrThrow). Registers JWT bearer + "FL" + "GPS" policies.
(GPS policy is removed in Jira B7.)
4. Resolve CORS configuration:
- allowedOrigins = Configuration.GetSection("CorsConfig:AllowedOrigins").Get<string[]>() ?? []
- allowAnyOrigin = Configuration.GetValue<bool>("CorsConfig:AllowAnyOrigin")
- CorsConfigurationValidator.EnsureSafeForEnvironment(allowedOrigins, allowAnyOrigin, EnvironmentName)
THROWS in Production when origins are empty AND AllowAnyOrigin is false (fail-fast guard).
5. Register CORS:
- If CorsConfigurationValidator.ShouldUsePermissivePolicy(...) -> AllowAnyOrigin/Method/Header
- Else -> WithOrigins(allowedOrigins).AllowAnyHeader().AllowAnyMethod()
6. Register MVC infra: AddControllers, AddEndpointsApiExplorer, AddSwaggerGen.
7. Build the WebApplication.
8. If CorsConfigurationValidator.ShouldWarnAboutPermissiveDefault(...) -> log warning
(logs PermissiveDefaultWarning with the resolved EnvironmentName).
9. Open a temp scope, resolve AppDataConnection, call DatabaseMigrator.Migrate(db).
10. Configure pipeline (order matters):
a. UseMiddleware<ErrorHandlingMiddleware>
b. UseCors
c. UseAuthentication
d. UseAuthorization
e. UseSwagger, UseSwaggerUI (unconditional — see ADR-005)
f. MapControllers
g. MapGet("/health", () => Results.Ok({status:"healthy"}))
11. app.Run()
ConvertPostgresUrl(url):
parses postgresql://user[:pass]@host[:port]/db into
"Host={host};Port={port};Database={db};Username={user};Password={pass}"
(defaults port to 5432; absent password becomes empty)
The four required configuration values (DATABASE_URL, JWT_ISSUER, JWT_AUDIENCE, JWT_JWKS_URL) all flow through Infrastructure/ConfigurationResolver.cs → ResolveRequiredOrThrow(IConfiguration, envVar, configKey, humanLabel):
- Read
Environment.GetEnvironmentVariable(envVar). If non-whitespace, return it. - Otherwise read
configuration[configKey]. If non-whitespace, return it. - Otherwise throw
InvalidOperationExceptionwith a human-readable message naming both the env var and the config key.
There is no hardcoded fallback for any of these values; an unset env var aborts startup. ADR-005 (in architecture.md) is obsolete for the secret-fallback aspect — only the Swagger-unconditional aspect of ADR-005 still applies today.
Dependencies
- All internal namespaces:
Azaion.Missions.{Auth, Database, Middleware, Services}. - ASP.NET Core, LinqToDB, Npgsql, Swashbuckle, JWT bearer (NuGet).
Consumers
- The container runtime (
ENTRYPOINT ["dotnet", "Azaion.Missions.dll"]inDockerfileafter B10). dotnet runfor local development.
Configuration
| Env var | Config key | Required? | Default | Notes |
|---|---|---|---|---|
DATABASE_URL |
Database:Url |
Yes | — (throws at startup if unset) | Either Npgsql key=value form OR a postgresql:// URI (converted via ConvertPostgresUrl) |
JWT_ISSUER |
Jwt:Issuer |
Yes | — (throws at startup if unset) | Expected iss claim value (per modules/auth.md) |
JWT_AUDIENCE |
Jwt:Audience |
Yes | — (throws at startup if unset) | Expected aud claim value (per modules/auth.md) |
JWT_JWKS_URL |
Jwt:JwksUrl |
Yes | — (throws at startup if unset) | HTTPS URL of admin's JWKS endpoint (per modules/auth.md) |
CorsConfig:AllowedOrigins |
CorsConfig:AllowedOrigins |
No (defaults to empty) | [] |
String array. When non-empty, the CORS policy uses WithOrigins(...) |
CorsConfig:AllowAnyOrigin |
CorsConfig:AllowAnyOrigin |
No (defaults to false) |
false |
When true, the CORS policy uses AllowAnyOrigin/Method/Header regardless of origins. When false AND origins are empty AND environment is Production, startup THROWS via CorsConfigurationValidator.EnsureSafeForEnvironment |
ASPNETCORE_ENVIRONMENT |
(n/a — framework) | No | Production |
Read by the framework. The CORS validator's hard-fail behavior triggers only in Production |
AZAION_REVISION |
(n/a) | No | unknown (Dockerfile build-arg default) |
Set by Dockerfile from CI_COMMIT_SHA |
There is no appsettings.json in this repo today (per discovery), so all values flow through env vars in practice. Both env-var and config-key resolution are wired so that an appsettings.json (or any other IConfiguration source) can be added later without code changes. Suite-wide env conventions live in ../../suite/_docs/00_top_level_architecture.md (Edge compose excerpt).
The legacy JWT_SECRET env var is no longer consulted — modules/auth.md documents the ECDSA + JWKS replacement.
External Integrations
- PostgreSQL (read/write) via Npgsql. Connection string resolved at startup; connection pool managed by Npgsql.
admin(JWKS) — outbound HTTPS once at startup (and on JWKS refresh per the .NETConfigurationManagerdefault schedule). Subsequent request-path JWT validation uses the cached keys and does not call back. Seemodules/auth.md§ External Integrations and § Security for details.
Security
- Fail-fast configuration:
ConfigurationResolver.ResolveRequiredOrThrowaborts startup if any ofDATABASE_URL,JWT_ISSUER,JWT_AUDIENCE,JWT_JWKS_URLis missing or whitespace-only. There are no hardcoded dev fallbacks for any of these values; a misconfigured production deploy cannot silently boot with a known-weak credential. - CORS is gated:
CorsConfigurationValidator.EnsureSafeForEnvironment(...)THROWS inProductionwhenCorsConfig:AllowedOriginsis empty ANDCorsConfig:AllowAnyOriginis nottrue. In non-Production environments (e.g. localdotnet run,Development,Staging) an empty allow-list withAllowAnyOrigin=falsefalls back to permissive (AllowAnyOrigin/Method/Header) and emits thePermissiveDefaultWarningstartup log. The "all-environments-permissive" model described in older docs no longer holds. - Swagger is unconditionally enabled — both the JSON document and the UI are served regardless of environment. Anyone reaching the host can enumerate the API surface. This is the only remaining aspect of ADR-005 that still applies after the fail-fast change; the "dev fallback secret" aspect of ADR-005 is now obsolete.
- No HTTPS redirection middleware (
UseHttpsRedirection) — TLS is assumed to terminate at an upstream reverse proxy. The containerEXPOSE 8080is plain HTTP. - HTTPS-only JWKS:
HttpDocumentRetriever { RequireHttps = true }(set insideAddJwtAuth) rejects plain-HTTP JWKS URLs at configuration time. A misconfiguredJWT_JWKS_URL=http://...aborts startup. app.UseMiddleware<ErrorHandlingMiddleware>runs beforeUseAuthentication/UseAuthorization— auth failures still emit the framework's stock 401/403 (which is fine), but any auth-stage exceptions ALSO run through the global handler (which convertsKeyNotFoundException→ 404, etc.; auth pipeline doesn't typically throw those).
Tests
None present.
Notes / Smells
DATABASE_URLURL parsing:ConvertPostgresUrlis a small ad-hoc parser. Fine for typical cases but does not URL-decode the user/password. A password containing@,:,/,%would break parsing or be interpreted wrong. Carry to verification log.- CORS gating is
Production-only at the hard-fail layer. InStagingor any customASPNETCORE_ENVIRONMENTname that is not literalProduction(case-insensitive), an empty allow-list withAllowAnyOrigin=falsefalls back to permissive instead of throwing. Operators deploying to a "Staging" tier that should be locked down need to setCorsConfig:AllowedOriginsexplicitly — the validator will not enforce it for them. - JWKS first-fetch is synchronous on the worker thread (
GetAwaiter().GetResult()inside theIssuerSigningKeyResolver). Ifadminis slow or unreachable when the first protected request arrives, that request blocks until the HTTP fetch returns. Seemodules/auth.md§ Notes #2. AddSwaggerGen()with no JWT bearer security definition — Swagger UI's "Authorize" button won't appear; users must supply tokens viacurl -H "Authorization: Bearer ...". Not a bug, but a usability issue.DatabaseMigrator.Migrateis fire-and-forget — if it throws (DB down at startup), the host process crashes. Acceptable for container orchestration that restarts on failure.GlobalUsings.csimportsLinqToDB.Asyncbut most async LINQ extensions used by the project (AnyAsync,FirstOrDefaultAsync,ToListAsync, etc.) actually live in theLinqToDBnamespace already. Harmless redundancy.- Service lifetime:
AppDataConnectionis scoped (per-HTTP-request) — correct, becauseDataConnectionholds a backing Npgsql connection that should not be shared across requests. The three domain services share this scope, so all DB calls within one request go through the same physical connection (good for correctness, no implicit transactions though).