feat(dashboard): GroupToTag / UntaggedSessionVisibility config (SEC-25)
Groundwork for the per-session dashboard event ACL (docs/plans/2026-07-10-dashboard-session-acl-tst15.md 3.2): a dashboard group can now grant visibility tags, and untagged sessions default to AdminOnly. Enforcement lands with the EventsHub ACL; nothing consumes the grant yet. GroupToTag is deliberately uncoupled from GroupToRole - a group may appear in either map, both, or neither - and is validated for shape only. Tags gate dashboard event visibility, never data access.
This commit is contained in:
@@ -884,6 +884,144 @@ public sealed class GatewayOptionsValidatorTests
|
||||
Assert.True(result.Succeeded);
|
||||
}
|
||||
|
||||
/// <summary>Verifies a populated GroupToTag map with well-formed tags passes validation.</summary>
|
||||
[Fact]
|
||||
public void Validate_Succeeds_WhenGroupToTagWellFormed()
|
||||
{
|
||||
GatewayOptions options = CloneWithDashboard(
|
||||
ValidOptions(),
|
||||
new DashboardOptions
|
||||
{
|
||||
GroupToTag = new Dictionary<string, string[]>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["GwViewer"] = ["team-a"],
|
||||
["TeamBViewers"] = ["team-b", "team-c"],
|
||||
},
|
||||
});
|
||||
ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, options);
|
||||
Assert.True(result.Succeeded);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies GroupToTag is not coupled to GroupToRole: a group that grants a tag
|
||||
/// but no role (and vice versa) is a legal configuration.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Validate_Succeeds_WhenGroupToTagAndGroupToRoleShareNoGroups()
|
||||
{
|
||||
GatewayOptions options = CloneWithDashboard(
|
||||
ValidOptions(),
|
||||
new DashboardOptions
|
||||
{
|
||||
GroupToRole = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["GwAdmin"] = "Administrator",
|
||||
},
|
||||
GroupToTag = new Dictionary<string, string[]>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["TeamBViewers"] = ["team-b"],
|
||||
},
|
||||
});
|
||||
ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, options);
|
||||
Assert.True(result.Succeeded);
|
||||
}
|
||||
|
||||
/// <summary>Verifies a blank GroupToTag key (LDAP group name) fails validation.</summary>
|
||||
[Fact]
|
||||
public void Validate_Fails_WhenGroupToTagKeyIsBlank()
|
||||
{
|
||||
GatewayOptions options = CloneWithDashboard(
|
||||
ValidOptions(),
|
||||
new DashboardOptions
|
||||
{
|
||||
GroupToTag = new Dictionary<string, string[]>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
[" "] = ["team-a"],
|
||||
},
|
||||
});
|
||||
ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, options);
|
||||
Assert.True(result.Failed);
|
||||
Assert.Contains(
|
||||
result.Failures!,
|
||||
f => f.Contains("MxGateway:Dashboard:GroupToTag") && f.Contains("non-blank"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies a blank tag entry fails validation.</summary>
|
||||
[Fact]
|
||||
public void Validate_Fails_WhenGroupToTagContainsBlankTag()
|
||||
{
|
||||
GatewayOptions options = CloneWithDashboard(
|
||||
ValidOptions(),
|
||||
new DashboardOptions
|
||||
{
|
||||
GroupToTag = new Dictionary<string, string[]>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["GwViewer"] = ["team-a", " "],
|
||||
},
|
||||
});
|
||||
ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, options);
|
||||
Assert.True(result.Failed);
|
||||
Assert.Contains(
|
||||
result.Failures!,
|
||||
f => f.Contains("MxGateway:Dashboard:GroupToTag['GwViewer']") && f.Contains("non-blank"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies a null tag list (e.g. <c>"GwViewer": null</c> in JSON) fails validation.</summary>
|
||||
[Fact]
|
||||
public void Validate_Fails_WhenGroupToTagValueIsNull()
|
||||
{
|
||||
GatewayOptions options = CloneWithDashboard(
|
||||
ValidOptions(),
|
||||
new DashboardOptions
|
||||
{
|
||||
GroupToTag = new Dictionary<string, string[]>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["GwViewer"] = null!,
|
||||
},
|
||||
});
|
||||
ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, options);
|
||||
Assert.True(result.Failed);
|
||||
Assert.Contains(
|
||||
result.Failures!,
|
||||
f => f.Contains("MxGateway:Dashboard:GroupToTag['GwViewer']") && f.Contains("null"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies both defined <see cref="UntaggedSessionVisibility"/> values pass validation.</summary>
|
||||
/// <param name="visibility">The visibility value under test.</param>
|
||||
[Theory]
|
||||
[InlineData(UntaggedSessionVisibility.AdminOnly)]
|
||||
[InlineData(UntaggedSessionVisibility.AllViewers)]
|
||||
public void Validate_Succeeds_ForDefinedUntaggedSessionVisibility(UntaggedSessionVisibility visibility)
|
||||
{
|
||||
GatewayOptions options = CloneWithDashboard(
|
||||
ValidOptions(),
|
||||
new DashboardOptions { UntaggedSessionVisibility = visibility });
|
||||
ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, options);
|
||||
Assert.True(result.Succeeded);
|
||||
}
|
||||
|
||||
/// <summary>Verifies an out-of-range <see cref="UntaggedSessionVisibility"/> fails validation.</summary>
|
||||
[Fact]
|
||||
public void Validate_Fails_WhenUntaggedSessionVisibilityUndefined()
|
||||
{
|
||||
GatewayOptions options = CloneWithDashboard(
|
||||
ValidOptions(),
|
||||
new DashboardOptions { UntaggedSessionVisibility = (UntaggedSessionVisibility)42 });
|
||||
ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, options);
|
||||
Assert.True(result.Failed);
|
||||
Assert.Contains(
|
||||
result.Failures!,
|
||||
f => f.Contains("MxGateway:Dashboard:UntaggedSessionVisibility"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies the shipped default for untagged sessions is the strict AdminOnly.</summary>
|
||||
[Fact]
|
||||
public void DashboardOptions_UntaggedSessionVisibility_DefaultsToAdminOnly()
|
||||
{
|
||||
Assert.Equal(UntaggedSessionVisibility.AdminOnly, new DashboardOptions().UntaggedSessionVisibility);
|
||||
Assert.Empty(new DashboardOptions().GroupToTag);
|
||||
}
|
||||
|
||||
/// <summary>Verifies plaintext LDAP transport (None) aborts startup in Production.</summary>
|
||||
[Fact]
|
||||
public void Validate_Fails_WhenLdapTransportNoneInProduction()
|
||||
|
||||
Reference in New Issue
Block a user