Added priorities for configuration plugins and fixed how they collide (#899)
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Sync Flatpak repo (push) Blocked by required conditions
Build and Release / Collect Flatpak artifacts (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions

This commit is contained in:
Thorsten Sommer authored and GitHub committed 2026-08-08 18:35:46 +02:00
1 parent d1a6781ea6
commit 0eb747b386
17 files changed
+562 -104

No files matched your search

+20 -17
View File
@@ -33,26 +33,29 @@ public record ConfigMeta<TClass, TValue> : ConfigMetaBase
public required TValue Default { get; init; }
/// <summary>
/// The additive value contribution provided by a configuration plugin.
/// The additive value contributions, one per contributing configuration plugin.
/// </summary>
public TValue PluginContribution { get; private set; } = default!;
/// <summary>
/// Stores an additive plugin contribution.
/// </summary>
public void SetPluginContribution(TValue value, Guid pluginId)
{
this.PluginContribution = value;
this.PluginContributionByConfigPluginId = pluginId;
this.HasPluginContribution = true;
}
/// <remarks>
/// Every configuration plugin keeps its own contribution, so removing one of them leaves the
/// contributions of the others intact. Callers that need the overall contribution combine the
/// values themselves: only they know how to combine the concrete type.
/// </remarks>
public IReadOnlyDictionary<Guid, TValue> PluginContributions => this.pluginContributions;
/// <inheritdoc/>
public override void ClearPluginContribution()
{
this.PluginContribution = default!;
base.ClearPluginContribution();
}
public override IReadOnlyCollection<Guid> ContributingConfigPluginIds => this.pluginContributions.Keys;
private readonly Dictionary<Guid, TValue> pluginContributions = [];
/// <summary>
/// Stores the additive contribution of one configuration plugin, replacing its previous one.
/// </summary>
/// <param name="value">The contributed value.</param>
/// <param name="pluginId">The contributing configuration plugin.</param>
public void SetPluginContribution(TValue value, Guid pluginId) => this.pluginContributions[pluginId] = value;
/// <inheritdoc/>
public override bool RemovePluginContribution(Guid configPluginId) => this.pluginContributions.Remove(configPluginId);
/// <inheritdoc/>
protected override void Reset()
@@ -38,14 +38,19 @@ public abstract record ConfigMetaBase(string SettingName) : IConfig
public Guid EditableDefaultByConfigPluginId { get; private set; }
/// <summary>
/// Indicates whether a plugin contribution is available.
/// The configuration plugins which contribute to this setting.
/// </summary>
public bool HasPluginContribution { get; protected set; }
/// <remarks>
/// Contributions are additive, so several configuration plugins may contribute at the same time
/// and each of them keeps its own contribution. An organization might enable one preview feature
/// for everybody and another one for a single department, for example.
/// </remarks>
public abstract IReadOnlyCollection<Guid> ContributingConfigPluginIds { get; }
/// <summary>
/// The ID of the plugin that provided the additive value contribution.
/// Indicates whether at least one configuration plugin contributes to this setting.
/// </summary>
public Guid PluginContributionByConfigPluginId { get; protected set; }
public bool HasPluginContribution => this.ContributingConfigPluginIds.Count > 0;
/// <summary>
/// Locks the configuration state, indicating that it is controlled by a specific plugin.
@@ -133,13 +138,11 @@ public abstract record ConfigMetaBase(string SettingName) : IConfig
}
/// <summary>
/// Clears the additive plugin contribution without changing the current value.
/// Removes the contribution of one configuration plugin without changing the current value.
/// </summary>
public virtual void ClearPluginContribution()
{
this.PluginContributionByConfigPluginId = Guid.Empty;
this.HasPluginContribution = false;
}
/// <param name="configPluginId">The configuration plugin whose contribution is removed.</param>
/// <returns>True when that plugin had a contribution, otherwise false.</returns>
public abstract bool RemovePluginContribution(Guid configPluginId);
/// <summary>
/// Resets the configuration property to its default value.
@@ -654,6 +654,11 @@ public static partial class ManagedConfiguration
if (dryRun)
return successful;
//
// Contributions need no protection against a takeover: every configuration plugin has its
// own contribution, so no plugin can replace or drop the contribution of another one. This
// is also why a local configuration plugin may contribute next to one of an organization.
//
if (successful)
{
var configInstance = configSelection.Compile().Invoke(SettingsManagerAccess.ConfigurationData);
@@ -663,10 +668,8 @@ public static partial class ManagedConfiguration
configMeta.SetValue(merged);
configMeta.SetPluginContribution(new HashSet<TValue>(configuredValue), configPluginId);
}
else if (configMeta.HasPluginContribution && configMeta.PluginContributionByConfigPluginId == configPluginId)
{
configMeta.ClearPluginContribution();
}
else
configMeta.RemovePluginContribution(configPluginId);
if (configMeta.IsLocked && configMeta.LockedByConfigPluginId == configPluginId)
configMeta.UnlockConfiguration();
@@ -905,6 +908,11 @@ public static partial class ManagedConfiguration
if(dryRun)
return successful;
// The setting might belong to the IT department of an organization. In that case, no local
// configuration plugin may touch it, no matter what it declares:
if (!MayManageSetting(configPluginId, configMeta))
return false;
switch (successful)
{
case true:
@@ -954,6 +962,11 @@ public static partial class ManagedConfiguration
if (dryRun)
return successful;
// The setting might belong to the IT department of an organization. In that case, no local
// configuration plugin may touch it, no matter what it declares:
if (!MayManageSetting(configPluginId, configMeta))
return false;
switch (successful)
{
case true when managedMode is ManagedConfigurationMode.LOCKED:
@@ -257,6 +257,51 @@ public static partial class ManagedConfiguration
return false;
}
/// <summary>
/// Checks whether a configuration plugin may manage a setting, or whether that setting belongs
/// to the IT department of an organization.
/// </summary>
/// <remarks>
/// A local configuration plugin must not take over a setting an organization manages. Otherwise,
/// anyone could hand out a configuration plugin that quietly replaces parts of the organization
/// configuration, e.g. the address of a self-hosted provider.<br/><br/>
/// Between two configuration plugins of the same organization, we do not interfere: both belong
/// to the IT department, so the one processed later wins, as before.
/// </remarks>
/// <param name="configPluginId">The configuration plugin which wants to manage the setting.</param>
/// <param name="configMeta">The configuration metadata of the setting.</param>
/// <returns>True when the plugin may manage this setting, otherwise false.</returns>
private static bool MayManageSetting(Guid configPluginId, ConfigMetaBase configMeta)
{
var owningConfigPluginId = GetSettingOwner(configMeta);
if (owningConfigPluginId == Guid.Empty || owningConfigPluginId == configPluginId)
return true;
if (!PluginFactory.IsEnterpriseConfigurationPlugin(owningConfigPluginId))
return true;
if (PluginFactory.IsEnterpriseConfigurationPlugin(configPluginId))
return true;
Log.LogWarning($"The configuration plugin '{configPluginId}' tried to manage the setting '{configMeta.SettingName}', which is managed by the configuration plugin '{owningConfigPluginId}' of your organization. Ignoring the attempt: configurations deployed by your organization's IT take precedence.");
return false;
}
/// <summary>
/// Determines the configuration plugin which currently manages a setting, if any.
/// </summary>
private static Guid GetSettingOwner(ConfigMetaBase configMeta)
{
if (configMeta.IsLocked && configMeta.LockedByConfigPluginId != Guid.Empty)
return configMeta.LockedByConfigPluginId;
// The editable default is persisted as well, so we prefer it over the in-memory state:
if (TryGetEditableDefaultState(configMeta.SettingName, out var editableDefaultState) && editableDefaultState.ConfigPluginId != Guid.Empty)
return editableDefaultState.ConfigPluginId;
return configMeta.EditableDefaultByConfigPluginId;
}
/// <summary>
/// Removes all managed states whose configuration plugin is not available anymore.
/// </summary>
@@ -269,12 +314,13 @@ public static partial class ManagedConfiguration
/// change it at all.
/// </remarks>
/// <param name="availablePlugins">The collection of available plugins to check against.</param>
/// <param name="deployedConfigPluginIds">
/// The IDs of all configuration plugins which are deployed on this machine, including those which
/// could not be loaded. A deployed plugin was not removed, so its settings must stay untouched.
/// <param name="deployedEnterpriseConfigPluginIds">
/// The IDs of the configuration plugins which an organization deployed on this machine, including
/// those which could not be loaded. A deployed plugin was not removed, so its settings must stay
/// untouched.
/// </param>
/// <returns>True when at least one setting was changed, otherwise false.</returns>
public static bool CleanupLeftOverManagedConfigurations(IReadOnlyCollection<IAvailablePlugin> availablePlugins, IReadOnlySet<Guid> deployedConfigPluginIds)
public static bool CleanupLeftOverManagedConfigurations(IReadOnlyCollection<IAvailablePlugin> availablePlugins, IReadOnlySet<Guid> deployedEnterpriseConfigPluginIds)
{
var wasChanged = false;
var registeredSettingNames = new HashSet<string>(StringComparer.Ordinal);
@@ -293,7 +339,7 @@ public static partial class ManagedConfiguration
configMeta.RestoreLockedConfiguration();
// Check the locked state:
if (configMeta.IsLocked && configMeta.LockedByConfigPluginId != Guid.Empty && !IsPluginPresent(configMeta.LockedByConfigPluginId, availablePlugins, deployedConfigPluginIds))
if (configMeta.IsLocked && configMeta.LockedByConfigPluginId != Guid.Empty && !IsPluginPresent(configMeta.LockedByConfigPluginId, availablePlugins, deployedEnterpriseConfigPluginIds))
{
Log.LogInformation($"Resetting the setting '{configMeta.SettingName}': it was locked by the configuration plugin '{configMeta.LockedByConfigPluginId}', which is not available anymore.");
configMeta.ResetLockedConfiguration();
@@ -301,14 +347,18 @@ public static partial class ManagedConfiguration
}
// Check the editable default state:
if (CleanupEditableDefaultState(configMeta, availablePlugins, deployedConfigPluginIds))
if (CleanupEditableDefaultState(configMeta, availablePlugins, deployedEnterpriseConfigPluginIds))
wasChanged = true;
// Check the additive plugin contribution:
if (configMeta.HasPluginContribution && configMeta.PluginContributionByConfigPluginId != Guid.Empty && !IsPluginPresent(configMeta.PluginContributionByConfigPluginId, availablePlugins, deployedConfigPluginIds))
// Check the additive plugin contributions. Every contributing plugin is checked on its
// own, so one removed plugin does not take the contributions of the others with it:
foreach (var contributingConfigPluginId in configMeta.ContributingConfigPluginIds.ToList())
{
Log.LogInformation($"Clearing the plugin contribution for the setting '{configMeta.SettingName}': the configuration plugin '{configMeta.PluginContributionByConfigPluginId}' is not available anymore.");
configMeta.ClearPluginContribution();
if (contributingConfigPluginId != Guid.Empty && IsPluginPresent(contributingConfigPluginId, availablePlugins, deployedEnterpriseConfigPluginIds))
continue;
Log.LogInformation($"Clearing the contribution of the configuration plugin '{contributingConfigPluginId}' to the setting '{configMeta.SettingName}': the plugin is not available anymore.");
configMeta.RemovePluginContribution(contributingConfigPluginId);
wasChanged = true;
}
}
@@ -328,7 +378,7 @@ public static partial class ManagedConfiguration
/// The latter matters for organizations: a broken configuration plugin is still in charge, so we
/// must not treat its settings as left over.
/// </remarks>
private static bool IsPluginPresent(Guid configPluginId, IReadOnlyCollection<IAvailablePlugin> availablePlugins, IReadOnlySet<Guid> deployedConfigPluginIds) => deployedConfigPluginIds.Contains(configPluginId) || availablePlugins.Any(x => x.Id == configPluginId);
private static bool IsPluginPresent(Guid configPluginId, IReadOnlyCollection<IAvailablePlugin> availablePlugins, IReadOnlySet<Guid> deployedEnterpriseConfigPluginIds) => deployedEnterpriseConfigPluginIds.Contains(configPluginId) || availablePlugins.Any(x => x.Id == configPluginId);
/// <summary>
/// Removes persisted managed states which belong to settings that are not registered anymore.
@@ -390,7 +440,7 @@ public static partial class ManagedConfiguration
private static bool ClearEditableDefaultState(string settingName) => SettingsManagerAccess.ConfigurationData.ManagedEditableDefaults.Remove(settingName);
private static bool CleanupEditableDefaultState(ConfigMetaBase configMeta, IReadOnlyCollection<IAvailablePlugin> availablePlugins, IReadOnlySet<Guid> deployedConfigPluginIds)
private static bool CleanupEditableDefaultState(ConfigMetaBase configMeta, IReadOnlyCollection<IAvailablePlugin> availablePlugins, IReadOnlySet<Guid> deployedEnterpriseConfigPluginIds)
{
if (!TryGetEditableDefaultState(configMeta.SettingName, out var editableDefaultState))
{
@@ -401,7 +451,7 @@ public static partial class ManagedConfiguration
return true;
}
if (IsPluginPresent(editableDefaultState.ConfigPluginId, availablePlugins, deployedConfigPluginIds))
if (IsPluginPresent(editableDefaultState.ConfigPluginId, availablePlugins, deployedEnterpriseConfigPluginIds))
return false;
Log.LogInformation($"Clearing the editable default of the setting '{configMeta.SettingName}': the configuration plugin '{editableDefaultState.ConfigPluginId}' is not available anymore.");