fix(core): resolve Medium code-review finding (Core-003)

Add FolderSegment member to NodeAclScopeKind; update WalkSystemPlatform
to report NodeAclScopeKind.FolderSegment (not Equipment) for each
visited Galaxy folder level, so MatchedGrant.Scope in
AuthorizationDecision.Provenance correctly distinguishes Galaxy folder
grants from UNS Equipment grants in the audit trail and Admin UI
diagnostics.  Three regression tests added to PermissionTrieTests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-05-22 08:23:45 -04:00
parent a0b3a4c8a7
commit 09cd579220
3 changed files with 60 additions and 6 deletions

View File

@@ -125,6 +125,55 @@ public sealed class PermissionTrieTests
matchB.ShouldBeEmpty();
}
/// <summary>
/// Core-003 regression: grants matched during the SystemPlatform (Galaxy) folder walk must
/// report <see cref="NodeAclScopeKind.FolderSegment"/> in <see cref="MatchedGrant.Scope"/>,
/// not <see cref="NodeAclScopeKind.Equipment"/>. This distinguishes Galaxy folder grants
/// from UNS Equipment grants in the audit trail and Admin UI "Probe this permission" panel.
/// </summary>
[Fact]
public void Galaxy_FolderSegment_Grant_Reports_FolderSegment_Scope_Not_Equipment()
{
var paths = new Dictionary<string, NodeAclPath>(StringComparer.OrdinalIgnoreCase)
{
["section-X"] = new(new[] { "ns-gal", "section-X" }),
};
var rows = new[] { Row("cn=ops", NodeAclScopeKind.Equipment, "section-X", NodePermissions.Read) };
var trie = PermissionTrieBuilder.Build("c1", 1, rows, paths);
var matches = trie.CollectMatches(GalaxyTag("c1", "ns-gal", ["section-X"], "tag1"), ["cn=ops"]);
matches.Count.ShouldBe(1);
matches[0].Scope.ShouldBe(NodeAclScopeKind.FolderSegment,
"the trie walk reports the structural level where the grant was found — FolderSegment for Galaxy, not Equipment");
}
[Fact]
public void Galaxy_DeepFolderPath_AllSegments_Report_FolderSegment_Scope()
{
// A three-level folder path — each visited level that carries a grant must report FolderSegment.
var paths = new Dictionary<string, NodeAclPath>(StringComparer.OrdinalIgnoreCase)
{
["area"] = new(new[] { "ns-gal", "area" }),
["area/line"] = new(new[] { "ns-gal", "area", "line" }),
["area/line/cell"] = new(new[] { "ns-gal", "area", "line", "cell" }),
};
var rows = new[]
{
Row("cn=ops", NodeAclScopeKind.Equipment, "area", NodePermissions.Read),
Row("cn=ops", NodeAclScopeKind.Equipment, "area/line", NodePermissions.Read),
Row("cn=ops", NodeAclScopeKind.Equipment, "area/line/cell", NodePermissions.Read),
};
var trie = PermissionTrieBuilder.Build("c1", 1, rows, paths);
var matches = trie.CollectMatches(
GalaxyTag("c1", "ns-gal", ["area", "line", "cell"], "tag1"), ["cn=ops"]);
matches.Count.ShouldBe(3, "one match per folder level visited");
matches.ShouldAllBe(m => m.Scope == NodeAclScopeKind.FolderSegment,
"every matched folder level must report FolderSegment, never Equipment");
}
[Fact]
public void CrossCluster_Grant_DoesNotLeak()
{