fix(archreview-p1): SEC-02/12/20 dashboard + observability hardening
- SEC-02: DashboardAuthorizationHandler restricts the loopback and Authentication:Mode=Disabled bypasses to read-only. They now satisfy only a Viewer-bearing requirement (AnyDashboardRole), never AdminOnly, so anonymous localhost can view the dashboard but cannot reach API-key CRUD or session Close/Kill at the policy layer (previously guarded only by service re-checks). - SEC-12: DashboardSessionAdminService emits canonical AuditEvents through IAuditWriter (actions dashboard-close-session / dashboard-kill-worker, category SessionAdmin) on Success/Failure/Denied, mirroring the API-key audit path so destructive session actions leave durable, queryable rows. - SEC-20: drop the unbounded session_id tag from the exported mxgateway.heartbeats.failed counter (per-session detail stays in the snapshot/log). Docs updated same-change: CLAUDE.md (read-only loopback + 5-min bearer), GatewayDashboardDesign.md (bypass scoping + session-admin audit), Metrics.md. Server build clean; 30/30 targeted + 295/295 Dashboard/Security/App/Metrics sweep.
This commit is contained in:
@@ -157,6 +157,56 @@ public sealed class GatewayMetricsTests
|
||||
Assert.Equal(2, capturedMode);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that <see cref="GatewayMetrics.HeartbeatFailed"/> increments
|
||||
/// <c>mxgateway.heartbeats.failed</c> without emitting a <c>session_id</c> tag: the tag is
|
||||
/// unbounded cardinality (every session mints a new exporter time series), so per-session
|
||||
/// attribution is deliberately kept out of the exported counter (SEC-20).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void HeartbeatFailed_IncrementsCounterWithoutSessionIdTag()
|
||||
{
|
||||
using GatewayMetrics metrics = new();
|
||||
using MeterListener listener = new();
|
||||
|
||||
long capturedValue = 0;
|
||||
bool sawSessionIdTag = false;
|
||||
|
||||
listener.InstrumentPublished = (instrument, meterListener) =>
|
||||
{
|
||||
if (ReferenceEquals(instrument.Meter, metrics.Meter)
|
||||
&& instrument.Name == "mxgateway.heartbeats.failed")
|
||||
{
|
||||
meterListener.EnableMeasurementEvents(instrument);
|
||||
}
|
||||
};
|
||||
listener.SetMeasurementEventCallback<long>(
|
||||
(instrument, measurement, tags, _) =>
|
||||
{
|
||||
if (!ReferenceEquals(instrument.Meter, metrics.Meter)
|
||||
|| instrument.Name != "mxgateway.heartbeats.failed")
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
capturedValue += measurement;
|
||||
foreach (KeyValuePair<string, object?> tag in tags)
|
||||
{
|
||||
if (tag.Key == "session_id")
|
||||
{
|
||||
sawSessionIdTag = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
listener.Start();
|
||||
|
||||
metrics.HeartbeatFailed("session-1");
|
||||
|
||||
Assert.Equal(1, capturedValue);
|
||||
Assert.False(sawSessionIdTag);
|
||||
Assert.Equal(1, metrics.GetSnapshot().HeartbeatFailures);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that removing session events only affects that session.</summary>
|
||||
[Fact]
|
||||
public void RemoveSessionEvents_RemovesOnlyThatSession()
|
||||
|
||||
Reference in New Issue
Block a user