Files
git.stella-ops.org/src/Scanner/StellaOps.Scanner.WebService/Tenancy/ScannerRequestContextResolver.cs

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();
}