Added protection against replacing config server managed assistant plugins

This commit is contained in:
Thorsten Sommer 2026-08-05 19:51:53 +02:00
parent 38710fdd08
commit 111465b25e
Signed by untrusted user who does not match committer: tsommer
GPG Key ID: 371BBA77A02C0108

View File

@ -437,6 +437,10 @@ public sealed class AssistantPluginInstallService
if (!IsPathInsideDirectory(assistantPluginsRoot, finalDirectory)) if (!IsPathInsideDirectory(assistantPluginsRoot, finalDirectory))
return Error(TB("The resolved plugin directory is outside the assistant plugin directory.")); return Error(TB("The resolved plugin directory is outside the assistant plugin directory."));
var replacementIssue = GetAssistantReplacementIssue(assistantPlugin.Id);
if (!string.IsNullOrWhiteSpace(replacementIssue))
return Error(replacementIssue);
if (Directory.Exists(finalDirectory)) if (Directory.Exists(finalDirectory))
{ {
replacedExisting = true; replacedExisting = true;
@ -632,15 +636,51 @@ public sealed class AssistantPluginInstallService
private static string DetermineFinalDirectory(string assistantPluginsRoot, PluginAssistants assistantPlugin) private static string DetermineFinalDirectory(string assistantPluginsRoot, PluginAssistants assistantPlugin)
{ {
var existingPlugin = PluginFactory.AvailablePlugins var existingPlugin = FindReplaceableAssistantPlugin(assistantPlugin.Id);
.OfType<IAvailablePlugin>()
.FirstOrDefault(plugin => plugin.Type is PluginType.ASSISTANT && plugin.Id == assistantPlugin.Id && !plugin.IsInternal);
return existingPlugin is not null return existingPlugin is not null
? existingPlugin.LocalPath ? existingPlugin.LocalPath
: Path.Join(assistantPluginsRoot, CreatePluginDirectoryName(assistantPlugin)); : Path.Join(assistantPluginsRoot, CreatePluginDirectoryName(assistantPlugin));
} }
/// <summary>
/// Finds the local assistant plugin that an installation with the given ID would replace.
/// </summary>
/// <param name="pluginId">The ID of the assistant plugin about to be installed.</param>
/// <returns>The plugin that would be replaced, or null when the installation adds a new plugin.</returns>
private static IAvailablePlugin? FindReplaceableAssistantPlugin(Guid pluginId) => PluginFactory.AvailablePlugins
.OfType<IAvailablePlugin>()
.FirstOrDefault(plugin => plugin.Type is PluginType.ASSISTANT && plugin.Id == pluginId && !plugin.IsInternal);
/// <summary>
/// Checks whether an installation may replace the assistant plugin that currently uses the given ID.
/// Plugins deployed by a Config Server belong to the organization's IT, so neither an import nor
/// the Assistant Builder may overwrite them.
/// </summary>
/// <param name="pluginId">The ID of the assistant plugin about to be installed.</param>
/// <returns>A user-facing issue when the existing plugin must not be replaced, an empty string otherwise.</returns>
private static string GetAssistantReplacementIssue(Guid pluginId)
{
var existingPlugin = FindReplaceableAssistantPlugin(pluginId);
if (existingPlugin is null)
return string.Empty;
if (existingPlugin.IsManagedByConfigServer)
return TB("Config server managed assistant plugins cannot be replaced.");
if (string.IsNullOrWhiteSpace(existingPlugin.LocalPath))
return string.Empty;
// The metadata above and the running plugin read the same Lua field. We check both, though,
// just like the deletion path does:
var runningPlugin = PluginFactory.RunningPlugins
.OfType<PluginAssistants>()
.FirstOrDefault(candidate => candidate.Id == pluginId && IsSameDirectory(candidate.PluginPath, existingPlugin.LocalPath));
return runningPlugin?.IsManagedByConfigServer is true
? TB("Config server managed assistant plugins cannot be replaced.")
: string.Empty;
}
private static string CreatePluginDirectoryName(PluginAssistants assistantPlugin) private static string CreatePluginDirectoryName(PluginAssistants assistantPlugin)
{ {
var safeName = CreateSafeDirectoryNamePart(assistantPlugin.Name); var safeName = CreateSafeDirectoryNamePart(assistantPlugin.Name);