diff --git a/src/Infrastructure/AppRunner.cs b/src/Infrastructure/AppRunner.cs index 8cdd2415..74d5b0ca 100644 --- a/src/Infrastructure/AppRunner.cs +++ b/src/Infrastructure/AppRunner.cs @@ -4,6 +4,9 @@ using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; +using WinHome.Infrastructure.Helpers; // <-- add this line + + namespace WinHome.Infrastructure; /// Orchestrates configuration loading, validation, secret resolution, and engine execution. @@ -36,7 +39,9 @@ public AppRunner(IEngine engine, IConfigValidator validator, ISecretResolver sec public async Task RunAsync(FileInfo configFile, bool dryRun, string? profile, bool debug, bool diff, bool json, bool force = false, bool continueOnError = false) { try + { + AdminGuard.EnsureAdministrator(); if (!configFile.Exists) { _logger.LogError($"[Error] Configuration file not found: {configFile.FullName}"); diff --git a/src/Infrastructure/Helpers/AdminGuard.cs b/src/Infrastructure/Helpers/AdminGuard.cs new file mode 100644 index 00000000..0bb2a062 --- /dev/null +++ b/src/Infrastructure/Helpers/AdminGuard.cs @@ -0,0 +1,39 @@ +using System.Security.Principal; +using System.Runtime.Versioning; + +namespace WinHome.Infrastructure.Helpers; + +/// Validates that the current process has administrative privileges. +[SupportedOSPlatform("windows")] +public static class AdminGuard +{ + internal static Func IsAdministrator = () => + { + using var identity = WindowsIdentity.GetCurrent(); + var principal = new WindowsPrincipal(identity); + return principal.IsInRole(WindowsBuiltInRole.Administrator); + }; + + /// Throws if the current process is not running as Administrator. + /// Thrown when not running with admin privileges. + public static void EnsureAdministrator() + { + if (!IsAdministrator()) + { + throw new UnauthorizedAccessException( + "Error: WinHome requires Administrative Privileges to manage system configurations. " + + "Please re-run this command from an elevated (Administrator) terminal."); + } + } + + /// Resets the administrator check delegate to its default implementation (used for testing). + internal static void ResetAdminCheck() + { + IsAdministrator = () => + { + using var identity = WindowsIdentity.GetCurrent(); + var principal = new WindowsPrincipal(identity); + return principal.IsInRole(WindowsBuiltInRole.Administrator); + }; + } +} \ No newline at end of file diff --git a/tests/WinHome.Tests/AdminGuardTests.cs b/tests/WinHome.Tests/AdminGuardTests.cs new file mode 100644 index 00000000..16793c3f --- /dev/null +++ b/tests/WinHome.Tests/AdminGuardTests.cs @@ -0,0 +1,32 @@ +using WinHome.Infrastructure.Helpers; +using Xunit; + +namespace WinHome.Tests; + +[Collection("SequentialTests")] +public class AdminGuardTests +{ + [Fact] + public void EnsureAdministrator_DoesNotThrow_WhenAdmin() + { + AdminGuard.IsAdministrator = () => true; + var ex = Record.Exception(() => AdminGuard.EnsureAdministrator()); + Assert.Null(ex); + } + + [Fact] + public void EnsureAdministrator_ThrowsUnauthorizedAccessException_WhenNotAdmin() + { + AdminGuard.IsAdministrator = () => false; + var ex = Assert.Throws(() => AdminGuard.EnsureAdministrator()); + Assert.Contains("Administrative Privileges", ex.Message); + } + + [Fact] + public void ResetAdminCheck_RestoresDefaultDelegate() + { + AdminGuard.IsAdministrator = () => false; + AdminGuard.ResetAdminCheck(); + Assert.NotNull(AdminGuard.IsAdministrator); + } +} \ No newline at end of file