Repository navigation
Expand file tree
/
Copy pathProgram.cs
More file actions
160 lines (137 loc) · 8.46 KB
/
Copy pathProgram.cs
File metadata and controls
160 lines (137 loc) · 8.46 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
using LearnStack.Api.Authentication;
using LearnStack.Api.Common;
using LearnStack.Api.Composition;
using LearnStack.Api.PublicReads;
using LearnStack.Api.Tenancy;
using LearnStack.Api.Versioning;
using LearnStack.SharedKernel.Hosting;
using Microsoft.AspNetCore.Authorization;
var builder = WebApplication.CreateBuilder(args);
// Resolve the deployment mode once at the composition root. Modules never
// read DeploymentMode (architecture test
// Modules_Do_Not_Reference_DeploymentMode enforces it); the value selects
// the right error tracker, OTLP exporter target, and — each with a named
// owner rather than "later" — the host resolver (Packet 7), the entitlement
// provider (Packet 9 / Phase 02c) and the Dapr-backed event bus, cache and
// secret adapters (Phase 11, per ADR-0035's triggers), all per
// docs/standards/20-infrastructure-stack.md § Composition Root.
// No default. The key used to ship as "Development" in appsettings.json —
// the file that goes to every environment — while appsettings.Development.json
// set none, so every Development-guarded mechanism was on by default in a
// deployment that never configured it. A startup failure naming the key is the
// version of that mistake an operator can see (ADR-0036).
var deploymentMode = builder.Configuration.RequireDeploymentMode();
// Refuse the one way forwarded headers can be wired without touching this
// file. Forwarded_Headers_Are_Not_Wired reads the assembly reference table and
// the text of Program.cs; ASPNETCORE_FORWARDEDHEADERS_ENABLED touches neither,
// and measured, it turns the anonymous rate limiter off for anyone who sends
// X-Forwarded-For.
builder.Configuration.RefuseAmbientForwardedHeaders();
// The module assemblies MediatR scans for handlers. Tenancy's is here as of Packet 7,
// which shipped the first production request types — and the parameter existed all along,
// so the change was one argument rather than a new seam. Customization's joined it in
// Packet 8; Education joins for P02d-2's contextual verification queries.
// A module whose assembly is missing here has handlers nothing dispatches, and
// FluentValidation validators nothing runs — which fails as "no handler for request" at
// the call site rather than at startup, and as a command that skipped its guards.
builder.AddLearnStackCrossCuttingFoundation(
deploymentMode,
typeof(LearnStack.Modules.Tenancy.Application.AssemblyMarker).Assembly,
typeof(LearnStack.Modules.Customization.Application.AssemblyMarker).Assembly,
typeof(LearnStack.Modules.Education.Application.AssemblyMarker).Assembly,
typeof(LearnStack.Modules.Identity.Application.AssemblyMarker).Assembly);
builder.Services.AddLearnStackTenancyEdge(builder.Configuration);
builder.Services.AddLearnStackPersistence(builder.Configuration);
builder.Services.AddLearnStackRateLimiting();
// Register and activate the entire closed HTTP boundary together. Minimal hosting
// can insert authentication/authorization merely because their services exist;
// the explicit middleware order below owns both insertions and all guards.
builder.Services.AddLearnStackTenantAuthentication(
TenantAuthenticationOptions.FromConfiguration(builder.Configuration, deploymentMode));
builder.Services.AddLearnStackTenantSubjectAdmission();
builder.Services.AddLearnStackTenantIdentityBindingAdmission();
builder.Services.AddAuthorization();
builder.Services.AddSingleton<IAuthorizationMiddlewareResultHandler, TenantAuthorizationResultHandler>();
// The outer half of the body bound, and deliberately NOT the same number.
// RequestBodyLimit's middleware is the authoritative one — it is the only one
// TestServer honours, so it is the only one the integration suite can assert —
// and it counts decoded payload bytes. Kestrel counts raw bytes off the wire,
// chunk framing included, so an equal number would make Kestrel strictly
// tighter for a chunked body: measured, a 762 KB payload in 16-byte chunks is
// 413 when both are 1 MiB. Headroom makes the middleware the bound that
// decides for any realistic framing, and leaves Kestrel the case the middleware
// cannot see — a body nothing ever reads. See KestrelBackstopBytes for the
// residual it does not close and why that one is accepted.
builder.WebHost.ConfigureKestrel(options =>
options.Limits.MaxRequestBodySize = RequestBodyLimit.KestrelBackstopBytes);
// Controllers + the /api/v{N} route convention + one OpenAPI document per
// live major, per ADR-0024. AddLearnStackApiVersioning owns the
// AddControllers call so the convention cannot be registered twice or, worse,
// registered after a second AddControllers has already built the model.
builder.Services.AddLearnStackApiVersioning();
var app = builder.Build();
app.UseLearnStackPublicResponsePolicy();
app.UseExceptionHandler();
// UseStatusCodePages goes here, ahead of everything that can short-circuit
// with a bodyless status. It only wraps middleware DOWNSTREAM of itself, so
// registering it after the rate limiter left a 429 with no body at all — the
// one client error that skipped the shape every other one carries. 404 and 405
// come from routing, further down, for the same reason: no action runs, so no
// MVC hook sees them.
app.MapLearnStackClientErrors();
// Every response carries the correlation id, including the ones the middleware
// below short-circuits.
app.UseLearnStackCorrelationHeader();
// Before anything that costs a database round trip. From Packet 7 every novel
// Host value buys a Postgres transaction and a cache entry on a pre-auth
// surface; architecture/30 has promised this middleware since Phase 01 and
// nothing delivered it, and ADR-0035 puts the gateway that would replace it in
// Phase 11.
app.UseRateLimiter();
// Invalid authenticated visitor metadata has spent its bounded fallback budget.
// Refuse before host classification so malformed values cannot purchase lookups.
app.UseLearnStackVisitorAdmission();
// Below MapLearnStackClientErrors, so a 413 acquires the one Problem Details
// shape without a second writer; after the rate limiter, so a client flooding
// oversized bodies hears about the rate limit rather than the size.
app.UseLearnStackRequestBodyLimit();
// The OpenAPI document and its reference UI are served in every environment,
// not only Development. The document IS the contract Standards 04 § OpenAPI
// publishes and the SDK generates from; hiding it outside Development would
// mean the artefact CI diffs is one no deployed instance serves. Exposure is
// an edge concern — APISIX blocks or allow-lists /openapi per environment.
app.MapLearnStackOpenApi();
// Which host is this /api/v1 request for? Before authentication, so an unknown
// host is refused before any token is validated (ADR-0036 § Rules) — and after
// the rate limiter, so a flood of novel hostnames is bounded before it buys a
// Postgres transaction each. Context construction runs later, after
// authentication, so the factory sees both signals at once.
app.UseLearnStackHostClassification();
app.UseAuthentication();
app.UseLearnStackTenantCredentialAdmission();
app.UseLearnStackTenantSubjectAdmission();
app.UseLearnStackTenantIdentityBindingAdmission();
// Reconcile both trusted signals once, after credential/quota/binding admission.
// Authentication precedes resolution; authorization follows header assertions.
// Keeping these insertions separate preserves ADR-0036/ADR-0064's order.
app.UseLearnStackTenantResolution();
// X-Tenant-Id / X-Organization-Id are assertions: compared against what the
// API resolved, never a source of it (ADR-0036). Registered after
// MapLearnStackClientErrors so a rejection gets the one Problem Details shape,
// after the resolver so there is something to compare against — until this
// packet the comparison had no resolved value and was unreachable in traffic —
// and before the endpoints so no handler runs on a request that lost it.
app.UseLearnStackTenantAssertions();
app.UseAuthorization();
app.UseLearnStackTenantSurfaceAdmission();
app.MapGet("/healthz", () => Results.Ok(new { status = "healthy" }))
.WithName("HealthCheck");
app.MapControllers();
app.Run();
// `public partial class Program` is the top-level-statements escape hatch
// that lets WebApplicationFactory<Program> in the test assemblies resolve
// the entry-point type. CA1515 is downgraded to `none` for this project
// in `backend/src/LearnStack.Api/.editorconfig` — the test harness is the
// external consumer and it cannot see `internal` types without an
// InternalsVisibleTo dance that confuses Program-discovery.
public partial class Program;