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; /// /// Frozen authorization matrix (arch-review UA2). Every registered management command /// MUST have an entry in giving its required-roles set (any-of /// semantics) or null when the command is available to any authenticated user. /// A newly-added command with no entry fails , /// 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 /// ManagementActor.GetRequiredRoles; it is authored by hand from the switch and /// cross-checked against docs/requirements/Component-ManagementService.md /// §Authorization. Do not regenerate it mechanically from the source under test — that /// would defeat the freeze. /// 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 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], ["SetInstanceNativeAlarmSourceOverrides"] = [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 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)); } } }