Init Git
This commit is contained in:
114
AMREZ.EOP.API/Middleware/StrictTenantGuardMiddleware.cs
Normal file
114
AMREZ.EOP.API/Middleware/StrictTenantGuardMiddleware.cs
Normal file
@@ -0,0 +1,114 @@
|
||||
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/"),
|
||||
};
|
||||
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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user