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:
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user