From 6c2d4a1e70b668910d548648fc71f219c6506b2f Mon Sep 17 00:00:00 2001 From: Alexandru Macocian Date: Sat, 24 Apr 2021 01:20:36 +0200 Subject: [PATCH] No longer require administrator rights for normal usage. Request elevation only in necesarry parts of the code. Request elevation on update. After update, launch application without elevation. --- .../Configuration/ProjectConfiguration.cs | 3 ++ Daybreak/Daybreak.csproj | 3 +- Daybreak/Models/ElevationRequest.cs | 11 ++++ .../ApplicationLauncher.cs | 41 ++++++++++++++- .../IApplicationLauncher.cs | 1 + Daybreak/Services/Navigation/IViewManager.cs | 7 ++- Daybreak/Services/Navigation/ViewManager.cs | 31 ++++++++--- .../Services/Privilege/IPrivilegeManager.cs | 12 +++++ .../Services/Privilege/PrivilegeManager.cs | 34 +++++++++++++ .../Services/Updater/ApplicationUpdater.cs | 24 ++++++--- Daybreak/Views/AskUpdateView.xaml.cs | 22 +++++++- Daybreak/Views/RequestElevationView.xaml | 28 ++++++++++ Daybreak/Views/RequestElevationView.xaml.cs | 51 +++++++++++++++++++ 13 files changed, 250 insertions(+), 18 deletions(-) create mode 100644 Daybreak/Models/ElevationRequest.cs create mode 100644 Daybreak/Services/Privilege/IPrivilegeManager.cs create mode 100644 Daybreak/Services/Privilege/PrivilegeManager.cs create mode 100644 Daybreak/Views/RequestElevationView.xaml create mode 100644 Daybreak/Views/RequestElevationView.xaml.cs diff --git a/Daybreak/Configuration/ProjectConfiguration.cs b/Daybreak/Configuration/ProjectConfiguration.cs index dc4eb2ca..fd0bb37d 100644 --- a/Daybreak/Configuration/ProjectConfiguration.cs +++ b/Daybreak/Configuration/ProjectConfiguration.cs @@ -7,6 +7,7 @@ using Daybreak.Services.IconRetrieve; using Daybreak.Services.Logging; using Daybreak.Services.Mutex; +using Daybreak.Services.Privilege; using Daybreak.Services.Runtime; using Daybreak.Services.Screenshots; using Daybreak.Services.Updater; @@ -37,6 +38,7 @@ public static void RegisterServices(IServiceProducer serviceProducer) serviceProducer.RegisterSingleton(); serviceProducer.RegisterSingleton(); serviceProducer.RegisterSingleton(); + serviceProducer.RegisterSingleton(); } public static void RegisterLifetimeServices(IApplicationLifetimeProducer applicationLifetimeProducer) { @@ -60,6 +62,7 @@ public static void RegisterViews(IViewProducer viewProducer) viewProducer.RegisterView(); viewProducer.RegisterView(); viewProducer.RegisterView(); + viewProducer.RegisterView(); } } } diff --git a/Daybreak/Daybreak.csproj b/Daybreak/Daybreak.csproj index db9f60bb..9dc0ca81 100644 --- a/Daybreak/Daybreak.csproj +++ b/Daybreak/Daybreak.csproj @@ -9,8 +9,7 @@ false preview Daybreak.ico - 0.7.7 - app.manifest + 0.8.0 diff --git a/Daybreak/Models/ElevationRequest.cs b/Daybreak/Models/ElevationRequest.cs new file mode 100644 index 00000000..68481e39 --- /dev/null +++ b/Daybreak/Models/ElevationRequest.cs @@ -0,0 +1,11 @@ +using System; + +namespace Daybreak.Models +{ + public sealed class ElevationRequest + { + public object DataContext { get; set; } + public Type View { get; set; } + public string MessageToUser { get; set; } + } +} diff --git a/Daybreak/Services/ApplicationLauncher/ApplicationLauncher.cs b/Daybreak/Services/ApplicationLauncher/ApplicationLauncher.cs index 3ff8bc17..ec52a41a 100644 --- a/Daybreak/Services/ApplicationLauncher/ApplicationLauncher.cs +++ b/Daybreak/Services/ApplicationLauncher/ApplicationLauncher.cs @@ -3,7 +3,9 @@ using Daybreak.Services.Credentials; using Daybreak.Services.Logging; using Daybreak.Services.Mutex; +using Daybreak.Services.Privilege; using Daybreak.Utils; +using Daybreak.Views; using Microsoft.Win32; using Pepa.Wpf.Utilities; using System; @@ -15,6 +17,7 @@ using System.Security; using System.Text; using System.Threading.Tasks; +using System.Windows; namespace Daybreak.Services.ApplicationLauncher { @@ -30,6 +33,7 @@ public class ApplicationLauncher : IApplicationLauncher private readonly ICredentialManager credentialManager; private readonly IMutexHandler mutexHandler; private readonly ILogger logger; + private readonly IPrivilegeManager privilegeManager; public bool IsTexmodRunning => TexModProcessDetected(); public bool IsGuildwarsRunning => this.GuildwarsProcessDetected(); @@ -39,12 +43,14 @@ public ApplicationLauncher( IConfigurationManager configurationManager, ICredentialManager credentialManager, IMutexHandler mutexHandler, - ILogger logger) + ILogger logger, + IPrivilegeManager privilegeManager) { this.logger = logger.ThrowIfNull(nameof(logger)); this.mutexHandler = mutexHandler.ThrowIfNull(nameof(mutexHandler)); this.credentialManager = credentialManager.ThrowIfNull(nameof(credentialManager)); this.configurationManager = configurationManager.ThrowIfNull(nameof(configurationManager)); + this.privilegeManager = privilegeManager.ThrowIfNull(nameof(privilegeManager)); } public async Task LaunchGuildwars() @@ -56,6 +62,12 @@ public async Task LaunchGuildwars() { if (configuration.ExperimentalFeatures.MultiLaunchSupport is true) { + if (this.privilegeManager.AdminPrivileges is false) + { + this.privilegeManager.RequestAdminPrivileges("You need administrator rights in order to start using multi-launch"); + return; + } + ClearGwLocks(); } @@ -103,6 +115,33 @@ public Task LaunchTexmod() }); } + public void RestartDaybreakAsAdmin() + { + this.logger.LogInformation("Restarting daybreak with admin rights"); + var processName = Process.GetCurrentProcess()?.MainModule?.FileName; + if (processName.IsNullOrWhiteSpace() || File.Exists(processName) is false) + { + throw new InvalidOperationException("Unable to find executable. Aborting restart"); + } + + var process = new Process() + { + StartInfo = new() + { + Verb = "runas", + WorkingDirectory = Environment.CurrentDirectory, + UseShellExecute = true, + FileName = processName + } + }; + if (process.Start() is false) + { + throw new InvalidOperationException($"Unable to start {processName} as admin"); + } + + Application.Current.Shutdown(); + } + private void LaunchGuildwarsProcess(string email, Models.SecureString password, string character) { var executable = this.configurationManager.GetConfiguration().GuildwarsPaths.Where(path => path.Default).FirstOrDefault(); diff --git a/Daybreak/Services/ApplicationLauncher/IApplicationLauncher.cs b/Daybreak/Services/ApplicationLauncher/IApplicationLauncher.cs index 5ba79d5a..46fee9ae 100644 --- a/Daybreak/Services/ApplicationLauncher/IApplicationLauncher.cs +++ b/Daybreak/Services/ApplicationLauncher/IApplicationLauncher.cs @@ -10,5 +10,6 @@ public interface IApplicationLauncher Task LaunchGuildwars(); Task LaunchGuildwarsToolbox(); Task LaunchTexmod(); + void RestartDaybreakAsAdmin(); } } diff --git a/Daybreak/Services/Navigation/IViewManager.cs b/Daybreak/Services/Navigation/IViewManager.cs index 531ec8b8..4f4e7fd2 100644 --- a/Daybreak/Services/Navigation/IViewManager.cs +++ b/Daybreak/Services/Navigation/IViewManager.cs @@ -1,4 +1,5 @@ -using System.Windows.Controls; +using System; +using System.Windows.Controls; namespace Daybreak.Services.ViewManagement { @@ -11,5 +12,9 @@ void ShowView() void ShowView(object dataContext) where T : UserControl; + + void ShowView(Type type); + + void ShowView(Type type, object dataContext); } } diff --git a/Daybreak/Services/Navigation/ViewManager.cs b/Daybreak/Services/Navigation/ViewManager.cs index 3a5188b1..a82f2373 100644 --- a/Daybreak/Services/Navigation/ViewManager.cs +++ b/Daybreak/Services/Navigation/ViewManager.cs @@ -1,6 +1,7 @@ using Slim; using System; using System.Extensions; +using System.Windows; using System.Windows.Controls; namespace Daybreak.Services.ViewManagement @@ -32,17 +33,33 @@ public void RegisterView() where T : UserControl public void ShowView() where T : UserControl { - var view = this.serviceManager.GetService(); - this.container.Children.Clear(); - this.container.Children.Add(view); + this.ShowViewInner(typeof(T), null); } public void ShowView(object dataContext) where T : UserControl { - var view = this.serviceManager.GetService(); - this.container.Children.Clear(); - this.container.Children.Add(view); - view.DataContext = dataContext; + this.ShowViewInner(typeof(T), dataContext); + } + + public void ShowView(Type type) + { + this.ShowViewInner(type, null); + } + + public void ShowView(Type type, object dataContext) + { + this.ShowViewInner(type, dataContext); + } + + private void ShowViewInner(Type viewType, object dataContext) + { + Application.Current.Dispatcher.Invoke(() => + { + var view = this.serviceManager.GetService(viewType).As(); + this.container.Children.Clear(); + this.container.Children.Add(view); + view.DataContext = dataContext; + }); } } } diff --git a/Daybreak/Services/Privilege/IPrivilegeManager.cs b/Daybreak/Services/Privilege/IPrivilegeManager.cs new file mode 100644 index 00000000..d286e4fe --- /dev/null +++ b/Daybreak/Services/Privilege/IPrivilegeManager.cs @@ -0,0 +1,12 @@ +using System.Windows.Controls; + +namespace Daybreak.Services.Privilege +{ + public interface IPrivilegeManager + { + bool AdminPrivileges { get; } + + void RequestAdminPrivileges(string messageToUser, object dataContextOfCancelView = null) + where TCancelView : UserControl; + } +} diff --git a/Daybreak/Services/Privilege/PrivilegeManager.cs b/Daybreak/Services/Privilege/PrivilegeManager.cs new file mode 100644 index 00000000..ec612125 --- /dev/null +++ b/Daybreak/Services/Privilege/PrivilegeManager.cs @@ -0,0 +1,34 @@ +using Daybreak.Models; +using Daybreak.Services.Logging; +using Daybreak.Services.ViewManagement; +using Daybreak.Utils; +using Daybreak.Views; +using System.Extensions; +using System.Security.Principal; +using System.Windows.Controls; + +namespace Daybreak.Services.Privilege +{ + public sealed class PrivilegeManager : IPrivilegeManager + { + public bool AdminPrivileges => new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator); + + private readonly IViewManager viewManager; + private readonly ILogger logger; + + public PrivilegeManager( + IViewManager viewManager, + ILogger logger) + { + this.logger = logger.ThrowIfNull(nameof(logger)); + this.viewManager = viewManager.ThrowIfNull(nameof(viewManager)); + } + + public void RequestAdminPrivileges(string messageToUser, object dataContextOfCancelView = null) + where TCancelView : UserControl + { + this.logger.LogInformation("Requesting admin privileges"); + this.viewManager.ShowView(new ElevationRequest { View = typeof(TCancelView), DataContext = dataContextOfCancelView, MessageToUser = messageToUser }); + } + } +} diff --git a/Daybreak/Services/Updater/ApplicationUpdater.cs b/Daybreak/Services/Updater/ApplicationUpdater.cs index 7f2a6cfb..0bb276e0 100644 --- a/Daybreak/Services/Updater/ApplicationUpdater.cs +++ b/Daybreak/Services/Updater/ApplicationUpdater.cs @@ -21,6 +21,7 @@ namespace Daybreak.Services.Updater { public sealed class ApplicationUpdater : IApplicationUpdater { + private const string LaunchActionName = "Launch_Daybreak"; private const string UpdateDesiredKey = "UpdateDesired"; private const string ExecutionPolicyKey = "ExecutionPolicy"; private const string UpdatedKey = "Updating"; @@ -32,13 +33,20 @@ public sealed class ApplicationUpdater : IApplicationUpdater private const string OutputPathTag = "{OUTPUTPATH}"; private const string ExecutionPolicyTag = "{EXECUTIONPOLICY}"; private const string ProcessIdTag = "{PROCESSID}"; + private const string ExecutableNameTag = "{EXECUTABLE}"; + private const string WorkingDirectoryTag = "{WORKINGDIRECTORY}"; private const string Url = "https://github.com/AlexMacocian/Daybreak/releases/latest"; private const string DownloadUrl = $"https://github.com/AlexMacocian/Daybreak/releases/download/v{VersionTag}/Daybreakv{VersionTag}.zip"; private const string GetExecutionPolicyCommand = "Get-ExecutionPolicy -Scope CurrentUser"; private const string SetExecutionPolicyCommand = $"Set-ExecutionPolicy {ExecutionPolicyTag} -Scope CurrentUser"; private const string WaitCommand = $"Wait-Process -Id {ProcessIdTag}"; private const string ExtractCommandTemplate = $"Expand-Archive -Path '{InputFileTag}' -DestinationPath '{OutputPathTag}' -Force"; - private const string RunClientCommand = @".\Daybreak.exe"; + private const string PrepareScheduledAction = $"$action = New-ScheduledTaskAction -Execute {ExecutableNameTag} -WorkingDirectory {WorkingDirectoryTag}"; + private const string PrepareTriggerForAction = $"$trigger = New-ScheduledTaskTrigger -Once -At (Get-Date)"; + private const string RegisterScheduledAction = $"Register-ScheduledTask -Action $action -Trigger $trigger -TaskName {LaunchActionName} | Out-Null"; + private const string LaunchScheduledAction = $"Start-ScheduledTask -TaskName {LaunchActionName}"; + private const string SleepOneSecond = $"Start-Sleep -s 1"; + private const string UnregisterScheduledAction = $"Unregister-ScheduledTask -TaskName {LaunchActionName} -Confirm:$false"; private const string RemoveTempFile = $"Remove-item {TempFile}"; private const string RemovePs1 = $"Remove-item {ExtractAndRunPs1}"; @@ -251,8 +259,15 @@ private void LaunchExtractor() .Replace(InputFileTag, Path.GetFullPath(TempFile)) .Replace(OutputPathTag, Directory.GetCurrentDirectory()), RemoveTempFile, + PrepareScheduledAction + .Replace(ExecutableNameTag, Process.GetCurrentProcess()?.MainModule?.FileName) + .Replace(WorkingDirectoryTag, Directory.GetCurrentDirectory()), + PrepareTriggerForAction, + RegisterScheduledAction, + LaunchScheduledAction, + SleepOneSecond, + UnregisterScheduledAction, RemovePs1, - RunClientCommand }); var process = new Process() { @@ -260,10 +275,7 @@ private void LaunchExtractor() { FileName = "powershell.exe", Arguments = $@"{Directory.GetCurrentDirectory()}\{ExtractAndRunPs1}", - UseShellExecute = false, - RedirectStandardError = true, - RedirectStandardInput = true, - RedirectStandardOutput = true, + UseShellExecute = true, WindowStyle = ProcessWindowStyle.Maximized, WorkingDirectory = Directory.GetCurrentDirectory(), Verb = "runas" diff --git a/Daybreak/Views/AskUpdateView.xaml.cs b/Daybreak/Views/AskUpdateView.xaml.cs index 8d979088..58a19035 100644 --- a/Daybreak/Views/AskUpdateView.xaml.cs +++ b/Daybreak/Views/AskUpdateView.xaml.cs @@ -1,4 +1,5 @@ using Daybreak.Services.Logging; +using Daybreak.Services.Privilege; using Daybreak.Services.Runtime; using Daybreak.Services.Updater; using Daybreak.Services.ViewManagement; @@ -18,18 +19,32 @@ public partial class AskUpdateView : UserControl private readonly ILogger logger; private readonly IViewManager viewManager; private readonly IRuntimeStore runtimeStore; + private readonly IPrivilegeManager privilegeManager; public AskUpdateView( ILogger logger, IViewManager viewManager, - IRuntimeStore runtimeStore) + IRuntimeStore runtimeStore, + IPrivilegeManager privilegeManager) { this.logger = logger.ThrowIfNull(nameof(logger)); this.viewManager = viewManager.ThrowIfNull(nameof(viewManager)); this.runtimeStore = runtimeStore.ThrowIfNull(nameof(runtimeStore)); + this.privilegeManager = privilegeManager.ThrowIfNull(nameof(privilegeManager)); this.InitializeComponent(); } + private bool CheckIfAdmin() + { + if (this.privilegeManager.AdminPrivileges is false) + { + this.privilegeManager.RequestAdminPrivileges("Application needs to be in administrator mode in order to update."); + return false; + } + + return true; + } + private void NoButton_Clicked(object sender, System.EventArgs e) { this.logger.LogInformation("User declined update"); @@ -41,6 +56,11 @@ private void YesButton_Clicked(object sender, System.EventArgs e) { this.logger.LogInformation("User accepted update"); this.runtimeStore.StoreValue(UpdateDesiredKey, true); + if (this.CheckIfAdmin() is false) + { + return; + } + this.viewManager.ShowView(); } } diff --git a/Daybreak/Views/RequestElevationView.xaml b/Daybreak/Views/RequestElevationView.xaml new file mode 100644 index 00000000..c4d07dca --- /dev/null +++ b/Daybreak/Views/RequestElevationView.xaml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + diff --git a/Daybreak/Views/RequestElevationView.xaml.cs b/Daybreak/Views/RequestElevationView.xaml.cs new file mode 100644 index 00000000..326595e6 --- /dev/null +++ b/Daybreak/Views/RequestElevationView.xaml.cs @@ -0,0 +1,51 @@ +using Daybreak.Models; +using Daybreak.Services.ApplicationLauncher; +using Daybreak.Services.Logging; +using Daybreak.Services.Privilege; +using Daybreak.Services.ViewManagement; +using Daybreak.Utils; +using System.Extensions; +using System.Windows.Controls; + +namespace Daybreak.Views +{ + /// + /// Interaction logic for RequestElevationView.xaml + /// + public partial class RequestElevationView : UserControl + { + private readonly IApplicationLauncher applicationLauncher; + private readonly IViewManager viewManager; + private readonly ILogger logger; + + public RequestElevationView( + IApplicationLauncher applicationLauncher, + IViewManager viewManager, + ILogger logger) + { + this.applicationLauncher = applicationLauncher.ThrowIfNull(nameof(applicationLauncher)); + this.viewManager = viewManager.ThrowIfNull(nameof(viewManager)); + this.logger = logger.ThrowIfNull(nameof(logger)); + this.InitializeComponent(); + } + + private void YesButton_Clicked(object sender, System.EventArgs e) + { + this.applicationLauncher.RestartDaybreakAsAdmin(); + } + + private void NoButton_Clicked(object sender, System.EventArgs e) + { + if (this.DataContext is ElevationRequest elevationRequest) + { + this.logger.LogWarning($"Elevation request denied. Showing {elevationRequest.View} with {elevationRequest.DataContext}"); + this.viewManager.ShowView(elevationRequest.View, elevationRequest.DataContext); + } + else + { + this.logger.LogWarning("Elevation request context is not set. Returning to home page"); + this.viewManager.ShowView(); + } + } + } +}