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