consolidation of some of the modules, localization fixes, product advisories work, qa work

This commit is contained in:
master
2026-03-05 03:54:22 +02:00
parent 7bafcc3eef
commit 8e1cb9448d
3878 changed files with 72600 additions and 46861 deletions

View File

@@ -66,12 +66,12 @@ public static class StellaOpsClaimTypes
public const string IdentityProvider = "stellaops:idp";
/// <summary>
/// Operator reason supplied when issuing orchestrator control tokens.
/// Operator reason supplied when issuing jobengine control tokens.
/// </summary>
public const string OperatorReason = "stellaops:operator_reason";
/// <summary>
/// Operator ticket supplied when issuing orchestrator control tokens.
/// Operator ticket supplied when issuing jobengine control tokens.
/// </summary>
public const string OperatorTicket = "stellaops:operator_ticket";
@@ -86,12 +86,12 @@ public static class StellaOpsClaimTypes
public const string QuotaTicket = "stellaops:quota_ticket";
/// <summary>
/// Backfill activation reason supplied when issuing orchestrator backfill tokens.
/// Backfill activation reason supplied when issuing jobengine backfill tokens.
/// </summary>
public const string BackfillReason = "stellaops:backfill_reason";
/// <summary>
/// Backfill ticket/incident reference supplied when issuing orchestrator backfill tokens.
/// Backfill ticket/incident reference supplied when issuing jobengine backfill tokens.
/// </summary>
public const string BackfillTicket = "stellaops:backfill_ticket";

View File

@@ -373,7 +373,7 @@ public static class StellaOpsScopes
public const string OrchQuota = "orch:quota";
/// <summary>
/// Scope granting permission to initiate orchestrator-controlled backfill runs.
/// Scope granting permission to initiate jobengine-controlled backfill runs.
/// </summary>
public const string OrchBackfill = "orch:backfill";
@@ -597,6 +597,13 @@ public static class StellaOpsScopes
/// </summary>
public const string AnalyticsRead = "analytics.read";
// UI preferences scopes
public const string UiPreferencesRead = "ui.preferences.read";
public const string UiPreferencesWrite = "ui.preferences.write";
// Platform ops health scope
public const string OpsHealth = "ops.health";
// Platform context scopes
public const string PlatformContextRead = "platform.context.read";
public const string PlatformContextWrite = "platform.context.write";

View File

@@ -3,10 +3,12 @@ using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using StellaOps.Auth.Abstractions;
using System;
using System.Linq;
using System.Security.Claims;
namespace StellaOps.Auth.ServerIntegration;
@@ -90,8 +92,30 @@ public static class ServiceCollectionExtensions
// Accept both "Bearer" and "DPoP" authorization schemes.
// The StellaOps UI sends DPoP-bound access tokens with "Authorization: DPoP <token>".
jwt.Events ??= new JwtBearerEvents();
var bridgeLogger = provider.GetService<ILoggerFactory>()?.CreateLogger("StellaOps.Auth.EnvelopeBridge");
jwt.Events.OnMessageReceived = context =>
{
// Bridge: accept Router-signed identity envelope as a valid StellaOpsBearer identity.
// Valkey-transported requests carry no JWT; the gateway already validated the token
// and signed the claims into an HMAC-SHA256 envelope that the Router SDK verified
// and populated onto httpContext.User before the ASP.NET Core pipeline runs.
var identity = context.HttpContext.User?.Identity;
if (identity is ClaimsIdentity { IsAuthenticated: true, AuthenticationType: "StellaRouterEnvelope" } envelopeId)
{
bridgeLogger?.LogInformation(
"Envelope bridge: accepting identity {Subject} with {ScopeCount} scopes as StellaOpsBearer",
envelopeId.FindFirst("sub")?.Value ?? "(unknown)",
envelopeId.FindAll("scope").Count());
context.Principal = context.HttpContext.User;
context.Success();
return System.Threading.Tasks.Task.CompletedTask;
}
bridgeLogger?.LogDebug(
"Envelope bridge: no envelope identity (AuthType={AuthType}, IsAuth={IsAuth})",
identity?.AuthenticationType ?? "(null)",
identity?.IsAuthenticated ?? false);
if (!string.IsNullOrEmpty(context.Token))
{
return System.Threading.Tasks.Task.CompletedTask;

View File

@@ -1,6 +1,7 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.DependencyInjection;
using StellaOps.Auth.Abstractions;
using System;
namespace StellaOps.Auth.ServerIntegration;
@@ -12,6 +13,9 @@ public static class StellaOpsAuthorizationPolicyBuilderExtensions
{
/// <summary>
/// Requires the specified scopes using the StellaOps scope requirement.
/// Explicitly binds the policy to the StellaOpsBearer authentication scheme
/// so that token validation uses the correct JWT handler regardless of the
/// application's default authentication scheme.
/// </summary>
public static AuthorizationPolicyBuilder RequireStellaOpsScopes(
this AuthorizationPolicyBuilder builder,
@@ -19,6 +23,11 @@ public static class StellaOpsAuthorizationPolicyBuilderExtensions
{
ArgumentNullException.ThrowIfNull(builder);
if (!builder.AuthenticationSchemes.Contains(StellaOpsAuthenticationDefaults.AuthenticationScheme))
{
builder.AuthenticationSchemes.Add(StellaOpsAuthenticationDefaults.AuthenticationScheme);
}
var requirement = new StellaOpsScopeRequirement(scopes);
builder.AddRequirements(requirement);
return builder;
@@ -37,6 +46,7 @@ public static class StellaOpsAuthorizationPolicyBuilderExtensions
options.AddPolicy(policyName, policy =>
{
policy.AuthenticationSchemes.Add(StellaOpsAuthenticationDefaults.AuthenticationScheme);
policy.Requirements.Add(new StellaOpsScopeRequirement(scopes));
});
}

View File

@@ -3,7 +3,12 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Auth.Abstractions;
using StellaOps.Authority.Persistence.Postgres.Models;
using StellaOps.Authority.Persistence.Postgres.Repositories;
using StellaOps.Authority.Plugin.Standard.Storage;
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -11,6 +16,8 @@ namespace StellaOps.Authority.Plugin.Standard.Bootstrap;
internal sealed class StandardPluginBootstrapper : IHostedService
{
private const string DefaultTenantId = "default";
private readonly string pluginName;
private readonly IServiceScopeFactory scopeFactory;
private readonly ILogger<StandardPluginBootstrapper> logger;
@@ -46,6 +53,122 @@ internal sealed class StandardPluginBootstrapper : IHostedService
{
logger.LogError(ex, "Standard Authority plugin '{PluginName}' failed to ensure bootstrap user.", pluginName);
}
var tenantId = options.TenantId ?? DefaultTenantId;
var bootstrapRoles = options.BootstrapUser.Roles ?? new[] { "admin" };
try
{
await EnsureAdminRoleAsync(scope.ServiceProvider, tenantId, options.BootstrapUser.Username!, bootstrapRoles, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
logger.LogError(ex, "Standard Authority plugin '{PluginName}' failed to seed admin role with scopes.", pluginName);
}
}
private async Task EnsureAdminRoleAsync(
IServiceProvider services,
string tenantId,
string bootstrapUsername,
string[] roleNames,
CancellationToken cancellationToken)
{
var roleRepository = services.GetRequiredService<IRoleRepository>();
var permissionRepository = services.GetRequiredService<IPermissionRepository>();
var userRepository = services.GetRequiredService<IUserRepository>();
var allScopes = StellaOpsScopes.All;
foreach (var roleName in roleNames)
{
var existingRole = await roleRepository.GetByNameAsync(tenantId, roleName, cancellationToken).ConfigureAwait(false);
Guid roleId;
if (existingRole is null)
{
logger.LogInformation("Standard Authority plugin '{PluginName}' creating system role '{RoleName}' with {ScopeCount} scopes.",
pluginName, roleName, allScopes.Count);
roleId = await roleRepository.CreateAsync(tenantId, new RoleEntity
{
Id = Guid.NewGuid(),
TenantId = tenantId,
Name = roleName,
DisplayName = roleName == "admin" ? "Administrator" : roleName,
Description = roleName == "admin" ? "Full platform access. Auto-seeded by bootstrap." : $"System role '{roleName}'. Auto-seeded by bootstrap.",
IsSystem = true,
Metadata = "{}",
CreatedAt = DateTimeOffset.UtcNow,
UpdatedAt = DateTimeOffset.UtcNow,
}, cancellationToken).ConfigureAwait(false);
}
else
{
roleId = existingRole.Id;
logger.LogInformation("Standard Authority plugin '{PluginName}' role '{RoleName}' already exists (id={RoleId}). Ensuring scope assignments.",
pluginName, roleName, roleId);
}
// Ensure permissions exist for all scopes and are assigned to the role
var existingPermissions = await permissionRepository.GetRolePermissionsAsync(tenantId, roleId, cancellationToken).ConfigureAwait(false);
var existingPermissionNames = existingPermissions.Select(p => p.Name).ToHashSet(StringComparer.OrdinalIgnoreCase);
foreach (var scope in allScopes)
{
if (existingPermissionNames.Contains(scope))
{
continue;
}
// Parse scope into resource:action (e.g. "release:read" -> resource="release", action="read")
var separatorIndex = scope.IndexOfAny(new[] { ':', '.' });
var resource = separatorIndex > 0 ? scope[..separatorIndex] : scope;
var action = separatorIndex > 0 ? scope[(separatorIndex + 1)..] : "access";
// Check if the permission already exists globally
var existingPermission = await permissionRepository.GetByNameAsync(tenantId, scope, cancellationToken).ConfigureAwait(false);
Guid permissionId;
if (existingPermission is null)
{
permissionId = await permissionRepository.CreateAsync(tenantId, new PermissionEntity
{
Id = Guid.NewGuid(),
TenantId = tenantId,
Name = scope,
Resource = resource,
Action = action,
Description = $"Auto-seeded permission for scope '{scope}'.",
CreatedAt = DateTimeOffset.UtcNow,
}, cancellationToken).ConfigureAwait(false);
}
else
{
permissionId = existingPermission.Id;
}
await permissionRepository.AssignToRoleAsync(tenantId, roleId, permissionId, cancellationToken).ConfigureAwait(false);
}
logger.LogInformation("Standard Authority plugin '{PluginName}' role '{RoleName}' now has {ScopeCount} scope permissions.",
pluginName, roleName, allScopes.Count);
// Assign the role to the bootstrap user
var normalizedUsername = bootstrapUsername.Trim().ToLowerInvariant();
var bootstrapUser = await userRepository.GetByUsernameAsync(tenantId, normalizedUsername, cancellationToken).ConfigureAwait(false);
if (bootstrapUser is not null)
{
await roleRepository.AssignToUserAsync(tenantId, bootstrapUser.Id, roleId, "bootstrap", expiresAt: null, cancellationToken).ConfigureAwait(false);
logger.LogInformation("Standard Authority plugin '{PluginName}' assigned role '{RoleName}' to bootstrap user '{Username}'.",
pluginName, roleName, normalizedUsername);
}
else
{
logger.LogWarning("Standard Authority plugin '{PluginName}' could not find bootstrap user '{Username}' to assign role '{RoleName}'.",
pluginName, normalizedUsername, roleName);
}
}
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;

View File

@@ -52,6 +52,8 @@ internal sealed class BootstrapUserOptions
public bool RequirePasswordReset { get; set; } = true;
public string[]? Roles { get; set; }
public bool IsConfigured => !string.IsNullOrWhiteSpace(Username) && !string.IsNullOrWhiteSpace(Password);
public void Validate(string pluginName)

View File

@@ -344,7 +344,7 @@ internal sealed class StandardUserCredentialStore : IUserCredentialStore
displayName: bootstrap.Username,
email: null,
requirePasswordReset: bootstrap.RequirePasswordReset,
roles: Array.Empty<string>(),
roles: bootstrap.Roles ?? new[] { "admin" },
attributes: new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase));
var result = await UpsertUserAsync(registration, cancellationToken).ConfigureAwait(false);

View File

@@ -871,7 +871,7 @@ internal sealed class ValidateClientCredentialsHandler : IOpenIddictServerHandle
context.Transaction.Properties[AuthorityOpenIddictConstants.AuditInvalidScopeProperty] = invalidScope;
context.Reject(OpenIddictConstants.Errors.InvalidClient, "Orchestrator scopes require a tenant assignment.");
logger.LogWarning(
"Client credentials validation failed for {ClientId}: orchestrator scopes require tenant assignment.",
"Client credentials validation failed for {ClientId}: jobengine scopes require tenant assignment.",
document.ClientId);
return;
}

View File

@@ -68,7 +68,7 @@ internal static class OpenIddictGatewayBridgeEndpointExtensions
.WithSummary("OAuth2 revocation endpoint.")
.WithDescription("Bridges Gateway microservice `/connect/revoke` requests to Authority `/revoke`.");
endpoints.MapGet("/well-known/openid-configuration", (
endpoints.MapGet("/.well-known/openid-configuration", (
HttpContext context,
IHttpClientFactory httpClientFactory,
CancellationToken cancellationToken) =>

View File

@@ -319,7 +319,10 @@ builder.Services.AddOptions<StellaOpsResourceServerOptions>()
.Configure(options =>
{
options.Authority = issuerUri.ToString();
options.RequireHttpsMetadata = !issuerUri.IsLoopback;
// Use loopback metadata endpoint so the Authority can fetch its own JWKS
// without requiring external DNS resolution or TLS certificate trust.
options.MetadataAddress = "http://127.0.0.1/.well-known/openid-configuration";
options.RequireHttpsMetadata = false;
})
.PostConfigure(static options => options.Validate());