fix(mesh): stop reporting override-keyed clusters as REVOKED (lmxopcua#784)
The docker-dev centrals logged "Mesh PSK secret ot-mesh-psk-<cluster> ... is absent (or blank) in the store; the key is REVOKED" for every cluster on every contact refresh, while every gRPC dispatch and ack authenticated. Investigated: not an auth bypass. The rig supplies each cluster's key through MeshTransport:ClusterPsks (central side, equal to that cluster's nodes' MeshTransport:GrpcPsk) and seeds nothing in the store. TryGetPsk/Matches consult the override first on every call, so enforcement was on; only RefreshAsync read the store for those clusters regardless and logged a revocation that never took effect. RefreshAsync now skips clusters covered by a non-blank override, so the REVOKED warning is only logged for a cluster that genuinely has no key. Test pins it with a positive control (a cluster with no override is still read, warned about and refused) and enforcement on the override key (wrong token rejected). Mutation-checked: removing the skip fails the test. Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
+47
@@ -1,3 +1,4 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Shouldly;
|
||||
@@ -129,6 +130,52 @@ public sealed class MeshPskProviderRefreshTests
|
||||
foreach (var c in clusters) provider.TryGetPsk(c, out _).ShouldBeTrue();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// lmxopcua#784 — the docker-dev centrals supply every cluster's key through
|
||||
/// <c>MeshTransport:ClusterPsks</c> and seed nothing in the store, and logged "absent in the store; the
|
||||
/// key is REVOKED — commands … refused" for all three clusters on every contact refresh while every
|
||||
/// dispatch and ack authenticated. The override wins on every lookup, so the key was never revoked: the
|
||||
/// warning was false. The refresh now leaves override-covered clusters alone. SITE-B (no override) is the
|
||||
/// positive control — a cluster with no key anywhere still gets the REVOKED warning and is refused.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task A_cluster_keyed_by_the_config_override_is_not_reported_revoked_and_stays_authenticated()
|
||||
{
|
||||
var resolver = new GatedResolver { Value = null }; // the store holds nothing, as on the rig
|
||||
var options = new MeshTransportOptions();
|
||||
options.ClusterPsks["SITE-A"] = "rig-psk-site-a";
|
||||
var logger = new RecordingLogger();
|
||||
var provider = new MeshPskProvider(Options.Create(options), resolver, logger);
|
||||
|
||||
await provider.RefreshAsync(new[] { "site-a", "SITE-B" }, CancellationToken.None);
|
||||
|
||||
resolver.Calls.ShouldBe(1, "only SITE-B, which has no override, is read from the store");
|
||||
var revoked = logger.Entries.Where(e => e.Level == LogLevel.Warning && e.Message.Contains("REVOKED")).ToList();
|
||||
revoked.Count.ShouldBe(1);
|
||||
revoked[0].Message.ShouldContain("SITE-B");
|
||||
|
||||
// Enforcement is real for both: SITE-A authenticates against the override key and nothing else,
|
||||
// SITE-B (no key anywhere) is refused.
|
||||
provider.Matches("SITE-A", "rig-psk-site-a").ShouldBeTrue();
|
||||
provider.Matches("SITE-A", "wrong").ShouldBeFalse();
|
||||
provider.TryGetPsk("SITE-B", out _).ShouldBeFalse();
|
||||
}
|
||||
|
||||
private sealed class RecordingLogger : ILogger<MeshPskProvider>
|
||||
{
|
||||
public List<(LogLevel Level, string Message)> Entries { get; } = new();
|
||||
|
||||
public IDisposable? BeginScope<TState>(TState state) where TState : notnull => null;
|
||||
|
||||
public bool IsEnabled(LogLevel logLevel) => true;
|
||||
|
||||
public void Log<TState>(
|
||||
LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
|
||||
{
|
||||
lock (Entries) Entries.Add((logLevel, formatter(state, exception)));
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class ConcurrencyTrackingResolver : ISecretResolver
|
||||
{
|
||||
private int _inFlight;
|
||||
|
||||
Reference in New Issue
Block a user