forked from gongxuegit/tiku-backend.net
feat: enforce tenant isolation and shared question bank
This commit is contained in:
@@ -6,11 +6,9 @@ public sealed class CurrentPrincipalMiddleware(RequestDelegate next)
|
||||
{
|
||||
public async Task InvokeAsync(
|
||||
HttpContext context,
|
||||
ICurrentUser currentUser,
|
||||
ICurrentTenant currentTenant)
|
||||
ICurrentUser currentUser)
|
||||
{
|
||||
currentUser.Load(context.User);
|
||||
currentTenant.Load(context.User);
|
||||
await next(context);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,12 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using Tiku.Api.Controllers;
|
||||
using Tiku.Application.Assets;
|
||||
using Tiku.Application.Auth;
|
||||
using Tiku.Application.Security;
|
||||
using Tiku.Application.Commerce;
|
||||
using Tiku.Application.Content;
|
||||
using Tiku.Application.Growth;
|
||||
using Tiku.Application.Points;
|
||||
using Tiku.Application.QuestionBanks;
|
||||
using Tiku.Application.Storage;
|
||||
using Tiku.Infrastructure.Content;
|
||||
using Tiku.Infrastructure.Learning;
|
||||
@@ -13,6 +15,7 @@ using Tiku.Infrastructure.Profile;
|
||||
using Tiku.Infrastructure.QuestionBanks;
|
||||
using Tiku.Infrastructure.Scoreline;
|
||||
using Tiku.Application.TenantAdmin;
|
||||
using Tiku.Application.Tenancy;
|
||||
|
||||
namespace Tiku.Api.Middleware;
|
||||
|
||||
@@ -45,6 +48,52 @@ public sealed class ExceptionHandlingMiddleware(
|
||||
return;
|
||||
}
|
||||
|
||||
if (exception is TenantContextConflictException)
|
||||
{
|
||||
await WriteProblemAsync(
|
||||
context,
|
||||
exception.Message,
|
||||
StatusCodes.Status403Forbidden,
|
||||
"tenant_context_conflict");
|
||||
return;
|
||||
}
|
||||
|
||||
if (exception is TenantFrontendConfigException frontendConfigException)
|
||||
{
|
||||
var status = frontendConfigException.Code switch
|
||||
{
|
||||
"tenant_not_found" or "frontend_config_not_found" => StatusCodes.Status404NotFound,
|
||||
"frontend_config_version_conflict" => StatusCodes.Status409Conflict,
|
||||
_ => StatusCodes.Status400BadRequest
|
||||
};
|
||||
await WriteProblemAsync(
|
||||
context,
|
||||
frontendConfigException.Message,
|
||||
status,
|
||||
frontendConfigException.Code);
|
||||
return;
|
||||
}
|
||||
|
||||
if (exception is PublicQuestionAccessDeniedException publicQuestionAccessDeniedException)
|
||||
{
|
||||
await WriteProblemAsync(
|
||||
context,
|
||||
publicQuestionAccessDeniedException.Message,
|
||||
StatusCodes.Status403Forbidden,
|
||||
publicQuestionAccessDeniedException.Code);
|
||||
return;
|
||||
}
|
||||
|
||||
if (exception is QuestionLocatorException questionLocatorException)
|
||||
{
|
||||
await WriteProblemAsync(
|
||||
context,
|
||||
questionLocatorException.Message,
|
||||
StatusCodes.Status404NotFound,
|
||||
questionLocatorException.Code);
|
||||
return;
|
||||
}
|
||||
|
||||
if (exception is RequiredFieldException)
|
||||
{
|
||||
await WriteProblemAsync(
|
||||
|
||||
74
Tiku.Api/Middleware/TenantResolutionMiddleware.cs
Normal file
74
Tiku.Api/Middleware/TenantResolutionMiddleware.cs
Normal file
@@ -0,0 +1,74 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Tiku.Api.Options;
|
||||
using Tiku.Application.Security;
|
||||
using Tiku.Application.Tenancy;
|
||||
|
||||
namespace Tiku.Api.Middleware;
|
||||
|
||||
public sealed class TenantResolutionMiddleware(RequestDelegate next)
|
||||
{
|
||||
public async Task InvokeAsync(
|
||||
HttpContext context,
|
||||
ITenantDirectory tenantDirectory,
|
||||
ITenantContextInitializer tenantInitializer,
|
||||
IOptions<TenantResolutionOptions> options)
|
||||
{
|
||||
var host = NormalizeHost(context.Request.Host.Host);
|
||||
var isPlatformHost = options.Value.PlatformHosts.Any(candidate =>
|
||||
string.Equals(NormalizeHost(candidate), host, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
TenantDirectoryEntry? tenant = null;
|
||||
if (!isPlatformHost && host is not null)
|
||||
{
|
||||
tenant = await tenantDirectory.FindByHostAsync(host, context.RequestAborted);
|
||||
if (tenant is null && !IsExemptPath(context.Request.Path, options.Value.ExemptPathPrefixes))
|
||||
{
|
||||
context.Response.StatusCode = StatusCodes.Status404NotFound;
|
||||
await context.Response.WriteAsJsonAsync(new ProblemDetails
|
||||
{
|
||||
Title = "Tenant was not found.",
|
||||
Status = StatusCodes.Status404NotFound,
|
||||
Detail = "The request host is not assigned to an active tenant."
|
||||
}, context.RequestAborted);
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (isPlatformHost && IsAllowedTenantCodePath(
|
||||
context.Request.Path,
|
||||
options.Value.TenantCodePathPrefixes))
|
||||
{
|
||||
var tenantCode = context.Request.Headers["x-tenant-code"].FirstOrDefault()
|
||||
?? context.Request.Query["tenantCode"].FirstOrDefault();
|
||||
if (!string.IsNullOrWhiteSpace(tenantCode))
|
||||
{
|
||||
tenant = await tenantDirectory.FindByCodeAsync(tenantCode.Trim(), context.RequestAborted);
|
||||
}
|
||||
}
|
||||
|
||||
if (tenant is not null)
|
||||
{
|
||||
tenantInitializer.Initialize(
|
||||
tenant.TenantId,
|
||||
tenant.TenantCode,
|
||||
tenant.Host is null ? TenantResolutionSource.TenantCode : TenantResolutionSource.Host);
|
||||
}
|
||||
|
||||
await next(context);
|
||||
}
|
||||
|
||||
private static string? NormalizeHost(string? host)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(host) ? null : host.Trim().TrimEnd('.').ToLowerInvariant();
|
||||
}
|
||||
|
||||
private static bool IsExemptPath(PathString path, IEnumerable<string> prefixes)
|
||||
{
|
||||
return prefixes.Any(prefix => path.StartsWithSegments(prefix, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
private static bool IsAllowedTenantCodePath(PathString path, IEnumerable<string> prefixes)
|
||||
{
|
||||
return prefixes.Any(prefix => path.StartsWithSegments(prefix, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user