mirror of
https://github.com/dorssel/usbipd-win.git
synced 2026-11-10 22:50:48 +08:00
Add policy rules
This commit is contained in:
14 files changed
+547
-29
No files matched your search
@@ -73,6 +73,10 @@ SPDX-License-Identifier: GPL-3.0-only
|
||||
Key="Devices"
|
||||
ForceCreateOnInstall="yes"
|
||||
/>
|
||||
<RegistryKey
|
||||
Key="Policy"
|
||||
ForceCreateOnInstall="yes"
|
||||
/>
|
||||
</RegistryKey>
|
||||
<Environment
|
||||
Id="PATH"
|
||||
|
||||
+14
-14
@@ -74,8 +74,8 @@ sealed class BusId_Tests
|
||||
"0-0",
|
||||
"0-1",
|
||||
"1-0",
|
||||
"1-65536",
|
||||
"65536-1",
|
||||
"1-100",
|
||||
"100-1",
|
||||
"01-1",
|
||||
"1-01",
|
||||
];
|
||||
@@ -89,20 +89,20 @@ sealed class BusId_Tests
|
||||
"IncompatibleHub",
|
||||
"1-1",
|
||||
"1-2",
|
||||
"1-65534",
|
||||
"1-65535",
|
||||
"1-98",
|
||||
"1-99",
|
||||
"2-1",
|
||||
"2-2",
|
||||
"2-65534",
|
||||
"2-65535",
|
||||
"65534-1",
|
||||
"65534-2",
|
||||
"65534-65534",
|
||||
"65534-65535",
|
||||
"65535-1",
|
||||
"65535-2",
|
||||
"65535-65534",
|
||||
"65535-65535",
|
||||
"2-98",
|
||||
"2-99",
|
||||
"98-1",
|
||||
"98-2",
|
||||
"98-98",
|
||||
"98-99",
|
||||
"99-1",
|
||||
"99-2",
|
||||
"99-98",
|
||||
"99-99",
|
||||
];
|
||||
|
||||
public static IEnumerable<string[]> Valid => from value in _Valid select new string[] { value };
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
// SPDX-FileCopyrightText: 2022 Frans van Dorsselaer
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
using System.CommandLine;
|
||||
using Usbipd.Automation;
|
||||
|
||||
namespace UnitTests;
|
||||
|
||||
[TestClass]
|
||||
sealed class Parse_policy_Tests
|
||||
: ParseTestBase
|
||||
{
|
||||
static readonly BusId TestBusId = BusId.Parse("3-42");
|
||||
static readonly Guid TestGuid = Guid.Parse("{E863A2AF-AE47-440B-A32B-FAB1C03017AB}");
|
||||
static readonly VidPid TestHardwareId = VidPid.Parse("0123:cdef");
|
||||
|
||||
[TestMethod]
|
||||
public void Help()
|
||||
{
|
||||
Test(ExitCode.Success, "policy", "--help");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void CommandMissing()
|
||||
{
|
||||
Test(ExitCode.ParseError, "policy");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void CommandInvalid()
|
||||
{
|
||||
Test(ExitCode.ParseError, "policy", "invalid-command");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void OptionInvalid()
|
||||
{
|
||||
Test(ExitCode.ParseError, "policy", "--invalid-option");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void list_Success()
|
||||
{
|
||||
var mock = CreateMock();
|
||||
mock.Setup(m => m.PolicyList(It.IsNotNull<IConsole>(), It.IsAny<CancellationToken>())).Returns(Task.FromResult(ExitCode.Success));
|
||||
|
||||
Test(ExitCode.Success, mock, "policy", "list");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void list_Help()
|
||||
{
|
||||
Test(ExitCode.Success, "policy", "list", "--help");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void list_OptionInvalid()
|
||||
{
|
||||
Test(ExitCode.ParseError, "policy", "list", "--invalid-option");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void list_StrayArgument()
|
||||
{
|
||||
Test(ExitCode.ParseError, "policy", "list", "stray-argument");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void add_Success()
|
||||
{
|
||||
var mock = CreateMock();
|
||||
mock.Setup(m => m.PolicyAdd(It.IsAny<PolicyRule>(),
|
||||
It.IsNotNull<IConsole>(), It.IsAny<CancellationToken>())).Returns(Task.FromResult(ExitCode.Success));
|
||||
|
||||
Test(ExitCode.Success, mock, "policy", "add", "--type", "bind", "--access", "allow",
|
||||
"--busid", TestBusId.ToString(), "--hardware-id", TestHardwareId.ToString());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void remove_Success()
|
||||
{
|
||||
var mock = CreateMock();
|
||||
mock.Setup(m => m.PolicyRemove(It.IsAny<Guid>(),
|
||||
It.IsNotNull<IConsole>(), It.IsAny<CancellationToken>())).Returns(Task.FromResult(ExitCode.Success));
|
||||
|
||||
Test(ExitCode.Success, mock, "policy", "remove", "--guid", TestGuid.ToString());
|
||||
}
|
||||
}
|
||||
@@ -9,9 +9,9 @@ sealed class Parse_usbipd_Tests
|
||||
: ParseTestBase
|
||||
{
|
||||
[TestMethod]
|
||||
public void Success()
|
||||
public void NoCommand()
|
||||
{
|
||||
Test(ExitCode.Success);
|
||||
Test(ExitCode.ParseError);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
|
||||
@@ -22,11 +22,13 @@ public readonly record struct BusId
|
||||
{
|
||||
// Do not allow the explicit creation of the special IncompatibleHub value.
|
||||
// Instead, use the static IncompatibleHub field (preferrable) or "default".
|
||||
if (bus == 0)
|
||||
// USB supports up to 127 devices, but that would require multiple hubs; the "per hub" port will never be >99.
|
||||
// And if you have more than 99 hubs on one system, then you win a prize! (but we're not going to support it...)
|
||||
if (bus == 0 || bus > 99)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(bus));
|
||||
}
|
||||
if (port == 0)
|
||||
if (port == 0 || port > 99)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(port));
|
||||
}
|
||||
@@ -63,8 +65,8 @@ public readonly record struct BusId
|
||||
}
|
||||
var match = Regex.Match(input, "^([1-9][0-9]*)-([1-9][0-9]*)$");
|
||||
if (match.Success
|
||||
&& ushort.TryParse(match.Groups[1].Value, out var bus)
|
||||
&& ushort.TryParse(match.Groups[2].Value, out var port))
|
||||
&& ushort.TryParse(match.Groups[1].Value, out var bus) && bus <= 99
|
||||
&& ushort.TryParse(match.Groups[2].Value, out var port) && port <= 99)
|
||||
{
|
||||
busId = new(bus, port);
|
||||
return true;
|
||||
|
||||
@@ -142,7 +142,6 @@ sealed partial class CommandHandlers : ICommandHandlers
|
||||
{
|
||||
state = "Not shared";
|
||||
}
|
||||
// NOTE: Strictly speaking, both Bus and Port can be > 99. If you have one of those, you win a prize!
|
||||
console.Write($"{(device.BusId.Value.IsIncompatibleHub ? string.Empty : device.BusId.Value),-5} ");
|
||||
console.Write($"{device.HardwareId,-9} ");
|
||||
console.WriteTruncated(GetDescription(device, usbids), 60, true);
|
||||
@@ -474,4 +473,63 @@ sealed partial class CommandHandlers : ICommandHandlers
|
||||
Console.Write(json);
|
||||
return Task.FromResult(ExitCode.Success);
|
||||
}
|
||||
|
||||
Task<ExitCode> ICommandHandlers.PolicyAdd(PolicyRule rule, IConsole console, CancellationToken cancellationToken)
|
||||
{
|
||||
if (RegistryUtils.GetPolicyRules().FirstOrDefault(r => r.Value == rule) is var existingRule && existingRule.Key != default)
|
||||
{
|
||||
console.ReportError($"Policy rule already exists with guid '{existingRule.Key:D}'.");
|
||||
return Task.FromResult(ExitCode.Failure);
|
||||
}
|
||||
|
||||
if (!CheckWriteAccess(console))
|
||||
{
|
||||
return Task.FromResult(ExitCode.AccessDenied);
|
||||
}
|
||||
|
||||
var guid = RegistryUtils.AddPolicyRule(rule);
|
||||
console.ReportInfo($"Policy rule created with guid '{guid:D}'.");
|
||||
return Task.FromResult(ExitCode.Success);
|
||||
}
|
||||
|
||||
Task<ExitCode> ICommandHandlers.PolicyList(IConsole console, CancellationToken cancellationToken)
|
||||
{
|
||||
var policyRules = RegistryUtils.GetPolicyRules();
|
||||
console.WriteLine("Policy rules:");
|
||||
console.WriteLine($"{"GUID",-36} {"TYPE",-4} {"ACCESS",-6} {"BUSID",-5} {"VID:PID",-9}");
|
||||
foreach (var rule in policyRules)
|
||||
{
|
||||
console.Write($"{rule.Key,-36} ");
|
||||
console.Write($"{rule.Value.Type,-4} ");
|
||||
console.Write($"{(rule.Value.Allow ? "Allow" : "Deny"),-6} ");
|
||||
switch (rule.Value.Type)
|
||||
{
|
||||
case PolicyRuleType.Bind:
|
||||
var ruleBind = (PolicyRuleBind)rule.Value;
|
||||
console.Write($"{(ruleBind.BusId.HasValue ? ruleBind.BusId.Value : string.Empty),-5} ");
|
||||
console.Write($"{(ruleBind.HardwareId.HasValue ? ruleBind.HardwareId.Value : string.Empty),-9}");
|
||||
break;
|
||||
}
|
||||
console.WriteLine(string.Empty);
|
||||
}
|
||||
console.WriteLine(string.Empty);
|
||||
return Task.FromResult(ExitCode.Success);
|
||||
}
|
||||
|
||||
Task<ExitCode> ICommandHandlers.PolicyRemove(Guid guid, IConsole console, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!RegistryUtils.GetPolicyRules().ContainsKey(guid))
|
||||
{
|
||||
console.ReportError($"There is no policy rule with guid '{guid:D}'.");
|
||||
return Task.FromResult(ExitCode.Failure);
|
||||
}
|
||||
|
||||
if (!CheckWriteAccess(console))
|
||||
{
|
||||
return Task.FromResult(ExitCode.AccessDenied);
|
||||
}
|
||||
|
||||
RegistryUtils.RemovePolicyRule(guid);
|
||||
return Task.FromResult(ExitCode.Success);
|
||||
}
|
||||
}
|
||||
@@ -26,4 +26,7 @@ interface ICommandHandlers
|
||||
public Task<ExitCode> State(IConsole console, CancellationToken cancellationToken);
|
||||
public Task<ExitCode> Install(IConsole console, CancellationToken cancellationToken);
|
||||
public Task<ExitCode> Uninstall(IConsole console, CancellationToken cancellationToken);
|
||||
public Task<ExitCode> PolicyAdd(PolicyRule rule, IConsole console, CancellationToken cancellationToken);
|
||||
public Task<ExitCode> PolicyList(IConsole console, CancellationToken cancellationToken);
|
||||
public Task<ExitCode> PolicyRemove(Guid guid, IConsole console, CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// SPDX-FileCopyrightText: 2024 Frans van Dorsselaer
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
using System.CommandLine;
|
||||
using System.CommandLine.Parsing;
|
||||
|
||||
namespace Usbipd;
|
||||
|
||||
static class ParseResultExtensions
|
||||
{
|
||||
public static T? GetValueForOptionOrNull<T>(this ParseResult parseResult, Option<T> option) where T : struct
|
||||
=> parseResult.HasOption(option) ? parseResult.GetValueForOption(option) : null;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// SPDX-FileCopyrightText: 2024 Frans van Dorsselaer
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace Usbipd;
|
||||
|
||||
abstract record PolicyRule(bool Allow, PolicyRuleType Type)
|
||||
{
|
||||
public abstract bool IsValid();
|
||||
|
||||
public abstract bool Matches(UsbDevice usbDevice);
|
||||
|
||||
public abstract void Save(RegistryKey registryKey);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// SPDX-FileCopyrightText: 2024 Frans van Dorsselaer
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
namespace Usbipd;
|
||||
|
||||
public enum PolicyRuleAccess
|
||||
{
|
||||
Allow,
|
||||
Deny,
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
// SPDX-FileCopyrightText: 2024 Frans van Dorsselaer
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
using Microsoft.Win32;
|
||||
using Usbipd.Automation;
|
||||
|
||||
namespace Usbipd;
|
||||
|
||||
sealed record PolicyRuleBind(bool allow, BusId? BusId, VidPid? HardwareId)
|
||||
: PolicyRule(allow, PolicyRuleType.Bind)
|
||||
{
|
||||
const string BusIdName = "BusId";
|
||||
const string HardwareIdName = "HardwareId";
|
||||
|
||||
public override bool IsValid() => BusId.HasValue || HardwareId.HasValue;
|
||||
|
||||
public override bool Matches(UsbDevice usbDevice)
|
||||
{
|
||||
if (!IsValid())
|
||||
{
|
||||
throw new InvalidOperationException("Invalid policy rule");
|
||||
}
|
||||
|
||||
if (BusId.HasValue && BusId.Value != usbDevice.BusId)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (HardwareId.HasValue && HardwareId.Value != usbDevice.HardwareId)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public override void Save(RegistryKey registryKey)
|
||||
{
|
||||
if (BusId.HasValue)
|
||||
{
|
||||
registryKey.SetValue(BusIdName, BusId.Value.ToString());
|
||||
}
|
||||
if (HardwareId.HasValue)
|
||||
{
|
||||
registryKey.SetValue(HardwareIdName, HardwareId.Value.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
public static PolicyRuleBind Load(bool allow, RegistryKey registryKey)
|
||||
{
|
||||
BusId? busId = null;
|
||||
if (Automation.BusId.TryParse(registryKey.GetValue(BusIdName) as string ?? string.Empty, out var parsedBusId))
|
||||
{
|
||||
busId = parsedBusId;
|
||||
}
|
||||
VidPid? hardwareId = null;
|
||||
if (VidPid.TryParse(registryKey.GetValue(HardwareIdName) as string ?? string.Empty, out var parsedHardwareId))
|
||||
{
|
||||
hardwareId = parsedHardwareId;
|
||||
}
|
||||
return new(allow, busId, hardwareId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// SPDX-FileCopyrightText: 2024 Frans van Dorsselaer
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
namespace Usbipd;
|
||||
|
||||
public enum PolicyRuleType
|
||||
{
|
||||
Bind,
|
||||
}
|
||||
+153
-8
@@ -7,7 +7,6 @@ using System.CommandLine;
|
||||
using System.CommandLine.Builder;
|
||||
using System.CommandLine.Completions;
|
||||
using System.CommandLine.Help;
|
||||
using System.CommandLine.IO;
|
||||
using System.CommandLine.Parsing;
|
||||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
@@ -63,6 +62,17 @@ static class Program
|
||||
return $"Exactly one of the options {list} is required.";
|
||||
}
|
||||
|
||||
static string AtLeastOneOfRequiredText(params Option[] options)
|
||||
{
|
||||
Debug.Assert(options.Length >= 2);
|
||||
|
||||
var names = options.Select(o => $"'--{o.Name}'").ToArray();
|
||||
var list = names.Length == 2
|
||||
? $"{names[0]} or {names[1]}"
|
||||
: string.Join(", ", names[0..(names.Length - 1)]) + ", or " + names[^1];
|
||||
return $"At least one of the options {list} is required.";
|
||||
}
|
||||
|
||||
static void ValidateOneOf(CommandResult commandResult, params Option[] options)
|
||||
{
|
||||
Debug.Assert(options.Length >= 2);
|
||||
@@ -73,6 +83,16 @@ static class Program
|
||||
}
|
||||
}
|
||||
|
||||
static void ValidateAtLeastOneOf(CommandResult commandResult, params Option[] options)
|
||||
{
|
||||
Debug.Assert(options.Length >= 2);
|
||||
|
||||
if (!options.Any(option => commandResult.FindResultFor(option) is not null))
|
||||
{
|
||||
commandResult.ErrorMessage = AtLeastOneOfRequiredText(options);
|
||||
}
|
||||
}
|
||||
|
||||
internal static IEnumerable<string> CompletionGuard(CompletionContext completionContext, Func<IEnumerable<string>?> complete)
|
||||
{
|
||||
try
|
||||
@@ -115,11 +135,6 @@ static class Program
|
||||
internal static ExitCode Run(IConsole? optionalTestConsole, ICommandHandlers commandHandlers, params string[] args)
|
||||
{
|
||||
var rootCommand = new RootCommand("Shares locally connected USB devices to other machines, including Hyper-V guests and WSL 2.");
|
||||
rootCommand.SetHandler(invocationContext =>
|
||||
{
|
||||
invocationContext.HelpBuilder.Write(rootCommand, invocationContext.Console.Out.CreateTextWriter());
|
||||
});
|
||||
|
||||
{
|
||||
//
|
||||
// attach [--auto-attach]
|
||||
@@ -169,7 +184,7 @@ static class Program
|
||||
}.AddCompletions(completionContext => CompletionGuard(completionContext, () =>
|
||||
UsbDevice.GetAll().Where(d => d.BusId.HasValue).GroupBy(d => d.HardwareId).Select(g => g.Key.ToString())));
|
||||
//
|
||||
// wsl attach
|
||||
// attach
|
||||
//
|
||||
var attachCommand = new Command("attach", "Attach a USB device to a client\0"
|
||||
+ "Attaches a USB device to a client.\n"
|
||||
@@ -317,7 +332,7 @@ static class Program
|
||||
}.AddCompletions(completionContext => CompletionGuard(completionContext, () =>
|
||||
UsbDevice.GetAll().Where(d => d.BusId.HasValue).GroupBy(d => d.HardwareId).Select(g => g.Key.ToString())));
|
||||
//
|
||||
// wsl detach
|
||||
// detach
|
||||
//
|
||||
var detachCommand = new Command("detach", "Detach a USB device from a client\0"
|
||||
+ "Detaches one or more USB devices. The client sees this as a surprise "
|
||||
@@ -396,6 +411,136 @@ static class Program
|
||||
});
|
||||
rootCommand.AddCommand(listCommand);
|
||||
}
|
||||
{
|
||||
//
|
||||
// policy
|
||||
//
|
||||
var policyCommand = new Command("policy", "Manage policy rules\0"
|
||||
+ "Policy rules allow or deny specific functionality, such as bind and attach.\n");
|
||||
{
|
||||
//
|
||||
// policy add --access <ACCESS>
|
||||
//
|
||||
var accessOption = new Option<PolicyRuleAccess>(
|
||||
aliases: ["--access", "-a"]
|
||||
)
|
||||
{
|
||||
ArgumentHelpName = "ACCESS",
|
||||
Description = "Allow or Deny",
|
||||
IsRequired = true,
|
||||
};
|
||||
//
|
||||
// policy add --type <TYPE>
|
||||
//
|
||||
var typeOption = new Option<PolicyRuleType>(
|
||||
aliases: ["--type", "-t"]
|
||||
)
|
||||
{
|
||||
ArgumentHelpName = "TYPE",
|
||||
Description = "Add a policy rule of type <TYPE>",
|
||||
IsRequired = true,
|
||||
};
|
||||
//
|
||||
// policy add [--busid <BUSID>]
|
||||
//
|
||||
var busIdOption = new Option<BusId>(
|
||||
aliases: ["--busid", "-b"],
|
||||
parseArgument: ParseCompatibleBusId
|
||||
)
|
||||
{
|
||||
ArgumentHelpName = "BUSID",
|
||||
Description = "Share device having <BUSID>",
|
||||
}.AddCompletions(CompatibleBusIdCompletions);
|
||||
//
|
||||
// policy add [--hardware-id <VID>:<PID>]
|
||||
//
|
||||
var hardwareIdOption = new Option<VidPid>(
|
||||
// NOTE: the alias '-h' is already for '--help'
|
||||
aliases: ["--hardware-id", "-i"],
|
||||
parseArgument: ParseVidPid
|
||||
)
|
||||
{
|
||||
ArgumentHelpName = "VID:PID",
|
||||
Description = "Attach device having <VID>:<PID>",
|
||||
}.AddCompletions(completionContext => CompletionGuard(completionContext, () =>
|
||||
UsbDevice.GetAll().GroupBy(d => d.HardwareId).Select(g => g.Key.ToString())));
|
||||
//
|
||||
// policy add
|
||||
//
|
||||
var addCommand = new Command("add", "Add a policy rule\0"
|
||||
+ "Add a new policy rule. The resulting policy will be effective immediately.\n"
|
||||
+ "\n"
|
||||
+ AtLeastOneOfRequiredText(busIdOption, hardwareIdOption))
|
||||
{
|
||||
accessOption,
|
||||
typeOption,
|
||||
busIdOption,
|
||||
hardwareIdOption,
|
||||
};
|
||||
addCommand.AddValidator(commandResult =>
|
||||
{
|
||||
ValidateAtLeastOneOf(commandResult, busIdOption, hardwareIdOption);
|
||||
});
|
||||
addCommand.SetHandler(async invocationContext =>
|
||||
{
|
||||
var policyRuleType = invocationContext.ParseResult.GetValueForOption(typeOption);
|
||||
invocationContext.ExitCode = (int)await (policyRuleType switch
|
||||
{
|
||||
PolicyRuleType.Bind =>
|
||||
commandHandlers.PolicyAdd(new PolicyRuleBind(invocationContext.ParseResult.GetValueForOption(accessOption) == PolicyRuleAccess.Allow,
|
||||
invocationContext.ParseResult.GetValueForOptionOrNull(busIdOption),
|
||||
invocationContext.ParseResult.GetValueForOptionOrNull(hardwareIdOption)),
|
||||
invocationContext.Console, invocationContext.GetCancellationToken()),
|
||||
_ => throw new UnexpectedResultException($"Unexpected policy rule type '{policyRuleType}'."),
|
||||
});
|
||||
});
|
||||
policyCommand.AddCommand(addCommand);
|
||||
}
|
||||
{
|
||||
//
|
||||
// policy list
|
||||
//
|
||||
var listCommand = new Command("list", "List policy rules\0"
|
||||
+ "List all policy rules.\n");
|
||||
listCommand.SetHandler(async invocationContext =>
|
||||
{
|
||||
invocationContext.ExitCode = (int)
|
||||
await commandHandlers.PolicyList(invocationContext.Console, invocationContext.GetCancellationToken());
|
||||
});
|
||||
policyCommand.AddCommand(listCommand);
|
||||
}
|
||||
{
|
||||
//
|
||||
// policy remove --guid <GUID>
|
||||
//
|
||||
var guidOption = new Option<Guid>(
|
||||
aliases: ["--guid", "-g"],
|
||||
parseArgument: ParseGuid
|
||||
)
|
||||
{
|
||||
ArgumentHelpName = "GUID",
|
||||
Description = "Remove the policy rule having <GUID>",
|
||||
IsRequired = true,
|
||||
}.AddCompletions(completionContext => CompletionGuard(completionContext, () =>
|
||||
RegistryUtils.GetPolicyRules().Select(r => r.Key.ToString("D"))));
|
||||
//
|
||||
// policy remove
|
||||
//
|
||||
var removeCommand = new Command("remove", "Remove a policy rule\0"
|
||||
+ "Remove an existing policy rule. The resulting policy will be effective immediately.\n")
|
||||
{
|
||||
guidOption,
|
||||
};
|
||||
removeCommand.SetHandler(async invocationContext =>
|
||||
{
|
||||
invocationContext.ExitCode = (int)
|
||||
await commandHandlers.PolicyRemove(invocationContext.ParseResult.GetValueForOption(guidOption),
|
||||
invocationContext.Console, invocationContext.GetCancellationToken());
|
||||
});
|
||||
policyCommand.AddCommand(removeCommand);
|
||||
}
|
||||
rootCommand.AddCommand(policyCommand);
|
||||
}
|
||||
{
|
||||
//
|
||||
// server [<KEY=VALUE>...]
|
||||
|
||||
@@ -34,6 +34,9 @@ static class RegistryUtils
|
||||
const string AttachedName = "Attached";
|
||||
const string BusIdName = "BusId";
|
||||
const string IPAddressName = "IPAddress";
|
||||
const string PolicyName = "Policy";
|
||||
const string TypeName = "Type";
|
||||
const string AllowName = "Allow";
|
||||
|
||||
/// <summary>
|
||||
/// <see langword="null"/> if not installed
|
||||
@@ -52,6 +55,18 @@ static class RegistryUtils
|
||||
return devicesKey.OpenSubKey(guid.ToString("B"), writable);
|
||||
}
|
||||
|
||||
static RegistryKey GetPolicyKey(bool writable)
|
||||
{
|
||||
return BaseKey(writable).OpenSubKey(PolicyName, writable)
|
||||
?? throw new UnexpectedResultException("Registry key not found; try reinstalling the software.");
|
||||
}
|
||||
|
||||
static RegistryKey? GetPolicyRuleKey(Guid guid, bool writable)
|
||||
{
|
||||
using var devicesKey = GetPolicyKey(writable);
|
||||
return devicesKey.OpenSubKey(guid.ToString("B"), writable);
|
||||
}
|
||||
|
||||
public static void Persist(string instanceId, string description)
|
||||
{
|
||||
var guid = Guid.NewGuid();
|
||||
@@ -224,4 +239,92 @@ static class RegistryUtils
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static Guid AddPolicyRule(PolicyRule rule)
|
||||
{
|
||||
if (!rule.IsValid())
|
||||
{
|
||||
throw new ArgumentException("Invalid policy rule", nameof(rule));
|
||||
}
|
||||
if (GetPolicyRules().ContainsValue(rule))
|
||||
{
|
||||
throw new ArgumentException("Duplicate policy rule", nameof(rule));
|
||||
}
|
||||
var guid = Guid.NewGuid();
|
||||
using var ruleKey = GetPolicyKey(true).CreateSubKey($"{guid:B}");
|
||||
ruleKey.SetValue(AllowName, rule.Allow ? 1 : 0);
|
||||
ruleKey.SetValue(TypeName, rule.Type.ToString());
|
||||
rule.Save(ruleKey);
|
||||
return guid;
|
||||
}
|
||||
|
||||
public static void RemovePolicyRule(Guid guid)
|
||||
{
|
||||
using var policyKey = GetPolicyKey(true);
|
||||
policyKey.DeleteSubKeyTree(guid.ToString("B"), false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enumerates all rules.
|
||||
/// <para>
|
||||
/// This retrieves the entire (valid) registry state; it ignores invalid rules, as well as any duplicates.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static SortedDictionary<Guid, PolicyRule> GetPolicyRules()
|
||||
{
|
||||
var guids = new SortedSet<Guid>();
|
||||
using var policyKey = GetPolicyKey(false);
|
||||
foreach (var subKeyName in policyKey.GetSubKeyNames())
|
||||
{
|
||||
if (Guid.TryParseExact(subKeyName, "B", out var guid))
|
||||
{
|
||||
// Sanitize uniqueness.
|
||||
guids.Add(guid);
|
||||
}
|
||||
}
|
||||
|
||||
var rules = new SortedDictionary<Guid, PolicyRule>();
|
||||
foreach (var guid in guids)
|
||||
{
|
||||
using var ruleKey = GetPolicyRuleKey(guid, false);
|
||||
if (ruleKey is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (ruleKey.GetValue(AllowName) is not int allowValue)
|
||||
{
|
||||
// Must exist and be a DWORD.
|
||||
continue;
|
||||
}
|
||||
var allow = (allowValue != 0);
|
||||
if (!Enum.TryParse<PolicyRuleType>(ruleKey.GetValue(TypeName) as string, true, out var ruleType))
|
||||
{
|
||||
// Must exist and be a valid enum string.
|
||||
continue;
|
||||
}
|
||||
PolicyRule rule;
|
||||
switch (ruleType)
|
||||
{
|
||||
case PolicyRuleType.Bind:
|
||||
rule = PolicyRuleBind.Load(allow, ruleKey);
|
||||
break;
|
||||
default:
|
||||
// Invalid, ignore.
|
||||
continue;
|
||||
}
|
||||
if (!rule.IsValid())
|
||||
{
|
||||
// Invalid, ignore.
|
||||
continue;
|
||||
}
|
||||
if (rules.ContainsValue(rule))
|
||||
{
|
||||
// Duplicate, ignore.
|
||||
continue;
|
||||
}
|
||||
rules.Add(guid, rule);
|
||||
}
|
||||
// All unique and valid.
|
||||
return rules;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user