diff --git a/Source/NETworkManager.Localization/Resources/Strings.Designer.cs b/Source/NETworkManager.Localization/Resources/Strings.Designer.cs index b57b12bff7..8fa04498df 100644 --- a/Source/NETworkManager.Localization/Resources/Strings.Designer.cs +++ b/Source/NETworkManager.Localization/Resources/Strings.Designer.cs @@ -9271,6 +9271,15 @@ public static string SettingsHaveBeenReset { } } + /// + /// Sucht eine lokalisierte Zeichenfolge, die This setting is managed by your administrator. ähnelt. + /// + public static string SettingManagedByAdministrator { + get { + return ResourceManager.GetString("SettingManagedByAdministrator", resourceCulture); + } + } + /// /// Sucht eine lokalisierte Zeichenfolge, die Appearance ähnelt. /// diff --git a/Source/NETworkManager.Localization/Resources/Strings.resx b/Source/NETworkManager.Localization/Resources/Strings.resx index 3dd4967eaf..e4f0fef96e 100644 --- a/Source/NETworkManager.Localization/Resources/Strings.resx +++ b/Source/NETworkManager.Localization/Resources/Strings.resx @@ -3960,4 +3960,7 @@ If you click Cancel, the profile file will remain unencrypted. Number of backups that are retained before the oldest one is deleted. + + This setting is managed by your administrator. + \ No newline at end of file diff --git a/Source/NETworkManager.Models/Network/NetworkInterface.cs b/Source/NETworkManager.Models/Network/NetworkInterface.cs index 520b1135f1..6fbc000e2b 100644 --- a/Source/NETworkManager.Models/Network/NetworkInterface.cs +++ b/Source/NETworkManager.Models/Network/NetworkInterface.cs @@ -39,7 +39,7 @@ public sealed class NetworkInterface "Npcap Packet Driver (NPCAP)", "QoS Packet Scheduler", "WFP 802.3 MAC Layer LightWeight Filter", - "Ethernet (Kerneldebugger)", + "Ethernet (Kerneldebugger)", "Filter Driver", "WAN Miniport", "Microsoft Wi-Fi Direct Virtual Adapter" @@ -84,8 +84,8 @@ public static List GetNetworkInterfaces() // Filter out virtual/filter adapters introduced in .NET 9/10 // Check if the adapter name or description contains any filtered pattern // See: https://github.com/dotnet/runtime/issues/122751 - if (NetworkInterfaceFilteredPatterns.Any(pattern => - networkInterface.Name.Contains(pattern) || + if (NetworkInterfaceFilteredPatterns.Any(pattern => + networkInterface.Name.Contains(pattern) || networkInterface.Description.Contains(pattern))) continue; diff --git a/Source/NETworkManager.Profiles/ProfileManager.cs b/Source/NETworkManager.Profiles/ProfileManager.cs index 29ff30c471..d9c0d33891 100644 --- a/Source/NETworkManager.Profiles/ProfileManager.cs +++ b/Source/NETworkManager.Profiles/ProfileManager.cs @@ -731,7 +731,7 @@ private static void Load(ProfileFileInfo profileFileInfo) if (loadedProfileUpdated) LoadedProfileFileChanged(LoadedProfileFile, true); - + // Notify subscribers that profiles have been loaded/updated ProfilesUpdated(false); } @@ -994,7 +994,7 @@ private static void AddGroups(List groups, bool profilesChanged = tru ProfilesUpdated(profilesChanged); } - + /// /// Method to add a to the loaded profile data. /// @@ -1022,7 +1022,7 @@ public static GroupInfo GetGroupByName(string name) var group = LoadedProfileFileData.Groups.FirstOrDefault(x => x.Name.Equals(name)); - + if (group == null) throw new InvalidOperationException($"Group '{name}' not found."); @@ -1089,12 +1089,12 @@ public static bool GroupExists(string name) public static bool IsGroupEmpty(string name) { ArgumentException.ThrowIfNullOrWhiteSpace(name); - + var group = LoadedProfileFileData.Groups.FirstOrDefault(x => x.Name == name); - + if (group == null) throw new InvalidOperationException($"Group '{name}' not found."); - + return group.Profiles.Count == 0; } @@ -1118,7 +1118,7 @@ public static void AddProfile(ProfileInfo profile) AddGroup(new GroupInfo(profile.Group)); var group = LoadedProfileFileData.Groups.FirstOrDefault(x => x.Name.Equals(profile.Group)); - + if (group == null) throw new InvalidOperationException($"Group '{profile.Group}' not found for profile after creation attempt."); @@ -1144,7 +1144,7 @@ public static void ReplaceProfile(ProfileInfo oldProfile, ProfileInfo newProfile // Remove from old group var oldGroup = LoadedProfileFileData.Groups.FirstOrDefault(x => x.Name.Equals(oldProfile.Group)); - + if (oldGroup == null) throw new InvalidOperationException($"Group '{oldProfile.Group}' not found for old profile."); @@ -1155,7 +1155,7 @@ public static void ReplaceProfile(ProfileInfo oldProfile, ProfileInfo newProfile AddGroup(new GroupInfo(newProfile.Group)); var newGroup = LoadedProfileFileData.Groups.FirstOrDefault(x => x.Name.Equals(newProfile.Group)); - + if (newGroup == null) throw new InvalidOperationException($"Group '{newProfile.Group}' not found for new profile after creation attempt."); @@ -1177,7 +1177,7 @@ public static void RemoveProfile(ProfileInfo profile) ArgumentException.ThrowIfNullOrWhiteSpace(profile.Group, nameof(profile)); var group = LoadedProfileFileData.Groups.FirstOrDefault(x => x.Name.Equals(profile.Group)); - + if (group == null) throw new InvalidOperationException($"Group '{profile.Group}' not found."); @@ -1205,7 +1205,7 @@ public static void RemoveProfiles(IEnumerable profiles) } var group = LoadedProfileFileData.Groups.FirstOrDefault(x => x.Name.Equals(profile.Group)); - + if (group == null) { Log.Warn($"RemoveProfiles: Group '{profile.Group}' not found for profile '{profile.Name ?? ""}'."); diff --git a/Source/NETworkManager.Settings/GlobalStaticConfiguration.cs b/Source/NETworkManager.Settings/GlobalStaticConfiguration.cs index 851a5c233c..61260da4e5 100644 --- a/Source/NETworkManager.Settings/GlobalStaticConfiguration.cs +++ b/Source/NETworkManager.Settings/GlobalStaticConfiguration.cs @@ -87,7 +87,7 @@ public static class GlobalStaticConfiguration // Settings: Settings public static bool Settings_IsDailyBackupEnabled => true; public static int Settings_MaximumNumberOfBackups => 10; - + // Application: Dashboard public static string Dashboard_PublicIPv4Address => "1.1.1.1"; public static string Dashboard_PublicIPv6Address => "2606:4700:4700::1111"; diff --git a/Source/NETworkManager.Settings/NETworkManager.Settings.csproj b/Source/NETworkManager.Settings/NETworkManager.Settings.csproj index eb78d87f4d..9d4f289bd3 100644 --- a/Source/NETworkManager.Settings/NETworkManager.Settings.csproj +++ b/Source/NETworkManager.Settings/NETworkManager.Settings.csproj @@ -22,6 +22,9 @@ + + PreserveNewest + PreserveNewest Designer diff --git a/Source/NETworkManager.Settings/PolicyInfo.cs b/Source/NETworkManager.Settings/PolicyInfo.cs new file mode 100644 index 0000000000..7b9b64a6a0 --- /dev/null +++ b/Source/NETworkManager.Settings/PolicyInfo.cs @@ -0,0 +1,13 @@ +using System.Text.Json.Serialization; + +namespace NETworkManager.Settings; + +/// +/// Class that represents system-wide policies that override user settings. +/// This configuration is loaded from a config.json file in the application directory. +/// +public class PolicyInfo +{ + [JsonPropertyName("Update_CheckForUpdatesAtStartup")] + public bool? Update_CheckForUpdatesAtStartup { get; set; } +} diff --git a/Source/NETworkManager.Settings/PolicyManager.cs b/Source/NETworkManager.Settings/PolicyManager.cs new file mode 100644 index 0000000000..7dd762c8de --- /dev/null +++ b/Source/NETworkManager.Settings/PolicyManager.cs @@ -0,0 +1,102 @@ +using log4net; +using System; +using System.IO; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace NETworkManager.Settings; + +/// +/// Manager for system-wide policies that are loaded from a config.json file +/// in the application directory. These policies override user settings. +/// +public static class PolicyManager +{ + #region Variables + + /// + /// Logger for logging. + /// + private static readonly ILog Log = LogManager.GetLogger(typeof(PolicyManager)); + + /// + /// Config file name. + /// + private static string ConfigFileName => "config.json"; + + /// + /// System-wide policies that are currently loaded. + /// + public static PolicyInfo Current { get; private set; } + + /// + /// JSON serializer options for consistent serialization/deserialization. + /// + private static readonly JsonSerializerOptions JsonOptions = new() + { + WriteIndented = true, + PropertyNameCaseInsensitive = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + Converters = { new JsonStringEnumConverter() } + }; + + #endregion + + #region Methods + + /// + /// Method to get the config file path in the application directory. + /// + /// Config file path. + private static string GetConfigFilePath() + { + return Path.Combine(AssemblyManager.Current.Location, ConfigFileName); + } + + /// + /// Method to load the system-wide policies from config.json file in the application directory. + /// + public static void Load() + { + var filePath = GetConfigFilePath(); + + // Check if config file exists + if (File.Exists(filePath)) + { + try + { + Log.Info($"Loading system-wide policies from: {filePath}"); + + var jsonString = File.ReadAllText(filePath); + + // Treat empty or JSON "null" as "no policies" instead of crashing + if (string.IsNullOrWhiteSpace(jsonString)) + { + Current = new PolicyInfo(); + Log.Info("Config file is empty, no system-wide policies loaded."); + } + else + { + Current = JsonSerializer.Deserialize(jsonString, JsonOptions) ?? new PolicyInfo(); + + Log.Info("System-wide policies loaded successfully."); + + // Log enabled settings + Log.Info($"System-wide policy - Update_CheckForUpdatesAtStartup: {Current.Update_CheckForUpdatesAtStartup?.ToString() ?? "Not set"}"); + } + } + catch (Exception ex) + { + Log.Error($"Failed to load system-wide policies from: {filePath}", ex); + Current = new PolicyInfo(); + } + } + else + { + Log.Debug($"No system-wide policy file found at: {filePath}"); + Current = new PolicyInfo(); + } + } + + #endregion +} diff --git a/Source/NETworkManager.Settings/SettingsManager.cs b/Source/NETworkManager.Settings/SettingsManager.cs index 34ff00a850..29a140218e 100644 --- a/Source/NETworkManager.Settings/SettingsManager.cs +++ b/Source/NETworkManager.Settings/SettingsManager.cs @@ -228,7 +228,7 @@ private static SettingsInfo DeserializeFromXmlFile(string filePath) /// Method to save the currently loaded settings (to a file). /// public static void Save() - { + { // Create the directory if it does not exist Directory.CreateDirectory(GetSettingsFolderLocation()); @@ -282,7 +282,7 @@ private static void CreateDailyBackupIfNeeded() // Create backup if needed var currentDate = DateTime.Now.Date; var lastBackupDate = Current.LastBackup.Date; - + if (lastBackupDate < currentDate) { Log.Info("Creating daily backup of settings..."); diff --git a/Source/NETworkManager.Settings/config.json.example b/Source/NETworkManager.Settings/config.json.example new file mode 100644 index 0000000000..8ca2bf05b3 --- /dev/null +++ b/Source/NETworkManager.Settings/config.json.example @@ -0,0 +1,3 @@ +{ + "Update_CheckForUpdatesAtStartup": false +} \ No newline at end of file diff --git a/Source/NETworkManager/App.xaml.cs b/Source/NETworkManager/App.xaml.cs index ede9bc5e36..42d0f8503e 100644 --- a/Source/NETworkManager/App.xaml.cs +++ b/Source/NETworkManager/App.xaml.cs @@ -19,13 +19,14 @@ namespace NETworkManager; * Class: App * 1) Get command line args * 2) Detect current configuration - * 3) Get assembly info - * 4) Load settings - * 5) Load localization / language + * 3) Get assembly info + * 4) Load system-wide policies + * 5) Load settings + * 6) Load localization / language * * Class: MainWindow - * 6) Load appearance - * 7) Load profiles + * 7) Load appearance + * 8) Load profiles */ public partial class App @@ -81,6 +82,9 @@ by BornToBeRoot Log.Info($"NETworkManager process with Pid {CommandLineManager.Current.RestartPid} has been exited."); } + // Load system-wide policies + PolicyManager.Load(); + // Load (or initialize) settings try { @@ -128,7 +132,7 @@ by BornToBeRoot Log.Info( $"Application localization culture has been set to {localizationManager.Current.Code} (Settings value is \"{SettingsManager.Current.Localization_CultureCode}\")."); - // Show (localized) help window + // Show help window if (CommandLineManager.Current.Help) { Log.Info("Set StartupUri to CommandLineWindow.xaml..."); @@ -149,70 +153,72 @@ by BornToBeRoot if (mutexIsAcquired) _mutex.ReleaseMutex(); - if (mutexIsAcquired || SettingsManager.Current.Window_MultipleInstances) + // If another instance is running, bring it to the foreground + if (!mutexIsAcquired && !SettingsManager.Current.Window_MultipleInstances) { - // Setup background job - if (SettingsManager.Current.General_BackgroundJobInterval != 0) - { - Log.Info( - $"Setup background job with interval {SettingsManager.Current.General_BackgroundJobInterval} minute(s)..."); - - _dispatcherTimer = new DispatcherTimer - { - Interval = TimeSpan.FromMinutes(SettingsManager.Current.General_BackgroundJobInterval) - }; - _dispatcherTimer.Tick += DispatcherTimer_Tick; - _dispatcherTimer.Start(); - } - else - { - Log.Info("Background job is disabled."); - } - - // Setup ThreadPool for the application - ThreadPool.GetMaxThreads(out var workerThreadsMax, out var completionPortThreadsMax); - ThreadPool.GetMinThreads(out var workerThreadsMin, out var completionPortThreadsMin); + // Bring the already running application into the foreground + Log.Info( + "Another NETworkManager process is already running. Trying to bring the window to the foreground..."); + SingleInstance.PostMessage(SingleInstance.HWND_BROADCAST, SingleInstance.WM_SHOWME, IntPtr.Zero, + IntPtr.Zero); - var workerThreadsMinNew = workerThreadsMin + SettingsManager.Current.General_ThreadPoolAdditionalMinThreads; - var completionPortThreadsMinNew = completionPortThreadsMin + - SettingsManager.Current.General_ThreadPoolAdditionalMinThreads; + // Close the application + _singleInstanceClose = true; + Shutdown(); - if (workerThreadsMinNew > workerThreadsMax) - workerThreadsMinNew = workerThreadsMax; + return; + } - if (completionPortThreadsMinNew > completionPortThreadsMax) - completionPortThreadsMinNew = completionPortThreadsMax; - if (ThreadPool.SetMinThreads(workerThreadsMinNew, completionPortThreadsMinNew)) - Log.Info( - $"ThreadPool min threads set to: workerThreads: {workerThreadsMinNew}, completionPortThreads: {completionPortThreadsMinNew}"); - else - Log.Warn( - $"ThreadPool min threads could not be set to workerThreads: {workerThreadsMinNew}, completionPortThreads: {completionPortThreadsMinNew}"); + // Setup background job + if (SettingsManager.Current.General_BackgroundJobInterval != 0) + { + Log.Info( + $"Setup background job with interval {SettingsManager.Current.General_BackgroundJobInterval} minute(s)..."); - // Show splash screen - if (SettingsManager.Current.SplashScreen_Enabled) + _dispatcherTimer = new DispatcherTimer { - Log.Info("Show SplashScreen while application is loading..."); - new SplashScreen(@"SplashScreen.png").Show(true, true); - } - - // Show main window - Log.Info("Set StartupUri to MainWindow.xaml..."); - StartupUri = new Uri("MainWindow.xaml", UriKind.Relative); + Interval = TimeSpan.FromMinutes(SettingsManager.Current.General_BackgroundJobInterval) + }; + _dispatcherTimer.Tick += DispatcherTimer_Tick; + _dispatcherTimer.Start(); } else { - // Bring the already running application into the foreground + Log.Info("Background job is disabled."); + } + + // Setup ThreadPool for the application + ThreadPool.GetMaxThreads(out var workerThreadsMax, out var completionPortThreadsMax); + ThreadPool.GetMinThreads(out var workerThreadsMin, out var completionPortThreadsMin); + + var workerThreadsMinNew = workerThreadsMin + SettingsManager.Current.General_ThreadPoolAdditionalMinThreads; + var completionPortThreadsMinNew = completionPortThreadsMin + + SettingsManager.Current.General_ThreadPoolAdditionalMinThreads; + + if (workerThreadsMinNew > workerThreadsMax) + workerThreadsMinNew = workerThreadsMax; + + if (completionPortThreadsMinNew > completionPortThreadsMax) + completionPortThreadsMinNew = completionPortThreadsMax; + + if (ThreadPool.SetMinThreads(workerThreadsMinNew, completionPortThreadsMinNew)) Log.Info( - "Another NETworkManager process is already running. Trying to bring the window to the foreground..."); - SingleInstance.PostMessage(SingleInstance.HWND_BROADCAST, SingleInstance.WM_SHOWME, IntPtr.Zero, - IntPtr.Zero); + $"ThreadPool min threads set to: workerThreads: {workerThreadsMinNew}, completionPortThreads: {completionPortThreadsMinNew}"); + else + Log.Warn( + $"ThreadPool min threads could not be set to workerThreads: {workerThreadsMinNew}, completionPortThreads: {completionPortThreadsMinNew}"); - // Close the application - _singleInstanceClose = true; - Shutdown(); + // Show splash screen + if (SettingsManager.Current.SplashScreen_Enabled) + { + Log.Info("Show SplashScreen while application is loading..."); + new SplashScreen(@"SplashScreen.png").Show(true, true); } + + // Show main window + Log.Info("Set StartupUri to MainWindow.xaml..."); + StartupUri = new Uri("MainWindow.xaml", UriKind.Relative); } /// @@ -236,6 +242,11 @@ private void HandleCorruptedSettingsFile() ConfigurationManager.Current.ShowSettingsResetNoteOnStartup = true; } + /// + /// Handles the tick event of the dispatcher timer to trigger a background job and save data. + /// + /// The source of the event, typically the dispatcher timer instance. + /// The event data associated with the timer tick. private void DispatcherTimer_Tick(object sender, EventArgs e) { Log.Info("Run background job..."); @@ -243,6 +254,14 @@ private void DispatcherTimer_Tick(object sender, EventArgs e) Save(); } + /// + /// Handles the session ending event by canceling the session termination and initiating application shutdown. + /// + /// This method overrides the default session ending behavior to prevent the session from ending + /// automatically. Instead, it cancels the session termination and shuts down the application gracefully. Use this + /// override to implement custom shutdown logic when the user logs off or shuts down the system. + /// The event data for the session ending event. Provides information about the session ending request and allows + /// cancellation of the event. protected override void OnSessionEnding(SessionEndingCancelEventArgs e) { base.OnSessionEnding(e); @@ -252,6 +271,15 @@ protected override void OnSessionEnding(SessionEndingCancelEventArgs e) Shutdown(); } + /// + /// Handles the application exit event to perform cleanup operations such as stopping background tasks and saving + /// settings. + /// + /// This method is intended to be called when the application is shutting down. It ensures that + /// any running background jobs are stopped and application settings are saved, unless the application is closed due + /// to a single instance or help command scenario. + /// The source of the event, typically the application instance. + /// The event data associated with the application exit event. private void Application_Exit(object sender, ExitEventArgs e) { Log.Info("Exiting NETworkManager..."); @@ -269,6 +297,12 @@ private void Application_Exit(object sender, ExitEventArgs e) Log.Info("Bye!"); } + /// + /// Saves application settings and profile data if changes have been detected. + /// + /// This method saves settings only if they have been modified. Profile data is saved if a + /// profile file is loaded, its data is available, and changes have been made. If no profile file is loaded or the + /// file is encrypted and not unlocked, profile data will not be saved and a warning is logged. private void Save() { // Save settings if they have changed diff --git a/Source/NETworkManager/MainWindow.xaml.cs b/Source/NETworkManager/MainWindow.xaml.cs index 6546c727be..3ae9740f5e 100644 --- a/Source/NETworkManager/MainWindow.xaml.cs +++ b/Source/NETworkManager/MainWindow.xaml.cs @@ -561,8 +561,11 @@ private void Load() NetworkChange.NetworkAddressChanged += (_, _) => OnNetworkHasChanged(); // Search for updates... - if (SettingsManager.Current.Update_CheckForUpdatesAtStartup) + if (PolicyManager.Current?.Update_CheckForUpdatesAtStartup + ?? SettingsManager.Current.Update_CheckForUpdatesAtStartup) CheckForUpdates(); + else + Log.Info("Skipping update check at startup because it is disabled in the settings or via policy"); } private async void MetroWindowMain_Closing(object sender, CancelEventArgs e) diff --git a/Source/NETworkManager/ViewModels/SettingsSettingsViewModel.cs b/Source/NETworkManager/ViewModels/SettingsSettingsViewModel.cs index 7e15ada0af..330f98e21e 100644 --- a/Source/NETworkManager/ViewModels/SettingsSettingsViewModel.cs +++ b/Source/NETworkManager/ViewModels/SettingsSettingsViewModel.cs @@ -40,10 +40,10 @@ public bool IsDailyBackupEnabled { if (value == _isDailyBackupEnabled) return; - + if (!_isLoading) SettingsManager.Current.Settings_IsDailyBackupEnabled = value; - + _isDailyBackupEnabled = value; OnPropertyChanged(); } diff --git a/Source/NETworkManager/ViewModels/SettingsUpdateViewModel.cs b/Source/NETworkManager/ViewModels/SettingsUpdateViewModel.cs index b760064e6f..d04897f36c 100644 --- a/Source/NETworkManager/ViewModels/SettingsUpdateViewModel.cs +++ b/Source/NETworkManager/ViewModels/SettingsUpdateViewModel.cs @@ -26,6 +26,11 @@ public bool CheckForUpdatesAtStartup } } + /// + /// Gets whether the "Check for updates at startup" setting is managed by system-wide policy. + /// + public bool IsUpdateCheckManagedByPolicy => PolicyManager.Current?.Update_CheckForUpdatesAtStartup.HasValue == true; + private bool _checkForPreReleases; public bool CheckForPreReleases @@ -78,7 +83,9 @@ public SettingsUpdateViewModel() private void LoadSettings() { - CheckForUpdatesAtStartup = SettingsManager.Current.Update_CheckForUpdatesAtStartup; + // If policy is set, show the policy value; otherwise show the user's setting + CheckForUpdatesAtStartup = PolicyManager.Current?.Update_CheckForUpdatesAtStartup + ?? SettingsManager.Current.Update_CheckForUpdatesAtStartup; CheckForPreReleases = SettingsManager.Current.Update_CheckForPreReleases; EnableExperimentalFeatures = SettingsManager.Current.Experimental_EnableExperimentalFeatures; } diff --git a/Source/NETworkManager/Views/SettingsUpdateView.xaml b/Source/NETworkManager/Views/SettingsUpdateView.xaml index 1b39084ef0..5f6530f909 100644 --- a/Source/NETworkManager/Views/SettingsUpdateView.xaml +++ b/Source/NETworkManager/Views/SettingsUpdateView.xaml @@ -6,13 +6,20 @@ xmlns:mah="clr-namespace:MahApps.Metro.Controls;assembly=MahApps.Metro" xmlns:localization="clr-namespace:NETworkManager.Localization.Resources;assembly=NETworkManager.Localization" xmlns:viewModels="clr-namespace:NETworkManager.ViewModels" + xmlns:converters="clr-namespace:NETworkManager.Converters;assembly=NETworkManager.Converters" + xmlns:iconPacks="http://metro.mahapps.com/winfx/xaml/iconpacks" mc:Ignorable="d" d:DataContext="{d:DesignInstance viewModels:SettingsUpdateViewModel}"> + + + + + IsOn="{Binding CheckForUpdatesAtStartup}" + IsEnabled="{Binding IsUpdateCheckManagedByPolicy, Converter={StaticResource BooleanReverseConverter}}" /> + + + + + + + + diff --git a/Website/docs/changelog/next-release.md b/Website/docs/changelog/next-release.md index c477c766ae..6fdd01caf2 100644 --- a/Website/docs/changelog/next-release.md +++ b/Website/docs/changelog/next-release.md @@ -45,6 +45,7 @@ Release date: **xx.xx.2025** - New language Ukrainian (`uk-UA`) has been added. Thanks to [@vadickkt](https://github.com/vadickkt) [#3240](https://github.com/BornToBeRoot/NETworkManager/pull/3240) - Migrated all dialogs to child windows for improved usability and accessibility. [#3271](https://github.com/BornToBeRoot/NETworkManager/pull/3271) +- System-wide policies can now be configured via a `config.json` file in the application directory to control settings for all users. Currently supports controlling the "Check for updates at startup" setting. This is useful for enterprise deployments where administrators need centralized control over update behavior. See [System-Wide Policies](../system-wide-policies.md) documentation for more information. [#3313](https://github.com/BornToBeRoot/NETworkManager/pull/3313) **DNS Lookup** diff --git a/Website/docs/img/system-wide-policy-indicator.png b/Website/docs/img/system-wide-policy-indicator.png new file mode 100644 index 0000000000..36da34f920 Binary files /dev/null and b/Website/docs/img/system-wide-policy-indicator.png differ diff --git a/Website/docs/settings/update.md b/Website/docs/settings/update.md index 565a74fefe..9c006a108b 100644 --- a/Website/docs/settings/update.md +++ b/Website/docs/settings/update.md @@ -12,6 +12,27 @@ Check for new program versions on GitHub when the application is launched. **Default:** `Enabled` +:::info System-Wide Policy + +This setting can be controlled by administrators using a system-wide policy. See [System-Wide Policies](../system-wide-policies.md) for more information. + +**Policy Property:** `Update_CheckForUpdatesAtStartup` + +**Values:** +- `true` - Force enable automatic update checks at startup for all users +- `false` - Force disable automatic update checks at startup for all users +- Omit the property - Allow users to control this setting themselves + +**Example:** + +```json +{ + "Update_CheckForUpdatesAtStartup": false +} +``` + +::: + :::note The URL `https://api.github.com/` must be reachable to check for updates. diff --git a/Website/docs/system-wide-policies.md b/Website/docs/system-wide-policies.md new file mode 100644 index 0000000000..d8ef644453 --- /dev/null +++ b/Website/docs/system-wide-policies.md @@ -0,0 +1,102 @@ +--- +sidebar_position: 6 +--- + +# System-Wide Policies + +System-wide policies allow administrators to enforce specific settings for all users on a machine. These policies override user-specific settings and provide centralized control over application behavior in enterprise environments. + +## Overview + +Policies are defined in a `config.json` file placed in the application installation directory (the same folder as `NETworkManager.exe`). When this file exists, the application loads the policies at startup and applies them with precedence over user settings. + +Users will see a visual indicator in the Settings UI when a setting is controlled by a system-wide policy, showing them the administrator-enforced value and preventing them from changing it. + +![System-wide policy indicator](./img/system-wide-policy-indicator.png) + +## Configuration File + +The `config.json` file uses a simple JSON structure to define policy values. An example file (`config.json.example`) is included in the application installation directory for reference. + +**File Location:** +- **Installed version**: `C:\Program Files\NETworkManager\config.json` (or your custom installation path) +- **Portable version**: Same directory as `NETworkManager.exe` + +**File Format:** + +```json +{ + "Policy_Name1": true, + "Policy_Name2": "ExampleValue" +} +``` + + +**Example:** + +```json +{ + "Update_CheckForUpdatesAtStartup": false +} +``` + +Property names generally follow the pattern `Section_SettingName` (see each setting's documentation). Ensure values use the correct JSON type (boolean, string, number, etc.). + +:::note + +- The file must be named exactly `config.json` +- The file must contain valid JSON syntax +- Changes to the file require restarting the application to take effect +- If the file doesn't exist or contains invalid JSON, it will be ignored and user settings will apply + +::: + +## Deployment + +For enterprise deployments: + +1. **Create the configuration file**: + - Use the `config.json.example` as a template + - Rename it to `config.json` + - Set your desired policy values (you find them in the corresponding setting's documentation) + +2. **Deploy to installation directory**: + - Place the `config.json` file in the same directory as `NETworkManager.exe` + - For MSI installations, this is typically `C:\Program Files\NETworkManager\` + - For portable installations, place it next to the executable + +3. **Deploy methods**: + - Group Policy — copy the `config.json` file to the installation directory (use Group Policy preferences or a startup script) + - Configuration management tools — SCCM/ConfigMgr, Microsoft Intune, Ansible, etc. + - Scripts and deployment toolkits — PowerShell scripts, PSAppDeployToolkit (recommended for scripted MSI/App deployments) + - Manual deployment — hand-copy for small-scale rollouts + +4. **Verification**: + - Launch the application + - Navigate to Settings > Update (e.g., "Check for updates at startup") + - Verify the shield icon and the administrator message appear and that the control is disabled + - Confirm the displayed value matches the policy + +:::warning + +Ensure the `config.json` file has appropriate permissions so that regular users cannot modify it. On standard installations in `Program Files`, this is automatically enforced by Windows file permissions. + +::: + +## Troubleshooting + +**Policy not being applied:** +- Verify the file is named exactly `config.json` (not `config.json.txt`) +- Check that the JSON syntax is valid (use a JSON validator) +- Confirm the file is in the correct directory (same as `NETworkManager.exe`) +- Restart the application after creating or modifying the file +- Check the application logs for any error messages related to policy loading + +**Policy values not showing in UI:** +- Ensure the property name matches exactly (see the corresponding setting's documentation for the property name) +- Verify the value is a boolean (`true` or `false`), not a string (`"true"` or `"false"`) +- Check that there are no syntax errors in the JSON file + +## Future Policies + +Additional policy options will be added in future releases to provide more granular control over application behavior. If you have specific requirements for system-wide policies in your organization, please submit a feature request via the [GitHub issue tracker](https://github.com/BornToBeRoot/NETworkManager/issues/new/choose). diff --git a/Website/package.json b/Website/package.json index a0ce100b3b..9f5727df88 100644 --- a/Website/package.json +++ b/Website/package.json @@ -21,7 +21,7 @@ "clsx": "^2.0.0", "prism-react-renderer": "^2.4.1", "react": "^19.2.4", - "react-dom": "^19.2.3", + "react-dom": "^19.2.4", "react-image-gallery": "^2.0.5" }, "devDependencies": { diff --git a/Website/src/pages/index.js b/Website/src/pages/index.js index 60778377f6..364d3e5b14 100644 --- a/Website/src/pages/index.js +++ b/Website/src/pages/index.js @@ -5,7 +5,7 @@ import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; import Layout from "@theme/Layout"; import HomepageFeatures from "@site/src/components/HomepageFeatures"; import ImageGallery from "react-image-gallery"; -import "react-image-gallery/styles/css/image-gallery.css"; +import "react-image-gallery/styles/image-gallery.css"; import ImageGalleryDashboard from "@site/docs/img/dashboard.png"; import ImageGalleryNetworkInterfaceInformation from "@site/docs/img/network-interface--information.png"; diff --git a/Website/yarn.lock b/Website/yarn.lock index 57722cbb93..f9304b9de4 100644 --- a/Website/yarn.lock +++ b/Website/yarn.lock @@ -3238,12 +3238,7 @@ caniuse-api@^3.0.0: lodash.memoize "^4.1.2" lodash.uniq "^4.5.0" -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001702, caniuse-lite@^1.0.30001718: - version "1.0.30001721" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001721.tgz#36b90cd96901f8c98dd6698bf5c8af7d4c6872d7" - integrity sha512-cOuvmUVtKrtEaoKiO0rSc29jcjwMwX5tOHDy4MgVFEWiUXj4uBMJkwI8MDySkgXidpMiHUcviogAvFi4pA2hDQ== - -caniuse-lite@^1.0.30001759: +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001702, caniuse-lite@^1.0.30001718, caniuse-lite@^1.0.30001759: version "1.0.30001769" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz#1ad91594fad7dc233777c2781879ab5409f7d9c2" integrity sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg== @@ -7410,10 +7405,10 @@ rc@1.2.8: minimist "^1.2.0" strip-json-comments "~2.0.1" -react-dom@^19.2.3: - version "19.2.3" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.2.3.tgz#f0b61d7e5c4a86773889fcc1853af3ed5f215b17" - integrity sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg== +react-dom@^19.2.4: + version "19.2.4" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.2.4.tgz#6fac6bd96f7db477d966c7ec17c1a2b1ad8e6591" + integrity sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ== dependencies: scheduler "^0.27.0"