test(architecture): enforce capability boundaries
This commit is contained in:
121
Tiku.Api/Controllers/AuthRequestContextResolver.cs
Normal file
121
Tiku.Api/Controllers/AuthRequestContextResolver.cs
Normal file
@@ -0,0 +1,121 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using Tiku.Api.Options;
|
||||
using Tiku.Application.Auth;
|
||||
using Tiku.Application.Content;
|
||||
using Tiku.Application.Security;
|
||||
using Tiku.Application.Tenancy;
|
||||
using Tiku.Domain.Tenancy;
|
||||
|
||||
namespace Tiku.Api.Controllers;
|
||||
|
||||
public sealed class AuthRequestContextResolver(
|
||||
IAuthSessionStore sessionStore,
|
||||
ITenantContext tenantContext,
|
||||
ITenantContextInitializer tenantContextInitializer,
|
||||
ITenantDirectory tenantDirectory,
|
||||
IOptions<TenantResolutionOptions> tenantResolutionOptions)
|
||||
{
|
||||
internal void ResolveRefreshTokenTenant(string refreshToken, HttpRequest request)
|
||||
{
|
||||
if (!sessionStore.TryParseRefreshToken(refreshToken, out var locator)) return;
|
||||
|
||||
if (locator.Realm == AuthRealm.Platform)
|
||||
{
|
||||
EnsurePlatformHost(request);
|
||||
if (tenantContext.IsResolved)
|
||||
throw new TenantContextConflictException(tenantContext.TenantId!.Value, Guid.Empty);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!tenantContext.IsResolved)
|
||||
throw new RequiredFieldException(
|
||||
"tenant refresh/logout requires a tenant host or x-tenant-code matching the refresh token.");
|
||||
|
||||
tenantContextInitializer.Initialize(locator.TenantId!.Value, null, TenantResolutionSource.RefreshToken);
|
||||
}
|
||||
|
||||
internal void ResolveAuthChallengeTenant(string challengeToken, HttpRequest request)
|
||||
{
|
||||
var parts = challengeToken.Split('.', 4);
|
||||
if (parts.Length != 4 || parts[0] != "c1") return;
|
||||
|
||||
if (parts[1] == "p" && parts[2] == "-")
|
||||
{
|
||||
EnsurePlatformHost(request);
|
||||
if (tenantContext.IsResolved)
|
||||
throw new TenantContextConflictException(tenantContext.TenantId!.Value, Guid.Empty);
|
||||
return;
|
||||
}
|
||||
|
||||
if (parts[1] != "t" || !Guid.TryParseExact(parts[2], "N", out var tenantId) || !tenantContext.IsResolved)
|
||||
throw new RequiredFieldException(
|
||||
"tenant authentication challenge requires a tenant host or x-tenant-code.");
|
||||
|
||||
tenantContextInitializer.Initialize(tenantId, null, TenantResolutionSource.RefreshToken);
|
||||
}
|
||||
|
||||
internal async Task<Guid?> ResolveRealmTenantIdAsync(
|
||||
AuthRealm realm,
|
||||
string? tenantCode,
|
||||
HttpRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (realm == AuthRealm.Platform)
|
||||
{
|
||||
EnsurePlatformHost(request);
|
||||
if (tenantContext.IsResolved || !string.IsNullOrWhiteSpace(tenantCode))
|
||||
throw new RequiredFieldException(
|
||||
"platform realm does not accept tenantCode and must use a platform host.");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (tenantContext.TenantId.HasValue)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(tenantCode) &&
|
||||
!string.Equals(tenantContext.TenantCode, tenantCode.Trim(), StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var supplied = await tenantDirectory.FindByCodeAsync(tenantCode.Trim(), cancellationToken);
|
||||
if (supplied?.TenantId != tenantContext.TenantId.Value)
|
||||
throw new TenantContextConflictException(
|
||||
tenantContext.TenantId.Value,
|
||||
supplied?.TenantId ?? Guid.Empty);
|
||||
}
|
||||
|
||||
return tenantContext.TenantId.Value;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(tenantCode))
|
||||
throw new RequiredFieldException("tenantCode is required when the request host does not resolve a tenant.");
|
||||
|
||||
var tenant = await tenantDirectory.FindByCodeAsync(tenantCode.Trim(), cancellationToken)
|
||||
?? throw new TenantNotFoundException();
|
||||
tenantContextInitializer.Initialize(
|
||||
tenant.TenantId,
|
||||
tenant.TenantCode,
|
||||
TenantResolutionSource.TenantCode);
|
||||
return tenant.TenantId;
|
||||
}
|
||||
|
||||
internal void EnsureRouteRealm(AuthRealm realm, HttpRequest request)
|
||||
{
|
||||
var path = request.Path.Value ?? string.Empty;
|
||||
var expectedRealm = path.StartsWith("/api/platform/auth/", StringComparison.OrdinalIgnoreCase)
|
||||
? AuthRealm.Platform
|
||||
: AuthRealm.Tenant;
|
||||
if (realm != expectedRealm)
|
||||
throw new RequiredFieldException(
|
||||
$"{realm.ToString().ToLowerInvariant()} realm must use the {expectedRealm.ToString().ToLowerInvariant()} authentication route.");
|
||||
}
|
||||
|
||||
private void EnsurePlatformHost(HttpRequest request)
|
||||
{
|
||||
var requestHost = request.Host.Host.Trim().TrimEnd('.');
|
||||
if (!tenantResolutionOptions.Value.PlatformHosts.Any(host =>
|
||||
string.Equals(
|
||||
host.Trim().TrimEnd('.'),
|
||||
requestHost,
|
||||
StringComparison.OrdinalIgnoreCase)))
|
||||
throw new RequiredFieldException("platform realm is only available on a configured platform host.");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user