mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-13 00:14:26 +00:00
Added settings & features for administrators in organizations (#653)
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage deb updater) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Read metadata (push) Waiting to run
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg updater) (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) (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 deb updater) (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 updater) (push) Blocked by required conditions
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) (push) Blocked by required conditions
Build and Release / Publish release (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 deb updater) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Read metadata (push) Waiting to run
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg updater) (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) (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 deb updater) (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 updater) (push) Blocked by required conditions
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) (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
This commit is contained in:
1 parent
5af6a8db3e
commit
4cf62672de
38 files changed
+1330
-90
No files matched your search
@@ -0,0 +1,211 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace AIStudio.Tools;
|
||||
|
||||
/// <summary>
|
||||
/// Provides encryption and decryption functionality for enterprise configuration plugins.
|
||||
/// This is used to encrypt/decrypt API keys in Lua configuration files.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Important: This is obfuscation, not security. Users with administrative access
|
||||
/// to their machines can potentially extract the decrypted API keys. This feature
|
||||
/// is designed to prevent casual exposure of API keys in configuration files. It
|
||||
/// also protects against accidental leaks while sharing configuration snippets,
|
||||
/// as the encrypted values cannot be decrypted without the secret key.
|
||||
/// </remarks>
|
||||
public sealed class EnterpriseEncryption
|
||||
{
|
||||
/// <summary>
|
||||
/// The number of iterations to derive the key and IV from the password.
|
||||
/// We use a higher iteration count here because the secret is static
|
||||
/// (not regenerated each startup like the IPC encryption).
|
||||
/// </summary>
|
||||
private const int ITERATIONS = 10_000;
|
||||
|
||||
/// <summary>
|
||||
/// The length of the salt in bytes.
|
||||
/// </summary>
|
||||
private const int SALT_LENGTH = 16;
|
||||
|
||||
/// <summary>
|
||||
/// The prefix for encrypted values.
|
||||
/// </summary>
|
||||
private const string PREFIX = "ENC:v1:";
|
||||
|
||||
private readonly ILogger<EnterpriseEncryption> logger;
|
||||
private readonly byte[]? secretKey;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the encryption service is available.
|
||||
/// </summary>
|
||||
public bool IsAvailable { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the enterprise encryption service.
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger instance.</param>
|
||||
/// <param name="base64Secret">The base64-encoded 32-byte encryption secret.</param>
|
||||
public EnterpriseEncryption(ILogger<EnterpriseEncryption> logger, string? base64Secret)
|
||||
{
|
||||
this.logger = logger;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(base64Secret))
|
||||
{
|
||||
this.logger.LogWarning("No enterprise encryption secret configured. Encrypted API keys in configuration plugins will not be available.");
|
||||
this.IsAvailable = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
this.secretKey = Convert.FromBase64String(base64Secret);
|
||||
if (this.secretKey.Length != 32)
|
||||
{
|
||||
this.logger.LogWarning($"The enterprise encryption secret must be exactly 32 bytes (256 bits). Got {this.secretKey.Length} bytes.");
|
||||
this.secretKey = null;
|
||||
this.IsAvailable = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.IsAvailable = true;
|
||||
this.logger.LogInformation("Enterprise encryption service initialized successfully.");
|
||||
}
|
||||
catch (FormatException ex)
|
||||
{
|
||||
this.logger.LogWarning(ex, "Failed to decode the enterprise encryption secret from base64.");
|
||||
this.IsAvailable = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the given value is encrypted (has the encryption prefix).
|
||||
/// </summary>
|
||||
/// <param name="value">The value to check.</param>
|
||||
/// <returns>True if the value starts with the encryption prefix; otherwise, false.</returns>
|
||||
public static bool IsEncrypted(string? value) => value?.StartsWith(PREFIX, StringComparison.Ordinal) ?? false;
|
||||
|
||||
/// <summary>
|
||||
/// Tries to decrypt an encrypted value.
|
||||
/// </summary>
|
||||
/// <param name="encryptedValue">The encrypted value (with ENC:v1: prefix).</param>
|
||||
/// <param name="decryptedValue">When successful, contains the decrypted plaintext.</param>
|
||||
/// <returns>True if decryption was successful; otherwise, false.</returns>
|
||||
public bool TryDecrypt(string encryptedValue, out string decryptedValue)
|
||||
{
|
||||
decryptedValue = string.Empty;
|
||||
if (!this.IsAvailable)
|
||||
{
|
||||
this.logger.LogWarning("Cannot decrypt: Enterprise encryption service is not available.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!IsEncrypted(encryptedValue))
|
||||
{
|
||||
this.logger.LogWarning("Cannot decrypt: Value does not have the expected encryption prefix.");
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Extract the base64-encoded data after the prefix:
|
||||
var base64Data = encryptedValue[PREFIX.Length..];
|
||||
var encryptedBytes = Convert.FromBase64String(base64Data);
|
||||
if (encryptedBytes.Length < SALT_LENGTH + 1)
|
||||
{
|
||||
this.logger.LogWarning("Cannot decrypt: Encrypted data is too short.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Extract salt and encrypted content:
|
||||
var salt = encryptedBytes[..SALT_LENGTH];
|
||||
var cipherText = encryptedBytes[SALT_LENGTH..];
|
||||
|
||||
// Derive key and IV using PBKDF2:
|
||||
using var keyDerivation = new Rfc2898DeriveBytes(this.secretKey!, salt, ITERATIONS, HashAlgorithmName.SHA512);
|
||||
var key = keyDerivation.GetBytes(32); // AES-256
|
||||
var iv = keyDerivation.GetBytes(16); // AES block size
|
||||
|
||||
// Decrypt using AES-256-CBC:
|
||||
using var aes = Aes.Create();
|
||||
aes.Key = key;
|
||||
aes.IV = iv;
|
||||
aes.Mode = CipherMode.CBC;
|
||||
aes.Padding = PaddingMode.PKCS7;
|
||||
|
||||
using var decryptor = aes.CreateDecryptor();
|
||||
var decryptedBytes = decryptor.TransformFinalBlock(cipherText, 0, cipherText.Length);
|
||||
decryptedValue = Encoding.UTF8.GetString(decryptedBytes);
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (FormatException ex)
|
||||
{
|
||||
this.logger.LogWarning(ex, "Failed to decode encrypted value from base64.");
|
||||
return false;
|
||||
}
|
||||
catch (CryptographicException ex)
|
||||
{
|
||||
this.logger.LogWarning(ex, "Failed to decrypt value. The encryption secret may be incorrect.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Encrypts a plaintext value.
|
||||
/// </summary>
|
||||
/// <param name="plaintext">The plaintext to encrypt.</param>
|
||||
/// <param name="encryptedValue">When successful, contains the encrypted value with prefix.</param>
|
||||
/// <returns>True if encryption was successful; otherwise, false.</returns>
|
||||
public bool TryEncrypt(string plaintext, out string encryptedValue)
|
||||
{
|
||||
encryptedValue = string.Empty;
|
||||
if (!this.IsAvailable)
|
||||
{
|
||||
this.logger.LogWarning("Cannot encrypt: Enterprise encryption service is not available.");
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Generate a random salt:
|
||||
var salt = RandomNumberGenerator.GetBytes(SALT_LENGTH);
|
||||
|
||||
// Derive key and IV using PBKDF2:
|
||||
using var keyDerivation = new Rfc2898DeriveBytes(this.secretKey!, salt, ITERATIONS, HashAlgorithmName.SHA512);
|
||||
var key = keyDerivation.GetBytes(32); // AES-256
|
||||
var iv = keyDerivation.GetBytes(16); // AES block size
|
||||
|
||||
// Encrypt using AES-256-CBC:
|
||||
using var aes = Aes.Create();
|
||||
aes.Key = key;
|
||||
aes.IV = iv;
|
||||
aes.Mode = CipherMode.CBC;
|
||||
aes.Padding = PaddingMode.PKCS7;
|
||||
|
||||
using var encryptor = aes.CreateEncryptor();
|
||||
var plaintextBytes = Encoding.UTF8.GetBytes(plaintext);
|
||||
var cipherText = encryptor.TransformFinalBlock(plaintextBytes, 0, plaintextBytes.Length);
|
||||
|
||||
// Combine salt and ciphertext
|
||||
var combined = new byte[SALT_LENGTH + cipherText.Length];
|
||||
Array.Copy(salt, 0, combined, 0, SALT_LENGTH);
|
||||
Array.Copy(cipherText, 0, combined, SALT_LENGTH, cipherText.Length);
|
||||
|
||||
// Encode to base64 and add the prefix:
|
||||
encryptedValue = PREFIX + Convert.ToBase64String(combined);
|
||||
return true;
|
||||
}
|
||||
catch (CryptographicException ex)
|
||||
{
|
||||
this.logger.LogWarning(ex, "Failed to encrypt value.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a new random 32-byte secret key and returns it as a base64 string.
|
||||
/// </summary>
|
||||
/// <returns>A base64-encoded 32-byte secret key.</returns>
|
||||
public static string GenerateSecret() => Convert.ToBase64String(RandomNumberGenerator.GetBytes(32));
|
||||
}
|
||||
@@ -5,6 +5,13 @@ namespace AIStudio.Tools;
|
||||
/// </summary>
|
||||
public interface ISecretId
|
||||
{
|
||||
/// <summary>
|
||||
/// Prefix used for secrets imported from enterprise configuration plugins.
|
||||
/// This helps distinguish enterprise-managed keys from user-added keys
|
||||
/// in the OS keyring.
|
||||
/// </summary>
|
||||
public const string ENTERPRISE_KEY_PREFIX = "config-plugin";
|
||||
|
||||
/// <summary>
|
||||
/// The unique ID of the secret.
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace AIStudio.Tools;
|
||||
|
||||
public static class LuaTools
|
||||
{
|
||||
public static string EscapeLuaString(string? value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
return string.Empty;
|
||||
|
||||
return value
|
||||
.Replace("\\", "\\\\")
|
||||
.Replace("\"", "\\\"")
|
||||
.Replace("\r", "\\r")
|
||||
.Replace("\n", "\\n");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
namespace AIStudio.Tools.PluginSystem;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a pending API key that needs to be stored in the OS keyring.
|
||||
/// This is used during plugin loading to collect API keys from configuration plugins
|
||||
/// before storing them asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="SecretId">The secret ID (provider ID).</param>
|
||||
/// <param name="SecretName">The secret name (provider instance name).</param>
|
||||
/// <param name="ApiKey">The decrypted API key.</param>
|
||||
/// <param name="StoreType">The type of secret store to use.</param>
|
||||
public sealed record PendingEnterpriseApiKey(
|
||||
string SecretId,
|
||||
string SecretName,
|
||||
string ApiKey,
|
||||
SecretStoreType StoreType);
|
||||
|
||||
/// <summary>
|
||||
/// Static container for pending API keys during plugin loading.
|
||||
/// </summary>
|
||||
public static class PendingEnterpriseApiKeys
|
||||
{
|
||||
private static readonly List<PendingEnterpriseApiKey> PENDING_KEYS = [];
|
||||
private static readonly Lock LOCK = new();
|
||||
|
||||
/// <summary>
|
||||
/// Adds a pending API key to the list.
|
||||
/// </summary>
|
||||
/// <param name="key">The pending API key to add.</param>
|
||||
public static void Add(PendingEnterpriseApiKey key)
|
||||
{
|
||||
lock (LOCK)
|
||||
PENDING_KEYS.Add(key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets and clears all pending API keys.
|
||||
/// </summary>
|
||||
/// <returns>A list of all pending API keys.</returns>
|
||||
public static IReadOnlyList<PendingEnterpriseApiKey> GetAndClear()
|
||||
{
|
||||
lock (LOCK)
|
||||
{
|
||||
var keys = PENDING_KEYS.ToList();
|
||||
PENDING_KEYS.Clear();
|
||||
return keys;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Tools.Services;
|
||||
|
||||
using Lua;
|
||||
|
||||
@@ -8,7 +9,8 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
|
||||
{
|
||||
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(PluginConfiguration).Namespace, nameof(PluginConfiguration));
|
||||
private static readonly SettingsManager SETTINGS_MANAGER = Program.SERVICE_PROVIDER.GetRequiredService<SettingsManager>();
|
||||
|
||||
private static readonly ILogger LOG = Program.LOGGER_FACTORY.CreateLogger(nameof(PluginConfiguration));
|
||||
|
||||
private List<PluginConfigurationObject> configObjects = [];
|
||||
|
||||
/// <summary>
|
||||
@@ -23,11 +25,50 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
|
||||
|
||||
if (!dryRun)
|
||||
{
|
||||
// Store any decrypted API keys from enterprise configuration in the OS keyring:
|
||||
await StoreEnterpriseApiKeysAsync();
|
||||
|
||||
await SETTINGS_MANAGER.StoreSettings();
|
||||
await MessageBus.INSTANCE.SendMessage<bool>(null, Event.CONFIGURATION_CHANGED);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stores any pending enterprise API keys in the OS keyring.
|
||||
/// </summary>
|
||||
private static async Task StoreEnterpriseApiKeysAsync()
|
||||
{
|
||||
var pendingKeys = PendingEnterpriseApiKeys.GetAndClear();
|
||||
if (pendingKeys.Count == 0)
|
||||
return;
|
||||
|
||||
LOG.LogInformation($"Storing {pendingKeys.Count} enterprise API key(s) in the OS keyring.");
|
||||
var rustService = Program.SERVICE_PROVIDER.GetRequiredService<RustService>();
|
||||
foreach (var pendingKey in pendingKeys)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Create a temporary secret ID object for storing the key:
|
||||
var secretId = new TemporarySecretId(pendingKey.SecretId, pendingKey.SecretName);
|
||||
var result = await rustService.SetAPIKey(secretId, pendingKey.ApiKey, pendingKey.StoreType);
|
||||
|
||||
if (result.Success)
|
||||
LOG.LogDebug($"Successfully stored enterprise API key for '{pendingKey.SecretName}' in the OS keyring.");
|
||||
else
|
||||
LOG.LogWarning($"Failed to store enterprise API key for '{pendingKey.SecretName}': {result.Issue}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LOG.LogError(ex, $"Exception while storing enterprise API key for '{pendingKey.SecretName}'.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Temporary implementation of ISecretId for storing enterprise API keys.
|
||||
/// </summary>
|
||||
private sealed record TemporarySecretId(string SecretId, string SecretName) : ISecretId;
|
||||
|
||||
/// <summary>
|
||||
/// Tries to initialize the UI text content of the plugin.
|
||||
/// </summary>
|
||||
@@ -60,6 +101,9 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
|
||||
|
||||
// Config: allow the user to add providers?
|
||||
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.AllowUserToAddProvider, this.Id, settingsTable, dryRun);
|
||||
|
||||
// Config: show administration settings?
|
||||
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.ShowAdminSettings, this.Id, settingsTable, dryRun);
|
||||
|
||||
// Config: preview features visibility
|
||||
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.PreviewVisibility, this.Id, settingsTable, dryRun);
|
||||
@@ -100,4 +144,4 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
|
||||
message = string.Empty;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ using System.Linq.Expressions;
|
||||
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Settings.DataModel;
|
||||
using AIStudio.Tools.Services;
|
||||
|
||||
using Lua;
|
||||
|
||||
@@ -13,6 +14,7 @@ namespace AIStudio.Tools.PluginSystem;
|
||||
/// </summary>
|
||||
public sealed record PluginConfigurationObject
|
||||
{
|
||||
private static readonly RustService RUST_SERVICE = Program.SERVICE_PROVIDER.GetRequiredService<RustService>();
|
||||
private static readonly SettingsManager SETTINGS_MANAGER = Program.SERVICE_PROVIDER.GetRequiredService<SettingsManager>();
|
||||
private static readonly ILogger LOG = Program.LOGGER_FACTORY.CreateLogger<PluginConfigurationObject>();
|
||||
|
||||
@@ -159,7 +161,7 @@ public sealed record PluginConfigurationObject
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Cleans up configuration objects of a specified type that are no longer associated with any available plugin.
|
||||
/// </summary>
|
||||
@@ -168,37 +170,45 @@ public sealed record PluginConfigurationObject
|
||||
/// <param name="configObjectSelection">A selection expression to retrieve the configuration objects from the main configuration.</param>
|
||||
/// <param name="availablePlugins">A list of currently available plugins.</param>
|
||||
/// <param name="configObjectList">A list of all existing configuration objects.</param>
|
||||
/// <param name="secretStoreType">An optional parameter specifying the type of secret store to use for deleting associated API keys from the OS keyring, if applicable.</param>
|
||||
/// <returns>Returns true if the configuration was altered during cleanup; otherwise, false.</returns>
|
||||
public static bool CleanLeftOverConfigurationObjects<TClass>(
|
||||
public static async Task<bool> CleanLeftOverConfigurationObjects<TClass>(
|
||||
PluginConfigurationObjectType configObjectType,
|
||||
Expression<Func<Data, List<TClass>>> configObjectSelection,
|
||||
IList<IAvailablePlugin> availablePlugins,
|
||||
IList<PluginConfigurationObject> configObjectList) where TClass : IConfigurationObject
|
||||
IList<PluginConfigurationObject> configObjectList,
|
||||
SecretStoreType? secretStoreType = null) where TClass : IConfigurationObject
|
||||
{
|
||||
var configuredObjects = configObjectSelection.Compile()(SETTINGS_MANAGER.ConfigurationData);
|
||||
var leftOverObjects = new List<TClass>();
|
||||
foreach (var configuredObject in configuredObjects)
|
||||
{
|
||||
// Only process objects that are based on enterprise configuration plugins (aka configuration plugins),
|
||||
// as only those can be left over after a plugin was removed:
|
||||
if(!configuredObject.IsEnterpriseConfiguration)
|
||||
continue;
|
||||
|
||||
// From what plugin is this configuration object coming from?
|
||||
var configObjectSourcePluginId = configuredObject.EnterpriseConfigurationPluginId;
|
||||
if(configObjectSourcePluginId == Guid.Empty)
|
||||
continue;
|
||||
|
||||
// Is the source plugin still available? If not, we can be pretty sure that this configuration object is left
|
||||
// over and should be removed:
|
||||
var templateSourcePlugin = availablePlugins.FirstOrDefault(plugin => plugin.Id == configObjectSourcePluginId);
|
||||
if(templateSourcePlugin is null)
|
||||
{
|
||||
LOG.LogWarning($"The configured object '{configuredObject.Name}' (id={configuredObject.Id}) is based on a plugin that is not available anymore. Removing the chat template from the settings.");
|
||||
LOG.LogWarning($"The configured object '{configuredObject.Name}' (id={configuredObject.Id}) is based on a plugin that is not available anymore. Removing this object from the settings.");
|
||||
leftOverObjects.Add(configuredObject);
|
||||
}
|
||||
|
||||
// Is the configuration object still present in the configuration plugin? If not, it is also left over and should be removed:
|
||||
if(!configObjectList.Any(configObject =>
|
||||
configObject.Type == configObjectType &&
|
||||
configObject.ConfigPluginId == configObjectSourcePluginId &&
|
||||
configObject.Id.ToString() == configuredObject.Id))
|
||||
{
|
||||
LOG.LogWarning($"The configured object '{configuredObject.Name}' (id={configuredObject.Id}) is not present in the configuration plugin anymore. Removing the chat template from the settings.");
|
||||
LOG.LogWarning($"The configured object '{configuredObject.Name}' (id={configuredObject.Id}) is not present in the configuration plugin anymore. Removing the object from the settings.");
|
||||
leftOverObjects.Add(configuredObject);
|
||||
}
|
||||
}
|
||||
@@ -206,8 +216,20 @@ public sealed record PluginConfigurationObject
|
||||
// Remove collected items after enumeration to avoid modifying the collection during iteration:
|
||||
var wasConfigurationChanged = leftOverObjects.Count > 0;
|
||||
foreach (var item in leftOverObjects.Distinct())
|
||||
{
|
||||
configuredObjects.Remove(item);
|
||||
|
||||
// Delete the API key from the OS keyring if the removed object has one:
|
||||
if(secretStoreType is not null && item is ISecretId secretId)
|
||||
{
|
||||
var deleteResult = await RUST_SERVICE.DeleteAPIKey(secretId, secretStoreType.Value);
|
||||
if (deleteResult.Success)
|
||||
LOG.LogInformation($"Successfully deleted API key for removed enterprise provider '{item.Name}' from the OS keyring.");
|
||||
else
|
||||
LOG.LogWarning($"Failed to delete API key for removed enterprise provider '{item.Name}' from the OS keyring: {deleteResult.Issue}");
|
||||
}
|
||||
}
|
||||
|
||||
return wasConfigurationChanged;
|
||||
}
|
||||
}
|
||||
@@ -131,26 +131,26 @@ public static partial class PluginFactory
|
||||
//
|
||||
|
||||
// Check LLM providers:
|
||||
var wasConfigurationChanged = PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.LLM_PROVIDER, x => x.Providers, AVAILABLE_PLUGINS, configObjectList);
|
||||
var wasConfigurationChanged = await PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.LLM_PROVIDER, x => x.Providers, AVAILABLE_PLUGINS, configObjectList, SecretStoreType.LLM_PROVIDER);
|
||||
|
||||
// Check transcription providers:
|
||||
if(PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.TRANSCRIPTION_PROVIDER, x => x.TranscriptionProviders, AVAILABLE_PLUGINS, configObjectList))
|
||||
if(await PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.TRANSCRIPTION_PROVIDER, x => x.TranscriptionProviders, AVAILABLE_PLUGINS, configObjectList, SecretStoreType.TRANSCRIPTION_PROVIDER))
|
||||
wasConfigurationChanged = true;
|
||||
|
||||
// Check embedding providers:
|
||||
if(PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.EMBEDDING_PROVIDER, x => x.EmbeddingProviders, AVAILABLE_PLUGINS, configObjectList))
|
||||
if(await PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.EMBEDDING_PROVIDER, x => x.EmbeddingProviders, AVAILABLE_PLUGINS, configObjectList, SecretStoreType.EMBEDDING_PROVIDER))
|
||||
wasConfigurationChanged = true;
|
||||
|
||||
// Check chat templates:
|
||||
if(PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.CHAT_TEMPLATE, x => x.ChatTemplates, AVAILABLE_PLUGINS, configObjectList))
|
||||
if(await PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.CHAT_TEMPLATE, x => x.ChatTemplates, AVAILABLE_PLUGINS, configObjectList))
|
||||
wasConfigurationChanged = true;
|
||||
|
||||
|
||||
// Check profiles:
|
||||
if(PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.PROFILE, x => x.Profiles, AVAILABLE_PLUGINS, configObjectList))
|
||||
if(await PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.PROFILE, x => x.Profiles, AVAILABLE_PLUGINS, configObjectList))
|
||||
wasConfigurationChanged = true;
|
||||
|
||||
// Check document analysis policies:
|
||||
if(PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.DOCUMENT_ANALYSIS_POLICY, x => x.DocumentAnalysis.Policies, AVAILABLE_PLUGINS, configObjectList))
|
||||
if(await PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.DOCUMENT_ANALYSIS_POLICY, x => x.DocumentAnalysis.Policies, AVAILABLE_PLUGINS, configObjectList))
|
||||
wasConfigurationChanged = true;
|
||||
|
||||
// Check for a preselected profile:
|
||||
@@ -168,6 +168,10 @@ public static partial class PluginFactory
|
||||
// Check for users allowed to added providers:
|
||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.AllowUserToAddProvider, AVAILABLE_PLUGINS))
|
||||
wasConfigurationChanged = true;
|
||||
|
||||
// Check for admin settings visibility:
|
||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.ShowAdminSettings, AVAILABLE_PLUGINS))
|
||||
wasConfigurationChanged = true;
|
||||
|
||||
// Check for preview visibility:
|
||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.PreviewVisibility, AVAILABLE_PLUGINS))
|
||||
@@ -253,4 +257,4 @@ public static partial class PluginFactory
|
||||
return new NoPlugin("This plugin type is not supported yet. Please try again with a future version of AI Studio.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,29 @@ public static partial class PluginFactory
|
||||
|
||||
public static ILanguagePlugin BaseLanguage => BASE_LANGUAGE_PLUGIN;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the enterprise encryption instance for decrypting API keys in configuration plugins.
|
||||
/// </summary>
|
||||
public static EnterpriseEncryption? EnterpriseEncryption { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the enterprise encryption service by reading the encryption secret
|
||||
/// from the Windows Registry or environment variables.
|
||||
/// </summary>
|
||||
/// <param name="rustService">The Rust service to use for reading the encryption secret.</param>
|
||||
public static async Task InitializeEnterpriseEncryption(Services.RustService rustService)
|
||||
{
|
||||
LOG.LogInformation("Initializing enterprise encryption service...");
|
||||
var encryptionSecret = await rustService.EnterpriseEnvConfigEncryptionSecret();
|
||||
var enterpriseEncryptionLogger = Program.LOGGER_FACTORY.CreateLogger<EnterpriseEncryption>();
|
||||
EnterpriseEncryption = new EnterpriseEncryption(enterpriseEncryptionLogger, encryptionSecret);
|
||||
|
||||
if (EnterpriseEncryption.IsAvailable)
|
||||
LOG.LogInformation("Enterprise encryption service is available.");
|
||||
else
|
||||
LOG.LogWarning("Enterprise encryption service is not available (no secret configured).");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set up the plugin factory. We will read the data directory from the settings manager.
|
||||
/// Afterward, we will create the plugins directory and the internal plugin directory.
|
||||
|
||||
@@ -65,4 +65,24 @@ public sealed partial class RustService
|
||||
var serverUrl = await result.Content.ReadAsStringAsync();
|
||||
return string.IsNullOrWhiteSpace(serverUrl) ? string.Empty : serverUrl;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to read the enterprise environment for the configuration encryption secret.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// Returns an empty string when the environment is not set or the request fails.
|
||||
/// Otherwise, the base64-encoded encryption secret.
|
||||
/// </returns>
|
||||
public async Task<string> EnterpriseEnvConfigEncryptionSecret()
|
||||
{
|
||||
var result = await this.http.GetAsync("/system/enterprise/config/encryption_secret");
|
||||
if (!result.IsSuccessStatusCode)
|
||||
{
|
||||
this.logger!.LogError($"Failed to query the enterprise configuration encryption secret: '{result.StatusCode}'");
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var encryptionSecret = await result.Content.ReadAsStringAsync();
|
||||
return string.IsNullOrWhiteSpace(encryptionSecret) ? string.Empty : encryptionSecret;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user