search and ai stabilization work, localization stablized.
This commit is contained in:
@@ -107,6 +107,17 @@ public static class StellaOpsLocalHostnameExtensions
|
||||
var currentUrls = builder.WebHost.GetSetting(WebHostDefaults.ServerUrlsKey) ?? "";
|
||||
builder.WebHost.ConfigureKestrel((context, kestrel) =>
|
||||
{
|
||||
// Load the configured default certificate (if any) so programmatic
|
||||
// UseHttps() calls can present a valid cert instead of relying on
|
||||
// the ASP.NET dev-cert (which doesn't exist in containers).
|
||||
X509Certificate2? defaultCert = null;
|
||||
var certPath = context.Configuration["Kestrel:Certificates:Default:Path"];
|
||||
var certPass = context.Configuration["Kestrel:Certificates:Default:Password"];
|
||||
if (!string.IsNullOrEmpty(certPath) && System.IO.File.Exists(certPath))
|
||||
{
|
||||
defaultCert = X509CertificateLoader.LoadPkcs12FromFile(certPath, certPass);
|
||||
}
|
||||
|
||||
// Re-add dev-port bindings from launchSettings.json / ASPNETCORE_URLS
|
||||
foreach (var rawUrl in currentUrls.Split(';', StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
@@ -119,7 +130,13 @@ public static class StellaOpsLocalHostnameExtensions
|
||||
|
||||
if (isHttps)
|
||||
{
|
||||
kestrel.Listen(addr, uri.Port, lo => lo.UseHttps());
|
||||
kestrel.Listen(addr, uri.Port, lo =>
|
||||
{
|
||||
if (defaultCert is not null)
|
||||
lo.UseHttps(defaultCert);
|
||||
else
|
||||
lo.UseHttps();
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -133,7 +150,10 @@ public static class StellaOpsLocalHostnameExtensions
|
||||
{
|
||||
kestrel.Listen(bindIp, HttpsPort, listenOptions =>
|
||||
{
|
||||
listenOptions.UseHttps();
|
||||
if (defaultCert is not null)
|
||||
listenOptions.UseHttps(defaultCert);
|
||||
else
|
||||
listenOptions.UseHttps();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -13,20 +13,98 @@ namespace StellaOps.Authority;
|
||||
internal sealed class AuthorityIdentityProviderRegistry : IAuthorityIdentityProviderRegistry
|
||||
{
|
||||
private readonly IServiceProvider serviceProvider;
|
||||
private readonly IReadOnlyDictionary<string, AuthorityIdentityProviderMetadata> providersByName;
|
||||
private readonly ReadOnlyCollection<AuthorityIdentityProviderMetadata> providers;
|
||||
private readonly ReadOnlyCollection<AuthorityIdentityProviderMetadata> passwordProviders;
|
||||
private readonly ReadOnlyCollection<AuthorityIdentityProviderMetadata> mfaProviders;
|
||||
private readonly ReadOnlyCollection<AuthorityIdentityProviderMetadata> clientProvisioningProviders;
|
||||
private readonly ReadOnlyCollection<AuthorityIdentityProviderMetadata> bootstrapProviders;
|
||||
private readonly ILogger<AuthorityIdentityProviderRegistry> logger;
|
||||
private volatile IReadOnlyDictionary<string, AuthorityIdentityProviderMetadata> providersByName;
|
||||
private volatile ReadOnlyCollection<AuthorityIdentityProviderMetadata> providers;
|
||||
private volatile ReadOnlyCollection<AuthorityIdentityProviderMetadata> passwordProviders;
|
||||
private volatile ReadOnlyCollection<AuthorityIdentityProviderMetadata> mfaProviders;
|
||||
private volatile ReadOnlyCollection<AuthorityIdentityProviderMetadata> clientProvisioningProviders;
|
||||
private volatile ReadOnlyCollection<AuthorityIdentityProviderMetadata> bootstrapProviders;
|
||||
private volatile AuthorityIdentityProviderCapabilities aggregateCapabilities;
|
||||
|
||||
public AuthorityIdentityProviderRegistry(
|
||||
IServiceProvider serviceProvider,
|
||||
ILogger<AuthorityIdentityProviderRegistry> logger)
|
||||
{
|
||||
this.serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
|
||||
logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
|
||||
// Initialise all volatile fields to empty defaults so Rebuild never
|
||||
// reads uninitialised state from another thread.
|
||||
providersByName = new Dictionary<string, AuthorityIdentityProviderMetadata>(StringComparer.OrdinalIgnoreCase);
|
||||
providers = new ReadOnlyCollection<AuthorityIdentityProviderMetadata>(Array.Empty<AuthorityIdentityProviderMetadata>());
|
||||
passwordProviders = new ReadOnlyCollection<AuthorityIdentityProviderMetadata>(Array.Empty<AuthorityIdentityProviderMetadata>());
|
||||
mfaProviders = new ReadOnlyCollection<AuthorityIdentityProviderMetadata>(Array.Empty<AuthorityIdentityProviderMetadata>());
|
||||
clientProvisioningProviders = new ReadOnlyCollection<AuthorityIdentityProviderMetadata>(Array.Empty<AuthorityIdentityProviderMetadata>());
|
||||
bootstrapProviders = new ReadOnlyCollection<AuthorityIdentityProviderMetadata>(Array.Empty<AuthorityIdentityProviderMetadata>());
|
||||
aggregateCapabilities = new AuthorityIdentityProviderCapabilities(false, false, false, false);
|
||||
|
||||
Rebuild();
|
||||
}
|
||||
|
||||
public IReadOnlyCollection<AuthorityIdentityProviderMetadata> Providers => providers;
|
||||
|
||||
public IReadOnlyCollection<AuthorityIdentityProviderMetadata> PasswordProviders => passwordProviders;
|
||||
|
||||
public IReadOnlyCollection<AuthorityIdentityProviderMetadata> MfaProviders => mfaProviders;
|
||||
|
||||
public IReadOnlyCollection<AuthorityIdentityProviderMetadata> ClientProvisioningProviders => clientProvisioningProviders;
|
||||
|
||||
public IReadOnlyCollection<AuthorityIdentityProviderMetadata> BootstrapProviders => bootstrapProviders;
|
||||
|
||||
public AuthorityIdentityProviderCapabilities AggregateCapabilities => aggregateCapabilities;
|
||||
|
||||
public bool TryGet(string name, [NotNullWhen(true)] out AuthorityIdentityProviderMetadata? metadata)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
metadata = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
return providersByName.TryGetValue(name, out metadata);
|
||||
}
|
||||
|
||||
public async ValueTask<AuthorityIdentityProviderHandle> AcquireAsync(string name, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!providersByName.TryGetValue(name, out var metadata))
|
||||
{
|
||||
throw new KeyNotFoundException($"Identity provider plugin '{name}' is not registered.");
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var scope = serviceProvider.CreateAsyncScope();
|
||||
try
|
||||
{
|
||||
var provider = scope.ServiceProvider
|
||||
.GetServices<IIdentityProviderPlugin>()
|
||||
.FirstOrDefault(p => string.Equals(p.Name, metadata.Name, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (provider is null)
|
||||
{
|
||||
await scope.DisposeAsync().ConfigureAwait(false);
|
||||
throw new InvalidOperationException($"Identity provider plugin '{metadata.Name}' could not be resolved.");
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return new AuthorityIdentityProviderHandle(scope, metadata, provider);
|
||||
}
|
||||
catch
|
||||
{
|
||||
await scope.DisposeAsync().ConfigureAwait(false);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Re-scans <see cref="IIdentityProviderPlugin"/> instances from the DI
|
||||
/// container and rebuilds the metadata and capability indexes. This is
|
||||
/// called during startup and when the plugin configuration is reloaded at
|
||||
/// runtime.
|
||||
/// </summary>
|
||||
internal void Rebuild()
|
||||
{
|
||||
using var scope = serviceProvider.CreateScope();
|
||||
var providerInstances = scope.ServiceProvider.GetServices<IIdentityProviderPlugin>();
|
||||
|
||||
@@ -87,72 +165,17 @@ internal sealed class AuthorityIdentityProviderRegistry : IAuthorityIdentityProv
|
||||
}
|
||||
}
|
||||
|
||||
// Volatile writes ensure visibility to concurrent readers.
|
||||
providersByName = dictionary;
|
||||
providers = new ReadOnlyCollection<AuthorityIdentityProviderMetadata>(uniqueProviders);
|
||||
passwordProviders = new ReadOnlyCollection<AuthorityIdentityProviderMetadata>(password);
|
||||
mfaProviders = new ReadOnlyCollection<AuthorityIdentityProviderMetadata>(mfa);
|
||||
clientProvisioningProviders = new ReadOnlyCollection<AuthorityIdentityProviderMetadata>(clientProvisioning);
|
||||
bootstrapProviders = new ReadOnlyCollection<AuthorityIdentityProviderMetadata>(bootstrap);
|
||||
|
||||
AggregateCapabilities = new AuthorityIdentityProviderCapabilities(
|
||||
SupportsPassword: passwordProviders.Count > 0,
|
||||
SupportsMfa: mfaProviders.Count > 0,
|
||||
SupportsClientProvisioning: clientProvisioningProviders.Count > 0,
|
||||
SupportsBootstrap: bootstrapProviders.Count > 0);
|
||||
}
|
||||
|
||||
public IReadOnlyCollection<AuthorityIdentityProviderMetadata> Providers => providers;
|
||||
|
||||
public IReadOnlyCollection<AuthorityIdentityProviderMetadata> PasswordProviders => passwordProviders;
|
||||
|
||||
public IReadOnlyCollection<AuthorityIdentityProviderMetadata> MfaProviders => mfaProviders;
|
||||
|
||||
public IReadOnlyCollection<AuthorityIdentityProviderMetadata> ClientProvisioningProviders => clientProvisioningProviders;
|
||||
|
||||
public IReadOnlyCollection<AuthorityIdentityProviderMetadata> BootstrapProviders => bootstrapProviders;
|
||||
|
||||
public AuthorityIdentityProviderCapabilities AggregateCapabilities { get; }
|
||||
|
||||
public bool TryGet(string name, [NotNullWhen(true)] out AuthorityIdentityProviderMetadata? metadata)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
metadata = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
return providersByName.TryGetValue(name, out metadata);
|
||||
}
|
||||
|
||||
public async ValueTask<AuthorityIdentityProviderHandle> AcquireAsync(string name, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!providersByName.TryGetValue(name, out var metadata))
|
||||
{
|
||||
throw new KeyNotFoundException($"Identity provider plugin '{name}' is not registered.");
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var scope = serviceProvider.CreateAsyncScope();
|
||||
try
|
||||
{
|
||||
var provider = scope.ServiceProvider
|
||||
.GetServices<IIdentityProviderPlugin>()
|
||||
.FirstOrDefault(p => string.Equals(p.Name, metadata.Name, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (provider is null)
|
||||
{
|
||||
await scope.DisposeAsync().ConfigureAwait(false);
|
||||
throw new InvalidOperationException($"Identity provider plugin '{metadata.Name}' could not be resolved.");
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return new AuthorityIdentityProviderHandle(scope, metadata, provider);
|
||||
}
|
||||
catch
|
||||
{
|
||||
await scope.DisposeAsync().ConfigureAwait(false);
|
||||
throw;
|
||||
}
|
||||
aggregateCapabilities = new AuthorityIdentityProviderCapabilities(
|
||||
SupportsPassword: password.Count > 0,
|
||||
SupportsMfa: mfa.Count > 0,
|
||||
SupportsClientProvisioning: clientProvisioning.Count > 0,
|
||||
SupportsBootstrap: bootstrap.Count > 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,16 +9,30 @@ namespace StellaOps.Authority;
|
||||
|
||||
internal sealed class AuthorityPluginRegistry : IAuthorityPluginRegistry
|
||||
{
|
||||
private readonly IReadOnlyDictionary<string, AuthorityPluginContext> registry;
|
||||
private volatile IReadOnlyDictionary<string, AuthorityPluginContext> registry;
|
||||
private volatile IReadOnlyCollection<AuthorityPluginContext> plugins;
|
||||
|
||||
public AuthorityPluginRegistry(IEnumerable<AuthorityPluginContext> contexts)
|
||||
{
|
||||
registry = contexts.ToDictionary(c => c.Manifest.Name, StringComparer.OrdinalIgnoreCase);
|
||||
Plugins = registry.Values.ToArray();
|
||||
var dict = contexts.ToDictionary(c => c.Manifest.Name, StringComparer.OrdinalIgnoreCase);
|
||||
registry = dict;
|
||||
plugins = dict.Values.ToArray();
|
||||
}
|
||||
|
||||
public IReadOnlyCollection<AuthorityPluginContext> Plugins { get; }
|
||||
public IReadOnlyCollection<AuthorityPluginContext> Plugins => plugins;
|
||||
|
||||
public bool TryGet(string name, [NotNullWhen(true)] out AuthorityPluginContext? context)
|
||||
=> registry.TryGetValue(name, out context);
|
||||
|
||||
/// <summary>
|
||||
/// Atomically replaces the plugin context set. Callers are responsible for
|
||||
/// ensuring that downstream registries (e.g. identity-provider registry) are
|
||||
/// rebuilt after this call.
|
||||
/// </summary>
|
||||
internal void Reload(IEnumerable<AuthorityPluginContext> contexts)
|
||||
{
|
||||
var dict = contexts.ToDictionary(c => c.Manifest.Name, StringComparer.OrdinalIgnoreCase);
|
||||
plugins = dict.Values.ToArray();
|
||||
registry = dict;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
@@ -1736,6 +1737,50 @@ if (authorityOptions.Bootstrap.Enabled)
|
||||
return Results.Problem("Failed to rotate ack token key.");
|
||||
}
|
||||
});
|
||||
|
||||
bootstrapGroup.MapPost("/plugins/reload", (
|
||||
IAuthorityPluginRegistry pluginRegistry,
|
||||
IAuthorityIdentityProviderRegistry identityProviderRegistry,
|
||||
IOptions<StellaOpsAuthorityOptions> optionsAccessor,
|
||||
IWebHostEnvironment environment,
|
||||
ILogger<AuthorityPluginRegistry> reloadLogger) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var opts = optionsAccessor.Value;
|
||||
var reloadedContexts = AuthorityPluginConfigurationLoader
|
||||
.Load(opts, environment.ContentRootPath)
|
||||
.ToArray();
|
||||
|
||||
if (pluginRegistry is AuthorityPluginRegistry reloadable)
|
||||
{
|
||||
reloadable.Reload(reloadedContexts);
|
||||
reloadLogger.LogInformation(
|
||||
"Plugin registry reloaded with {Count} context(s).",
|
||||
reloadedContexts.Length);
|
||||
}
|
||||
|
||||
if (identityProviderRegistry is AuthorityIdentityProviderRegistry idpReloadable)
|
||||
{
|
||||
idpReloadable.Rebuild();
|
||||
reloadLogger.LogInformation(
|
||||
"Identity provider registry rebuilt with {Count} provider(s).",
|
||||
idpReloadable.Providers.Count);
|
||||
}
|
||||
|
||||
return Results.Ok(new
|
||||
{
|
||||
reloaded = true,
|
||||
pluginContexts = reloadedContexts.Length,
|
||||
identityProviders = identityProviderRegistry.Providers.Count
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
reloadLogger.LogError(ex, "Plugin reload failed.");
|
||||
return Results.Problem("Plugin reload failed: " + ex.Message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
app.UseSerilogRequestLogging(options =>
|
||||
|
||||
@@ -3,6 +3,7 @@ using StellaOps.Authority.Persistence.Documents;
|
||||
using StellaOps.Authority.Persistence.Sessions;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Authority.Persistence.InMemory.Stores;
|
||||
|
||||
@@ -716,7 +717,7 @@ public sealed class InMemoryRevocationExportStateStore : IAuthorityRevocationExp
|
||||
{
|
||||
if (state.Sequence != expectedSequence)
|
||||
{
|
||||
throw new InvalidOperationException($"Revocation export sequence mismatch. Expected {expectedSequence}, current {state.Sequence}.");
|
||||
throw new InvalidOperationException(_t("auth.persistence.revocation_sequence_mismatch", expectedSequence, state.Sequence));
|
||||
}
|
||||
|
||||
state = new AuthorityRevocationExportStateDocument
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Npgsql;
|
||||
using StellaOps.Authority.Persistence.EfCore.CompiledModels;
|
||||
using StellaOps.Authority.Persistence.EfCore.Context;
|
||||
|
||||
namespace StellaOps.Authority.Persistence.Postgres;
|
||||
|
||||
/// <summary>
|
||||
/// Runtime factory for creating <see cref="AuthorityDbContext"/> instances.
|
||||
/// Uses the static compiled model when schema matches the default; falls back to
|
||||
/// reflection-based model building for non-default schemas (integration tests).
|
||||
/// Always uses reflection-based model building from <see cref="AuthorityDbContext.OnModelCreating"/>.
|
||||
/// When a real compiled model is generated via <c>dotnet ef dbcontext optimize</c>,
|
||||
/// re-enable UseModel() here.
|
||||
/// </summary>
|
||||
internal static class AuthorityDbContextFactory
|
||||
{
|
||||
@@ -22,12 +22,6 @@ internal static class AuthorityDbContextFactory
|
||||
var optionsBuilder = new DbContextOptionsBuilder<AuthorityDbContext>()
|
||||
.UseNpgsql(connection, npgsql => npgsql.CommandTimeout(commandTimeoutSeconds));
|
||||
|
||||
if (string.Equals(normalizedSchema, AuthorityDataSource.DefaultSchemaName, StringComparison.Ordinal))
|
||||
{
|
||||
// Use the static compiled model when schema mapping matches the default model.
|
||||
optionsBuilder.UseModel(AuthorityDbContextModel.Instance);
|
||||
}
|
||||
|
||||
return new AuthorityDbContext(optionsBuilder.Options, normalizedSchema);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ public sealed class ApiKeyRepository : IApiKeyRepository
|
||||
|
||||
await dbContext.Database.ExecuteSqlRawAsync(
|
||||
"UPDATE authority.api_keys SET last_used_at = NOW() WHERE tenant_id = {0} AND id = {1}",
|
||||
tenantId, id,
|
||||
[tenantId, id],
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -123,7 +123,7 @@ public sealed class ApiKeyRepository : IApiKeyRepository
|
||||
UPDATE authority.api_keys SET status = 'revoked', revoked_at = NOW(), revoked_by = {0}
|
||||
WHERE tenant_id = {1} AND id = {2} AND status = 'active'
|
||||
""",
|
||||
revokedBy, tenantId, id,
|
||||
[revokedBy, tenantId, id],
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Authority.Persistence.EfCore.Models;
|
||||
using StellaOps.Authority.Persistence.Postgres.Models;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Authority.Persistence.Postgres.Repositories;
|
||||
|
||||
@@ -58,7 +59,7 @@ public sealed class RevocationExportStateRepository
|
||||
|
||||
if (affected == 0)
|
||||
{
|
||||
throw new InvalidOperationException($"Revocation export state update rejected. Expected sequence {expectedSequence}.");
|
||||
throw new InvalidOperationException(_t("auth.persistence.revocation_update_rejected", expectedSequence));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -119,7 +119,7 @@ public sealed class SessionRepository : ISessionRepository
|
||||
|
||||
await dbContext.Database.ExecuteSqlRawAsync(
|
||||
"UPDATE authority.sessions SET last_activity_at = NOW() WHERE tenant_id = {0} AND id = {1} AND ended_at IS NULL",
|
||||
tenantId, id,
|
||||
[tenantId, id],
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -133,7 +133,7 @@ public sealed class SessionRepository : ISessionRepository
|
||||
UPDATE authority.sessions SET ended_at = NOW(), end_reason = {0}
|
||||
WHERE tenant_id = {1} AND id = {2} AND ended_at IS NULL
|
||||
""",
|
||||
reason, tenantId, id,
|
||||
[reason, tenantId, id],
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -147,7 +147,7 @@ public sealed class SessionRepository : ISessionRepository
|
||||
UPDATE authority.sessions SET ended_at = NOW(), end_reason = {0}
|
||||
WHERE tenant_id = {1} AND user_id = {2} AND ended_at IS NULL
|
||||
""",
|
||||
reason, tenantId, userId,
|
||||
[reason, tenantId, userId],
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -158,6 +158,7 @@ public sealed class SessionRepository : ISessionRepository
|
||||
|
||||
await dbContext.Database.ExecuteSqlRawAsync(
|
||||
"DELETE FROM authority.sessions WHERE expires_at < NOW() - INTERVAL '30 days'",
|
||||
[],
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
|
||||
@@ -106,7 +106,7 @@ public sealed class TokenRepository : ITokenRepository
|
||||
UPDATE authority.tokens SET revoked_at = NOW(), revoked_by = {0}
|
||||
WHERE tenant_id = {1} AND id = {2} AND revoked_at IS NULL
|
||||
""",
|
||||
revokedBy, tenantId, id,
|
||||
[revokedBy, tenantId, id],
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@ public sealed class TokenRepository : ITokenRepository
|
||||
UPDATE authority.tokens SET revoked_at = NOW(), revoked_by = {0}
|
||||
WHERE tenant_id = {1} AND user_id = {2} AND revoked_at IS NULL
|
||||
""",
|
||||
revokedBy, tenantId, userId,
|
||||
[revokedBy, tenantId, userId],
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -131,6 +131,7 @@ public sealed class TokenRepository : ITokenRepository
|
||||
|
||||
await dbContext.Database.ExecuteSqlRawAsync(
|
||||
"DELETE FROM authority.tokens WHERE expires_at < NOW() - INTERVAL '7 days'",
|
||||
[],
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -251,9 +252,7 @@ public sealed class RefreshTokenRepository : IRefreshTokenRepository
|
||||
UPDATE authority.refresh_tokens SET revoked_at = NOW(), revoked_by = {0}, replaced_by = {1}
|
||||
WHERE tenant_id = {2} AND id = {3} AND revoked_at IS NULL
|
||||
""",
|
||||
revokedBy,
|
||||
(object?)replacedBy ?? DBNull.Value,
|
||||
tenantId, id,
|
||||
[revokedBy, (object?)replacedBy ?? DBNull.Value, tenantId, id],
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -267,7 +266,7 @@ public sealed class RefreshTokenRepository : IRefreshTokenRepository
|
||||
UPDATE authority.refresh_tokens SET revoked_at = NOW(), revoked_by = {0}
|
||||
WHERE tenant_id = {1} AND user_id = {2} AND revoked_at IS NULL
|
||||
""",
|
||||
revokedBy, tenantId, userId,
|
||||
[revokedBy, tenantId, userId],
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -278,6 +277,7 @@ public sealed class RefreshTokenRepository : IRefreshTokenRepository
|
||||
|
||||
await dbContext.Database.ExecuteSqlRawAsync(
|
||||
"DELETE FROM authority.refresh_tokens WHERE expires_at < NOW() - INTERVAL '30 days'",
|
||||
[],
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
|
||||
@@ -191,7 +191,7 @@ public sealed class UserRepository : IUserRepository
|
||||
SET password_hash = {0}, password_salt = {1}, password_changed_at = NOW()
|
||||
WHERE tenant_id = {2} AND id = {3}
|
||||
""",
|
||||
passwordHash, passwordSalt, tenantId, userId,
|
||||
[passwordHash, passwordSalt, tenantId, userId],
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return rows > 0;
|
||||
@@ -238,7 +238,7 @@ public sealed class UserRepository : IUserRepository
|
||||
SET failed_login_attempts = 0, locked_until = NULL, last_login_at = NOW()
|
||||
WHERE tenant_id = {0} AND id = {1}
|
||||
""",
|
||||
tenantId, userId,
|
||||
[tenantId, userId],
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ using StellaOps.Authority.Persistence.EfCore.Models;
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using static StellaOps.Localization.T;
|
||||
|
||||
namespace StellaOps.Authority.Persistence.Postgres;
|
||||
|
||||
@@ -61,14 +62,17 @@ public sealed class PostgresVerdictManifestStore : IVerdictManifestStore
|
||||
signature_base64 = EXCLUDED.signature_base64,
|
||||
rekor_log_id = EXCLUDED.rekor_log_id
|
||||
""",
|
||||
manifest.ManifestId, manifest.Tenant, manifest.AssetDigest, manifest.VulnerabilityId,
|
||||
JsonSerializer.Serialize(manifest.Inputs, s_jsonOptions),
|
||||
StatusToString(manifest.Result.Status),
|
||||
manifest.Result.Confidence,
|
||||
JsonSerializer.Serialize(manifest.Result, s_jsonOptions),
|
||||
manifest.PolicyHash, manifest.LatticeVersion, manifest.EvaluatedAt, manifest.ManifestDigest,
|
||||
(object?)manifest.SignatureBase64 ?? DBNull.Value,
|
||||
(object?)manifest.RekorLogId ?? DBNull.Value,
|
||||
new object[]
|
||||
{
|
||||
manifest.ManifestId, manifest.Tenant, manifest.AssetDigest, manifest.VulnerabilityId,
|
||||
JsonSerializer.Serialize(manifest.Inputs, s_jsonOptions),
|
||||
StatusToString(manifest.Result.Status),
|
||||
manifest.Result.Confidence,
|
||||
JsonSerializer.Serialize(manifest.Result, s_jsonOptions),
|
||||
manifest.PolicyHash, manifest.LatticeVersion, manifest.EvaluatedAt, manifest.ManifestDigest,
|
||||
(object?)manifest.SignatureBase64 ?? DBNull.Value,
|
||||
(object?)manifest.RekorLogId ?? DBNull.Value,
|
||||
},
|
||||
ct).ConfigureAwait(false);
|
||||
|
||||
return manifest;
|
||||
@@ -226,9 +230,9 @@ public sealed class PostgresVerdictManifestStore : IVerdictManifestStore
|
||||
private static VerdictManifest ToManifest(VerdictManifestEfEntity ef)
|
||||
{
|
||||
var inputs = JsonSerializer.Deserialize<VerdictInputs>(ef.InputsJson, s_jsonOptions)
|
||||
?? throw new InvalidOperationException("Failed to deserialize inputs");
|
||||
?? throw new InvalidOperationException(_t("auth.persistence.deserialize_inputs_failed"));
|
||||
var result = JsonSerializer.Deserialize<VerdictResult>(ef.ResultJson, s_jsonOptions)
|
||||
?? throw new InvalidOperationException("Failed to deserialize result");
|
||||
?? throw new InvalidOperationException(_t("auth.persistence.deserialize_result_failed"));
|
||||
|
||||
return new VerdictManifest
|
||||
{
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Infrastructure.EfCore\StellaOps.Infrastructure.EfCore.csproj" />
|
||||
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Determinism.Abstractions\StellaOps.Determinism.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\StellaOps.Authority.Core\StellaOps.Authority.Core.csproj" />
|
||||
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Localization\StellaOps.Localization.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
Reference in New Issue
Block a user