mirror of
https://github.com/azaion/missions.git
synced 2026-06-21 22:21:08 +00:00
78dea8ebab
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.
130 lines
11 KiB
Markdown
130 lines
11 KiB
Markdown
# 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:
|
|
|
|
```csharp
|
|
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
|
|
|
|
```text
|
|
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)`:
|
|
|
|
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 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 .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).
|