test(mesh): pin the admin-only role scoping of IMeshPskProvider/IMeshCommandDialer registrations (security register item 3)
This commit is contained in:
+169
@@ -0,0 +1,169 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Host.Tests.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Security register item 3 (DI role-scoping pin). <c>IMeshPskProvider</c> and
|
||||
/// <c>IMeshCommandDialer</c> are central's per-cluster key lookup + outbound command dialer —
|
||||
/// a driver-only node holds exactly its own <c>MeshTransport:GrpcPsk</c> and never dials
|
||||
/// another node, so these two singletons must be registered ONLY inside one of Program.cs's
|
||||
/// <c>if (hasAdmin)</c> blocks. Nothing in the language enforces that placement; this is a
|
||||
/// source scan so a refactor that hoists either registration out of its block fails loudly
|
||||
/// here instead of silently widening the mesh trust boundary to driver-only nodes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Program.cs gates several unrelated concerns behind repeated <c>if (hasAdmin)</c> checks
|
||||
/// (auth/AdminUI mounting, health probes, gRPC endpoint mapping, ...), so this pin cannot
|
||||
/// assume there is only one such block in the file — it must find the SPECIFIC block that
|
||||
/// contains each registration and prove that containment is unambiguous.
|
||||
/// </remarks>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class MeshRegistrationRoleScopingTests
|
||||
{
|
||||
private const string PskRegistration =
|
||||
"AddSingleton<IMeshPskProvider, MeshPskProvider>";
|
||||
|
||||
private const string DialerRegistration =
|
||||
"AddSingleton<IMeshCommandDialer, MeshGrpcCommandDialer>";
|
||||
|
||||
private const string HasAdminMarker = "if (hasAdmin)";
|
||||
|
||||
private static string ReadProgramSource()
|
||||
{
|
||||
var dir = new DirectoryInfo(AppContext.BaseDirectory);
|
||||
while (dir is not null && !File.Exists(Path.Combine(dir.FullName, "ZB.MOM.WW.OtOpcUa.slnx")))
|
||||
dir = dir.Parent;
|
||||
dir.ShouldNotBeNull();
|
||||
|
||||
return File.ReadAllText(Path.Combine(
|
||||
dir!.FullName, "src", "Server", "ZB.MOM.WW.OtOpcUa.Host", "Program.cs"));
|
||||
}
|
||||
|
||||
private static int CountOccurrences(string haystack, string needle)
|
||||
{
|
||||
var count = 0;
|
||||
var index = 0;
|
||||
while ((index = haystack.IndexOf(needle, index, StringComparison.Ordinal)) >= 0)
|
||||
{
|
||||
count++;
|
||||
index += needle.Length;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Brace-matches every <c>if (hasAdmin)</c> block in the file (not a regex over the block
|
||||
/// body — the body is free-form C# and a regex would be fragile to reformatting) and
|
||||
/// returns each block's (open-brace, close-brace) character range.
|
||||
/// </summary>
|
||||
private static List<(int Start, int End)> FindHasAdminBlockRanges(string source)
|
||||
{
|
||||
var ranges = new List<(int Start, int End)>();
|
||||
var searchIndex = 0;
|
||||
while (true)
|
||||
{
|
||||
var markerIndex = source.IndexOf(HasAdminMarker, searchIndex, StringComparison.Ordinal);
|
||||
if (markerIndex < 0)
|
||||
break;
|
||||
|
||||
var openBraceIndex = source.IndexOf('{', markerIndex + HasAdminMarker.Length);
|
||||
openBraceIndex.ShouldBeGreaterThan(-1,
|
||||
$"'{HasAdminMarker}' at offset {markerIndex} is not followed by an opening brace.");
|
||||
|
||||
var depth = 0;
|
||||
var closeBraceIndex = -1;
|
||||
for (var i = openBraceIndex; i < source.Length; i++)
|
||||
{
|
||||
if (source[i] == '{')
|
||||
depth++;
|
||||
else if (source[i] == '}')
|
||||
{
|
||||
depth--;
|
||||
if (depth == 0)
|
||||
{
|
||||
closeBraceIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
closeBraceIndex.ShouldBeGreaterThan(-1,
|
||||
$"brace-matching for the '{HasAdminMarker}' block starting at offset {markerIndex} never closed.");
|
||||
|
||||
ranges.Add((openBraceIndex, closeBraceIndex));
|
||||
searchIndex = closeBraceIndex + 1;
|
||||
}
|
||||
|
||||
return ranges;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Locates the single <c>if (hasAdmin)</c> block that contains <paramref name="registration"/>.
|
||||
/// Fails loudly — rather than silently picking the first match — if the registration is
|
||||
/// missing entirely, sits outside every admin-role block, or (should a future edit make two
|
||||
/// blocks both contain matching text) the containing block is ambiguous. Any of those is a
|
||||
/// reason to stop and re-examine the pin, not to guess.
|
||||
/// </summary>
|
||||
private static string ExtractContainingHasAdminBlock(string source, string registration, string whyItMatters)
|
||||
{
|
||||
var ranges = FindHasAdminBlockRanges(source);
|
||||
ranges.ShouldNotBeEmpty(
|
||||
$"Program.cs no longer contains any '{HasAdminMarker}' block — the admin-role gate this " +
|
||||
$"pin relies on is gone. {whyItMatters}");
|
||||
|
||||
var registrationIndex = source.IndexOf(registration, StringComparison.Ordinal);
|
||||
registrationIndex.ShouldBeGreaterThan(-1,
|
||||
$"'{registration}' was not found anywhere in Program.cs. {whyItMatters}");
|
||||
|
||||
var containingBlocks = ranges
|
||||
.Where(r => registrationIndex >= r.Start && registrationIndex <= r.End)
|
||||
.ToList();
|
||||
|
||||
containingBlocks.ShouldNotBeEmpty(
|
||||
$"'{registration}' is registered OUTSIDE every '{HasAdminMarker}' block. {whyItMatters}");
|
||||
|
||||
containingBlocks.Count.ShouldBe(1,
|
||||
$"'{registration}' falls inside {containingBlocks.Count} '{HasAdminMarker}' blocks, so " +
|
||||
"which one actually gates it is ambiguous — extend this pin to disambiguate rather than " +
|
||||
$"trusting the first match. {whyItMatters}");
|
||||
|
||||
var (start, end) = containingBlocks[0];
|
||||
return source.Substring(start, end - start + 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MeshPskProvider_IsRegisteredExactlyOnce_InsideAnUnambiguousHasAdminBlock()
|
||||
{
|
||||
const string why =
|
||||
"A driver-only node must not carry central's per-cluster key lookup, which it can never " +
|
||||
"legitimately use.";
|
||||
var source = ReadProgramSource();
|
||||
|
||||
// Global uniqueness first: if this text appeared twice, block-containment below could pass
|
||||
// even though a second, unguarded copy exists elsewhere in the file.
|
||||
CountOccurrences(source, PskRegistration).ShouldBe(1,
|
||||
$"IMeshPskProvider must be registered exactly once in Program.cs. {why}");
|
||||
|
||||
var block = ExtractContainingHasAdminBlock(source, PskRegistration, why);
|
||||
block.ShouldContain(PskRegistration, Case.Sensitive,
|
||||
$"IMeshPskProvider must be registered inside 'if (hasAdmin)'. {why}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MeshGrpcCommandDialer_IsRegisteredExactlyOnce_InsideAnUnambiguousHasAdminBlock()
|
||||
{
|
||||
const string why =
|
||||
"A driver-only node never dials another node and must not carry central's outbound mesh " +
|
||||
"command dialer.";
|
||||
var source = ReadProgramSource();
|
||||
|
||||
CountOccurrences(source, DialerRegistration).ShouldBe(1,
|
||||
$"IMeshCommandDialer must be registered exactly once in Program.cs. {why}");
|
||||
|
||||
var block = ExtractContainingHasAdminBlock(source, DialerRegistration, why);
|
||||
block.ShouldContain(DialerRegistration, Case.Sensitive,
|
||||
$"IMeshCommandDialer must be registered inside 'if (hasAdmin)'. {why}");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user