115 lines
4.4 KiB
C#
115 lines
4.4 KiB
C#
using System.Security.Claims;
|
|
using System.Text.RegularExpressions;
|
|
using AMREZ.EOP.API.Filters;
|
|
using AMREZ.EOP.Infrastructures.Tenancy;
|
|
|
|
namespace AMREZ.EOP.API.Middleware;
|
|
|
|
public sealed class StrictTenantGuardOptions
|
|
{
|
|
public string PlatformSlug { get; set; } = "public";
|
|
public string HeaderName { get; set; } = "X-Tenant";
|
|
public string ClaimName { get; set; } = "tenant"; // ปรับเป็น tid/org ได้ตามระบบ
|
|
public HashSet<(string method, string pathPrefix)> Allowlist { get; } = new()
|
|
{
|
|
("GET", "/health"),
|
|
("GET", "/swagger"),
|
|
("GET", "/swagger/index.html"),
|
|
("GET", "/swagger/"),
|
|
("POST", "/api/authentication/login")
|
|
};
|
|
public Regex SlugRegex { get; set; } = new(@"^[a-z0-9\-]{1,64}$", RegexOptions.Compiled);
|
|
}
|
|
|
|
public sealed class StrictTenantGuardMiddleware : IMiddleware
|
|
{
|
|
private readonly ILogger<StrictTenantGuardMiddleware> _log;
|
|
private readonly StrictTenantGuardOptions _opts;
|
|
private readonly TenantMap _map;
|
|
|
|
public StrictTenantGuardMiddleware(
|
|
ILogger<StrictTenantGuardMiddleware> log,
|
|
StrictTenantGuardOptions opts,
|
|
TenantMap map)
|
|
{ _log = log; _opts = opts; _map = map; }
|
|
|
|
public async Task InvokeAsync(HttpContext ctx, RequestDelegate next)
|
|
{
|
|
// 1) allowlist แบบเร็ว
|
|
var path = ctx.Request.Path.Value ?? string.Empty;
|
|
if (_opts.Allowlist.Any(p =>
|
|
ctx.Request.Method.Equals(p.method, StringComparison.OrdinalIgnoreCase) &&
|
|
path.StartsWith(p.pathPrefix, StringComparison.OrdinalIgnoreCase)))
|
|
{
|
|
await next(ctx);
|
|
return;
|
|
}
|
|
|
|
var endpoint = ctx.GetEndpoint();
|
|
if (endpoint?.Metadata?.GetMetadata<SkipTenantGuardAttribute>() is not null)
|
|
{
|
|
await next(ctx);
|
|
return;
|
|
}
|
|
|
|
// 3) ดึง header
|
|
var raw = ctx.Request.Headers[_opts.HeaderName].ToString()?.Trim();
|
|
if (string.IsNullOrWhiteSpace(raw))
|
|
{
|
|
ctx.Response.StatusCode = StatusCodes.Status400BadRequest;
|
|
await ctx.Response.WriteAsJsonAsync(new { code = "MISSING_TENANT", message = $"{_opts.HeaderName} required" });
|
|
return;
|
|
}
|
|
|
|
var slug = raw.ToLowerInvariant();
|
|
if (!_opts.SlugRegex.IsMatch(slug))
|
|
{
|
|
ctx.Response.StatusCode = StatusCodes.Status400BadRequest;
|
|
await ctx.Response.WriteAsJsonAsync(new { code = "INVALID_TENANT_FORMAT", message = "tenant slug invalid" });
|
|
return;
|
|
}
|
|
|
|
// 4) ตรวจ claim ให้ตรงกับ header
|
|
// var claim = GetTenantClaim(ctx.User, _opts.ClaimName);
|
|
// if (claim is null || !string.Equals(claim, slug, StringComparison.Ordinal))
|
|
// {
|
|
// ctx.Response.StatusCode = StatusCodes.Status403Forbidden;
|
|
// await ctx.Response.WriteAsJsonAsync(new { code = "TENANT_MISMATCH", message = "token tenant != header" });
|
|
// return;
|
|
// }
|
|
|
|
// 5) ตรวจว่ามีอยู่จริงในระบบ (TenantMap/meta)
|
|
if (!_map.TryGetBySlug(slug, out var ctxTenant))
|
|
{
|
|
ctx.Response.StatusCode = StatusCodes.Status404NotFound;
|
|
await ctx.Response.WriteAsJsonAsync(new { code = "TENANT_NOT_FOUND", message = "unknown tenant" });
|
|
return;
|
|
}
|
|
|
|
// 6) สำหรับ /api/Tenancy/** → ใช้ platform DB แต่ยังต้องรู้ target tenant
|
|
var isControlPlane = path.StartsWith("/api/Tenancy", StringComparison.OrdinalIgnoreCase);
|
|
if (isControlPlane)
|
|
{
|
|
// เก็บ target tenant ให้ use case ใช้
|
|
ctx.Items["TargetTenantKey"] = slug;
|
|
|
|
// บังคับให้ resolver ใช้ platform (ภายหลัง)
|
|
ctx.Items["ForcePlatformContext"] = true;
|
|
}
|
|
else
|
|
{
|
|
// business-plane: ใช้ tenant context ปกติ (resolver จะอ่าน X-Tenant ตรง ๆ)
|
|
}
|
|
|
|
await next(ctx);
|
|
}
|
|
|
|
private static string? GetTenantClaim(ClaimsPrincipal user, string name)
|
|
{
|
|
if (user?.Identity?.IsAuthenticated != true) return null;
|
|
return user.FindFirstValue(name)
|
|
?? user.FindFirstValue("tid")
|
|
?? user.FindFirstValue("org")
|
|
?? user.FindFirstValue("org_id");
|
|
}
|
|
} |