test(management): frozen GetRequiredRoles matrix over every registered command (arch-review UA2)

Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
This commit is contained in:
Joseph Doherty
2026-07-10 05:36:32 -04:00
parent 8d4ffa7ef1
commit 61df8a28fe
@@ -0,0 +1,208 @@
using System.Runtime.CompilerServices;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
using ZB.MOM.WW.ScadaBridge.Security;
namespace ZB.MOM.WW.ScadaBridge.ManagementService.Tests;
/// <summary>
/// Frozen authorization matrix (arch-review UA2). Every registered management command
/// MUST have an entry in <see cref="Expected"/> giving its required-roles set (any-of
/// semantics) or <c>null</c> when the command is available to any authenticated user.
/// A newly-added command with no entry fails <see cref="EveryRegisteredCommand_IsInTheFrozenTable"/>,
/// forcing a deliberate authorization decision instead of silently defaulting to the
/// read-only (any-authenticated) gate — the class of gap that produced C2/C4.
///
/// The table reflects ACTUAL current behavior of
/// <c>ManagementActor.GetRequiredRoles</c>; it is authored by hand from the switch and
/// cross-checked against <c>docs/requirements/Component-ManagementService.md</c>
/// §Authorization. Do not regenerate it mechanically from the source under test — that
/// would defeat the freeze.
/// </summary>
public class RequiredRoleMatrixTests
{
private static readonly string[] AreaManagers = [Roles.Designer, Roles.Deployer];
private static readonly string[] SecuredWriteReaders = [Roles.Operator, Roles.Verifier, Roles.Administrator];
private static readonly Dictionary<string, string[]?> Expected = new(StringComparer.OrdinalIgnoreCase)
{
// ---- Administrator-only -----------------------------------------------------
["CreateSite"] = [Roles.Administrator],
["UpdateSite"] = [Roles.Administrator],
["DeleteSite"] = [Roles.Administrator],
["ListRoleMappings"] = [Roles.Administrator],
["CreateRoleMapping"] = [Roles.Administrator],
["UpdateRoleMapping"] = [Roles.Administrator],
["DeleteRoleMapping"] = [Roles.Administrator],
["ListApiKeys"] = [Roles.Administrator],
["CreateApiKey"] = [Roles.Administrator],
["DeleteApiKey"] = [Roles.Administrator],
["UpdateApiKey"] = [Roles.Administrator],
["SetApiKeyMethods"] = [Roles.Administrator],
["ListScopeRules"] = [Roles.Administrator],
["AddScopeRule"] = [Roles.Administrator],
["DeleteScopeRule"] = [Roles.Administrator],
["QueryAuditLog"] = [Roles.Administrator],
["UpdateSmtpConfig"] = [Roles.Administrator],
["UpdateSmsConfig"] = [Roles.Administrator],
["TrustServerCert"] = [Roles.Administrator],
["RemoveServerCert"] = [Roles.Administrator],
["ListServerCerts"] = [Roles.Administrator],
// Transport inbound bundle handling mutates cross-cutting config → Admin.
["PreviewBundle"] = [Roles.Administrator],
["ImportBundle"] = [Roles.Administrator],
// ---- Area management: any-of [Designer, Deployer] ---------------------------
["CreateArea"] = AreaManagers,
["UpdateArea"] = AreaManagers,
["DeleteArea"] = AreaManagers,
// ---- Designer-only ----------------------------------------------------------
["CreateTemplate"] = [Roles.Designer],
["UpdateTemplate"] = [Roles.Designer],
["DeleteTemplate"] = [Roles.Designer],
["ValidateTemplate"] = [Roles.Designer],
["CreateExternalSystem"] = [Roles.Designer],
["UpdateExternalSystem"] = [Roles.Designer],
["DeleteExternalSystem"] = [Roles.Designer],
["CreateExternalSystemMethod"] = [Roles.Designer],
["UpdateExternalSystemMethod"] = [Roles.Designer],
["DeleteExternalSystemMethod"] = [Roles.Designer],
["CreateNotificationList"] = [Roles.Designer],
["UpdateNotificationList"] = [Roles.Designer],
["DeleteNotificationList"] = [Roles.Designer],
["CreateDataConnection"] = [Roles.Designer],
["UpdateDataConnection"] = [Roles.Designer],
["DeleteDataConnection"] = [Roles.Designer],
["MoveDataConnection"] = [Roles.Designer],
["AddTemplateAttribute"] = [Roles.Designer],
["UpdateTemplateAttribute"] = [Roles.Designer],
["DeleteTemplateAttribute"] = [Roles.Designer],
["AddTemplateAlarm"] = [Roles.Designer],
["UpdateTemplateAlarm"] = [Roles.Designer],
["DeleteTemplateAlarm"] = [Roles.Designer],
["AddTemplateNativeAlarmSource"] = [Roles.Designer],
["UpdateTemplateNativeAlarmSource"] = [Roles.Designer],
["DeleteTemplateNativeAlarmSource"] = [Roles.Designer],
["AddTemplateScript"] = [Roles.Designer],
["UpdateTemplateScript"] = [Roles.Designer],
["DeleteTemplateScript"] = [Roles.Designer],
["AddTemplateComposition"] = [Roles.Designer],
["DeleteTemplateComposition"] = [Roles.Designer],
["ResyncInheritedMembers"] = [Roles.Designer],
["CreateSharedScript"] = [Roles.Designer],
["UpdateSharedScript"] = [Roles.Designer],
["DeleteSharedScript"] = [Roles.Designer],
["CreateSharedSchema"] = [Roles.Designer],
["UpdateSharedSchema"] = [Roles.Designer],
["DeleteSharedSchema"] = [Roles.Designer],
["CreateDatabaseConnectionDef"] = [Roles.Designer],
["UpdateDatabaseConnectionDef"] = [Roles.Designer],
["DeleteDatabaseConnectionDef"] = [Roles.Designer],
["CreateApiMethod"] = [Roles.Designer],
["UpdateApiMethod"] = [Roles.Designer],
["DeleteApiMethod"] = [Roles.Designer],
["CreateTemplateFolder"] = [Roles.Designer],
["RenameTemplateFolder"] = [Roles.Designer],
["MoveTemplateFolder"] = [Roles.Designer],
["ReorderTemplateFolder"] = [Roles.Designer],
["DeleteTemplateFolder"] = [Roles.Designer],
["MoveTemplateToFolder"] = [Roles.Designer],
["BrowseNode"] = [Roles.Designer],
["SearchAddressSpace"] = [Roles.Designer],
["VerifyEndpoint"] = [Roles.Designer],
["ExportBundle"] = [Roles.Designer],
// ---- Deployer-only ----------------------------------------------------------
["CreateInstance"] = [Roles.Deployer],
["MgmtDeployInstance"] = [Roles.Deployer],
["MgmtEnableInstance"] = [Roles.Deployer],
["MgmtDisableInstance"] = [Roles.Deployer],
["MgmtDeleteInstance"] = [Roles.Deployer],
["SetConnectionBindings"] = [Roles.Deployer],
["SetInstanceOverrides"] = [Roles.Deployer],
["SetInstanceArea"] = [Roles.Deployer],
["SetInstanceAlarmOverride"] = [Roles.Deployer],
["DeleteInstanceAlarmOverride"] = [Roles.Deployer],
["SetInstanceNativeAlarmSourceOverride"] = [Roles.Deployer],
["DeleteInstanceNativeAlarmSourceOverride"] = [Roles.Deployer],
["GetDeploymentDiff"] = [Roles.Deployer],
["MgmtDeployArtifacts"] = [Roles.Deployer],
["QueryDeployments"] = [Roles.Deployer],
["RetryParkedMessage"] = [Roles.Deployer],
["DiscardParkedMessage"] = [Roles.Deployer],
["DebugSnapshot"] = [Roles.Deployer],
// ---- Two-person secured write ----------------------------------------------
["SubmitSecuredWrite"] = [Roles.Operator],
["ApproveSecuredWrite"] = [Roles.Verifier],
["RejectSecuredWrite"] = [Roles.Verifier],
["ListSecuredWrites"] = SecuredWriteReaders,
// ---- Read-only: any authenticated user (null gate) -------------------------
["ListTemplates"] = null,
["GetTemplate"] = null,
["GetResolvedTemplateMembers"] = null,
["ListTemplateFolders"] = null,
["ListTemplateNativeAlarmSources"] = null,
["ListInstances"] = null,
["GetInstance"] = null,
["ListInstanceAlarmOverrides"] = null,
["ListInstanceNativeAlarmSourceOverrides"] = null,
["ListSites"] = null,
["GetSite"] = null,
["ListAreas"] = null,
["ListDataConnections"] = null,
["GetDataConnection"] = null,
["ListExternalSystems"] = null,
["GetExternalSystem"] = null,
["ListExternalSystemMethods"] = null,
["GetExternalSystemMethod"] = null,
["ListNotificationLists"] = null,
["GetNotificationList"] = null,
["ListSmtpConfigs"] = null,
["ListSmsConfigs"] = null,
["ListSharedScripts"] = null,
["GetSharedScript"] = null,
["ListSharedSchemas"] = null,
["GetSharedSchema"] = null,
["ListDatabaseConnections"] = null,
["GetDatabaseConnection"] = null,
["ListApiMethods"] = null,
["GetApiMethod"] = null,
["GetHealthSummary"] = null,
["GetSiteHealth"] = null,
["QueryEventLogs"] = null,
["QueryParkedMessages"] = null,
["ReadTagValues"] = null,
// ResolveRoles is intentionally not dispatched (retired two-step flow); it
// falls through to the default null gate. Present here so the frozen table
// still covers the type reflected in the assembly.
["ResolveRoles"] = null,
};
public static IEnumerable<object[]> AllRegisteredCommands()
=> typeof(ManagementEnvelope).Assembly.GetTypes()
.Where(t => t.Namespace == typeof(ManagementEnvelope).Namespace
&& t.Name.EndsWith("Command", StringComparison.Ordinal) && !t.IsAbstract)
.Select(t => new object[] { t });
[Theory, MemberData(nameof(AllRegisteredCommands))]
public void EveryRegisteredCommand_IsInTheFrozenTable(Type commandType)
{
var name = commandType.Name[..^"Command".Length];
Assert.True(Expected.ContainsKey(name),
$"Command '{name}' has no entry in the frozen authorization table — add one deliberately.");
var instance = RuntimeHelpers.GetUninitializedObject(commandType);
var actual = ManagementActor.GetRequiredRoles(instance);
var expected = Expected[name];
if (expected is null)
{
Assert.Null(actual);
}
else
{
Assert.NotNull(actual);
Assert.Equal(expected.OrderBy(x => x), actual!.OrderBy(x => x));
}
}
}