chore: update configuration and Docker setup for JWT and test results
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.
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-15 03:23:23 +03:00
parent 7025f4d075
commit 78dea8ebab
40 changed files with 1990 additions and 510 deletions
+63 -35
View File
@@ -27,26 +27,37 @@ global using LinqToDB.Data;
```text
1. WebApplicationBuilder = WebApplication.CreateBuilder(args)
2. Resolve DATABASE_URL (Configuration -> Env -> fallback)
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. Resolve JWT_SECRET (Configuration -> Env -> fallback)
4. Register services (scoped where applicable):
3. Register services (scoped where applicable):
- AppDataConnection <- scoped, built via new DataOptions().UsePostgreSQL(connectionString)
- MissionService, WaypointService, VehicleService <- scoped
- AddJwtAuth(jwtSecret) -> JWT bearer + "FL" policy
- AddCors with default policy = AllowAnyOrigin/Method/Header
- AddControllers, AddEndpointsApiExplorer, AddSwaggerGen
5. Build the WebApplication.
6. Open a temp scope, resolve AppDataConnection, call DatabaseMigrator.Migrate(db).
7. Configure pipeline (order matters):
a. UseMiddleware<ErrorHandlingMiddleware>
b. UseCors
c. UseAuthentication
d. UseAuthorization
e. UseSwagger, UseSwaggerUI
f. MapControllers
g. MapGet("/health", () => Results.Ok({status:"healthy"}))
8. app.Run()
- 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
@@ -54,6 +65,14 @@ ConvertPostgresUrl(url):
(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}`.
@@ -66,26 +85,34 @@ ConvertPostgresUrl(url):
## Configuration
| Env / Config Key | Required? | Default |
|------------------|-----------|---------|
| `DATABASE_URL` | No (has dev fallback) | `Host=localhost;Database=azaion;Username=postgres;Password=changeme` |
| `JWT_SECRET` | No (has dev fallback) | `development-secret-key-min-32-chars!!` |
| `AZAION_REVISION` | Set by Dockerfile from `CI_COMMIT_SHA` | `unknown` (build arg default) |
| 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 (per discovery) -- config comes from env / process variables only. Suite-wide env conventions live in `../../suite/_docs/00_top_level_architecture.md` (Edge compose excerpt).
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.
- Identity provider: the suite's `admin` service mints JWTs against the central user PostgreSQL; `JWT_SECRET` is the shared HMAC secret. Local validation only -- no network round-trip per request.
- **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
- **Hardcoded fallbacks** for both `DATABASE_URL` and `JWT_SECRET` are dev-only. Production deployments MUST override them; failure to do so silently runs with weak/known credentials.
- **CORS is permissive** in all environments (`AllowAnyOrigin/Method/Header`). Combined with JWT auth this is not catastrophic (browser will send the bearer token only if the front-end opts in), but exposes the API to opportunistic browser-based scraping.
- **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.
- **No HTTPS redirection** middleware (`UseHttpsRedirection`) -- TLS is assumed to terminate at an upstream reverse proxy.
- **`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).
- **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
@@ -94,8 +121,9 @@ 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. **No `IsDevelopment()` checks** anywhere in `Program.cs`. Dev/prod behaviors (Swagger, fallback secrets) are not gated.
3. **`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.
4. **`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.
5. **`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.
6. **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).
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).