feat: enforce tenant isolation and shared question bank
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.RateLimiting;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
using System.Net;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using Scalar.AspNetCore;
|
||||
@@ -16,6 +18,7 @@ using Tiku.Api.Options;
|
||||
using Tiku.Api.Security;
|
||||
using Tiku.Application;
|
||||
using Tiku.Application.Security;
|
||||
using Tiku.Application.Tenancy;
|
||||
using Tiku.Infrastructure;
|
||||
using Tiku.Infrastructure.Commerce;
|
||||
using Tiku.Infrastructure.Persistence;
|
||||
@@ -49,6 +52,30 @@ try
|
||||
});
|
||||
builder.Services.AddProblemDetails();
|
||||
builder.Services.AddApplication();
|
||||
builder.Services.AddOptions<TenantResolutionOptions>()
|
||||
.Bind(builder.Configuration.GetSection(TenantResolutionOptions.SectionName));
|
||||
builder.Services.Configure<ForwardedHeadersOptions>(options =>
|
||||
{
|
||||
options.ForwardedHeaders =
|
||||
ForwardedHeaders.XForwardedFor |
|
||||
ForwardedHeaders.XForwardedHost |
|
||||
ForwardedHeaders.XForwardedProto;
|
||||
options.ForwardLimit = 1;
|
||||
options.KnownProxies.Clear();
|
||||
options.KnownIPNetworks.Clear();
|
||||
var resolution = builder.Configuration
|
||||
.GetSection(TenantResolutionOptions.SectionName)
|
||||
.Get<TenantResolutionOptions>() ?? new TenantResolutionOptions();
|
||||
foreach (var address in resolution.TrustedProxyAddresses)
|
||||
{
|
||||
if (IPAddress.TryParse(address, out var proxy))
|
||||
{
|
||||
options.KnownProxies.Add(proxy);
|
||||
}
|
||||
}
|
||||
});
|
||||
builder.Services.AddOptions<DomainLifecycleOptions>()
|
||||
.Bind(builder.Configuration.GetSection("TenantDomains"));
|
||||
builder.Services.AddOptions<CorsOptions>()
|
||||
.Bind(builder.Configuration.GetSection(CorsOptions.SectionName))
|
||||
.ValidateDataAnnotations()
|
||||
@@ -218,6 +245,26 @@ try
|
||||
{
|
||||
OnTokenValidated = async context =>
|
||||
{
|
||||
var tenantIdValue = context.Principal?.FindFirst(TikuClaimTypes.TenantId)?.Value;
|
||||
if (!Guid.TryParse(tenantIdValue, out var tenantId))
|
||||
{
|
||||
context.Fail("Missing tenant claim.");
|
||||
return;
|
||||
}
|
||||
|
||||
var tenantInitializer = context.HttpContext.RequestServices
|
||||
.GetRequiredService<ITenantContextInitializer>();
|
||||
try
|
||||
{
|
||||
tenantInitializer.Initialize(tenantId, null, TenantResolutionSource.Jwt);
|
||||
}
|
||||
catch (TenantContextConflictException)
|
||||
{
|
||||
context.HttpContext.Items["tenant_context_conflict"] = true;
|
||||
context.Fail("Authenticated tenant does not match the request host.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!jwtOptions.ValidateSessions)
|
||||
{
|
||||
return;
|
||||
@@ -242,6 +289,20 @@ try
|
||||
{
|
||||
context.Fail("Session has been revoked or expired.");
|
||||
}
|
||||
},
|
||||
OnChallenge = async context =>
|
||||
{
|
||||
if (context.HttpContext.Items.ContainsKey("tenant_context_conflict"))
|
||||
{
|
||||
context.HandleResponse();
|
||||
context.Response.StatusCode = StatusCodes.Status403Forbidden;
|
||||
await context.Response.WriteAsJsonAsync(new ProblemDetails
|
||||
{
|
||||
Title = "Authenticated tenant does not match the request host.",
|
||||
Status = StatusCodes.Status403Forbidden,
|
||||
Extensions = { ["code"] = "tenant_context_conflict" }
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -276,9 +337,11 @@ try
|
||||
|
||||
app.UseSerilogRequestLogging(SerilogRequestLogging.ConfigureRequestLogging);
|
||||
app.UseMiddleware<ExceptionHandlingMiddleware>();
|
||||
app.UseForwardedHeaders();
|
||||
app.UseHttpsRedirection();
|
||||
app.UseRouting();
|
||||
app.UseCors(CorsOptions.PolicyName);
|
||||
app.UseMiddleware<TenantResolutionMiddleware>();
|
||||
app.UseAuthentication();
|
||||
if (rateLimitOptions.Enabled)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user