188 lines
5.5 KiB
C#
188 lines
5.5 KiB
C#
using Microsoft.AspNetCore.Http;
|
|
using StellaOps.Auth.Abstractions;
|
|
using System.Security.Claims;
|
|
|
|
namespace StellaOps.Scanner.WebService.Tenancy;
|
|
|
|
internal static class ScannerRequestContextResolver
|
|
{
|
|
private const string DefaultTenant = "default";
|
|
private const string LegacyTenantClaim = "tid";
|
|
private const string LegacyTenantIdClaim = "tenant_id";
|
|
private const string LegacyTenantHeader = "X-Stella-Tenant";
|
|
private const string AlternateTenantHeader = "X-Tenant-Id";
|
|
private const string ActorHeader = "X-StellaOps-Actor";
|
|
|
|
public static bool TryResolveTenant(
|
|
HttpContext context,
|
|
out string tenantId,
|
|
out string? error,
|
|
bool allowDefaultTenant = false,
|
|
string defaultTenant = DefaultTenant)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(context);
|
|
|
|
tenantId = string.Empty;
|
|
error = null;
|
|
|
|
var claimTenant = NormalizeTenant(ResolveTenantClaim(context.User));
|
|
var canonicalHeaderTenant = ReadTenantHeader(context, StellaOpsHttpHeaderNames.Tenant);
|
|
var legacyHeaderTenant = ReadTenantHeader(context, LegacyTenantHeader);
|
|
var alternateHeaderTenant = ReadTenantHeader(context, AlternateTenantHeader);
|
|
|
|
if (HasConflictingTenants(canonicalHeaderTenant, legacyHeaderTenant, alternateHeaderTenant))
|
|
{
|
|
error = "tenant_conflict";
|
|
return false;
|
|
}
|
|
|
|
var headerTenant = canonicalHeaderTenant ?? legacyHeaderTenant ?? alternateHeaderTenant;
|
|
if (!string.IsNullOrWhiteSpace(claimTenant))
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(headerTenant)
|
|
&& !string.Equals(claimTenant, headerTenant, StringComparison.Ordinal))
|
|
{
|
|
error = "tenant_conflict";
|
|
return false;
|
|
}
|
|
|
|
tenantId = claimTenant;
|
|
return true;
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(headerTenant))
|
|
{
|
|
tenantId = headerTenant;
|
|
return true;
|
|
}
|
|
|
|
if (allowDefaultTenant)
|
|
{
|
|
tenantId = NormalizeTenant(defaultTenant) ?? DefaultTenant;
|
|
return true;
|
|
}
|
|
|
|
error = "tenant_missing";
|
|
return false;
|
|
}
|
|
|
|
public static string ResolveTenantOrDefault(HttpContext context, string defaultTenant = DefaultTenant)
|
|
{
|
|
if (TryResolveTenant(context, out var tenantId, out _, allowDefaultTenant: true, defaultTenant))
|
|
{
|
|
return tenantId;
|
|
}
|
|
|
|
return NormalizeTenant(defaultTenant) ?? DefaultTenant;
|
|
}
|
|
|
|
public static string ResolveTenantPartitionKey(HttpContext context)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(context);
|
|
|
|
if (TryResolveTenant(context, out var tenantId, out _, allowDefaultTenant: false))
|
|
{
|
|
return tenantId;
|
|
}
|
|
|
|
var remoteIp = context.Connection.RemoteIpAddress?.ToString();
|
|
if (!string.IsNullOrWhiteSpace(remoteIp))
|
|
{
|
|
return $"ip:{remoteIp.Trim()}";
|
|
}
|
|
|
|
return "anonymous";
|
|
}
|
|
|
|
public static string ResolveActor(HttpContext context, string fallback = "system")
|
|
{
|
|
ArgumentNullException.ThrowIfNull(context);
|
|
|
|
var subject = context.User.FindFirstValue(StellaOpsClaimTypes.Subject);
|
|
if (!string.IsNullOrWhiteSpace(subject))
|
|
{
|
|
return subject.Trim();
|
|
}
|
|
|
|
var clientId = context.User.FindFirstValue(StellaOpsClaimTypes.ClientId);
|
|
if (!string.IsNullOrWhiteSpace(clientId))
|
|
{
|
|
return clientId.Trim();
|
|
}
|
|
|
|
if (TryResolveHeader(context, ActorHeader, out var actorHeaderValue))
|
|
{
|
|
return actorHeaderValue;
|
|
}
|
|
|
|
var identityName = context.User.Identity?.Name;
|
|
if (!string.IsNullOrWhiteSpace(identityName))
|
|
{
|
|
return identityName.Trim();
|
|
}
|
|
|
|
return fallback;
|
|
}
|
|
|
|
private static bool HasConflictingTenants(params string?[] tenantCandidates)
|
|
{
|
|
string? baseline = null;
|
|
foreach (var candidate in tenantCandidates)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(candidate))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (baseline is null)
|
|
{
|
|
baseline = candidate;
|
|
continue;
|
|
}
|
|
|
|
if (!string.Equals(baseline, candidate, StringComparison.Ordinal))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static string? ResolveTenantClaim(ClaimsPrincipal principal)
|
|
{
|
|
return principal.FindFirstValue(StellaOpsClaimTypes.Tenant)
|
|
?? principal.FindFirstValue(LegacyTenantClaim)
|
|
?? principal.FindFirstValue(LegacyTenantIdClaim);
|
|
}
|
|
|
|
private static string? ReadTenantHeader(HttpContext context, string headerName)
|
|
{
|
|
return TryResolveHeader(context, headerName, out var value)
|
|
? NormalizeTenant(value)
|
|
: null;
|
|
}
|
|
|
|
private static bool TryResolveHeader(HttpContext context, string headerName, out string value)
|
|
{
|
|
value = string.Empty;
|
|
|
|
if (!context.Request.Headers.TryGetValue(headerName, out var values))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var raw = values.ToString();
|
|
if (string.IsNullOrWhiteSpace(raw))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
value = raw.Trim();
|
|
return true;
|
|
}
|
|
|
|
private static string? NormalizeTenant(string? value)
|
|
=> string.IsNullOrWhiteSpace(value) ? null : value.Trim().ToLowerInvariant();
|
|
}
|