Skip to content

Commit 04cb533

Browse files
authored
Feature: Delete / Enable / Disable multiple firewall rules (#3627)
* Feature: Delete / Enable / Disable multiple firewall rules * Feature: Firewall bulk actions
1 parent ccf1475 commit 04cb533

10 files changed

Lines changed: 341 additions & 144 deletions

File tree

‎Source/NETworkManager.Localization/Resources/Strings.Designer.cs‎

Lines changed: 29 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Source/NETworkManager.Localization/Resources/Strings.resx‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4729,4 +4729,15 @@ Cloudflare may log your IP address and network information. See Cloudflare's pri
47294729
<data name="CopyRule" xml:space="preserve">
47304730
<value>Copy rule</value>
47314731
</data>
4732+
<data name="DeleteFirewallRulesMessage" xml:space="preserve">
4733+
<value>The selected firewall rules are permanently deleted:
4734+
4735+
{0}</value>
4736+
</data>
4737+
<data name="DeleteRules" xml:space="preserve">
4738+
<value>Delete rules</value>
4739+
</data>
4740+
<data name="DeleteRulesX" xml:space="preserve">
4741+
<value>Delete rules ({0})</value>
4742+
</data>
47324743
</root>

‎Source/NETworkManager.Models/Firewall/Firewall.cs‎

Lines changed: 59 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -168,54 +168,77 @@ public static async Task<List<FirewallRule>> GetRulesAsync()
168168
}
169169

170170
/// <summary>
171-
/// Enables or disables the given <paramref name="rule"/> by running
172-
/// <c>Enable-NetFirewallRule</c> or <c>Disable-NetFirewallRule</c> against
173-
/// the rule's internal <see cref="FirewallRule.Id"/>.
171+
/// Enables or disables the given <paramref name="rules"/> by running
172+
/// <c>Enable-NetFirewallRule</c> or <c>Disable-NetFirewallRule</c> against the rules'
173+
/// internal <see cref="FirewallRule.Id"/>. The cmdlet is run for all rules (also for rules that already
174+
/// seem to be in the target state, since the state may have changed outside the application).
175+
/// Nothing is changed if one of the rules no longer exists.
174176
/// </summary>
175-
/// <param name="rule">
176-
/// The firewall rule to modify.
177+
/// <param name="rules">
178+
/// The firewall rules to modify.
177179
/// </param>
178180
/// <param name="enabled">
179-
/// <see langword="true"/> to enable the rule; <see langword="false"/> to disable it.
181+
/// <see langword="true"/> to enable the rules; <see langword="false"/> to disable them.
180182
/// </param>
181183
/// <exception cref="Exception">
182-
/// Thrown when the PowerShell pipeline reports one or more errors.
184+
/// Thrown when a rule no longer exists or the PowerShell pipeline reports one or more errors.
183185
/// </exception>
184-
public static async Task SetRuleEnabledAsync(FirewallRule rule, bool enabled)
186+
public static Task SetRulesEnabledAsync(IEnumerable<FirewallRule> rules, bool enabled)
185187
{
186-
await RunspaceLock.WaitAsync();
187-
try
188-
{
189-
await Task.Run(() =>
190-
{
191-
using var ps = SMA.PowerShell.Create();
192-
ps.Runspace = SharedRunspace;
193-
194-
ps.AddScript($@"{(enabled ? "Enable" : "Disable")}-NetFirewallRule -Name '{rule.Id}'");
195-
ps.Invoke();
196-
197-
if (ps.Streams.Error.Count > 0)
198-
throw new Exception(string.Join("; ", ps.Streams.Error));
199-
});
200-
}
201-
finally
202-
{
203-
RunspaceLock.Release();
204-
}
188+
return InvokeRuleCommandAsync(enabled ? "Enable-NetFirewallRule" : "Disable-NetFirewallRule",
189+
rules.Select(rule => rule.Id).ToList());
205190
}
206191

207192
/// <summary>
208-
/// Permanently removes the given <paramref name="rule"/> by running
209-
/// <c>Remove-NetFirewallRule</c> against the rule's internal <see cref="FirewallRule.Id"/>.
193+
/// Permanently removes the given <paramref name="rule"/>.
210194
/// </summary>
211195
/// <param name="rule">
212196
/// The firewall rule to delete.
213197
/// </param>
214198
/// <exception cref="Exception">
215-
/// Thrown when the PowerShell pipeline reports one or more errors.
199+
/// Thrown when the rule no longer exists or the PowerShell pipeline reports one or more errors.
200+
/// </exception>
201+
public static Task DeleteRuleAsync(FirewallRule rule)
202+
{
203+
return DeleteRulesAsync([rule]);
204+
}
205+
206+
/// <summary>
207+
/// Permanently removes the given <paramref name="rules"/> by running
208+
/// <c>Remove-NetFirewallRule</c> against the rules' internal <see cref="FirewallRule.Id"/>.
209+
/// Nothing is deleted if one of the rules no longer exists.
210+
/// </summary>
211+
/// <param name="rules">
212+
/// The firewall rules to delete.
213+
/// </param>
214+
/// <exception cref="Exception">
215+
/// Thrown when a rule no longer exists or the PowerShell pipeline reports one or more errors.
216+
/// </exception>
217+
public static Task DeleteRulesAsync(IEnumerable<FirewallRule> rules)
218+
{
219+
return InvokeRuleCommandAsync("Remove-NetFirewallRule", rules.Select(rule => rule.Id).ToList());
220+
}
221+
222+
/// <summary>
223+
/// Runs the given firewall cmdlet (e.g. <c>Enable-NetFirewallRule</c>) for all rule IDs in a
224+
/// single PowerShell call. It is checked first that all rules still exist, otherwise nothing is changed.
225+
/// This is not a transaction: if the cmdlet fails for a single rule (e.g. access denied), the other
226+
/// rules may already be changed.
227+
/// </summary>
228+
/// <param name="command">
229+
/// The cmdlet to run. Must accept the rule IDs via <c>-Name</c>.
230+
/// </param>
231+
/// <param name="ids">
232+
/// The internal IDs of the rules. Nothing is done if the list is empty.
233+
/// </param>
234+
/// <exception cref="Exception">
235+
/// Thrown when a rule no longer exists or the PowerShell pipeline reports one or more errors.
216236
/// </exception>
217-
public static async Task DeleteRuleAsync(FirewallRule rule)
237+
private static async Task InvokeRuleCommandAsync(string command, IReadOnlyCollection<string> ids)
218238
{
239+
if (ids.Count == 0)
240+
return;
241+
219242
await RunspaceLock.WaitAsync();
220243
try
221244
{
@@ -224,7 +247,11 @@ await Task.Run(() =>
224247
using var ps = SMA.PowerShell.Create();
225248
ps.Runspace = SharedRunspace;
226249

227-
ps.AddScript($@"Remove-NetFirewallRule -Name '{rule.Id}'");
250+
// Check that all rules still exist before changing anything
251+
ps.AddScript($@"
252+
$ids = {ToPsArray(ids)}
253+
Get-NetFirewallRule -Name $ids -ErrorAction Stop | Out-Null
254+
{command} -Name $ids");
228255
ps.Invoke();
229256

230257
if (ps.Streams.Error.Count > 0)

‎Source/NETworkManager/DialogHelper.cs‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
using NETworkManager.Utilities;
55
using NETworkManager.ViewModels;
66
using NETworkManager.Views;
7+
using System;
8+
using System.Collections.Generic;
9+
using System.Linq;
710
using System.Threading.Tasks;
811
using System.Windows;
912

@@ -94,5 +97,24 @@ public static async Task<bool> ShowConfirmationMessageAsync(Window parentWindow,
9497

9598
return result;
9699
}
100+
101+
/// <summary>
102+
/// Formats the given items as a list (one item per line) to show them in a dialog message,
103+
/// e.g. the entries that will be deleted. Only the first <paramref name="maxItems"/> items are
104+
/// listed, the remaining items are summarized as "(+X more)".
105+
/// </summary>
106+
/// <param name="items">The items to list.</param>
107+
/// <param name="maxItems">The maximum number of items to list.</param>
108+
/// <returns>The formatted list.</returns>
109+
public static string FormatItemList(IEnumerable<string> items, int maxItems = 10)
110+
{
111+
var itemList = items.ToList();
112+
var lines = itemList.Take(maxItems).ToList();
113+
114+
if (itemList.Count > maxItems)
115+
lines.Add(string.Format(Strings.PlusXMore, itemList.Count - maxItems));
116+
117+
return string.Join(Environment.NewLine, lines);
118+
}
97119
}
98120
}

0 commit comments

Comments
 (0)