From 78670b310bd54339a6ef2e152ea2b37ab06410c5 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 21 Jun 2026 14:41:44 +0200 Subject: [PATCH] Added support for organization-trusted providers --- .../Chat/ChatThreadExtensions.cs | 18 ++++---- .../DataSourceLocalDirectoryDialog.razor.cs | 2 +- ...ataSourceLocalDirectoryInfoDialog.razor.cs | 2 +- .../DataSourceLocalFileDialog.razor.cs | 2 +- .../DataSourceLocalFileInfoDialog.razor.cs | 2 +- .../Plugins/configuration/plugin.lua | 9 ++++ .../Provider/BaseProvider.cs | 3 ++ app/MindWork AI Studio/Provider/IProvider.cs | 5 ++ .../Provider/LLMProvidersExtensions.cs | 38 +++++++-------- app/MindWork AI Studio/Provider/NoProvider.cs | 2 + .../Settings/DataModel/Data.cs | 5 ++ .../Settings/DataSourceSecuritySettings.cs | 20 ++++++++ .../DataSourceSecurityTrustExtensions.cs | 46 +++++++++++++++++++ .../Settings/SettingsManager.cs | 3 +- .../Tools/PluginSystem/PluginConfiguration.cs | 3 ++ .../PluginSystem/PluginFactory.Loading.cs | 4 ++ .../Tools/Services/DataSourceService.cs | 31 ++++++------- .../wwwroot/changelog/v26.6.2.md | 1 + 18 files changed, 148 insertions(+), 48 deletions(-) create mode 100644 app/MindWork AI Studio/Settings/DataSourceSecuritySettings.cs create mode 100644 app/MindWork AI Studio/Settings/DataSourceSecurityTrustExtensions.cs diff --git a/app/MindWork AI Studio/Chat/ChatThreadExtensions.cs b/app/MindWork AI Studio/Chat/ChatThreadExtensions.cs index 6b1b6500..2eb5395b 100644 --- a/app/MindWork AI Studio/Chat/ChatThreadExtensions.cs +++ b/app/MindWork AI Studio/Chat/ChatThreadExtensions.cs @@ -1,4 +1,5 @@ -using AIStudio.Provider.SelfHosted; +using AIStudio.Provider; +using AIStudio.Settings; using AIStudio.Settings.DataModel; namespace AIStudio.Chat; @@ -33,12 +34,13 @@ public static class ChatThreadExtensions return true; // - // Is the provider self-hosted? + // Is the provider trusted for data-source security checks? // - var isSelfHostedProvider = provider switch + var settingsManager = Program.SERVICE_PROVIDER.GetRequiredService(); + var isTrustedProvider = provider switch { - ProviderSelfHosted => true, - AIStudio.Settings.Provider p => p.IsSelfHosted, + IProvider p => p.IsTrustedForDataSourceSecurityChecks(settingsManager), + AIStudio.Settings.Provider p => p.IsTrustedForDataSourceSecurityChecks(settingsManager), _ => false, }; @@ -46,12 +48,12 @@ public static class ChatThreadExtensions // // Check the chat data security against the selected provider: // - return isSelfHostedProvider switch + return isTrustedProvider switch { - // The provider is self-hosted -- we can use any data source: + // The provider is trusted -- we can use any data source: true => true, - // The provider is not self-hosted -- it depends on the data security of the chat thread: + // The provider is not trusted -- it depends on the data security of the chat thread: false => chatThread.DataSecurity is not DataSourceSecurity.SELF_HOSTED, }; } diff --git a/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryDialog.razor.cs b/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryDialog.razor.cs index 0137f068..42463e38 100644 --- a/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryDialog.razor.cs +++ b/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryDialog.razor.cs @@ -96,7 +96,7 @@ public partial class DataSourceLocalDirectoryDialog : MSGComponentBase #endregion - private bool SelectedCloudEmbedding => !this.SettingsManager.ConfigurationData.EmbeddingProviders.FirstOrDefault(x => x.Id == this.dataEmbeddingId)?.IsSelfHosted ?? false; + private bool SelectedCloudEmbedding => !(this.SettingsManager.ConfigurationData.EmbeddingProviders.FirstOrDefault(x => x.Id == this.dataEmbeddingId)?.IsTrustedForDataSourceSecurityChecks(this.SettingsManager) ?? false); private DataSourceLocalDirectory CreateDataSource() => new() { diff --git a/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryInfoDialog.razor.cs b/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryInfoDialog.razor.cs index b56bf06a..08ec4408 100644 --- a/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryInfoDialog.razor.cs +++ b/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryInfoDialog.razor.cs @@ -56,7 +56,7 @@ public partial class DataSourceLocalDirectoryInfoDialog : MSGComponentBase, IAsy private bool IsOperationInProgress { get; set; } = true; - private bool IsCloudEmbedding => !this.embeddingProvider.IsSelfHosted; + private bool IsCloudEmbedding => !this.embeddingProvider.IsTrustedForDataSourceSecurityChecks(this.SettingsManager); private bool IsDirectoryAvailable => this.directoryInfo.Exists; diff --git a/app/MindWork AI Studio/Dialogs/DataSourceLocalFileDialog.razor.cs b/app/MindWork AI Studio/Dialogs/DataSourceLocalFileDialog.razor.cs index 324b0d71..13b8df1e 100644 --- a/app/MindWork AI Studio/Dialogs/DataSourceLocalFileDialog.razor.cs +++ b/app/MindWork AI Studio/Dialogs/DataSourceLocalFileDialog.razor.cs @@ -96,7 +96,7 @@ public partial class DataSourceLocalFileDialog : MSGComponentBase #endregion - private bool SelectedCloudEmbedding => !this.SettingsManager.ConfigurationData.EmbeddingProviders.FirstOrDefault(x => x.Id == this.dataEmbeddingId)?.IsSelfHosted ?? false; + private bool SelectedCloudEmbedding => !(this.SettingsManager.ConfigurationData.EmbeddingProviders.FirstOrDefault(x => x.Id == this.dataEmbeddingId)?.IsTrustedForDataSourceSecurityChecks(this.SettingsManager) ?? false); private DataSourceLocalFile CreateDataSource() => new() { diff --git a/app/MindWork AI Studio/Dialogs/DataSourceLocalFileInfoDialog.razor.cs b/app/MindWork AI Studio/Dialogs/DataSourceLocalFileInfoDialog.razor.cs index 68f31aff..5926a907 100644 --- a/app/MindWork AI Studio/Dialogs/DataSourceLocalFileInfoDialog.razor.cs +++ b/app/MindWork AI Studio/Dialogs/DataSourceLocalFileInfoDialog.razor.cs @@ -28,7 +28,7 @@ public partial class DataSourceLocalFileInfoDialog : MSGComponentBase private EmbeddingProvider embeddingProvider = EmbeddingProvider.NONE; private FileInfo fileInfo = null!; - private bool IsCloudEmbedding => !this.embeddingProvider.IsSelfHosted; + private bool IsCloudEmbedding => !this.embeddingProvider.IsTrustedForDataSourceSecurityChecks(this.SettingsManager); private bool IsFileAvailable => this.fileInfo.Exists; diff --git a/app/MindWork AI Studio/Plugins/configuration/plugin.lua b/app/MindWork AI Studio/Plugins/configuration/plugin.lua index 52bdac3b..eb33af4f 100644 --- a/app/MindWork AI Studio/Plugins/configuration/plugin.lua +++ b/app/MindWork AI Studio/Plugins/configuration/plugin.lua @@ -337,6 +337,15 @@ CONFIG["SETTINGS"] = {} -- -- Configure whether users can change the custom confidence scheme locally. -- CONFIG["SETTINGS"]["DataConfidence.CustomConfidenceScheme.AllowUserOverride"] = false +-- +-- Configure provider instances trusted by your organization for data-source security checks. +-- These IDs may refer to LLM providers, embedding providers, or transcription providers +-- defined in this configuration. Trusted providers are treated like self-hosted providers +-- only for data-source security checks and related local data warnings. +-- CONFIG["SETTINGS"]["DataSourceSecurity.TrustedProviderIds"] = { +-- "00000000-0000-0000-0000-000000000000", +-- "00000000-0000-0000-0000-000000000001", +-- } -- Example chat templates for this configuration: CONFIG["CHAT_TEMPLATES"] = {} diff --git a/app/MindWork AI Studio/Provider/BaseProvider.cs b/app/MindWork AI Studio/Provider/BaseProvider.cs index 68ecbfae..68c0feee 100644 --- a/app/MindWork AI Studio/Provider/BaseProvider.cs +++ b/app/MindWork AI Studio/Provider/BaseProvider.cs @@ -77,6 +77,9 @@ public abstract class BaseProvider : IProvider, ISecretId /// public abstract string Id { get; } + + /// + public string ConfiguredProviderId { get; init; } = string.Empty; /// public abstract string InstanceName { get; set; } diff --git a/app/MindWork AI Studio/Provider/IProvider.cs b/app/MindWork AI Studio/Provider/IProvider.cs index 3656b5e3..47041e1b 100644 --- a/app/MindWork AI Studio/Provider/IProvider.cs +++ b/app/MindWork AI Studio/Provider/IProvider.cs @@ -18,6 +18,11 @@ public interface IProvider /// public string Id { get; } + /// + /// The ID of the configured provider instance. + /// + public string ConfiguredProviderId { get; } + /// /// The provider's instance name. Useful for multiple instances of the same provider, /// e.g., to distinguish between different OpenAI API keys. diff --git a/app/MindWork AI Studio/Provider/LLMProvidersExtensions.cs b/app/MindWork AI Studio/Provider/LLMProvidersExtensions.cs index d189b350..f4e34844 100644 --- a/app/MindWork AI Studio/Provider/LLMProvidersExtensions.cs +++ b/app/MindWork AI Studio/Provider/LLMProvidersExtensions.cs @@ -225,7 +225,7 @@ public static class LLMProvidersExtensions /// The provider instance. public static IProvider CreateProvider(this AIStudio.Settings.Provider providerSettings) { - return providerSettings.UsedLLMProvider.CreateProvider(providerSettings.InstanceName, providerSettings.Host, providerSettings.Hostname, providerSettings.Model, providerSettings.HFInferenceProvider, providerSettings.AdditionalJsonApiParameters, providerSettings.IsEnterpriseConfiguration); + return providerSettings.UsedLLMProvider.CreateProvider(providerSettings.InstanceName, providerSettings.Host, providerSettings.Hostname, providerSettings.Model, providerSettings.HFInferenceProvider, providerSettings.Id, providerSettings.AdditionalJsonApiParameters, providerSettings.IsEnterpriseConfiguration); } /// @@ -235,7 +235,7 @@ public static class LLMProvidersExtensions /// The provider instance. public static IProvider CreateProvider(this EmbeddingProvider embeddingProviderSettings) { - return embeddingProviderSettings.UsedLLMProvider.CreateProvider(embeddingProviderSettings.Name, embeddingProviderSettings.Host, embeddingProviderSettings.Hostname, embeddingProviderSettings.Model, HFInferenceProvider.NONE, isEnterpriseConfiguration: embeddingProviderSettings.IsEnterpriseConfiguration); + return embeddingProviderSettings.UsedLLMProvider.CreateProvider(embeddingProviderSettings.Name, embeddingProviderSettings.Host, embeddingProviderSettings.Hostname, embeddingProviderSettings.Model, HFInferenceProvider.NONE, configuredProviderId: embeddingProviderSettings.Id, isEnterpriseConfiguration: embeddingProviderSettings.IsEnterpriseConfiguration); } /// @@ -245,33 +245,33 @@ public static class LLMProvidersExtensions /// The provider instance. public static IProvider CreateProvider(this TranscriptionProvider transcriptionProviderSettings) { - return transcriptionProviderSettings.UsedLLMProvider.CreateProvider(transcriptionProviderSettings.Name, transcriptionProviderSettings.Host, transcriptionProviderSettings.Hostname, transcriptionProviderSettings.Model, HFInferenceProvider.NONE, isEnterpriseConfiguration: transcriptionProviderSettings.IsEnterpriseConfiguration); + return transcriptionProviderSettings.UsedLLMProvider.CreateProvider(transcriptionProviderSettings.Name, transcriptionProviderSettings.Host, transcriptionProviderSettings.Hostname, transcriptionProviderSettings.Model, HFInferenceProvider.NONE, configuredProviderId: transcriptionProviderSettings.Id, isEnterpriseConfiguration: transcriptionProviderSettings.IsEnterpriseConfiguration); } - private static IProvider CreateProvider(this LLMProviders provider, string instanceName, Host host, string hostname, Model model, HFInferenceProvider inferenceProvider, string expertProviderApiParameter = "", bool isEnterpriseConfiguration = false) + private static IProvider CreateProvider(this LLMProviders provider, string instanceName, Host host, string hostname, Model model, HFInferenceProvider inferenceProvider, string configuredProviderId = "", string expertProviderApiParameter = "", bool isEnterpriseConfiguration = false) { try { return provider switch { - LLMProviders.OPEN_AI => new ProviderOpenAI { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration }, - LLMProviders.ANTHROPIC => new ProviderAnthropic { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration }, - LLMProviders.MISTRAL => new ProviderMistral { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration }, - LLMProviders.GOOGLE => new ProviderGoogle { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration }, - LLMProviders.X => new ProviderX { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration }, - LLMProviders.DEEP_SEEK => new ProviderDeepSeek { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration }, - LLMProviders.ALIBABA_CLOUD => new ProviderAlibabaCloud { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration }, - LLMProviders.PERPLEXITY => new ProviderPerplexity { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration }, - LLMProviders.OPEN_ROUTER => new ProviderOpenRouter { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration }, + LLMProviders.OPEN_AI => new ProviderOpenAI { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration }, + LLMProviders.ANTHROPIC => new ProviderAnthropic { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration }, + LLMProviders.MISTRAL => new ProviderMistral { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration }, + LLMProviders.GOOGLE => new ProviderGoogle { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration }, + LLMProviders.X => new ProviderX { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration }, + LLMProviders.DEEP_SEEK => new ProviderDeepSeek { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration }, + LLMProviders.ALIBABA_CLOUD => new ProviderAlibabaCloud { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration }, + LLMProviders.PERPLEXITY => new ProviderPerplexity { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration }, + LLMProviders.OPEN_ROUTER => new ProviderOpenRouter { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration }, - LLMProviders.GROQ => new ProviderGroq { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration }, - LLMProviders.FIREWORKS => new ProviderFireworks { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration }, - LLMProviders.HUGGINGFACE => new ProviderHuggingFace(inferenceProvider, model) { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration }, + LLMProviders.GROQ => new ProviderGroq { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration }, + LLMProviders.FIREWORKS => new ProviderFireworks { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration }, + LLMProviders.HUGGINGFACE => new ProviderHuggingFace(inferenceProvider, model) { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration }, - LLMProviders.SELF_HOSTED => new ProviderSelfHosted(host, hostname) { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration }, + LLMProviders.SELF_HOSTED => new ProviderSelfHosted(host, hostname) { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration }, - LLMProviders.HELMHOLTZ => new ProviderHelmholtz { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration }, - LLMProviders.GWDG => new ProviderGWDG { InstanceName = instanceName, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration }, + LLMProviders.HELMHOLTZ => new ProviderHelmholtz { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration }, + LLMProviders.GWDG => new ProviderGWDG { InstanceName = instanceName, ConfiguredProviderId = configuredProviderId, AdditionalJsonApiParameters = expertProviderApiParameter, IsEnterpriseConfiguration = isEnterpriseConfiguration }, _ => new NoProvider(), }; diff --git a/app/MindWork AI Studio/Provider/NoProvider.cs b/app/MindWork AI Studio/Provider/NoProvider.cs index 1106292a..d69c9bd6 100644 --- a/app/MindWork AI Studio/Provider/NoProvider.cs +++ b/app/MindWork AI Studio/Provider/NoProvider.cs @@ -13,6 +13,8 @@ public class NoProvider : IProvider public string Id => "none"; + public string ConfiguredProviderId => string.Empty; + public string InstanceName { get; set; } = "None"; /// diff --git a/app/MindWork AI Studio/Settings/DataModel/Data.cs b/app/MindWork AI Studio/Settings/DataModel/Data.cs index cc351cc6..3a232017 100644 --- a/app/MindWork AI Studio/Settings/DataModel/Data.cs +++ b/app/MindWork AI Studio/Settings/DataModel/Data.cs @@ -23,6 +23,11 @@ public sealed class Data /// public DataConfidence Confidence { get; init; } = new(x => x.Confidence); + /// + /// Settings concerning data source security checks. + /// + public DataSourceSecuritySettings DataSourceSecurity { get; init; } = new(x => x.DataSourceSecurity); + /// /// A collection of embedding providers configured. /// diff --git a/app/MindWork AI Studio/Settings/DataSourceSecuritySettings.cs b/app/MindWork AI Studio/Settings/DataSourceSecuritySettings.cs new file mode 100644 index 00000000..5c84240a --- /dev/null +++ b/app/MindWork AI Studio/Settings/DataSourceSecuritySettings.cs @@ -0,0 +1,20 @@ +using System.Linq.Expressions; + +using AIStudio.Settings.DataModel; + +namespace AIStudio.Settings; + +public sealed class DataSourceSecuritySettings(Expression>? configSelection = null) +{ + /// + /// The default constructor for the JSON deserializer. + /// + public DataSourceSecuritySettings() : this(null) + { + } + + /// + /// Provider instance IDs trusted by an organization for data-source security checks. + /// + public HashSet TrustedProviderIds { get; set; } = ManagedConfiguration.Register(configSelection, n => n.TrustedProviderIds, []); +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/DataSourceSecurityTrustExtensions.cs b/app/MindWork AI Studio/Settings/DataSourceSecurityTrustExtensions.cs new file mode 100644 index 00000000..c53b4c76 --- /dev/null +++ b/app/MindWork AI Studio/Settings/DataSourceSecurityTrustExtensions.cs @@ -0,0 +1,46 @@ +using AIStudio.Provider; + +namespace AIStudio.Settings; + +public static class DataSourceSecurityTrustExtensions +{ + public static bool IsTrustedForDataSourceSecurityChecks(this Provider provider, SettingsManager settingsManager) + { + if (provider == Provider.NONE) + return false; + + return provider.IsSelfHosted || IsTrustedProviderId(provider.Id, settingsManager); + } + + public static bool IsTrustedForDataSourceSecurityChecks(this EmbeddingProvider provider, SettingsManager settingsManager) + { + if (provider == EmbeddingProvider.NONE) + return false; + + return provider.IsSelfHosted || IsTrustedProviderId(provider.Id, settingsManager); + } + + public static bool IsTrustedForDataSourceSecurityChecks(this TranscriptionProvider provider, SettingsManager settingsManager) + { + if (provider == TranscriptionProvider.NONE) + return false; + + return provider.IsSelfHosted || IsTrustedProviderId(provider.Id, settingsManager); + } + + public static bool IsTrustedForDataSourceSecurityChecks(this IProvider provider, SettingsManager settingsManager) + { + if (provider is NoProvider) + return false; + + return provider.Provider is LLMProviders.SELF_HOSTED || IsTrustedProviderId(provider.ConfiguredProviderId, settingsManager); + } + + private static bool IsTrustedProviderId(string providerId, SettingsManager settingsManager) + { + if (string.IsNullOrWhiteSpace(providerId)) + return false; + + return settingsManager.ConfigurationData.DataSourceSecurity.TrustedProviderIds.Any(id => string.Equals(id, providerId, StringComparison.OrdinalIgnoreCase)); + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/SettingsManager.cs b/app/MindWork AI Studio/Settings/SettingsManager.cs index 58390f71..ace9055e 100644 --- a/app/MindWork AI Studio/Settings/SettingsManager.cs +++ b/app/MindWork AI Studio/Settings/SettingsManager.cs @@ -483,6 +483,7 @@ public sealed class SettingsManager throw new ArgumentException("Expression must be a property access", nameof(propertyExpression)); // Return the full name of the property, including the class name: - return $"{typeof(TIn).Name}.{memberExpr.Member.Name}"; + var typeName = typeof(TIn) == typeof(DataSourceSecuritySettings) ? "DataSourceSecurity" : typeof(TIn).Name; + return $"{typeName}.{memberExpr.Member.Name}"; } } diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs index de3593d5..3677c252 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs @@ -201,6 +201,9 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT ManagedConfiguration.TryProcessConfiguration(x => x.Confidence, x => x.ShowProviderConfidence, this.Id, settingsTable, dryRun); ManagedConfiguration.TryProcessConfiguration(x => x.Confidence, x => x.ConfidenceScheme, this.Id, settingsTable, dryRun); ManagedConfiguration.TryProcessConfiguration(x => x.Confidence, x => x.CustomConfidenceScheme, this.Id, settingsTable, dryRun); + + // Config: data source security settings + ManagedConfiguration.TryProcessConfiguration(x => x.DataSourceSecurity, x => x.TrustedProviderIds, this.Id, settingsTable, dryRun); // Handle configured LLM providers: PluginConfigurationObject.TryParse(PluginConfigurationObjectType.LLM_PROVIDER, x => x.Providers, x => x.NextProviderNum, mainTable, this.Id, ref this.configObjects, dryRun); diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs index 4b7646f3..c4d488c5 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs @@ -283,6 +283,10 @@ public static partial class PluginFactory if(ManagedConfiguration.IsConfigurationLeftOver(x => x.Confidence, x => x.CustomConfidenceScheme, AVAILABLE_PLUGINS)) wasConfigurationChanged = true; + // Check data source security settings: + if(ManagedConfiguration.IsConfigurationLeftOver(x => x.DataSourceSecurity, x => x.TrustedProviderIds, AVAILABLE_PLUGINS)) + wasConfigurationChanged = true; + // Check if audit is required before it can be activated if(ManagedConfiguration.IsConfigurationLeftOver(x => x.AssistantPluginAudit, x => x.RequireAuditBeforeActivation, AVAILABLE_PLUGINS)) wasConfigurationChanged = true; diff --git a/app/MindWork AI Studio/Tools/Services/DataSourceService.cs b/app/MindWork AI Studio/Tools/Services/DataSourceService.cs index 06804474..dbd8954a 100644 --- a/app/MindWork AI Studio/Tools/Services/DataSourceService.cs +++ b/app/MindWork AI Studio/Tools/Services/DataSourceService.cs @@ -1,6 +1,5 @@ using AIStudio.Assistants.ERI; using AIStudio.Provider; -using AIStudio.Provider.SelfHosted; using AIStudio.Settings; using AIStudio.Settings.DataModel; using AIStudio.Tools.ERIClient; @@ -43,7 +42,7 @@ public sealed class DataSourceService return new([], []); } - return await this.GetDataSources(selectedLLMProvider.IsSelfHosted, previousSelectedDataSources); + return await this.GetDataSources(selectedLLMProvider.IsTrustedForDataSourceSecurityChecks(this.settingsManager), previousSelectedDataSources); } /// @@ -66,10 +65,10 @@ public sealed class DataSourceService return new([], []); } - return await this.GetDataSources(selectedLLMProvider is ProviderSelfHosted, previousSelectedDataSources); + return await this.GetDataSources(selectedLLMProvider.IsTrustedForDataSourceSecurityChecks(this.settingsManager), previousSelectedDataSources); } - private async Task GetDataSources(bool usingSelfHostedProvider, IReadOnlyCollection? previousSelectedDataSources = null) + private async Task GetDataSources(bool usingTrustedProvider, IReadOnlyCollection? previousSelectedDataSources = null) { var allDataSources = this.settingsManager.ConfigurationData.DataSources; var filteredDataSources = new List(allDataSources.Count); @@ -78,7 +77,7 @@ public sealed class DataSourceService // Start all checks in parallel: foreach (var source in allDataSources) - tasks.Add(this.CheckOneDataSource(source, usingSelfHostedProvider)); + tasks.Add(this.CheckOneDataSource(source, usingTrustedProvider)); // Wait for all checks and collect the results: foreach (var task in tasks) @@ -95,7 +94,7 @@ public sealed class DataSourceService return new(filteredDataSources, filteredSelectedDataSources); } - private async Task CheckOneDataSource(IDataSource source, bool usingSelfHostedProvider) + private async Task CheckOneDataSource(IDataSource source, bool usingTrustedProvider) { // // Unfortunately, we have to live-check any ERI source for its security requirements. @@ -137,10 +136,10 @@ public sealed class DataSourceService case DataSourceSecurity.ALLOW_ANY: // - // Case: The data source allows any provider type. We want to use a self-hosted provider. + // Case: The data source allows any provider type. We want to use a trusted provider. // There is no issue with this source. Accept it. // - if(usingSelfHostedProvider) + if(usingTrustedProvider) return source; // @@ -151,13 +150,13 @@ public sealed class DataSourceService return source; // - // Case: The ERI source requires a self-hosted provider. This misconfiguration happens + // Case: The ERI source requires a self-hosted or organization-trusted provider. This misconfiguration happens // when the ERI server operator changes the security requirements. The ERI server // operator owns the data -- we have to respect their rules. We skip this source. // if (eriSourceRequirements is { AllowedProviderType: ProviderType.SELF_HOSTED }) { - this.logger.LogWarning($"The ERI source '{source.Name}' (id={source.Id}) requires a self-hosted provider. We skip this source."); + this.logger.LogWarning($"The ERI source '{source.Name}' (id={source.Id}) requires a self-hosted or organization-trusted provider. We skip this source."); return null; } @@ -171,22 +170,22 @@ public sealed class DataSourceService // // Case: Missing rules. We skip this source. Better safe than sorry. // - this.logger.LogDebug($"The ERI source '{source.Name}' (id={source.Id}) was filtered out due to missing rules."); + this.logger.LogWarning($"The ERI source '{source.Name}' (id={source.Id}) was filtered out due to missing rules."); return null; // - // Case: The data source requires a self-hosted provider. We want to use a self-hosted provider. + // Case: The data source requires a trusted provider. We want to use a trusted provider. // There is no issue with this source. Accept it. // - case DataSourceSecurity.SELF_HOSTED when usingSelfHostedProvider: + case DataSourceSecurity.SELF_HOSTED when usingTrustedProvider: return source; // - // Case: The data source requires a self-hosted provider. We want to use a cloud provider. + // Case: The data source requires a trusted provider. We want to use an untrusted provider. // We skip this source. // - case DataSourceSecurity.SELF_HOSTED when !usingSelfHostedProvider: - this.logger.LogWarning($"The data source '{source.Name}' (id={source.Id}) requires a self-hosted provider. We skip this source."); + case DataSourceSecurity.SELF_HOSTED when !usingTrustedProvider: + this.logger.LogWarning($"The data source '{source.Name}' (id={source.Id}) requires a self-hosted or organization-trusted provider. We skip this source."); return null; // diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.6.2.md b/app/MindWork AI Studio/wwwroot/changelog/v26.6.2.md index 48dd540c..b606e448 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.6.2.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.6.2.md @@ -2,6 +2,7 @@ - Added a read-only view for organization-managed profiles and chat templates, so users can inspect the content while the organization remains in control of changes. - Added support for organization-managed introduction texts on the home page. Configuration plugins can now add custom Markdown introductions and hide the built-in introduction. - Added support for organization-managed provider confidence settings. Configuration plugins can now set confidence presets, custom confidence schemes, and an app-wide minimum confidence level. +- Added support for organization-trusted providers in data source security checks. Configuration plugins can now mark specific provider instances as trusted for data source usage and local embedding warnings. - Changed provider confidence settings to appear in their own settings panel, because they apply to LLM, embedding, and transcription providers. - Fixed organization-managed chat templates not showing the correct icon in the chat template selection menu. - Fixed self-hosted provider API keys sometimes being stored under a localized name. AI Studio now uses a stable key name, keeps correct entries working, and automatically migrates known localized entries for LLM, transcription, and embedding providers. Organizations using configuration plugins do not need to change their plugins; affected users who still see an invalid API key warning should open the provider, transcription, or embedding settings and update the API key once. \ No newline at end of file