Skip to content
Draft
3 changes: 3 additions & 0 deletions Source/NETworkManager.Settings/PolicyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,7 @@ public class PolicyInfo
{
[JsonPropertyName("Update_CheckForUpdatesAtStartup")]
public bool? Update_CheckForUpdatesAtStartup { get; set; }

[JsonPropertyName("SettingsFolderLocation")]
public string? SettingsFolderLocation { get; set; }
}
1 change: 1 addition & 0 deletions Source/NETworkManager.Settings/PolicyManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ public static void Load()

// Log enabled settings
Log.Info($"System-wide policy - Update_CheckForUpdatesAtStartup: {Current.Update_CheckForUpdatesAtStartup?.ToString() ?? "Not set"}");
Log.Info($"System-wide policy - SettingsFolderLocation: {Current.SettingsFolderLocation ?? "Not set"}");
}
}
catch (Exception ex)
Expand Down
45 changes: 45 additions & 0 deletions Source/NETworkManager.Settings/SettingsManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System;
using System.IO;
using System.Linq;
using System.Security;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Xml.Serialization;
Expand Down Expand Up @@ -77,6 +78,50 @@ public static class SettingsManager
/// <returns>Path to the settings folder.</returns>
public static string GetSettingsFolderLocation()
{
// Policy override takes precedence
if (!string.IsNullOrWhiteSpace(PolicyManager.Current?.SettingsFolderLocation))
{
var policyPath = PolicyManager.Current.SettingsFolderLocation;

// Validate that the policy-provided path is rooted (absolute)
if (!Path.IsPathRooted(policyPath))
{
Log.Error($"Policy-provided SettingsFolderLocation is not an absolute path: {policyPath}. Falling back to default location.");
}
else
{
// Validate that the path doesn't contain invalid characters
try
{
// This will throw ArgumentException, NotSupportedException, or SecurityException if the path is invalid
var fullPath = Path.GetFullPath(policyPath);

// Check if the path is a directory (not a file)
if (File.Exists(fullPath))
{
Log.Error($"Policy-provided SettingsFolderLocation is a file, not a directory: {policyPath}. Falling back to default location.");
}
else
{
return fullPath;
}
}
catch (ArgumentException ex)
{
Log.Error($"Policy-provided SettingsFolderLocation contains invalid characters: {policyPath}. Falling back to default location.", ex);
}
catch (NotSupportedException ex)
{
Log.Error($"Policy-provided SettingsFolderLocation format is not supported: {policyPath}. Falling back to default location.", ex);
}
catch (SecurityException ex)
{
Log.Error($"Insufficient permissions to access policy-provided SettingsFolderLocation: {policyPath}. Falling back to default location.", ex);
}
}
}

// Fall back to existing logic
return ConfigurationManager.Current.IsPortable
? Path.Combine(AssemblyManager.Current.Location, SettingsFolderName)
: Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),
Expand Down
3 changes: 2 additions & 1 deletion Source/NETworkManager.Settings/config.json.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{
"Update_CheckForUpdatesAtStartup": false
"Update_CheckForUpdatesAtStartup": false,
"SettingsFolderLocation": "C:\\CustomPath\\NETworkManager\\Settings"
}
5 changes: 5 additions & 0 deletions Source/NETworkManager/ViewModels/SettingsSettingsViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ public string Location
}
}

/// <summary>
/// Gets whether the settings folder location is managed by system-wide policy.
/// </summary>
public bool IsLocationManagedByPolicy => !string.IsNullOrWhiteSpace(PolicyManager.Current?.SettingsFolderLocation);

private bool _isDailyBackupEnabled;

public bool IsDailyBackupEnabled
Expand Down
60 changes: 43 additions & 17 deletions Source/NETworkManager/Views/SettingsSettingsView.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,30 +7,56 @@
xmlns:iconPacks="http://metro.mahapps.com/winfx/xaml/iconpacks"
xmlns:viewModels="clr-namespace:NETworkManager.ViewModels"
xmlns:localization="clr-namespace:NETworkManager.Localization.Resources;assembly=NETworkManager.Localization"
xmlns:converters="clr-namespace:NETworkManager.Converters;assembly=NETworkManager.Converters"
mc:Ignorable="d" Loaded="UserControl_Loaded"
d:DataContext="{d:DesignInstance viewModels:SettingsSettingsViewModel}">
<UserControl.Resources>
<converters:BooleanToVisibilityCollapsedConverter x:Key="BooleanToVisibilityCollapsedConverter" />
</UserControl.Resources>
<StackPanel>
<TextBlock Style="{StaticResource HeaderTextBlock}" Text="{x:Static localization:Strings.Location}" />
<TextBox x:Name="TextBoxLocation" Text="{Binding Location}" Style="{StaticResource ReadOnlyTextBox}"
IsEnabled="False" Margin="0,0,0,10" />
<Button Command="{Binding OpenLocationCommand}" Style="{StaticResource ImageWithTextButton}"
HorizontalAlignment="Right" Margin="0,0,0,20">
<Button.Content>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Rectangle Style="{StaticResource ButtonWithImageRectangle}">
<Rectangle.OpacityMask>
<VisualBrush Stretch="Uniform" Visual="{iconPacks:Material Kind=FolderOpen}" />
</Rectangle.OpacityMask>
</Rectangle>
<TextBlock Grid.Column="1" Text="{x:Static localization:Strings.OpenLocation}"
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="10" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0" Grid.Row="0"
Orientation="Horizontal"
Visibility="{Binding IsLocationManagedByPolicy, Converter={StaticResource BooleanToVisibilityCollapsedConverter}}"
Margin="0,0,0,10">
<Rectangle Width="24" Height="24" Fill="{DynamicResource MahApps.Brushes.Accent}" VerticalAlignment="Center" Margin="0,0,5,0">
<Rectangle.OpacityMask>
<VisualBrush Stretch="Uniform" Visual="{iconPacks:Material Kind=ShieldLock}" />
</Rectangle.OpacityMask>
</Rectangle>
<TextBlock Text="{x:Static localization:Strings.SettingManagedByAdministrator}"
Style="{StaticResource AccentTextBlock}"
VerticalAlignment="Center" />
</StackPanel>
<Button Grid.Column="2" Grid.Row="0"
Command="{Binding OpenLocationCommand}"
Style="{StaticResource ImageWithTextButton}"
Margin="0,0,0,20">
<Button.Content>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Rectangle Style="{StaticResource ButtonWithImageRectangle}">
<Rectangle.OpacityMask>
<VisualBrush Stretch="Uniform" Visual="{iconPacks:Material Kind=FolderOpen}" />
</Rectangle.OpacityMask>
</Rectangle>
<TextBlock Grid.Column="1" Text="{x:Static localization:Strings.OpenLocation}"
Style="{StaticResource ButtonWithImageTextBlock}" />
</Grid>
</Button.Content>
</Button>
</Grid>
</Button.Content>
</Button>
</Grid>
<TextBlock Style="{StaticResource HeaderTextBlock}" Text="{x:Static localization:Strings.Backup}" />
<mah:ToggleSwitch Header="{x:Static localization:Strings.CreateDailyBackup}"
IsOn="{Binding IsDailyBackupEnabled}" Margin="0,0,0,10" />
Expand Down
20 changes: 20 additions & 0 deletions Website/docs/settings/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,26 @@ Folder where the application settings are stored.
| Setup / Archiv | `%UserProfile%\Documents\NETworkManager\Settings` |
| Portable | `<APP_FOLDER>\Settings` |

:::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:** `SettingsFolderLocation`

**Values:**
- Absolute path (e.g., `C:\\CustomPath\\NETworkManager\\Settings`) - Force a custom settings folder location for all users
- Omit the property - Allow the default location logic to apply (portable vs. non-portable)

**Example:**

```json
{
"SettingsFolderLocation": "C:\\CustomPath\\NETworkManager\\Settings"
}
```

:::

:::note

**Recommendation**
Expand Down
3 changes: 2 additions & 1 deletion Website/docs/system-wide-policies.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ The `config.json` file uses a simple JSON structure to define policy values. An

```json
{
"Update_CheckForUpdatesAtStartup": false
"Update_CheckForUpdatesAtStartup": false,
"SettingsFolderLocation": "C:\\CustomPath\\NETworkManager\\Settings"
}
```

Expand Down