stela ops usage fixes roles propagation and timoeut, one account to support multi tenants, migrations consolidation, search to support documentation, doctor and open api vector db search
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
|
||||
using StellaOps.Auth.Abstractions;
|
||||
using StellaOps.Router.Common.Identity;
|
||||
using System.Security.Claims;
|
||||
using System.Text.Json;
|
||||
|
||||
@@ -42,6 +43,10 @@ public sealed class IdentityHeaderPolicyMiddleware
|
||||
// Headers used by downstream services in header-based auth mode
|
||||
"X-Scopes",
|
||||
"X-Tenant-Id",
|
||||
// Gateway-issued signed identity envelope headers
|
||||
"X-StellaOps-Identity-Envelope",
|
||||
"X-StellaOps-Identity-Envelope-Signature",
|
||||
"X-StellaOps-Identity-Envelope-Algorithm",
|
||||
// Raw claim headers (internal/legacy pass-through)
|
||||
"sub",
|
||||
"tid",
|
||||
@@ -74,18 +79,11 @@ public sealed class IdentityHeaderPolicyMiddleware
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 0: Preserve client-sent tenant header before stripping.
|
||||
// When the Gateway runs in AllowAnonymous mode (no JWT validation),
|
||||
// the principal has no claims and we cannot determine tenant from the token.
|
||||
// In that case, we pass through the client-provided value and let the
|
||||
// upstream service validate it against the JWT's tenant claim.
|
||||
var clientTenant = context.Request.Headers["X-StellaOps-Tenant"].ToString();
|
||||
|
||||
// Step 1: Strip all reserved identity headers from incoming request
|
||||
StripReservedHeaders(context);
|
||||
|
||||
// Step 2: Extract identity from validated principal
|
||||
var identity = ExtractIdentity(context, clientTenant);
|
||||
var identity = ExtractIdentity(context);
|
||||
|
||||
// Step 3: Store normalized identity in HttpContext.Items
|
||||
StoreIdentityContext(context, identity);
|
||||
@@ -121,23 +119,17 @@ public sealed class IdentityHeaderPolicyMiddleware
|
||||
}
|
||||
}
|
||||
|
||||
private IdentityContext ExtractIdentity(HttpContext context, string? clientTenant = null)
|
||||
private IdentityContext ExtractIdentity(HttpContext context)
|
||||
{
|
||||
var principal = context.User;
|
||||
var isAuthenticated = principal.Identity?.IsAuthenticated == true;
|
||||
|
||||
if (!isAuthenticated)
|
||||
{
|
||||
// In AllowAnonymous mode the Gateway cannot validate identity claims.
|
||||
// Pass through the client-provided tenant so the upstream service
|
||||
// can validate it against the JWT's own tenant claim.
|
||||
var passThruTenant = !string.IsNullOrWhiteSpace(clientTenant) ? clientTenant.Trim() : "default";
|
||||
|
||||
return new IdentityContext
|
||||
{
|
||||
IsAnonymous = true,
|
||||
Actor = "anonymous",
|
||||
Tenant = passThruTenant,
|
||||
Scopes = _options.AnonymousScopes ?? []
|
||||
};
|
||||
}
|
||||
@@ -146,10 +138,9 @@ public sealed class IdentityHeaderPolicyMiddleware
|
||||
var actor = principal.FindFirstValue(StellaOpsClaimTypes.Subject);
|
||||
|
||||
// Extract tenant - try canonical claim first, then legacy 'tid',
|
||||
// then client-provided header, then fall back to "default"
|
||||
// then fall back to "default".
|
||||
var tenant = principal.FindFirstValue(StellaOpsClaimTypes.Tenant)
|
||||
?? principal.FindFirstValue("tid")
|
||||
?? (!string.IsNullOrWhiteSpace(clientTenant) ? clientTenant.Trim() : null)
|
||||
?? "default";
|
||||
|
||||
// Extract project (optional)
|
||||
@@ -157,6 +148,12 @@ public sealed class IdentityHeaderPolicyMiddleware
|
||||
|
||||
// Extract scopes - try 'scp' claims first (individual items), then 'scope' (space-separated)
|
||||
var scopes = ExtractScopes(principal);
|
||||
var roles = principal.FindAll(ClaimTypes.Role)
|
||||
.Select(claim => claim.Value)
|
||||
.Where(value => !string.IsNullOrWhiteSpace(value))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.OrderBy(value => value, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
|
||||
// Extract cnf (confirmation claim) for DPoP/sender constraint
|
||||
var cnfJson = principal.FindFirstValue("cnf");
|
||||
@@ -173,6 +170,7 @@ public sealed class IdentityHeaderPolicyMiddleware
|
||||
Tenant = tenant,
|
||||
Project = project,
|
||||
Scopes = scopes,
|
||||
Roles = roles,
|
||||
CnfJson = cnfJson,
|
||||
DpopThumbprint = dpopThumbprint
|
||||
};
|
||||
@@ -338,6 +336,29 @@ public sealed class IdentityHeaderPolicyMiddleware
|
||||
{
|
||||
headers["cnf.jkt"] = identity.DpopThumbprint;
|
||||
}
|
||||
|
||||
if (_options.EmitIdentityEnvelope &&
|
||||
!string.IsNullOrWhiteSpace(_options.IdentityEnvelopeSigningKey))
|
||||
{
|
||||
var envelope = new GatewayIdentityEnvelope
|
||||
{
|
||||
Issuer = _options.IdentityEnvelopeIssuer,
|
||||
Subject = identity.Actor ?? "anonymous",
|
||||
Tenant = identity.Tenant,
|
||||
Project = identity.Project,
|
||||
Scopes = identity.Scopes.OrderBy(scope => scope, StringComparer.Ordinal).ToArray(),
|
||||
Roles = identity.Roles,
|
||||
SenderConfirmation = identity.DpopThumbprint,
|
||||
CorrelationId = context.TraceIdentifier,
|
||||
IssuedAtUtc = DateTimeOffset.UtcNow,
|
||||
ExpiresAtUtc = DateTimeOffset.UtcNow.Add(_options.IdentityEnvelopeTtl)
|
||||
};
|
||||
|
||||
var signature = GatewayIdentityEnvelopeCodec.Sign(envelope, _options.IdentityEnvelopeSigningKey!);
|
||||
headers["X-StellaOps-Identity-Envelope"] = signature.Payload;
|
||||
headers["X-StellaOps-Identity-Envelope-Signature"] = signature.Signature;
|
||||
headers["X-StellaOps-Identity-Envelope-Algorithm"] = signature.Algorithm;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryParseCnfThumbprint(string json, out string? jkt)
|
||||
@@ -368,6 +389,7 @@ public sealed class IdentityHeaderPolicyMiddleware
|
||||
public string? Tenant { get; init; }
|
||||
public string? Project { get; init; }
|
||||
public HashSet<string> Scopes { get; init; } = [];
|
||||
public IReadOnlyList<string> Roles { get; init; } = [];
|
||||
public string? CnfJson { get; init; }
|
||||
public string? DpopThumbprint { get; init; }
|
||||
}
|
||||
@@ -396,6 +418,26 @@ public sealed class IdentityHeaderPolicyOptions
|
||||
/// </summary>
|
||||
public bool AllowScopeHeaderOverride { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// When true, emit a signed identity envelope headers for downstream trust.
|
||||
/// </summary>
|
||||
public bool EmitIdentityEnvelope { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Shared signing key used to sign identity envelopes.
|
||||
/// </summary>
|
||||
public string? IdentityEnvelopeSigningKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Identity envelope issuer identifier.
|
||||
/// </summary>
|
||||
public string IdentityEnvelopeIssuer { get; set; } = "stellaops-gateway-router";
|
||||
|
||||
/// <summary>
|
||||
/// Identity envelope validity window.
|
||||
/// </summary>
|
||||
public TimeSpan IdentityEnvelopeTtl { get; set; } = TimeSpan.FromMinutes(2);
|
||||
|
||||
/// <summary>
|
||||
/// Route prefixes where Authorization and DPoP headers should be preserved
|
||||
/// (passed through to the upstream service) instead of stripped.
|
||||
|
||||
Reference in New Issue
Block a user