| | | 1 | | using ArturRios.Data.Relational.Core.DependencyInjection; |
| | | 2 | | using ArturRios.Heimdall.Command.Services; |
| | | 3 | | using ArturRios.Heimdall.Query.Handlers; |
| | | 4 | | using ArturRios.Heimdall.Query.HealthChecks; |
| | | 5 | | using ArturRios.Heimdall.Query.Input; |
| | | 6 | | using ArturRios.Heimdall.Query.Output; |
| | | 7 | | using ArturRios.Heimdall.Shared.Security; |
| | | 8 | | using ArturRios.Heimdall.Shared.Services; |
| | | 9 | | using ArturRios.Heimdall.WebApi.Binding; |
| | | 10 | | using ArturRios.Heimdall.WebApi.Monitoring; |
| | | 11 | | using ArturRios.Heimdall.WebApi.Security; |
| | | 12 | | using ArturRios.Mediator.Query.Interfaces; |
| | | 13 | | using ArturRios.Util.WebApi.Configuration; |
| | | 14 | | using ArturRios.Util.WebApi.Security.Extensions; |
| | | 15 | | |
| | | 16 | | namespace ArturRios.Heimdall.WebApi; |
| | | 17 | | |
| | | 18 | | public static class WebApiServiceCollectionExtensions |
| | | 19 | | { |
| | | 20 | | /// <summary> |
| | | 21 | | /// Registers the accessors that read the acting person and the retry hint from the current HTTP |
| | | 22 | | /// request. |
| | | 23 | | /// </summary> |
| | | 24 | | public static IServiceCollection AddRequestContext(this IServiceCollection services) |
| | | 25 | | { |
| | 973 | 26 | | services.AddHttpContextAccessor(); |
| | 973 | 27 | | services.AddScoped<IActorAccessor, HttpContextActorAccessor>(); |
| | 973 | 28 | | services.AddScoped<IRetryAfterAdvisor, HttpResponseRetryAfterAdvisor>(); |
| | | 29 | | |
| | 973 | 30 | | return services; |
| | | 31 | | } |
| | | 32 | | |
| | | 33 | | /// <summary> |
| | | 34 | | /// Registers the health checks (UC-30) and the readiness probe's cache (#215). |
| | | 35 | | /// </summary> |
| | | 36 | | public static IServiceCollection AddHealthProbes(this IServiceCollection services) |
| | | 37 | | { |
| | | 38 | | // Health checks (UC-30). Each IServiceHealthCheck is one verified dependency; the detailed |
| | | 39 | | // handler resolves them all as IEnumerable, so new checks are added by registering another. |
| | | 40 | | // The readiness probe (#215) asks the database check alone, so it is also resolvable as itself. |
| | 973 | 41 | | services.AddSingleton(TimeProvider.System); |
| | 973 | 42 | | services.AddSingleton<ReadinessCache>(); |
| | 973 | 43 | | services.AddScoped<DatabaseHealthCheck>(); |
| | 975 | 44 | | services.AddScoped<IServiceHealthCheck>(provider => provider.GetRequiredService<DatabaseHealthCheck>()); |
| | 973 | 45 | | services |
| | 973 | 46 | | .AddScoped<IQueryHandlerAsync<DetailedHealthQuery, HealthCheckOutput>, GetDetailedHealthQueryHandler>(); |
| | | 47 | | |
| | 973 | 48 | | return services; |
| | | 49 | | } |
| | | 50 | | |
| | | 51 | | /// <summary> |
| | | 52 | | /// Registers the person, challenge and ownership services that issue and check tokens. |
| | | 53 | | /// </summary> |
| | | 54 | | public static IServiceCollection AddTokenIssuing(this IServiceCollection services) |
| | | 55 | | { |
| | 973 | 56 | | services.AddScoped<IScopeOwnershipChecker, ScopeOwnershipChecker>(); |
| | | 57 | | |
| | | 58 | | // UC-11 issues tokens through the same claims mapper the middleware validates them with, |
| | | 59 | | // registered by AddTokenAuthentication in TokenSecurityRegistrationExtensions.AddTokenSecurity. |
| | 973 | 60 | | services.AddScoped<IAuthTokenIssuer, JwtAuthTokenIssuer>(); |
| | | 61 | | // Shared by LoginCommandHandler (UC-11) and VerifyTwoFactorAuthCommandHandler (UC-38) so the |
| | | 62 | | // scope-eligibility rules and the final token-issuing call live in one place. |
| | 973 | 63 | | services.AddScoped<PersonAuthTokenService>(); |
| | | 64 | | // UC-38's challenge token (FR-2F-07…FR-2F-10, NFR-17): one class both issues and validates |
| | | 65 | | // it, registered once and exposed through both of its interfaces. |
| | 973 | 66 | | services.AddScoped<JwtTwoFactorChallengeTokenIssuer>(); |
| | 973 | 67 | | services.AddScoped<ITwoFactorChallengeTokenIssuer>( |
| | 1082 | 68 | | provider => provider.GetRequiredService<JwtTwoFactorChallengeTokenIssuer>()); |
| | 973 | 69 | | services.AddScoped<ITwoFactorChallengeTokenValidator>( |
| | 1063 | 70 | | provider => provider.GetRequiredService<JwtTwoFactorChallengeTokenIssuer>()); |
| | | 71 | | |
| | 973 | 72 | | return services; |
| | | 73 | | } |
| | | 74 | | |
| | | 75 | | /// <summary> |
| | | 76 | | /// Adds the MVC configuration Heimdall applies on top of the standard sequence's controllers. |
| | | 77 | | /// </summary> |
| | | 78 | | public static IServiceCollection AddHeimdallControllers(this IServiceCollection services) |
| | | 79 | | { |
| | | 80 | | // The standard sequence has already called AddControllers(); calling it again adds this |
| | | 81 | | // configuration to the same MVC registration. |
| | | 82 | | // |
| | | 83 | | // MfaPendingGuardFilter (FR-2F-10, NFR-17) runs as a global MVC authorization filter, on |
| | | 84 | | // every controller action — added here rather than per-action, since a UC-38 challenge token |
| | | 85 | | // must be rejected everywhere except POST /api/auth/2fa/verify, and that endpoint needs no |
| | | 86 | | // opt-out: it never reads the challenge token as a bearer credential to begin with. |
| | 973 | 87 | | services.AddControllers(options => |
| | 973 | 88 | | { |
| | 966 | 89 | | options.Filters.Add<MfaPendingGuardFilter>(); |
| | 973 | 90 | | |
| | 973 | 91 | | // NFR-24: a subject-rights-only token, issued to a restricted identity at sign-in, is |
| | 973 | 92 | | // confined to the data-subject-rights actions — on its claims alone, so before the |
| | 973 | 93 | | // liveness check's database read, like the guard above. |
| | 966 | 94 | | options.Filters.Add<SubjectRightsOnlyGuardFilter>(); |
| | 973 | 95 | | |
| | 973 | 96 | | // Runs after the guard above, which is the cheap check: a challenge token misused as a |
| | 973 | 97 | | // bearer credential is rejected on its claims alone, without spending the database read |
| | 973 | 98 | | // this one makes. Global for the same reason — a token naming a deleted identity must be |
| | 973 | 99 | | // refused everywhere, and leaving that to each handler is what let a logically deleted |
| | 973 | 100 | | // System Admin keep acting until their token expired. |
| | 966 | 101 | | options.Filters.Add<ActorLivenessFilter>(); |
| | 973 | 102 | | |
| | 973 | 103 | | // Turns a saturated password-hash gate into 503 rather than 500 (TH-03). An exception |
| | 973 | 104 | | // filter because the condition is cross-cutting: every endpoint that derives a password |
| | 973 | 105 | | // can meet it, and each has its own message map. |
| | 966 | 106 | | options.Filters.Add<PasswordHashSaturationFilter>(); |
| | 973 | 107 | | |
| | 973 | 108 | | // A server-side line for every 5xx the pipeline produces as a result, which nothing else |
| | 973 | 109 | | // writes (#227); an unhandled exception is already logged by ExceptionMiddleware. |
| | 966 | 110 | | options.Filters.Add<ServerErrorLoggingFilter>(); |
| | 973 | 111 | | |
| | 973 | 112 | | // NFR-10 (#186): a request MVC could not bind answers 400 instead of running the action with |
| | 973 | 113 | | // a null or default argument. An action filter, so after the authorization filters above — |
| | 973 | 114 | | // an unauthenticated caller still gets 401 — and after model binding has filled ModelState. |
| | 966 | 115 | | options.Filters.Add<MalformedRequestFilter>(); |
| | 973 | 116 | | |
| | 973 | 117 | | // Also applied by ArturRios.Heimdall.OpenApiGen/Program.cs, which builds its own |
| | 973 | 118 | | // AddControllers() rather than running this Startup, so that call site cannot catch a |
| | 973 | 119 | | // removal here. Without it, [FromQuery] list-query properties marked server-populated |
| | 973 | 120 | | // would become bindable from the query string again on the running API — the controllers |
| | 973 | 121 | | // still overwrite them before use, so the query-string forgery tests would keep passing, |
| | 973 | 122 | | // and the published document (generated from the other call site) would look unchanged. |
| | 973 | 123 | | // The guarantee that these properties are non-bindable, not merely undocumented, would |
| | 973 | 124 | | // silently revert with no test catching it. |
| | 966 | 125 | | ModelBindingConfiguration.Configure(options); |
| | 1939 | 126 | | }); |
| | 973 | 127 | | services.AddEndpointsApiExplorer(); |
| | | 128 | | |
| | 973 | 129 | | return services; |
| | | 130 | | } |
| | | 131 | | } |