XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEToolBox

【WPF】XFE工具箱

公开
关注 0 Fork 0 Star 0
返回提交历史

XFEstudio/XFEToolBox

支持工具强制管理员模式,提升至 1.1.5

新增 requiresAdministrator 字段,支持在 manifest.json 声明强制管理员启动。ToolPackageManifest、ToolPackageSummary、工具包模板等同步更新。ToolCardViewModel 区分用户配置与 manifest 强制,UI 提示优化,强制时用户无法关闭管理员模式。新增 ToolLaunchPreferenceStore,统一管理用户偏好,替换原 ToolLaunchPreferenceService。ToolRuntimeProcessStartInfoFactory 合并用户与 manifest 需求,ToolProjectRunService 启动流程适配 UAC 提权及令牌校验。工具配置弹窗、卡片、启动流程等 UI 逻辑适配,编辑器支持可视化勾选强制管理员。补充相关测试用例。

6b8b6db
XFE工作室室长 <mail@xfegzs.com>
提交于

代码差异

20 个文件 +190 -60
Modified XFEToolBox.Client.Wpf.Test/ToolRuntimeProcessStartInfoFactoryTests.cs +27 -0
@@ -5,6 +5,33 @@ namespace XFEToolBox.Client.Wpf.Test;
5 5
6 6 public static class ToolRuntimeProcessStartInfoFactoryTests
7 7 {
8 [Test]
9 public static void UserAndManifestAdministratorModesAreBothEffective()
10 {
11 Ensure(!ToolRuntimeProcessStartInfoFactory.ResolveRunAsAdministrator(false, false),
12 "未配置管理员模式时不应提权。 ");
13 Ensure(ToolRuntimeProcessStartInfoFactory.ResolveRunAsAdministrator(true, false),
14 "用户勾选管理员模式后没有进入提权启动。 ");
15 Ensure(ToolRuntimeProcessStartInfoFactory.ResolveRunAsAdministrator(false, true),
16 "manifest 强制管理员模式后没有进入提权启动。 ");
17 Ensure(ToolRuntimeProcessStartInfoFactory.ResolveRunAsAdministrator(true, true),
18 "用户配置和 manifest 同时启用时丢失管理员模式。 ");
19 }
20
21 [Test]
22 public static void AdministratorPreferenceSurvivesSerializationAndIsCaseInsensitive()
23 {
24 var json = ToolLaunchPreferenceStore.SetRunAsAdministrator(null, "xfestudio.sample-tool", true);
25 Ensure(ToolLaunchPreferenceStore.GetRunAsAdministrator(json, "XFESTUDIO.SAMPLE-TOOL"),
26 "管理员启动配置写入后没有读回。 ");
27
28 json = ToolLaunchPreferenceStore.SetRunAsAdministrator(json, "xfestudio.sample-tool", false);
29 Ensure(!ToolLaunchPreferenceStore.GetRunAsAdministrator(json, "xfestudio.sample-tool"),
30 "关闭管理员模式后配置仍然生效。 ");
31 Ensure(!ToolLaunchPreferenceStore.GetRunAsAdministrator("{invalid", "xfestudio.sample-tool"),
32 "损坏的旧配置不应导致工具被意外提权。 ");
33 }
34
8 35 [Test]
9 36 public static void AdministratorLaunchUsesShellRunAsAndTheCompiledAppHost()
10 37 {
Modified XFEToolBox.Client.Wpf.Test/XFEToolBox.Client.Wpf.Test.csproj +1 -0
@@ -25,6 +25,7 @@
25 25 <Compile Include="..\XFEToolBox.Client.Installer\Utilities\ZipHelper.cs" Link="Installer\ZipHelper.cs" />
26 26 <Compile Include="..\XFEToolBox\Utilities\WebImageSourceLoader.cs" Link="Utilities\WebImageSourceLoader.cs" />
27 27 <Compile Include="..\XFEToolBox\Utilities\ToolRuntimeProcessStartInfoFactory.cs" Link="Utilities\ToolRuntimeProcessStartInfoFactory.cs" />
28 <Compile Include="..\XFEToolBox\Utilities\ToolLaunchPreferenceStore.cs" Link="Utilities\ToolLaunchPreferenceStore.cs" />
28 29 <Compile Include="..\XFEToolBox\Models\RecentUsageEntry.cs" Link="Models\RecentUsageEntry.cs" />
29 30 <Compile Include="..\XFEToolBox\Utilities\RecentUsageIconCache.cs" Link="Utilities\RecentUsageIconCache.cs" />
30 31 </ItemGroup>
Modified XFEToolBox.Core/Tools/ToolCatalogContracts.cs +2 -0
@@ -18,6 +18,8 @@ public sealed class ToolPackageSummary
18 18
19 19 public required string[] Tags { get; init; }
20 20
21 public bool RequiresAdministrator { get; init; }
22
21 23 public DateTimeOffset UpdatedAtUtc { get; init; }
22 24 }
23 25
Modified XFEToolBox.Core/Tools/ToolPackageManifest.cs +6 -0
@@ -38,6 +38,12 @@ public sealed class ToolPackageManifest
38 38
39 39 public string? ReleaseNotes { get; init; }
40 40
41 /// <summary>
42 /// Requires the host to display elevation state and launch this tool through Windows UAC.
43 /// Users cannot override this requirement with a per-tool preference.
44 /// </summary>
45 public bool RequiresAdministrator { get; init; }
46
41 47 public required ToolEntryManifest Entry { get; init; }
42 48
43 49 /// <summary>
Modified XFEToolBox.Server.Test/Program.cs +2 -0
@@ -41,6 +41,7 @@ static void ValidPackagePasses()
41 41 using var package = CreatePackage();
42 42 var result = CreateValidator().Inspect(package);
43 43 Assert(result.Manifest.Id == "base64-generator", "工具 ID 不正确。");
44 Assert(result.Manifest.RequiresAdministrator, "管理员启动要求没有从 manifest 保留下来。");
44 45 Assert(result.Files.Contains("src/Views/Base64Tool.xaml"), "未发现入口 XAML。");
45 46 Assert(result.Files.Contains("src/ViewModels/Base64ToolViewModel.cs"), "未发现 ViewModel。");
46 47 Assert(result.IconDataUrl?.StartsWith("data:image/png;base64,", StringComparison.Ordinal) == true, "工具图标未被读取。");
@@ -206,6 +207,7 @@ static MemoryStream CreatePackage(Action<ZipArchive>? customize = null)
206 207 Category = "编码",
207 208 Tags = ["base64"],
208 209 MinimumHostVersion = "0.2.0",
210 RequiresAdministrator = true,
209 211 Entry = new ToolEntryManifest
210 212 {
211 213 ViewXaml = "src/Views/Base64Tool.xaml",
Modified XFEToolBox.Server/Services/ToolPackageContractMapper.cs +1 -0
@@ -15,6 +15,7 @@ internal static class ToolPackageContractMapper
15 15 Category = package.Manifest.Category,
16 16 LatestVersion = package.Manifest.Version,
17 17 Tags = package.Manifest.Tags,
18 RequiresAdministrator = package.Manifest.RequiresAdministrator,
18 19 UpdatedAtUtc = package.UploadedAtUtc
19 20 };
20 21
Modified XFEToolBox/Utilities/ToolLaunchPreferenceService.cs +6 -37
@@ -1,48 +1,17 @@
1 using System.Text.Json;
2 1 using XFEToolBox.Client.Profiles.CacheProfiles;
3 2
4 3 namespace XFEToolBox.Client.Utilities;
5 4
6 5 internal static class ToolLaunchPreferenceService
7 6 {
8 private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
9
10 public static bool GetRunAsAdministrator(string toolId)
11 {
12 if (string.IsNullOrWhiteSpace(toolId)) return false;
13 return ReadPreferences().TryGetValue(toolId, out var preference) && preference.RunAsAdministrator;
14 }
7 public static bool GetRunAsAdministrator(string toolId) =>
8 ToolLaunchPreferenceStore.GetRunAsAdministrator(AppCacheProfile.ToolLaunchPreferencesJson, toolId);
15 9
16 10 public static void SetRunAsAdministrator(string toolId, bool runAsAdministrator)
17 11 {
18 ArgumentException.ThrowIfNullOrWhiteSpace(toolId);
19 var preferences = ReadPreferences();
20 if (runAsAdministrator)
21 preferences[toolId] = new ToolLaunchPreference(true);
22 else
23 preferences.Remove(toolId);
24
25 AppCacheProfile.ToolLaunchPreferencesJson = JsonSerializer.Serialize(preferences, JsonOptions);
12 AppCacheProfile.ToolLaunchPreferencesJson = ToolLaunchPreferenceStore.SetRunAsAdministrator(
13 AppCacheProfile.ToolLaunchPreferencesJson,
14 toolId,
15 runAsAdministrator);
26 16 }
27
28 private static Dictionary<string, ToolLaunchPreference> ReadPreferences()
29 {
30 try
31 {
32 var json = AppCacheProfile.ToolLaunchPreferencesJson;
33 if (string.IsNullOrWhiteSpace(json))
34 return new Dictionary<string, ToolLaunchPreference>(StringComparer.OrdinalIgnoreCase);
35
36 var stored = JsonSerializer.Deserialize<Dictionary<string, ToolLaunchPreference>>(json, JsonOptions);
37 return stored is null
38 ? new Dictionary<string, ToolLaunchPreference>(StringComparer.OrdinalIgnoreCase)
39 : new Dictionary<string, ToolLaunchPreference>(stored, StringComparer.OrdinalIgnoreCase);
40 }
41 catch (JsonException)
42 {
43 return new Dictionary<string, ToolLaunchPreference>(StringComparer.OrdinalIgnoreCase);
44 }
45 }
46
47 private sealed record ToolLaunchPreference(bool RunAsAdministrator);
48 17 }
Added XFEToolBox/Utilities/ToolLaunchPreferenceStore.cs +45 -0
@@ -0,0 +1,45 @@
1 using System.Text.Json;
2
3 namespace XFEToolBox.Client.Utilities;
4
5 internal static class ToolLaunchPreferenceStore
6 {
7 private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
8
9 public static bool GetRunAsAdministrator(string? json, string toolId)
10 {
11 if (string.IsNullOrWhiteSpace(toolId)) return false;
12 return Read(json).TryGetValue(toolId, out var preference) && preference.RunAsAdministrator;
13 }
14
15 public static string SetRunAsAdministrator(string? json, string toolId, bool runAsAdministrator)
16 {
17 ArgumentException.ThrowIfNullOrWhiteSpace(toolId);
18 var preferences = Read(json);
19 if (runAsAdministrator)
20 preferences[toolId] = new ToolLaunchPreference(true);
21 else
22 preferences.Remove(toolId);
23 return JsonSerializer.Serialize(preferences, JsonOptions);
24 }
25
26 private static Dictionary<string, ToolLaunchPreference> Read(string? json)
27 {
28 try
29 {
30 if (string.IsNullOrWhiteSpace(json))
31 return new Dictionary<string, ToolLaunchPreference>(StringComparer.OrdinalIgnoreCase);
32
33 var stored = JsonSerializer.Deserialize<Dictionary<string, ToolLaunchPreference>>(json, JsonOptions);
34 return stored is null
35 ? new Dictionary<string, ToolLaunchPreference>(StringComparer.OrdinalIgnoreCase)
36 : new Dictionary<string, ToolLaunchPreference>(stored, StringComparer.OrdinalIgnoreCase);
37 }
38 catch (JsonException)
39 {
40 return new Dictionary<string, ToolLaunchPreference>(StringComparer.OrdinalIgnoreCase);
41 }
42 }
43
44 private sealed record ToolLaunchPreference(bool RunAsAdministrator);
45 }
Modified XFEToolBox/Utilities/ToolProjectRunService.cs +22 -3
@@ -108,6 +108,10 @@ internal static class ToolProjectRunService
108 108 var assemblyName = $"XFEToolRuntime_{Guid.NewGuid():N}";
109 109 var hostAssemblyName = typeof(ToolProjectRunService).Assembly.GetName().Name
110 110 ?? throw new InvalidOperationException("无法确定宿主程序集名称。");
111 var requiresElevatedProcess = launchAfterBuild &&
112 ToolRuntimeProcessStartInfoFactory.ResolveRunAsAdministrator(
113 runAsAdministrator,
114 manifest.RequiresAdministrator);
111 115 var preparedWorkspaceRoot = Path.Combine(runtimeRoot, "source");
112 116 await PrepareWorkspaceAsync(
113 117 workspaceRoot,
@@ -118,7 +122,11 @@ internal static class ToolProjectRunService
118 122 var entryPath = Path.Combine(runtimeRoot, "RuntimeEntry.g.cs");
119 123 var toolIconPath = ResolveToolIconPath(preparedWorkspaceRoot, manifest.Icon);
120 124 await File.WriteAllTextAsync(projectPath, CreateProjectFile(preparedWorkspaceRoot, assemblyName, hostAssemblyName), new UTF8Encoding(false), cancellationToken);
121 await File.WriteAllTextAsync(entryPath, CreateRuntimeEntry(manifest, hostAssemblyName, windowTitle, toolIconPath), new UTF8Encoding(false), cancellationToken);
125 await File.WriteAllTextAsync(
126 entryPath,
127 CreateRuntimeEntry(manifest, hostAssemblyName, windowTitle, toolIconPath, requiresElevatedProcess),
128 new UTF8Encoding(false),
129 cancellationToken);
122 130
123 131 var buildInfo = new ProcessStartInfo("dotnet")
124 132 {
@@ -169,7 +177,7 @@ internal static class ToolProjectRunService
169 177 var runInfo = ToolRuntimeProcessStartInfoFactory.Create(
170 178 runtimeExecutable,
171 179 workspaceRoot,
172 runAsAdministrator);
180 requiresElevatedProcess);
173 181 var runtimeProcess = Process.Start(runInfo)
174 182 ?? throw new InvalidOperationException("工具运行进程启动失败。");
175 183 var runtimeStandardOutputTask = runInfo.RedirectStandardOutput
@@ -283,7 +291,8 @@ internal static class ToolProjectRunService
283 291 ToolPackageManifest manifest,
284 292 string hostAssemblyName,
285 293 string windowTitle,
286 string? toolIconPath)
294 string? toolIconPath,
295 bool requiresElevatedProcess)
287 296 {
288 297 var window = NormalizeWindowSettings(manifest.Window);
289 298 var toolId = JsonSerializer.Serialize(manifest.Id.Trim());
@@ -309,9 +318,11 @@ internal static class ToolProjectRunService
309 318 var allowMaximize = JsonSerializer.Serialize(window.AllowMaximize);
310 319 var showMinimizeButton = JsonSerializer.Serialize(window.ShowMinimizeButton);
311 320 var showCloseButton = JsonSerializer.Serialize(window.ShowCloseButton);
321 var requiresAdministrator = JsonSerializer.Serialize(requiresElevatedProcess);
312 322 return $$"""
313 323 using System.IO;
314 324 using System.Reflection;
325 using System.Security.Principal;
315 326 using System.Windows;
316 327 using System.Windows.Controls;
317 328 using System.Windows.Media;
@@ -340,6 +351,8 @@ internal static class ToolProjectRunService
340 351 });
341 352 try
342 353 {
354 if ({{requiresAdministrator}} && !IsCurrentProcessAdministrator())
355 throw new UnauthorizedAccessException("工具要求管理员权限,但运行进程没有获得管理员令牌。");
343 356 ToolDataStore.Initialize({{toolId}});
344 357 var viewType = Assembly.GetExecutingAssembly().GetType({{viewClass}}, throwOnError: true)!;
345 358 var instance = Activator.CreateInstance(viewType)
@@ -369,6 +382,12 @@ internal static class ToolProjectRunService
369 382 }
370 383 }
371 384
385 private static bool IsCurrentProcessAdministrator()
386 {
387 using var identity = WindowsIdentity.GetCurrent();
388 return new WindowsPrincipal(identity).IsInRole(WindowsBuiltInRole.Administrator);
389 }
390
372 391 private static void ConfigureWindow(Application application, Window window, object content)
373 392 {
374 393 var savedPlacement = ToolDataStore.ReadWindowPlacement();
Modified XFEToolBox/Utilities/ToolProjectWorkspaceService.cs +1 -0
@@ -67,6 +67,7 @@ internal static class ToolProjectWorkspaceService
67 67 "icon": "{{DefaultToolIconRelativePath}}",
68 68 "category": "开发工具",
69 69 "tags": [ "WPF" ],
70 "requiresAdministrator": false,
70 71 "entry": {
71 72 "viewXaml": "Code/Views/MainPage.xaml",
72 73 "viewClass": "{{toolNamespace}}.MainPage",
Modified XFEToolBox/Utilities/ToolRuntimeProcessStartInfoFactory.cs +3 -0
@@ -5,6 +5,9 @@ namespace XFEToolBox.Client.Utilities;
5 5
6 6 internal static class ToolRuntimeProcessStartInfoFactory
7 7 {
8 public static bool ResolveRunAsAdministrator(bool userPreference, bool manifestRequirement) =>
9 userPreference || manifestRequirement;
10
8 11 public static ProcessStartInfo Create(
9 12 string runtimeExecutable,
10 13 string workingDirectory,
Modified XFEToolBox/ViewModel/Pages/ToolCardViewModel.cs +17 -4
@@ -17,7 +17,7 @@ public sealed class ToolCardViewModel(
17 17 private bool _isDownloadIndeterminate;
18 18 private double _downloadProgress;
19 19 private string _downloadProgressText = string.Empty;
20 private bool _runAsAdministrator = runAsAdministrator;
20 private bool _userRunAsAdministrator = runAsAdministrator;
21 21
22 22 public ToolPackageSummary Package { get; } = package;
23 23 public string Id => Package.Id;
@@ -29,12 +29,25 @@ public sealed class ToolCardViewModel(
29 29 public ImageSource IconSource { get; } = iconSource;
30 30 public ImageSource UacIconSource { get; } = uacIconSource;
31 31
32 public bool RunAsAdministrator
32 public bool RequiresAdministrator => Package.RequiresAdministrator;
33
34 public bool UserRunAsAdministrator
33 35 {
34 get => _runAsAdministrator;
35 set => SetProperty(ref _runAsAdministrator, value);
36 get => _userRunAsAdministrator;
37 set
38 {
39 if (!SetProperty(ref _userRunAsAdministrator, value)) return;
40 OnPropertyChanged(nameof(RunAsAdministrator));
41 OnPropertyChanged(nameof(AdministratorLaunchDescription));
42 }
36 43 }
37 44
45 public bool RunAsAdministrator => RequiresAdministrator || UserRunAsAdministrator;
46
47 public string AdministratorLaunchDescription => RequiresAdministrator
48 ? "工具清单要求以管理员身份启动,用户无法关闭"
49 : "已由用户配置为以管理员身份打开";
50
38 51 public bool IsEnabled
39 52 {
40 53 get => _isEnabled;
Modified XFEToolBox/Views/Pages/Popups/ToolConfigurationPopupPage.xaml +4 -1
Modified XFEToolBox/Views/Pages/Popups/ToolConfigurationPopupPage.xaml.cs +17 -4
Modified XFEToolBox/Views/Pages/ToolBoxPage.xaml +1 -1
Modified XFEToolBox/Views/Pages/ToolBoxPage.xaml.cs +12 -8
Modified XFEToolBox/Views/Windows/ToolCodeEditorWindow.xaml +16 -1
Modified XFEToolBox/Views/Windows/ToolCodeEditorWindow.xaml.cs +2 -0
Modified XFEToolBox/XFEToolBox.Client.csproj +1 -1
Modified docs/tool-packages.md +4 -0