Files
mxaccessgw/src/ZB.MOM.WW.MxGateway.Server/Security/Authentication/AuthStoreServiceCollectionExtensions.cs
T

111 lines
6.3 KiB
C#

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
using ZB.MOM.WW.Audit;
using ZB.MOM.WW.Auth.Abstractions.ApiKeys;
using ZB.MOM.WW.Auth.ApiKeys;
using ZB.MOM.WW.Auth.ApiKeys.Admin;
using ZB.MOM.WW.Auth.ApiKeys.DependencyInjection;
using ZB.MOM.WW.Auth.ApiKeys.Sqlite;
using ZB.MOM.WW.MxGateway.Server.Security.Audit;
namespace ZB.MOM.WW.MxGateway.Server.Security.Authentication;
/// <summary>
/// Extension methods for configuring the SQLite authentication store.
/// </summary>
/// <remarks>
/// The peppered-HMAC API-key pipeline (token format, hashing, constant-time compare, SQLite
/// schema, stores, verifier and migration) is provided by the shared
/// <c>ZB.MOM.WW.Auth.ApiKeys</c> library, of which this gateway is the donor. This wiring binds
/// the library's <see cref="ApiKeyOptions"/> from the gateway's <c>MxGateway:Authentication</c>
/// section and layers the gateway-specific constraint enforcement, gRPC interceptor, CLI and
/// dashboard on top.
/// </remarks>
public static class AuthStoreServiceCollectionExtensions
{
/// <summary>The configuration section the gateway binds API-key options from.</summary>
public const string AuthenticationSectionPath = "MxGateway:Authentication";
/// <summary>The gateway API-key token prefix (token format <c>mxgw_&lt;id&gt;_&lt;secret&gt;</c>).</summary>
public const string TokenPrefix = "mxgw";
/// <summary>The configuration key the API-key pepper is resolved from.</summary>
public const string PepperSecretName = "MxGateway:ApiKeyPepper";
/// <summary>
/// Adds the SQLite authentication store and related services to the dependency container.
/// </summary>
/// <param name="services">Service collection to configure.</param>
/// <param name="configuration">Application configuration carrying the API-key options.</param>
/// <returns>The service collection for chaining.</returns>
public static IServiceCollection AddSqliteAuthStore(
this IServiceCollection services,
IConfiguration configuration)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(configuration);
// Pin the gateway's API-key contract (token prefix "mxgw"; pepper resolved from
// MxGateway:ApiKeyPepper) by layering fallback defaults UNDER the supplied configuration:
// an in-memory source provides TokenPrefix/PepperSecretName only when the bound
// MxGateway:Authentication section omits them (the section has no TokenPrefix, and the pepper
// is intentionally not in appsettings — it is supplied at runtime). Explicit config wins
// because it is added last. ApiKeyOptions is an init-only record, so the values must be
// present at bind time rather than mutated post-configure.
IConfiguration effectiveConfig = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
[$"{AuthenticationSectionPath}:TokenPrefix"] = TokenPrefix,
[$"{AuthenticationSectionPath}:PepperSecretName"] = PepperSecretName,
})
.AddConfiguration(configuration)
.Build();
// Register the shared API-key provider: binds ApiKeyOptions from MxGateway:Authentication,
// wires up the SQLite stores, the configuration-backed pepper provider, the verifier, the
// migrator and the migration hosted service.
services.AddZbApiKeyAuth(effectiveConfig, AuthenticationSectionPath);
// Canonical audit (Task 2.3 — DEEP adopt ZB.MOM.WW.Audit). All MxGateway audit flows as a
// canonical AuditEvent through the library IAuditWriter, persisted in a NEW gateway-owned
// audit_event table that lives in the SAME SQLite DB file as the api-key stores (it reuses
// the library's AuthSqliteConnectionFactory, registered by AddZbApiKeyAuth above).
services.AddSingleton(sp =>
new SqliteCanonicalAuditStore(sp.GetRequiredService<AuthSqliteConnectionFactory>()));
// Resolve the logger defensively: the production host always registers ILogger<T>, but the
// DI-only auth/CLI/dashboard unit tests build a bare ServiceCollection without AddLogging().
// Fall back to NullLogger there so the audit writer (and the IApiKeyAuditStore override that
// depends on it) still resolve. The write path is best-effort regardless.
services.AddSingleton<IAuditWriter>(sp =>
new CanonicalAuditWriter(
sp.GetRequiredService<SqliteCanonicalAuditStore>(),
sp.GetService<ILogger<CanonicalAuditWriter>>()
?? Microsoft.Extensions.Logging.Abstractions.NullLogger<CanonicalAuditWriter>.Instance));
// OVERRIDE the library's IApiKeyAuditStore (AddZbApiKeyAuth registered the library's
// SqliteApiKeyAuditStore via TryAddSingleton) with an adapter that canonicalizes every
// library-emitted ApiKeyAuditEntry onto AuditEvent and forwards it through IAuditWriter.
// This is the only way to canonicalize the library-internal ApiKeyAdminCommands verbs
// (create/revoke/rotate/init-db, etc.), which we cannot edit. The adapter is registered
// after AddZbApiKeyAuth so it (last registration) is what ApiKeyAdminCommands resolves and
// what the dashboard "recent audit" view reads via IApiKeyAuditStore.ListRecentAsync.
// The library's api_key_audit table is left in place but is now UNUSED — nothing writes to
// it once this adapter replaces the library's audit store.
services.AddSingleton<IApiKeyAuditStore, CanonicalForwardingApiKeyAuditStore>();
// The shared admin command set (create/revoke/rotate/list/init-db with audit) is not
// auto-registered by AddZbApiKeyAuth; the gateway CLI and dashboard drive it, so register
// it here over the already-wired stores, pepper provider and migrator.
services.AddSingleton(sp => new ApiKeyAdminCommands(
sp.GetRequiredService<IOptions<ApiKeyOptions>>().Value,
sp.GetRequiredService<IApiKeyAdminStore>(),
sp.GetRequiredService<IApiKeyAuditStore>(),
sp.GetRequiredService<IApiKeyPepperProvider>(),
sp.GetRequiredService<SqliteAuthStoreMigrator>()));
services.AddSingleton<ApiKeyAdminCliRunner>();
return services;
}
}