First Commit
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
|
||||
namespace ADS.Helpers.MultiTenant.InternalRouting;
|
||||
|
||||
public static class ApplicationBuilderExtensions
|
||||
{
|
||||
// MICRO: valida headers internos antes de controllers
|
||||
public static IApplicationBuilder UseInternalTenantDb(this IApplicationBuilder app)
|
||||
=> app.UseMiddleware<InternalTenantDbMiddleware>();
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
namespace ADS.Helpers.MultiTenant.InternalRouting;
|
||||
|
||||
public static class InternalHeaders
|
||||
{
|
||||
public const string Tenant = "X-Internal-Tenant";
|
||||
public const string Conn = "X-Internal-Conn";
|
||||
public const string Timestamp = "X-Internal-Ts";
|
||||
public const string Signature = "X-Internal-Sig";
|
||||
// NUEVOS (lo que faltaba para no “inventar” en GPS)
|
||||
public const string EmpresaId = "X-Internal-EmpresaId";
|
||||
public const string ClienteId = "X-Internal-ClienteId";
|
||||
|
||||
// Opcionales
|
||||
public const string Culture = "X-Internal-Culture";
|
||||
public const string Theme = "X-Internal-Theme";
|
||||
public const string DbProvider= "X-Internal-DbProvider";
|
||||
|
||||
public const string CveUser = "X-Internal-CveUser";
|
||||
public const string CveEmpresa = "X-Internal-CveEmpresa";
|
||||
public const string CveCliente = "X-Internal-CveCliente";
|
||||
public const string User_Timestamp = "X-Internal-User-Timestamp";
|
||||
public const string User_Signature = "X-Internal-User-Signature";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
namespace ADS.Helpers.MultiTenant.InternalRouting;
|
||||
|
||||
public sealed class InternalRoutingOptions
|
||||
{
|
||||
public string SharedSecret { get; set; } = default!;
|
||||
public int MaxSkewSeconds { get; set; } = 120;
|
||||
|
||||
public string HeaderTenant { get; set; } = InternalHeaders.Tenant;
|
||||
public string HeaderConn { get; set; } = InternalHeaders.Conn;
|
||||
public string HeaderTs { get; set; } = InternalHeaders.Timestamp;
|
||||
public string HeaderSig { get; set; } = InternalHeaders.Signature;
|
||||
|
||||
// NUEVOS
|
||||
public string HeaderEmpresaId { get; set; } = InternalHeaders.EmpresaId;
|
||||
public string HeaderClienteId { get; set; } = InternalHeaders.ClienteId;
|
||||
|
||||
// Opcionales
|
||||
public string HeaderCulture { get; set; } = InternalHeaders.Culture;
|
||||
public string HeaderTheme { get; set; } = InternalHeaders.Theme;
|
||||
public string HeaderDbProvider{ get; set; } = InternalHeaders.DbProvider;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace ADS.Helpers.MultiTenant.InternalRouting;
|
||||
|
||||
public static class InternalSignature
|
||||
{
|
||||
public static string Compute(
|
||||
string secret,
|
||||
string tenantId,
|
||||
string conn,
|
||||
int empresaId,
|
||||
int clienteId,
|
||||
string provider,
|
||||
string culture,
|
||||
string theme,
|
||||
long tsUnix)
|
||||
{
|
||||
var payload = $"{tenantId}\n{conn}\n{empresaId}\n{clienteId}\n{provider}\n{culture}\n{theme}\n{tsUnix}";
|
||||
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
|
||||
var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(payload));
|
||||
return Convert.ToHexString(hash); // HEX uppercase
|
||||
}
|
||||
|
||||
// Backward compatible (por si algún host viejo sigue firmando con el formato anterior)
|
||||
public static string Compute(string secret, string tenantId, string conn, long tsUnix)
|
||||
=> Compute(secret, tenantId, conn, 0, 0, "SqlServer", "es-MX", "", tsUnix);
|
||||
|
||||
public static bool FixedTimeEqualsHex(string expectedHex, string providedHex)
|
||||
{
|
||||
if (expectedHex.Length != providedHex.Length) return false;
|
||||
return CryptographicOperations.FixedTimeEquals(
|
||||
Encoding.ASCII.GetBytes(expectedHex),
|
||||
Encoding.ASCII.GetBytes(providedHex));
|
||||
}
|
||||
|
||||
public static bool IsTimestampValid(long tsUnix, int maxSkewSeconds)
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
var diff = Math.Abs(now - tsUnix);
|
||||
return diff <= maxSkewSeconds;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using ADS.Helpers.MultiTenant.Context;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace ADS.Helpers.MultiTenant.InternalRouting;
|
||||
|
||||
public interface IInternalTenantDbHeaderWriter
|
||||
{
|
||||
long AddInternalHeaders(IDictionary<string, string> headers, TenantContext tenant);
|
||||
}
|
||||
|
||||
public sealed class InternalTenantDbHeaderWriter : IInternalTenantDbHeaderWriter
|
||||
{
|
||||
private readonly InternalRoutingOptions _opt;
|
||||
|
||||
public InternalTenantDbHeaderWriter(IOptions<InternalRoutingOptions> opt) => _opt = opt.Value;
|
||||
|
||||
public long AddInternalHeaders(IDictionary<string, string> headers, TenantContext tenant)
|
||||
{
|
||||
var ts = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
|
||||
var tenantId = tenant.TenantId ?? "";
|
||||
var conn = tenant.Database?.ConnectionString ?? "";
|
||||
var empresaId = tenant.EmpresaId;
|
||||
var clienteId = tenant.ClienteId;
|
||||
var provider = tenant.Database?.Provider ?? "SqlServer";
|
||||
var culture = tenant.Culture ?? "es-MX";
|
||||
var theme = tenant.Theme ?? "";
|
||||
|
||||
var sig = InternalSignature.Compute(_opt.SharedSecret, tenantId, conn, empresaId, clienteId, provider, culture, theme, ts);
|
||||
|
||||
headers[_opt.HeaderTenant] = tenantId;
|
||||
headers[_opt.HeaderConn] = conn;
|
||||
|
||||
headers[_opt.HeaderEmpresaId] = empresaId.ToString();
|
||||
headers[_opt.HeaderClienteId] = clienteId.ToString();
|
||||
|
||||
headers[_opt.HeaderDbProvider] = provider;
|
||||
headers[_opt.HeaderCulture] = culture;
|
||||
headers[_opt.HeaderTheme] = theme;
|
||||
|
||||
headers[_opt.HeaderTs] = ts.ToString();
|
||||
headers[_opt.HeaderSig] = sig;
|
||||
|
||||
return ts;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace ADS.Helpers.MultiTenant.InternalRouting;
|
||||
|
||||
public static class InternalTenantDbHeaders
|
||||
{
|
||||
public const string ItemTenantId = "Internal.TenantId";
|
||||
public const string ItemConn = "Internal.Conn";
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
using ADS.Helpers.MultiTenant.Context;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace ADS.Helpers.MultiTenant.InternalRouting;
|
||||
|
||||
public sealed class InternalTenantDbMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly InternalRoutingOptions _opt;
|
||||
private readonly ITenantContextAccessor _tenantAccessor;
|
||||
|
||||
public InternalTenantDbMiddleware(
|
||||
RequestDelegate next,
|
||||
IOptions<InternalRoutingOptions> opt,
|
||||
ITenantContextAccessor tenantAccessor)
|
||||
{
|
||||
_next = next;
|
||||
_opt = opt.Value;
|
||||
_tenantAccessor = tenantAccessor;
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(HttpContext ctx)
|
||||
{
|
||||
if (!TryRead(ctx,
|
||||
out var tenantId,
|
||||
out var conn,
|
||||
out var empresaId,
|
||||
out var clienteId,
|
||||
out var provider,
|
||||
out var culture,
|
||||
out var theme,
|
||||
out var ts,
|
||||
out var sig))
|
||||
{
|
||||
ctx.Response.StatusCode = StatusCodes.Status401Unauthorized;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!InternalSignature.IsTimestampValid(ts, _opt.MaxSkewSeconds))
|
||||
{
|
||||
ctx.Response.StatusCode = StatusCodes.Status401Unauthorized;
|
||||
return;
|
||||
}
|
||||
|
||||
// Firma NUEVA (con empresa/cliente/etc)
|
||||
var expected = InternalSignature.Compute(_opt.SharedSecret, tenantId, conn, empresaId, clienteId, provider, culture, theme, ts);
|
||||
|
||||
// Backward compatible: si el Host todavía firma “viejo”, cae al compute viejo
|
||||
if (!InternalSignature.FixedTimeEqualsHex(expected, sig))
|
||||
{
|
||||
var expectedOld = InternalSignature.Compute(_opt.SharedSecret, tenantId, conn, ts);
|
||||
if (!InternalSignature.FixedTimeEqualsHex(expectedOld, sig))
|
||||
{
|
||||
ctx.Response.StatusCode = StatusCodes.Status401Unauthorized;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// backward compatible items
|
||||
ctx.Items[InternalTenantDbHeaders.ItemTenantId] = tenantId;
|
||||
ctx.Items[InternalTenantDbHeaders.ItemConn] = conn;
|
||||
|
||||
_tenantAccessor.SetCurrent(new TenantContext
|
||||
{
|
||||
TenantId = tenantId,
|
||||
EmpresaId = empresaId,
|
||||
ClienteId = clienteId,
|
||||
Culture = culture,
|
||||
Theme = theme,
|
||||
Database = new DatabaseConfig
|
||||
{
|
||||
Provider = provider,
|
||||
ConnectionString = conn
|
||||
}
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
await _next(ctx);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_tenantAccessor.SetCurrent(null);
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryRead(
|
||||
HttpContext ctx,
|
||||
out string tenantId,
|
||||
out string conn,
|
||||
out int empresaId,
|
||||
out int clienteId,
|
||||
out string provider,
|
||||
out string culture,
|
||||
out string theme,
|
||||
out long ts,
|
||||
out string sig)
|
||||
{
|
||||
tenantId = conn = sig = "";
|
||||
empresaId = 0;
|
||||
clienteId = 0;
|
||||
provider = "SqlServer";
|
||||
culture = "es-MX";
|
||||
theme = "";
|
||||
ts = 0;
|
||||
|
||||
if (!ctx.Request.Headers.TryGetValue(_opt.HeaderTenant, out var t)) return false;
|
||||
if (!ctx.Request.Headers.TryGetValue(_opt.HeaderConn, out var c)) return false;
|
||||
if (!ctx.Request.Headers.TryGetValue(_opt.HeaderTs, out var tsStr)) return false;
|
||||
if (!ctx.Request.Headers.TryGetValue(_opt.HeaderSig, out var s)) return false;
|
||||
|
||||
tenantId = t.ToString();
|
||||
conn = c.ToString();
|
||||
sig = s.ToString();
|
||||
|
||||
// NUEVOS obligatorios para Sicrep (si no vienen, dejamos 0 y el SP puede trabajar con -1)
|
||||
if (ctx.Request.Headers.TryGetValue(_opt.HeaderEmpresaId, out var eStr))
|
||||
int.TryParse(eStr.ToString(), out empresaId);
|
||||
|
||||
if (ctx.Request.Headers.TryGetValue(_opt.HeaderClienteId, out var clStr))
|
||||
int.TryParse(clStr.ToString(), out clienteId);
|
||||
|
||||
// Opcionales
|
||||
if (ctx.Request.Headers.TryGetValue(_opt.HeaderDbProvider, out var p)) provider = p.ToString();
|
||||
if (ctx.Request.Headers.TryGetValue(_opt.HeaderCulture, out var cul)) culture = cul.ToString();
|
||||
if (ctx.Request.Headers.TryGetValue(_opt.HeaderTheme, out var th)) theme = th.ToString();
|
||||
|
||||
return !string.IsNullOrWhiteSpace(tenantId)
|
||||
&& !string.IsNullOrWhiteSpace(conn)
|
||||
&& long.TryParse(tsStr.ToString(), out ts)
|
||||
&& !string.IsNullOrWhiteSpace(sig);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using ADS.Helpers.MultiTenant.Context;
|
||||
using ADS.Helpers.MultiTenant.Data;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace ADS.Helpers.MultiTenant.InternalRouting;
|
||||
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
// MICRO: registra middleware options + HttpContextAccessor + DB factory
|
||||
public static IServiceCollection AddInternalTenantDb(this IServiceCollection services, IConfiguration cfg)
|
||||
{
|
||||
services.AddHttpContextAccessor();
|
||||
services.Configure<InternalRoutingOptions>(cfg.GetSection("InternalRouting"));
|
||||
services.AddSingleton<IDbConnectionFactory, TenantDbConnectionFactory>();
|
||||
services.AddSingleton<ITenantContextAccessor, TenantContextAccessor>();
|
||||
|
||||
services.AddSingleton<IDbConnectionFactory, TenantDbConnectionFactory>();
|
||||
return services;
|
||||
}
|
||||
|
||||
// HOST: registra solo el writer (para agregar headers internos)
|
||||
public static IServiceCollection AddInternalTenantDbHeaderWriter(this IServiceCollection services, IConfiguration cfg)
|
||||
{
|
||||
services.Configure<InternalRoutingOptions>(cfg.GetSection("InternalRouting"));
|
||||
services.AddSingleton<IInternalTenantDbHeaderWriter, InternalTenantDbHeaderWriter>();
|
||||
return services;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user