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:
master
2026-02-22 19:27:54 +02:00
parent a29f438f53
commit bd8fee6ed8
373 changed files with 832097 additions and 3369 deletions

View File

@@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.Auth.Abstractions;
@@ -12,8 +13,6 @@ using StellaOps.Gateway.WebService.Middleware;
using StellaOps.Gateway.WebService.Routing;
using StellaOps.Gateway.WebService.Security;
using StellaOps.Gateway.WebService.Services;
using StellaOps.Messaging.DependencyInjection;
using StellaOps.Messaging.Transport.Valkey;
using StellaOps.Router.AspNet;
using StellaOps.Router.Common.Abstractions;
using StellaOps.Router.Common.Models;
@@ -25,11 +24,6 @@ using StellaOps.Router.Gateway.Middleware;
using StellaOps.Router.Gateway.OpenApi;
using StellaOps.Router.Gateway.RateLimit;
using StellaOps.Router.Gateway.Routing;
using StellaOps.Router.Transport.Messaging;
using StellaOps.Router.Transport.Messaging.Options;
using StellaOps.Router.Transport.Tcp;
using StellaOps.Router.Transport.Tls;
using System.Net;
var builder = WebApplication.CreateBuilder(args);
@@ -55,6 +49,25 @@ builder.Services.AddRouterGatewayCore();
builder.Services.AddRouterRateLimiting(builder.Configuration);
builder.Services.AddSingleton<IEffectiveClaimsStore, EffectiveClaimsStore>();
builder.Services.AddSingleton<StellaOps.Router.Gateway.Authorization.IEffectiveClaimsStore>(sp =>
sp.GetRequiredService<IEffectiveClaimsStore>());
var authorityClaimsUrl = ResolveAuthorityClaimsUrl(bootstrapOptions.Auth.Authority);
StellaOps.Router.Gateway.Authorization.AuthorizationServiceCollectionExtensions.AddAuthorityIntegration(
builder.Services,
options =>
{
options.Enabled = !string.IsNullOrWhiteSpace(authorityClaimsUrl);
options.AuthorityUrl = authorityClaimsUrl ?? string.Empty;
options.RefreshInterval = TimeSpan.FromSeconds(30);
options.WaitForAuthorityOnStartup = false;
options.StartupTimeout = TimeSpan.FromSeconds(10);
options.UseAuthorityPushNotifications = false;
});
builder.Services.Replace(ServiceDescriptor.Singleton<StellaOps.Router.Gateway.Authorization.IEffectiveClaimsStore>(
sp => sp.GetRequiredService<IEffectiveClaimsStore>()));
builder.Services.AddSingleton<GatewayServiceStatus>();
builder.Services.AddSingleton<GatewayMetrics>();
@@ -62,55 +75,35 @@ builder.Services.AddSingleton<GatewayMetrics>();
var transportPluginLoader = new RouterTransportPluginLoader(
NullLoggerFactory.Instance.CreateLogger<RouterTransportPluginLoader>());
// Try to load from plugins directory, fallback to direct registration if not found
var pluginsPath = Path.Combine(AppContext.BaseDirectory, "plugins", "router", "transports");
if (Directory.Exists(pluginsPath))
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()
.Where(assembly =>
assembly.GetName().Name?.StartsWith("StellaOps.Router.Transport.", StringComparison.OrdinalIgnoreCase) == true))
{
transportPluginLoader.LoadFromDirectory(pluginsPath);
transportPluginLoader.LoadFromAssembly(assembly);
}
// Register TCP and TLS transports (from plugins or fallback to compile-time references)
var tcpPlugin = transportPluginLoader.GetPlugin("tcp");
var tlsPlugin = transportPluginLoader.GetPlugin("tls");
if (tcpPlugin is not null)
var pluginsPath = builder.Configuration["Gateway:TransportPlugins:Directory"];
if (string.IsNullOrWhiteSpace(pluginsPath))
{
tcpPlugin.Register(new RouterTransportRegistrationContext(
builder.Services, builder.Configuration, RouterTransportMode.Server)
{
ConfigurationSection = "Gateway:Transports:Tcp"
});
}
else
{
// Fallback to compile-time registration
builder.Services.AddTcpTransportServer();
pluginsPath = Path.Combine(AppContext.BaseDirectory, "plugins", "router", "transports");
}
if (tlsPlugin is not null)
var transportSearchPattern = builder.Configuration["Gateway:TransportPlugins:SearchPattern"];
if (string.IsNullOrWhiteSpace(transportSearchPattern))
{
tlsPlugin.Register(new RouterTransportRegistrationContext(
builder.Services, builder.Configuration, RouterTransportMode.Server)
{
ConfigurationSection = "Gateway:Transports:Tls"
});
}
else
{
// Fallback to compile-time registration
builder.Services.AddTlsTransportServer();
transportSearchPattern = "StellaOps.Router.Transport.*.dll";
}
// Messaging transport (Valkey)
if (bootstrapOptions.Transports.Messaging.Enabled)
{
builder.Services.AddMessagingTransport<ValkeyTransportPlugin>(builder.Configuration, "Gateway:Transports:Messaging");
builder.Services.AddMessagingTransportServer();
}
transportPluginLoader.LoadFromDirectory(pluginsPath, transportSearchPattern);
RegisterGatewayTransportIfEnabled("tcp", bootstrapOptions.Transports.Tcp.Enabled, "Gateway:Transports:Tcp");
RegisterGatewayTransportIfEnabled("tls", bootstrapOptions.Transports.Tls.Enabled, "Gateway:Transports:Tls");
RegisterGatewayTransportIfEnabled("messaging", bootstrapOptions.Transports.Messaging.Enabled, "Gateway:Transports:Messaging");
builder.Services.AddSingleton<GatewayTransportClient>();
builder.Services.AddSingleton<ITransportClient>(sp => sp.GetRequiredService<GatewayTransportClient>());
builder.Services.AddSingleton(new GatewayRouteCatalog(bootstrapOptions.Routes));
builder.Services.AddSingleton<IOpenApiDocumentGenerator, OpenApiDocumentGenerator>();
builder.Services.AddSingleton<IRouterOpenApiDocumentCache, RouterOpenApiDocumentCache>();
@@ -125,6 +118,10 @@ builder.Services.AddSingleton(new IdentityHeaderPolicyOptions
{
EnableLegacyHeaders = bootstrapOptions.Auth.EnableLegacyHeaders,
AllowScopeHeaderOverride = bootstrapOptions.Auth.AllowScopeHeader,
EmitIdentityEnvelope = bootstrapOptions.Auth.EmitIdentityEnvelope,
IdentityEnvelopeSigningKey = bootstrapOptions.Auth.IdentityEnvelopeSigningKey,
IdentityEnvelopeIssuer = bootstrapOptions.Auth.IdentityEnvelopeIssuer,
IdentityEnvelopeTtl = TimeSpan.FromSeconds(Math.Max(1, bootstrapOptions.Auth.IdentityEnvelopeTtlSeconds)),
JwtPassthroughPrefixes = bootstrapOptions.Routes
.Where(r => r.PreserveAuthHeaders)
.Select(r => r.Path)
@@ -149,11 +146,11 @@ ConfigureAuthentication(builder, bootstrapOptions);
ConfigureGatewayOptionsMapping(builder, bootstrapOptions);
// Stella Router integration
var routerOptions = builder.Configuration.GetSection("Gateway:Router").Get<StellaRouterOptionsBase>();
builder.Services.TryAddStellaRouter(
var routerEnabled = builder.Services.AddRouterMicroservice(
builder.Configuration,
serviceName: "gateway",
version: typeof(Program).Assembly.GetName().Version?.ToString() ?? "1.0.0",
routerOptions: routerOptions);
version: System.Reflection.CustomAttributeExtensions.GetCustomAttribute<System.Reflection.AssemblyInformationalVersionAttribute>(System.Reflection.Assembly.GetExecutingAssembly())?.InformationalVersion ?? "1.0.0",
routerOptionsSection: "Router");
builder.Services.AddStellaOpsCors(builder.Environment, builder.Configuration);
@@ -164,9 +161,14 @@ app.LogStellaOpsLocalHostname("router");
// Force browser traffic onto HTTPS so auth (PKCE/DPoP/WebCrypto) always runs in a secure context.
app.Use(async (context, next) =>
{
var isWebSocketUpgrade =
context.WebSockets.IsWebSocketRequest ||
string.Equals(context.Request.Headers.Upgrade, "websocket", StringComparison.OrdinalIgnoreCase);
if (!context.Request.IsHttps &&
context.Request.Host.HasValue &&
!GatewayRoutes.IsSystemPath(context.Request.Path))
!GatewayRoutes.IsSystemPath(context.Request.Path) &&
!isWebSocketUpgrade)
{
var host = context.Request.Host.Host;
var redirect = $"https://{host}{context.Request.PathBase}{context.Request.Path}{context.Request.QueryString}";
@@ -185,7 +187,7 @@ app.UseMiddleware<SenderConstraintMiddleware>();
// It strips reserved identity headers and overwrites them from validated claims (security fix)
app.UseMiddleware<IdentityHeaderPolicyMiddleware>();
app.UseMiddleware<HealthCheckMiddleware>();
app.TryUseStellaRouter(routerOptions);
app.TryUseStellaRouter(routerEnabled);
// WebSocket support (before route dispatch)
app.UseWebSockets();
@@ -216,10 +218,34 @@ app.UseWhen(
app.UseMiddleware<ErrorPageFallbackMiddleware>();
// Refresh Router endpoint cache
app.TryRefreshStellaRouterEndpoints(routerOptions);
app.TryRefreshStellaRouterEndpoints(routerEnabled);
await app.RunAsync();
void RegisterGatewayTransportIfEnabled(string transportName, bool enabled, string configurationSection)
{
if (!enabled)
{
return;
}
var plugin = transportPluginLoader.GetPlugin(transportName);
if (plugin is null)
{
throw new InvalidOperationException(
$"Gateway transport plugin '{transportName}' is not available. " +
$"Provide a plugin assembly in '{pluginsPath}' or add the transport plugin dependency.");
}
plugin.Register(new RouterTransportRegistrationContext(
builder.Services,
builder.Configuration,
RouterTransportMode.Server)
{
ConfigurationSection = configurationSection
});
}
static void ConfigureAuthentication(WebApplicationBuilder builder, GatewayOptions options)
{
var authOptions = options.Auth;
@@ -312,6 +338,7 @@ static void ConfigureGatewayOptionsMapping(WebApplicationBuilder builder, Gatewa
{
var routing = gateway.Value.Routing;
options.RoutingTimeoutMs = (int)GatewayValueParser.ParseDuration(routing.DefaultTimeout, TimeSpan.FromSeconds(30)).TotalMilliseconds;
options.GlobalTimeoutCapMs = (int)GatewayValueParser.ParseDuration(routing.GlobalTimeoutCap, TimeSpan.FromSeconds(120)).TotalMilliseconds;
options.PreferLocalRegion = routing.PreferLocalRegion;
options.AllowDegradedInstances = routing.AllowDegradedInstances;
options.StrictVersionMatching = routing.StrictVersionMatching;
@@ -346,51 +373,47 @@ static void ConfigureGatewayOptionsMapping(WebApplicationBuilder builder, Gatewa
options.TokenUrl = openApi.TokenUrl;
});
builder.Services.AddOptions<TcpTransportOptions>()
.Configure<IOptions<GatewayOptions>>((options, gateway) =>
{
var tcp = gateway.Value.Transports.Tcp;
options.Port = tcp.Port;
options.ReceiveBufferSize = tcp.ReceiveBufferSize;
options.SendBufferSize = tcp.SendBufferSize;
options.MaxFrameSize = tcp.MaxFrameSize;
options.BindAddress = IPAddress.Parse(tcp.BindAddress);
});
builder.Services.AddOptions<TlsTransportOptions>()
.Configure<IOptions<GatewayOptions>>((options, gateway) =>
{
var tls = gateway.Value.Transports.Tls;
options.Port = tls.Port;
options.ReceiveBufferSize = tls.ReceiveBufferSize;
options.SendBufferSize = tls.SendBufferSize;
options.MaxFrameSize = tls.MaxFrameSize;
options.BindAddress = IPAddress.Parse(tls.BindAddress);
options.ServerCertificatePath = tls.CertificatePath;
options.ServerCertificateKeyPath = tls.CertificateKeyPath;
options.ServerCertificatePassword = tls.CertificatePassword;
options.RequireClientCertificate = tls.RequireClientCertificate;
options.AllowSelfSigned = tls.AllowSelfSigned;
});
builder.Services.AddOptions<MessagingTransportOptions>()
.Configure<IOptions<GatewayOptions>>((options, gateway) =>
{
var messaging = gateway.Value.Transports.Messaging;
options.RequestQueueTemplate = messaging.RequestQueueTemplate;
options.ResponseQueueName = messaging.ResponseQueueName;
options.ConsumerGroup = messaging.ConsumerGroup;
options.RequestTimeout = GatewayValueParser.ParseDuration(messaging.RequestTimeout, TimeSpan.FromSeconds(30));
options.LeaseDuration = GatewayValueParser.ParseDuration(messaging.LeaseDuration, TimeSpan.FromMinutes(5));
options.BatchSize = messaging.BatchSize;
options.HeartbeatInterval = GatewayValueParser.ParseDuration(messaging.HeartbeatInterval, TimeSpan.FromSeconds(10));
});
builder.Services.AddOptions<ValkeyTransportOptions>()
.Configure<IOptions<GatewayOptions>>((options, gateway) =>
{
var messaging = gateway.Value.Transports.Messaging;
options.ConnectionString = messaging.ConnectionString;
options.Database = messaging.Database;
});
}
static string? ResolveAuthorityClaimsUrl(GatewayAuthorityOptions authorityOptions)
{
if (!string.IsNullOrWhiteSpace(authorityOptions.ClaimsOverridesUrl))
{
return authorityOptions.ClaimsOverridesUrl.TrimEnd('/');
}
var candidate = authorityOptions.Issuer;
if (string.IsNullOrWhiteSpace(candidate))
{
candidate = authorityOptions.MetadataAddress;
}
if (string.IsNullOrWhiteSpace(candidate))
{
return null;
}
if (!Uri.TryCreate(candidate, UriKind.Absolute, out var uri))
{
return candidate.TrimEnd('/');
}
// Authority runs HTTP on the internal compose network by default.
var builder = new UriBuilder(uri)
{
Path = string.Empty,
Query = string.Empty,
Fragment = string.Empty
};
if (string.Equals(builder.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase))
{
builder.Scheme = Uri.UriSchemeHttp;
builder.Port = builder.Port == 443 ? 80 : builder.Port;
}
return builder.Uri.GetLeftPart(UriPartial.Authority).TrimEnd('/');
}