test(auditlog): audit failures never abort user-facing actions (#23 M4)
This commit is contained in:
@@ -0,0 +1,472 @@
|
|||||||
|
using Akka.Actor;
|
||||||
|
using Akka.TestKit.Xunit2;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.TestHost;
|
||||||
|
using Microsoft.Data.Sqlite;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using NSubstitute;
|
||||||
|
using ScadaLink.Commons.Entities.Audit;
|
||||||
|
using ScadaLink.Commons.Entities.Notifications;
|
||||||
|
using ScadaLink.Commons.Interfaces.Repositories;
|
||||||
|
using ScadaLink.Commons.Interfaces.Services;
|
||||||
|
using ScadaLink.Commons.Messages.Integration;
|
||||||
|
using ScadaLink.Commons.Types;
|
||||||
|
using ScadaLink.Commons.Types.Enums;
|
||||||
|
using ScadaLink.ConfigurationDatabase;
|
||||||
|
using ScadaLink.ConfigurationDatabase.Repositories;
|
||||||
|
using ScadaLink.ConfigurationDatabase.Tests.Migrations;
|
||||||
|
using ScadaLink.InboundAPI.Middleware;
|
||||||
|
using ScadaLink.NotificationOutbox;
|
||||||
|
using ScadaLink.NotificationOutbox.Delivery;
|
||||||
|
using ScadaLink.NotificationOutbox.Messages;
|
||||||
|
using ScadaLink.SiteRuntime.Scripts;
|
||||||
|
using System.Net;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Encodings.Web;
|
||||||
|
|
||||||
|
namespace ScadaLink.AuditLog.Tests.Integration;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Audit Log #23 — M4 Bundle E (Task E4) cross-boundary safety suite verifying
|
||||||
|
/// the alog.md §13 contract: an always-throwing audit writer NEVER aborts the
|
||||||
|
/// user-facing action. Exercises every boundary that emits audit rows in M2,
|
||||||
|
/// M3, and M4:
|
||||||
|
///
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item><description>External system sync call (M2 Bundle F).</description></item>
|
||||||
|
/// <item><description>External system cached call (M3 Bundle E).</description></item>
|
||||||
|
/// <item><description>Database sync write (M4 Bundle A).</description></item>
|
||||||
|
/// <item><description>Inbound API request (M4 Bundle D).</description></item>
|
||||||
|
/// <item><description>Notification dispatcher (M4 Bundle B).</description></item>
|
||||||
|
/// </list>
|
||||||
|
///
|
||||||
|
/// <para>
|
||||||
|
/// The site-local boundaries (ESG sync/cached, DB sync) take the always-throw
|
||||||
|
/// <see cref="ThrowingAuditWriter"/> in place of the production
|
||||||
|
/// <see cref="IAuditWriter"/>; the central boundaries (Inbound API,
|
||||||
|
/// Notification dispatcher) take the always-throw
|
||||||
|
/// <see cref="ThrowingCentralAuditWriter"/> in place of
|
||||||
|
/// <see cref="ICentralAuditWriter"/>. In each case the wrapped action's
|
||||||
|
/// original return value (or original exception) must still flow back to the
|
||||||
|
/// caller untouched.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
public class AuditWriteFailureSafetyTests : TestKit, IClassFixture<MsSqlMigrationFixture>
|
||||||
|
{
|
||||||
|
private readonly MsSqlMigrationFixture _fixture;
|
||||||
|
|
||||||
|
public AuditWriteFailureSafetyTests(MsSqlMigrationFixture fixture)
|
||||||
|
{
|
||||||
|
_fixture = fixture;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
// Always-throwing writer test doubles
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Site-side <see cref="IAuditWriter"/> that ALWAYS throws on
|
||||||
|
/// <see cref="WriteAsync"/>. Used to verify that ESG / DB script-side
|
||||||
|
/// helpers swallow the throw and return their normal result to the script.
|
||||||
|
/// </summary>
|
||||||
|
private sealed class ThrowingAuditWriter : IAuditWriter
|
||||||
|
{
|
||||||
|
private int _attempts;
|
||||||
|
public int Attempts => Volatile.Read(ref _attempts);
|
||||||
|
|
||||||
|
public Task WriteAsync(AuditEvent evt, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
Interlocked.Increment(ref _attempts);
|
||||||
|
return Task.FromException(new InvalidOperationException(
|
||||||
|
"test-only ThrowingAuditWriter — audit pipeline unavailable"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Central-side <see cref="ICentralAuditWriter"/> that ALWAYS throws on
|
||||||
|
/// <see cref="WriteAsync"/>. Used to verify Inbound API + Notification
|
||||||
|
/// dispatcher absorb audit-write failures rather than propagating them
|
||||||
|
/// into the response / state transition.
|
||||||
|
/// </summary>
|
||||||
|
private sealed class ThrowingCentralAuditWriter : ICentralAuditWriter
|
||||||
|
{
|
||||||
|
private int _attempts;
|
||||||
|
public int Attempts => Volatile.Read(ref _attempts);
|
||||||
|
|
||||||
|
public Task WriteAsync(AuditEvent evt, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
Interlocked.Increment(ref _attempts);
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"test-only ThrowingCentralAuditWriter — audit subsystem unavailable");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Site-side <see cref="ICachedCallTelemetryForwarder"/> that ALWAYS
|
||||||
|
/// throws on <see cref="ForwardAsync"/>. The cached-call helpers absorb
|
||||||
|
/// the throw and still return a valid <see cref="TrackedOperationId"/>.
|
||||||
|
/// </summary>
|
||||||
|
private sealed class ThrowingCachedForwarder : ICachedCallTelemetryForwarder
|
||||||
|
{
|
||||||
|
private int _attempts;
|
||||||
|
public int Attempts => Volatile.Read(ref _attempts);
|
||||||
|
|
||||||
|
public Task ForwardAsync(CachedCallTelemetry telemetry, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
Interlocked.Increment(ref _attempts);
|
||||||
|
return Task.FromException(new InvalidOperationException(
|
||||||
|
"test-only ThrowingCachedForwarder — telemetry pipeline unavailable"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
// Test 1 — ESG sync call still returns the original ExternalCallResult.
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task EsgSyncCall_BrokenAuditWriter_StillReturnsResult()
|
||||||
|
{
|
||||||
|
var client = Substitute.For<IExternalSystemClient>();
|
||||||
|
var expected = new ExternalCallResult(
|
||||||
|
Success: true,
|
||||||
|
ResponseJson: "{\"orderId\":42}",
|
||||||
|
ErrorMessage: null,
|
||||||
|
WasBuffered: false);
|
||||||
|
client.CallAsync(
|
||||||
|
"ERP", "GetOrder",
|
||||||
|
Arg.Any<IReadOnlyDictionary<string, object?>?>(),
|
||||||
|
Arg.Any<CancellationToken>())
|
||||||
|
.Returns(expected);
|
||||||
|
|
||||||
|
var writer = new ThrowingAuditWriter();
|
||||||
|
var helper = new ScriptRuntimeContext.ExternalSystemHelper(
|
||||||
|
client,
|
||||||
|
instanceName: "Plant.Pump42",
|
||||||
|
NullLogger.Instance,
|
||||||
|
auditWriter: writer,
|
||||||
|
siteId: "site-77",
|
||||||
|
sourceScript: "ScriptActor:Sync",
|
||||||
|
cachedForwarder: null);
|
||||||
|
|
||||||
|
var result = await helper.Call("ERP", "GetOrder");
|
||||||
|
|
||||||
|
Assert.Same(expected, result);
|
||||||
|
// Proof the audit writer was attempted — otherwise the test wouldn't
|
||||||
|
// actually exercise the safety contract.
|
||||||
|
Assert.True(writer.Attempts >= 1,
|
||||||
|
$"Expected audit writer to be invoked at least once; saw {writer.Attempts}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
// Test 2 — ESG cached call still returns a TrackedOperationId.
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task EsgCachedCall_BrokenAuditWriter_StillReturnsTrackedOperationId()
|
||||||
|
{
|
||||||
|
var client = Substitute.For<IExternalSystemClient>();
|
||||||
|
// CachedCallAsync returns WasBuffered=true so the helper takes the
|
||||||
|
// S&F-deferred path — no immediate-terminal telemetry, which keeps the
|
||||||
|
// forwarder attempt count at exactly one (the CachedSubmit emission).
|
||||||
|
client.CachedCallAsync(
|
||||||
|
"ERP", "GetOrder",
|
||||||
|
Arg.Any<IReadOnlyDictionary<string, object?>?>(),
|
||||||
|
Arg.Any<string?>(),
|
||||||
|
Arg.Any<CancellationToken>(),
|
||||||
|
Arg.Any<TrackedOperationId?>())
|
||||||
|
.Returns(new ExternalCallResult(true, null, null, WasBuffered: true));
|
||||||
|
|
||||||
|
// BOTH the audit writer AND the cached forwarder throw — the
|
||||||
|
// CachedSubmit emission goes through the forwarder in production, so
|
||||||
|
// breaking only the writer wouldn't actually exercise the cached
|
||||||
|
// path's safety contract.
|
||||||
|
var writer = new ThrowingAuditWriter();
|
||||||
|
var forwarder = new ThrowingCachedForwarder();
|
||||||
|
var helper = new ScriptRuntimeContext.ExternalSystemHelper(
|
||||||
|
client,
|
||||||
|
instanceName: "Plant.Pump42",
|
||||||
|
NullLogger.Instance,
|
||||||
|
auditWriter: writer,
|
||||||
|
siteId: "site-77",
|
||||||
|
sourceScript: "ScriptActor:Cached",
|
||||||
|
cachedForwarder: forwarder);
|
||||||
|
|
||||||
|
var trackedId = await helper.CachedCall("ERP", "GetOrder");
|
||||||
|
|
||||||
|
// Non-default id materialised despite the forwarder failing.
|
||||||
|
Assert.NotEqual(default, trackedId);
|
||||||
|
Assert.True(forwarder.Attempts >= 1,
|
||||||
|
$"Expected cached forwarder to be invoked at least once; saw {forwarder.Attempts}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
// Test 3 — DB sync write still returns the rows-affected count.
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DbSyncWrite_BrokenAuditWriter_StillReturnsRowsAffected()
|
||||||
|
{
|
||||||
|
const string connectionName = "machineData";
|
||||||
|
const string instanceName = "Plant.Pump42";
|
||||||
|
|
||||||
|
using var keepAlive = new SqliteConnection(
|
||||||
|
"Data Source=k-safety-db;Mode=Memory;Cache=Shared");
|
||||||
|
keepAlive.Open();
|
||||||
|
|
||||||
|
// Schema + seed inside a unique in-memory DB.
|
||||||
|
var dbName = $"db-{Guid.NewGuid():N}";
|
||||||
|
var connStr = $"Data Source={dbName};Mode=Memory;Cache=Shared";
|
||||||
|
using var dbKeepAlive = new SqliteConnection(connStr);
|
||||||
|
dbKeepAlive.Open();
|
||||||
|
using (var seed = dbKeepAlive.CreateCommand())
|
||||||
|
{
|
||||||
|
seed.CommandText =
|
||||||
|
"CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT NOT NULL);";
|
||||||
|
seed.ExecuteNonQuery();
|
||||||
|
}
|
||||||
|
var inner = new SqliteConnection(connStr);
|
||||||
|
inner.Open();
|
||||||
|
|
||||||
|
var gateway = Substitute.For<IDatabaseGateway>();
|
||||||
|
gateway.GetConnectionAsync(connectionName, Arg.Any<CancellationToken>())
|
||||||
|
.Returns(inner);
|
||||||
|
|
||||||
|
var writer = new ThrowingAuditWriter();
|
||||||
|
var helper = new ScriptRuntimeContext.DatabaseHelper(
|
||||||
|
gateway,
|
||||||
|
instanceName,
|
||||||
|
NullLogger.Instance,
|
||||||
|
auditWriter: writer,
|
||||||
|
siteId: "site-77",
|
||||||
|
sourceScript: "ScriptActor:Db",
|
||||||
|
cachedForwarder: null);
|
||||||
|
|
||||||
|
await using (var conn = await helper.Connection(connectionName))
|
||||||
|
await using (var cmd = conn.CreateCommand())
|
||||||
|
{
|
||||||
|
cmd.CommandText = "INSERT INTO t (id, name) VALUES (1, 'safety')";
|
||||||
|
var rows = await cmd.ExecuteNonQueryAsync();
|
||||||
|
Assert.Equal(1, rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.True(writer.Attempts >= 1,
|
||||||
|
$"Expected audit writer to be invoked at least once; saw {writer.Attempts}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
// Test 4 — Inbound API request still returns HTTP 200.
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task InboundApi_BrokenAuditWriter_StillReturns200()
|
||||||
|
{
|
||||||
|
var writer = new ThrowingCentralAuditWriter();
|
||||||
|
|
||||||
|
using var host = await BuildInboundApiHostAsync(writer, endpointStatus: 200);
|
||||||
|
var client = host.GetTestClient();
|
||||||
|
|
||||||
|
var resp = await client.PostAsync(
|
||||||
|
"/api/echo",
|
||||||
|
new StringContent("{\"x\":1}", Encoding.UTF8, "application/json"));
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.OK, resp.StatusCode);
|
||||||
|
Assert.True(writer.Attempts >= 1,
|
||||||
|
$"Expected central audit writer to be invoked at least once; saw {writer.Attempts}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
// Test 5 — Notification dispatcher still transitions to Delivered.
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
|
||||||
|
[SkippableFact]
|
||||||
|
public async Task NotificationDispatch_BrokenAuditWriter_StillTransitionsToDelivered()
|
||||||
|
{
|
||||||
|
Skip.IfNot(_fixture.Available, _fixture.SkipReason);
|
||||||
|
|
||||||
|
var siteId = "test-e4-safety-" + Guid.NewGuid().ToString("N").Substring(0, 8);
|
||||||
|
var notificationId = Guid.NewGuid();
|
||||||
|
|
||||||
|
await SeedSmtpConfigAsync();
|
||||||
|
await SeedNotificationAsync(notificationId, siteId);
|
||||||
|
|
||||||
|
var adapter = new SingleOutcomeAdapter(DeliveryOutcome.Success("ops@example.com"));
|
||||||
|
var serviceProvider = BuildNotificationDispatcherProvider(adapter);
|
||||||
|
var throwingWriter = new ThrowingCentralAuditWriter();
|
||||||
|
|
||||||
|
var actor = Sys.ActorOf(Props.Create(() => new NotificationOutboxActor(
|
||||||
|
serviceProvider,
|
||||||
|
new NotificationOutboxOptions
|
||||||
|
{
|
||||||
|
DispatchInterval = TimeSpan.FromHours(1),
|
||||||
|
PurgeInterval = TimeSpan.FromDays(1),
|
||||||
|
},
|
||||||
|
(ICentralAuditWriter)throwingWriter,
|
||||||
|
NullLogger<NotificationOutboxActor>.Instance)));
|
||||||
|
|
||||||
|
actor.Tell(InternalMessages.DispatchTick.Instance);
|
||||||
|
|
||||||
|
// Notifications table reflects the successful delivery even though
|
||||||
|
// every audit write threw — the central direct-write writer
|
||||||
|
// catches/logs internally and the dispatcher catches defensively too
|
||||||
|
// (alog.md §13).
|
||||||
|
await AwaitAssertAsync(async () =>
|
||||||
|
{
|
||||||
|
await using var ctx = CreateContext();
|
||||||
|
var row = await ctx.Notifications.SingleAsync(
|
||||||
|
n => n.NotificationId == notificationId.ToString("D"));
|
||||||
|
Assert.Equal(NotificationStatus.Delivered, row.Status);
|
||||||
|
Assert.NotNull(row.DeliveredAt);
|
||||||
|
}, TimeSpan.FromSeconds(15));
|
||||||
|
|
||||||
|
Assert.True(throwingWriter.Attempts >= 1,
|
||||||
|
$"Expected dispatcher to attempt audit write at least once; saw {throwingWriter.Attempts}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
// Test infrastructure
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
|
||||||
|
private ScadaLinkDbContext CreateContext()
|
||||||
|
{
|
||||||
|
var options = new DbContextOptionsBuilder<ScadaLinkDbContext>()
|
||||||
|
.UseSqlServer(_fixture.ConnectionString)
|
||||||
|
.ConfigureWarnings(w => w.Ignore(
|
||||||
|
Microsoft.EntityFrameworkCore.Diagnostics.RelationalEventId.PendingModelChangesWarning))
|
||||||
|
.Options;
|
||||||
|
return new ScadaLinkDbContext(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IServiceProvider BuildNotificationDispatcherProvider(
|
||||||
|
INotificationDeliveryAdapter adapter)
|
||||||
|
{
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
services.AddDbContext<ScadaLinkDbContext>(opts =>
|
||||||
|
opts.UseSqlServer(_fixture.ConnectionString)
|
||||||
|
.ConfigureWarnings(w => w.Ignore(
|
||||||
|
Microsoft.EntityFrameworkCore.Diagnostics.RelationalEventId.PendingModelChangesWarning)));
|
||||||
|
services.AddScoped<INotificationOutboxRepository>(sp =>
|
||||||
|
new NotificationOutboxRepository(sp.GetRequiredService<ScadaLinkDbContext>()));
|
||||||
|
services.AddScoped<INotificationRepository>(sp =>
|
||||||
|
new NotificationRepository(sp.GetRequiredService<ScadaLinkDbContext>()));
|
||||||
|
services.AddScoped<INotificationDeliveryAdapter>(_ => adapter);
|
||||||
|
return services.BuildServiceProvider();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SeedSmtpConfigAsync()
|
||||||
|
{
|
||||||
|
await using var ctx = CreateContext();
|
||||||
|
ctx.SmtpConfigurations.Add(new SmtpConfiguration(
|
||||||
|
"smtp.example.com", "Basic", "noreply@example.com")
|
||||||
|
{
|
||||||
|
MaxRetries = 5,
|
||||||
|
RetryDelay = TimeSpan.Zero,
|
||||||
|
});
|
||||||
|
await ctx.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SeedNotificationAsync(Guid notificationId, string siteId)
|
||||||
|
{
|
||||||
|
await using var ctx = CreateContext();
|
||||||
|
ctx.Notifications.Add(new Notification(
|
||||||
|
notificationId.ToString("D"),
|
||||||
|
NotificationType.Email,
|
||||||
|
"ops-team",
|
||||||
|
"Safety subject",
|
||||||
|
"Safety body",
|
||||||
|
siteId)
|
||||||
|
{
|
||||||
|
SourceInstanceId = "Plant.Pump42",
|
||||||
|
SourceScript = "AlarmScript",
|
||||||
|
CreatedAt = DateTimeOffset.UtcNow.AddMinutes(-1),
|
||||||
|
});
|
||||||
|
await ctx.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Single-outcome adapter — returns the same <see cref="DeliveryOutcome"/>
|
||||||
|
/// for every call. Used by the dispatcher safety test where we only need
|
||||||
|
/// one happy-path delivery.
|
||||||
|
/// </summary>
|
||||||
|
private sealed class SingleOutcomeAdapter : INotificationDeliveryAdapter
|
||||||
|
{
|
||||||
|
private readonly DeliveryOutcome _outcome;
|
||||||
|
public SingleOutcomeAdapter(DeliveryOutcome outcome) { _outcome = outcome; }
|
||||||
|
public NotificationType Type => NotificationType.Email;
|
||||||
|
public Task<DeliveryOutcome> DeliverAsync(
|
||||||
|
Notification notification, CancellationToken cancellationToken = default)
|
||||||
|
=> Task.FromResult(_outcome);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Builds an in-memory TestHost mirroring the production inbound-API
|
||||||
|
/// pipeline order. The supplied <paramref name="writer"/> stands in for
|
||||||
|
/// the production <see cref="ICentralAuditWriter"/> so the safety test can
|
||||||
|
/// install the always-throwing variant without standing up any DB.
|
||||||
|
/// </summary>
|
||||||
|
private static async Task<IHost> BuildInboundApiHostAsync(
|
||||||
|
ICentralAuditWriter writer, int endpointStatus)
|
||||||
|
{
|
||||||
|
var hostBuilder = new HostBuilder()
|
||||||
|
.ConfigureWebHost(webBuilder =>
|
||||||
|
{
|
||||||
|
webBuilder
|
||||||
|
.UseTestServer()
|
||||||
|
.ConfigureServices(services =>
|
||||||
|
{
|
||||||
|
services.AddSingleton(writer);
|
||||||
|
services.AddRouting();
|
||||||
|
services.AddAuthorization();
|
||||||
|
services.AddAuthentication("TestScheme")
|
||||||
|
.AddScheme<Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions,
|
||||||
|
AlwaysAuthenticatedHandler>("TestScheme", _ => { });
|
||||||
|
})
|
||||||
|
.Configure(app =>
|
||||||
|
{
|
||||||
|
app.UseRouting();
|
||||||
|
app.UseAuthentication();
|
||||||
|
app.UseAuthorization();
|
||||||
|
app.UseAuditWriteMiddleware();
|
||||||
|
app.UseEndpoints(endpoints =>
|
||||||
|
{
|
||||||
|
endpoints.MapPost("/api/{methodName}", async ctx =>
|
||||||
|
{
|
||||||
|
ctx.Items[AuditWriteMiddleware.AuditActorItemKey] = "safety-actor";
|
||||||
|
ctx.Response.StatusCode = endpointStatus;
|
||||||
|
await ctx.Response.WriteAsync("ok");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return await hostBuilder.StartAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class AlwaysAuthenticatedHandler
|
||||||
|
: Microsoft.AspNetCore.Authentication.AuthenticationHandler<
|
||||||
|
Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions>
|
||||||
|
{
|
||||||
|
public AlwaysAuthenticatedHandler(
|
||||||
|
IOptionsMonitor<Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions> options,
|
||||||
|
Microsoft.Extensions.Logging.ILoggerFactory logger,
|
||||||
|
UrlEncoder encoder)
|
||||||
|
: base(options, logger, encoder) { }
|
||||||
|
|
||||||
|
protected override Task<Microsoft.AspNetCore.Authentication.AuthenticateResult>
|
||||||
|
HandleAuthenticateAsync()
|
||||||
|
{
|
||||||
|
var identity = new ClaimsIdentity(
|
||||||
|
new[] { new Claim(ClaimTypes.Name, "framework-user") }, "TestScheme");
|
||||||
|
var principal = new ClaimsPrincipal(identity);
|
||||||
|
var ticket = new Microsoft.AspNetCore.Authentication.AuthenticationTicket(
|
||||||
|
principal, "TestScheme");
|
||||||
|
return Task.FromResult(
|
||||||
|
Microsoft.AspNetCore.Authentication.AuthenticateResult.Success(ticket));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user