Skip to content

Commit 908f891

Browse files
Add system-wide policy configuration for centralized update control (#3313)
* Initial plan * Add system-wide config feature to disable update checks Co-authored-by: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> * Add documentation for system-wide config feature Co-authored-by: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> * Fix documentation formatting in config README Co-authored-by: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> * Rename ConfigManager to PolicyManager to avoid naming conflict Co-authored-by: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> * Add config.json.example to installation and UI for policy-managed settings Co-authored-by: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> * Add localization string for policy-managed setting message Co-authored-by: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> * Improve accessibility with icon and add documentation to config example Co-authored-by: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> * Chore: Move config file to settings namespace * Rename policy property to Update_CheckForUpdatesAtStartup and allow enabling/disabling Co-authored-by: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> * Chore: Minor color adjustments * Use direct pattern for policy check and remove helper property Co-authored-by: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> * Add Docusaurus documentation for system-wide policies and remove old docs Co-authored-by: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> * Fix changelog PR number placeholder Co-authored-by: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> * Update system-wide-policies.md * Move policy property details to settings/update.md to avoid duplication Co-authored-by: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> * Feature: System wide policy * Fix Docusaurus build error with react-image-gallery CSS import Co-authored-by: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> * Update browserslist database to latest version Co-authored-by: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> * Add null-safety checks for PolicyManager deserialization Co-authored-by: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> * Fix react-image-gallery CSS import path for v2.0+ Co-authored-by: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> * Fix React version mismatch - update react-dom to 19.2.4 Co-authored-by: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> * Update yarn.lock for react-dom version change Co-authored-by: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> * Load PolicyManager in SettingsManager.Initialize() to fix reset settings issue Co-authored-by: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> * Move PolicyManager.Load() to App.xaml.cs to avoid duplicate calls Co-authored-by: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> * Chore: Refactoring & dotnet format * Update MainWindow.xaml.cs --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com>
1 parent 00b93ee commit 908f891

File tree

22 files changed

+405
-91
lines changed

22 files changed

+405
-91
lines changed

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

Lines changed: 9 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: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3960,4 +3960,7 @@ If you click Cancel, the profile file will remain unencrypted.</value>
39603960
<data name="HelpMessage_MaximumNumberOfBackups" xml:space="preserve">
39613961
<value>Number of backups that are retained before the oldest one is deleted.</value>
39623962
</data>
3963+
<data name="SettingManagedByAdministrator" xml:space="preserve">
3964+
<value>This setting is managed by your administrator.</value>
3965+
</data>
39633966
</root>

Source/NETworkManager.Models/Network/NetworkInterface.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public sealed class NetworkInterface
3939
"Npcap Packet Driver (NPCAP)",
4040
"QoS Packet Scheduler",
4141
"WFP 802.3 MAC Layer LightWeight Filter",
42-
"Ethernet (Kerneldebugger)",
42+
"Ethernet (Kerneldebugger)",
4343
"Filter Driver",
4444
"WAN Miniport",
4545
"Microsoft Wi-Fi Direct Virtual Adapter"
@@ -84,8 +84,8 @@ public static List<NetworkInterfaceInfo> GetNetworkInterfaces()
8484
// Filter out virtual/filter adapters introduced in .NET 9/10
8585
// Check if the adapter name or description contains any filtered pattern
8686
// See: https://github.com/dotnet/runtime/issues/122751
87-
if (NetworkInterfaceFilteredPatterns.Any(pattern =>
88-
networkInterface.Name.Contains(pattern) ||
87+
if (NetworkInterfaceFilteredPatterns.Any(pattern =>
88+
networkInterface.Name.Contains(pattern) ||
8989
networkInterface.Description.Contains(pattern)))
9090
continue;
9191

Source/NETworkManager.Profiles/ProfileManager.cs

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -731,7 +731,7 @@ private static void Load(ProfileFileInfo profileFileInfo)
731731

732732
if (loadedProfileUpdated)
733733
LoadedProfileFileChanged(LoadedProfileFile, true);
734-
734+
735735
// Notify subscribers that profiles have been loaded/updated
736736
ProfilesUpdated(false);
737737
}
@@ -994,7 +994,7 @@ private static void AddGroups(List<GroupInfo> groups, bool profilesChanged = tru
994994

995995
ProfilesUpdated(profilesChanged);
996996
}
997-
997+
998998
/// <summary>
999999
/// Method to add a <see cref="GroupInfo" /> to the loaded profile data.
10001000
/// </summary>
@@ -1022,7 +1022,7 @@ public static GroupInfo GetGroupByName(string name)
10221022

10231023

10241024
var group = LoadedProfileFileData.Groups.FirstOrDefault(x => x.Name.Equals(name));
1025-
1025+
10261026
if (group == null)
10271027
throw new InvalidOperationException($"Group '{name}' not found.");
10281028

@@ -1089,12 +1089,12 @@ public static bool GroupExists(string name)
10891089
public static bool IsGroupEmpty(string name)
10901090
{
10911091
ArgumentException.ThrowIfNullOrWhiteSpace(name);
1092-
1092+
10931093
var group = LoadedProfileFileData.Groups.FirstOrDefault(x => x.Name == name);
1094-
1094+
10951095
if (group == null)
10961096
throw new InvalidOperationException($"Group '{name}' not found.");
1097-
1097+
10981098
return group.Profiles.Count == 0;
10991099
}
11001100

@@ -1118,7 +1118,7 @@ public static void AddProfile(ProfileInfo profile)
11181118
AddGroup(new GroupInfo(profile.Group));
11191119

11201120
var group = LoadedProfileFileData.Groups.FirstOrDefault(x => x.Name.Equals(profile.Group));
1121-
1121+
11221122
if (group == null)
11231123
throw new InvalidOperationException($"Group '{profile.Group}' not found for profile after creation attempt.");
11241124

@@ -1144,7 +1144,7 @@ public static void ReplaceProfile(ProfileInfo oldProfile, ProfileInfo newProfile
11441144

11451145
// Remove from old group
11461146
var oldGroup = LoadedProfileFileData.Groups.FirstOrDefault(x => x.Name.Equals(oldProfile.Group));
1147-
1147+
11481148
if (oldGroup == null)
11491149
throw new InvalidOperationException($"Group '{oldProfile.Group}' not found for old profile.");
11501150

@@ -1155,7 +1155,7 @@ public static void ReplaceProfile(ProfileInfo oldProfile, ProfileInfo newProfile
11551155
AddGroup(new GroupInfo(newProfile.Group));
11561156

11571157
var newGroup = LoadedProfileFileData.Groups.FirstOrDefault(x => x.Name.Equals(newProfile.Group));
1158-
1158+
11591159
if (newGroup == null)
11601160
throw new InvalidOperationException($"Group '{newProfile.Group}' not found for new profile after creation attempt.");
11611161

@@ -1177,7 +1177,7 @@ public static void RemoveProfile(ProfileInfo profile)
11771177
ArgumentException.ThrowIfNullOrWhiteSpace(profile.Group, nameof(profile));
11781178

11791179
var group = LoadedProfileFileData.Groups.FirstOrDefault(x => x.Name.Equals(profile.Group));
1180-
1180+
11811181
if (group == null)
11821182
throw new InvalidOperationException($"Group '{profile.Group}' not found.");
11831183

@@ -1205,7 +1205,7 @@ public static void RemoveProfiles(IEnumerable<ProfileInfo> profiles)
12051205
}
12061206

12071207
var group = LoadedProfileFileData.Groups.FirstOrDefault(x => x.Name.Equals(profile.Group));
1208-
1208+
12091209
if (group == null)
12101210
{
12111211
Log.Warn($"RemoveProfiles: Group '{profile.Group}' not found for profile '{profile.Name ?? "<unnamed>"}'.");

Source/NETworkManager.Settings/GlobalStaticConfiguration.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ public static class GlobalStaticConfiguration
8787
// Settings: Settings
8888
public static bool Settings_IsDailyBackupEnabled => true;
8989
public static int Settings_MaximumNumberOfBackups => 10;
90-
90+
9191
// Application: Dashboard
9292
public static string Dashboard_PublicIPv4Address => "1.1.1.1";
9393
public static string Dashboard_PublicIPv6Address => "2606:4700:4700::1111";

Source/NETworkManager.Settings/NETworkManager.Settings.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@
2222
<Compile Include="..\GlobalAssemblyInfo.cs" Link="Properties\GlobalAssemblyInfo.cs" />
2323
</ItemGroup>
2424
<ItemGroup>
25+
<Content Include="config.json.example">
26+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
27+
</Content>
2528
<Content Include="Themes\Dark.Accent1.xaml">
2629
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
2730
<SubType>Designer</SubType>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace NETworkManager.Settings;
4+
5+
/// <summary>
6+
/// Class that represents system-wide policies that override user settings.
7+
/// This configuration is loaded from a config.json file in the application directory.
8+
/// </summary>
9+
public class PolicyInfo
10+
{
11+
[JsonPropertyName("Update_CheckForUpdatesAtStartup")]
12+
public bool? Update_CheckForUpdatesAtStartup { get; set; }
13+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
using log4net;
2+
using System;
3+
using System.IO;
4+
using System.Text.Json;
5+
using System.Text.Json.Serialization;
6+
7+
namespace NETworkManager.Settings;
8+
9+
/// <summary>
10+
/// Manager for system-wide policies that are loaded from a config.json file
11+
/// in the application directory. These policies override user settings.
12+
/// </summary>
13+
public static class PolicyManager
14+
{
15+
#region Variables
16+
17+
/// <summary>
18+
/// Logger for logging.
19+
/// </summary>
20+
private static readonly ILog Log = LogManager.GetLogger(typeof(PolicyManager));
21+
22+
/// <summary>
23+
/// Config file name.
24+
/// </summary>
25+
private static string ConfigFileName => "config.json";
26+
27+
/// <summary>
28+
/// System-wide policies that are currently loaded.
29+
/// </summary>
30+
public static PolicyInfo Current { get; private set; }
31+
32+
/// <summary>
33+
/// JSON serializer options for consistent serialization/deserialization.
34+
/// </summary>
35+
private static readonly JsonSerializerOptions JsonOptions = new()
36+
{
37+
WriteIndented = true,
38+
PropertyNameCaseInsensitive = true,
39+
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
40+
Converters = { new JsonStringEnumConverter() }
41+
};
42+
43+
#endregion
44+
45+
#region Methods
46+
47+
/// <summary>
48+
/// Method to get the config file path in the application directory.
49+
/// </summary>
50+
/// <returns>Config file path.</returns>
51+
private static string GetConfigFilePath()
52+
{
53+
return Path.Combine(AssemblyManager.Current.Location, ConfigFileName);
54+
}
55+
56+
/// <summary>
57+
/// Method to load the system-wide policies from config.json file in the application directory.
58+
/// </summary>
59+
public static void Load()
60+
{
61+
var filePath = GetConfigFilePath();
62+
63+
// Check if config file exists
64+
if (File.Exists(filePath))
65+
{
66+
try
67+
{
68+
Log.Info($"Loading system-wide policies from: {filePath}");
69+
70+
var jsonString = File.ReadAllText(filePath);
71+
72+
// Treat empty or JSON "null" as "no policies" instead of crashing
73+
if (string.IsNullOrWhiteSpace(jsonString))
74+
{
75+
Current = new PolicyInfo();
76+
Log.Info("Config file is empty, no system-wide policies loaded.");
77+
}
78+
else
79+
{
80+
Current = JsonSerializer.Deserialize<PolicyInfo>(jsonString, JsonOptions) ?? new PolicyInfo();
81+
82+
Log.Info("System-wide policies loaded successfully.");
83+
84+
// Log enabled settings
85+
Log.Info($"System-wide policy - Update_CheckForUpdatesAtStartup: {Current.Update_CheckForUpdatesAtStartup?.ToString() ?? "Not set"}");
86+
}
87+
}
88+
catch (Exception ex)
89+
{
90+
Log.Error($"Failed to load system-wide policies from: {filePath}", ex);
91+
Current = new PolicyInfo();
92+
}
93+
}
94+
else
95+
{
96+
Log.Debug($"No system-wide policy file found at: {filePath}");
97+
Current = new PolicyInfo();
98+
}
99+
}
100+
101+
#endregion
102+
}

Source/NETworkManager.Settings/SettingsManager.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ private static SettingsInfo DeserializeFromXmlFile(string filePath)
228228
/// Method to save the currently loaded settings (to a file).
229229
/// </summary>
230230
public static void Save()
231-
{
231+
{
232232
// Create the directory if it does not exist
233233
Directory.CreateDirectory(GetSettingsFolderLocation());
234234

@@ -282,7 +282,7 @@ private static void CreateDailyBackupIfNeeded()
282282
// Create backup if needed
283283
var currentDate = DateTime.Now.Date;
284284
var lastBackupDate = Current.LastBackup.Date;
285-
285+
286286
if (lastBackupDate < currentDate)
287287
{
288288
Log.Info("Creating daily backup of settings...");
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"Update_CheckForUpdatesAtStartup": false
3+
}

0 commit comments

Comments
 (0)