Files
Oleksandr Bezdieniezhnykh 78dea8ebab
ci/woodpecker/push/build-arm Pipeline was successful
chore: update configuration and Docker setup for JWT and test results
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.
2026-05-15 03:23:23 +03:00

11 KiB

Module: Program (composition root) + GlobalUsings

Files (2): Program.cs, GlobalUsings.cs

NOTE (forward-looking): post-rename. Today's source has Azaion.Flights namespace and dotnet Azaion.Flights.dll entrypoint. 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:8080 in container per Dockerfile EXPOSE 8080).
  • Exposes routes mapped by MapControllers (see controller_vehicles.md, controller_missions.md) plus GET /health.
  • Serves Swagger UI at the default /swagger route (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.csResolveRequiredOrThrow(IConfiguration, envVar, configKey, humanLabel):

  1. Read Environment.GetEnvironmentVariable(envVar). If non-whitespace, return it.
  2. Otherwise read configuration[configKey]. If non-whitespace, return it.
  3. Otherwise throw InvalidOperationException with 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"] in Dockerfile after B10).
  • dotnet run for 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 consultedmodules/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 .NET ConfigurationManager default schedule). Subsequent request-path JWT validation uses the cached keys and does not call back. See modules/auth.md § External Integrations and § Security for details.

Security

  • Fail-fast configuration: ConfigurationResolver.ResolveRequiredOrThrow aborts startup if any of DATABASE_URL, JWT_ISSUER, JWT_AUDIENCE, JWT_JWKS_URL is 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 in Production when CorsConfig:AllowedOrigins is empty AND CorsConfig:AllowAnyOrigin is not true. In non-Production environments (e.g. local dotnet run, Development, Staging) an empty allow-list with AllowAnyOrigin=false falls back to permissive (AllowAnyOrigin/Method/Header) and emits the PermissiveDefaultWarning startup 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 container EXPOSE 8080 is plain HTTP.
  • HTTPS-only JWKS: HttpDocumentRetriever { RequireHttps = true } (set inside AddJwtAuth) rejects plain-HTTP JWKS URLs at configuration time. A misconfigured JWT_JWKS_URL=http://... aborts startup.
  • app.UseMiddleware<ErrorHandlingMiddleware> runs before UseAuthentication/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 converts KeyNotFoundException → 404, etc.; auth pipeline doesn't typically throw those).

Tests

None present.

Notes / Smells

  1. DATABASE_URL URL parsing: ConvertPostgresUrl is 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.
  2. CORS gating is Production-only at the hard-fail layer. In Staging or any custom ASPNETCORE_ENVIRONMENT name that is not literal Production (case-insensitive), an empty allow-list with AllowAnyOrigin=false falls back to permissive instead of throwing. Operators deploying to a "Staging" tier that should be locked down need to set CorsConfig:AllowedOrigins explicitly — the validator will not enforce it for them.
  3. JWKS first-fetch is synchronous on the worker thread (GetAwaiter().GetResult() inside the IssuerSigningKeyResolver). If admin is slow or unreachable when the first protected request arrives, that request blocks until the HTTP fetch returns. See modules/auth.md § Notes #2.
  4. AddSwaggerGen() with no JWT bearer security definition — Swagger UI's "Authorize" button won't appear; users must supply tokens via curl -H "Authorization: Bearer ...". Not a bug, but a usability issue.
  5. DatabaseMigrator.Migrate is fire-and-forget — if it throws (DB down at startup), the host process crashes. Acceptable for container orchestration that restarts on failure.
  6. GlobalUsings.cs imports LinqToDB.Async but most async LINQ extensions used by the project (AnyAsync, FirstOrDefaultAsync, ToListAsync, etc.) actually live in the LinqToDB namespace already. Harmless redundancy.
  7. Service lifetime: AppDataConnection is scoped (per-HTTP-request) — correct, because DataConnection holds 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).