mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-13 19:33:48 +00:00
Configure ERI servers in config plugins (#767)
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (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
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 / 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
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
This commit is contained in:
1 parent
378aaaa368
commit
7a09241888
43 files changed
+1165
-108
No files matched your search
@@ -2,6 +2,7 @@ using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Settings.DataModel;
|
||||
using AIStudio.Tools.ERIClient.DataModel;
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
using AIStudio.Tools.Services;
|
||||
@@ -102,10 +103,23 @@ public class ERIClientV1(IERIDataSource dataSource) : ERIClientBase(dataSource),
|
||||
}
|
||||
|
||||
case AuthMethod.USERNAME_PASSWORD:
|
||||
if (this.DataSource.UsernamePasswordMode is DataSourceERIUsernamePasswordMode.OS_USERNAME_SHARED_PASSWORD)
|
||||
{
|
||||
username = await rustService.ReadUserName();
|
||||
if (string.IsNullOrWhiteSpace(username))
|
||||
{
|
||||
return new()
|
||||
{
|
||||
Successful = false,
|
||||
Message = TB("Failed to read the user's username from the operating system.")
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
string password;
|
||||
if (string.IsNullOrWhiteSpace(temporarySecret))
|
||||
{
|
||||
var passwordResponse = await rustService.GetSecret(this.DataSource);
|
||||
var passwordResponse = await rustService.GetSecret(this.DataSource, SecretStoreType.DATA_SOURCE);
|
||||
if (!passwordResponse.Success)
|
||||
{
|
||||
return new()
|
||||
@@ -159,7 +173,7 @@ public class ERIClientV1(IERIDataSource dataSource) : ERIClientBase(dataSource),
|
||||
string token;
|
||||
if (string.IsNullOrWhiteSpace(temporarySecret))
|
||||
{
|
||||
var tokenResponse = await rustService.GetSecret(this.DataSource);
|
||||
var tokenResponse = await rustService.GetSecret(this.DataSource, SecretStoreType.DATA_SOURCE);
|
||||
if (!tokenResponse.Success)
|
||||
{
|
||||
return new()
|
||||
|
||||
@@ -13,37 +13,4 @@ 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
SecretStoreType StoreType);
|
||||
@@ -0,0 +1,34 @@
|
||||
namespace AIStudio.Tools.PluginSystem;
|
||||
|
||||
/// <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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace AIStudio.Tools.PluginSystem;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a pending enterprise secret that needs to be stored in the OS keyring.
|
||||
/// </summary>
|
||||
/// <param name="SecretId">The secret ID.</param>
|
||||
/// <param name="SecretName">The secret name.</param>
|
||||
/// <param name="SecretData">The decrypted secret data.</param>
|
||||
/// <param name="StoreType">The type of secret store to use.</param>
|
||||
public sealed record PendingEnterpriseSecret(
|
||||
string SecretId,
|
||||
string SecretName,
|
||||
string SecretData,
|
||||
SecretStoreType StoreType);
|
||||
@@ -0,0 +1,34 @@
|
||||
namespace AIStudio.Tools.PluginSystem;
|
||||
|
||||
/// <summary>
|
||||
/// Static container for pending enterprise secrets during plugin loading.
|
||||
/// </summary>
|
||||
public static class PendingEnterpriseSecrets
|
||||
{
|
||||
private static readonly List<PendingEnterpriseSecret> PENDING_SECRETS = [];
|
||||
private static readonly Lock LOCK = new();
|
||||
|
||||
/// <summary>
|
||||
/// Adds a pending enterprise secret to the list.
|
||||
/// </summary>
|
||||
/// <param name="secret">The pending enterprise secret to add.</param>
|
||||
public static void Add(PendingEnterpriseSecret secret)
|
||||
{
|
||||
lock (LOCK)
|
||||
PENDING_SECRETS.Add(secret);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets and clears all pending enterprise secrets.
|
||||
/// </summary>
|
||||
/// <returns>A list of all pending enterprise secrets.</returns>
|
||||
public static IReadOnlyList<PendingEnterpriseSecret> GetAndClear()
|
||||
{
|
||||
lock (LOCK)
|
||||
{
|
||||
var secrets = PENDING_SECRETS.ToList();
|
||||
PENDING_SECRETS.Clear();
|
||||
return secrets;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -39,12 +39,43 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
|
||||
{
|
||||
// Store any decrypted API keys from enterprise configuration in the OS keyring:
|
||||
await StoreEnterpriseApiKeysAsync();
|
||||
await StoreEnterpriseSecretsAsync();
|
||||
|
||||
await SETTINGS_MANAGER.StoreSettings();
|
||||
await MessageBus.INSTANCE.SendMessage<bool>(null, Event.CONFIGURATION_CHANGED);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stores any pending enterprise secrets in the OS keyring.
|
||||
/// </summary>
|
||||
private static async Task StoreEnterpriseSecretsAsync()
|
||||
{
|
||||
var pendingSecrets = PendingEnterpriseSecrets.GetAndClear();
|
||||
if (pendingSecrets.Count == 0)
|
||||
return;
|
||||
|
||||
LOG.LogInformation($"Storing {pendingSecrets.Count} enterprise secret(s) in the OS keyring.");
|
||||
var rustService = Program.SERVICE_PROVIDER.GetRequiredService<RustService>();
|
||||
foreach (var pendingSecret in pendingSecrets)
|
||||
{
|
||||
try
|
||||
{
|
||||
var secretId = new TemporarySecretId(pendingSecret.SecretId, pendingSecret.SecretName);
|
||||
var result = await rustService.SetSecret(secretId, pendingSecret.SecretData, pendingSecret.StoreType);
|
||||
|
||||
if (result.Success)
|
||||
LOG.LogDebug($"Successfully stored enterprise secret for '{pendingSecret.SecretName}' in the OS keyring.");
|
||||
else
|
||||
LOG.LogWarning($"Failed to store enterprise secret for '{pendingSecret.SecretName}': {result.Issue}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LOG.LogError(ex, $"Exception while storing enterprise secret for '{pendingSecret.SecretName}'.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stores any pending enterprise API keys in the OS keyring.
|
||||
/// </summary>
|
||||
@@ -152,6 +183,9 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
|
||||
|
||||
// Handle configured chat templates:
|
||||
PluginConfigurationObject.TryParse(PluginConfigurationObjectType.CHAT_TEMPLATE, x => x.ChatTemplates, x => x.NextChatTemplateNum, mainTable, this.Id, ref this.configObjects, dryRun);
|
||||
|
||||
// Handle configured data sources:
|
||||
PluginConfigurationObject.TryParseDataSources(mainTable, this.Id, ref this.configObjects, dryRun);
|
||||
|
||||
// Handle configured profiles:
|
||||
PluginConfigurationObject.TryParse(PluginConfigurationObjectType.PROFILE, x => x.Profiles, x => x.NextProfileNum, mainTable, this.Id, ref this.configObjects, dryRun);
|
||||
|
||||
@@ -162,6 +162,87 @@ public sealed record PluginConfigurationObject
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses configured data sources from a configuration plugin.
|
||||
/// </summary>
|
||||
/// <param name="mainTable">The Lua table containing entries to parse into data sources.</param>
|
||||
/// <param name="configPluginId">The unique identifier of the plugin associated with the data sources.</param>
|
||||
/// <param name="configObjects">The list to populate with the parsed configuration objects.</param>
|
||||
/// <param name="dryRun">Specifies whether to perform the operation as a dry run.</param>
|
||||
/// <returns>True if the table was present and processed; otherwise false.</returns>
|
||||
public static bool TryParseDataSources(
|
||||
LuaTable mainTable,
|
||||
Guid configPluginId,
|
||||
ref List<PluginConfigurationObject> configObjects,
|
||||
bool dryRun)
|
||||
{
|
||||
const string LUA_TABLE_NAME = "DATA_SOURCES";
|
||||
if (!mainTable.TryGetValue(LUA_TABLE_NAME, out var luaValue) || !luaValue.TryRead<LuaTable>(out var luaTable))
|
||||
{
|
||||
LOG.LogWarning("The table '{LuaTableName}' does not exist or is not a valid table (config plugin id: {ConfigPluginId}).", LUA_TABLE_NAME, configPluginId);
|
||||
return false;
|
||||
}
|
||||
|
||||
var storedObjects = SETTINGS_MANAGER.ConfigurationData.DataSources;
|
||||
var numberObjects = luaTable.ArrayLength;
|
||||
ThreadSafeRandom? random = null;
|
||||
for (var i = 1; i <= numberObjects; i++)
|
||||
{
|
||||
var luaObjectTableValue = luaTable[i];
|
||||
if (!luaObjectTableValue.TryRead<LuaTable>(out var luaObjectTable))
|
||||
{
|
||||
LOG.LogWarning("The table '{LuaTableName}' entry at index {Index} is not a valid table (config plugin id: {ConfigPluginId}).", LUA_TABLE_NAME, i, configPluginId);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!DataSourceERI_V1.TryParseConfiguration(i, luaObjectTable, configPluginId, out var configObject))
|
||||
{
|
||||
LOG.LogWarning("The table '{LuaTableName}' entry at index {Index} does not contain a valid data source (config plugin id: {ConfigPluginId}).", LUA_TABLE_NAME, i, configPluginId);
|
||||
continue;
|
||||
}
|
||||
|
||||
configObjects.Add(new()
|
||||
{
|
||||
ConfigPluginId = configPluginId,
|
||||
Id = Guid.Parse(configObject.Id),
|
||||
Type = PluginConfigurationObjectType.DATA_SOURCE,
|
||||
});
|
||||
|
||||
if (dryRun)
|
||||
continue;
|
||||
|
||||
var objectIndex = storedObjects.FindIndex(t => t.Id == configObject.Id);
|
||||
if (objectIndex > -1)
|
||||
{
|
||||
var existingObject = storedObjects[objectIndex];
|
||||
configObject = configObject with { Num = existingObject.Num };
|
||||
storedObjects[objectIndex] = configObject;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (IncrementDataSourceNum() is { Success: true, UpdatedValue: var nextNum })
|
||||
{
|
||||
configObject = configObject with { Num = nextNum };
|
||||
storedObjects.Add(configObject);
|
||||
}
|
||||
else
|
||||
{
|
||||
random ??= new ThreadSafeRandom();
|
||||
configObject = configObject with { Num = (uint)random.Next(500_000, 1_000_000) };
|
||||
storedObjects.Add(configObject);
|
||||
LOG.LogWarning("The next number for the data source '{ConfigObjectName}' (id={ConfigObjectId}) could not be incremented. Using a random number instead (config plugin id: {ConfigPluginId}).", configObject.Name, configObject.Id, configPluginId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
static IncrementResult<uint> IncrementDataSourceNum()
|
||||
{
|
||||
return ((Expression<Func<Data, uint>>)(x => x.NextDataSourceNum)).TryIncrement(SETTINGS_MANAGER.ConfigurationData, IncrementType.POST);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cleans up configuration objects of a specified type that are no longer associated with any available plugin.
|
||||
/// </summary>
|
||||
@@ -171,13 +252,15 @@ public sealed record PluginConfigurationObject
|
||||
/// <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>
|
||||
/// <param name="deleteSecret">When true, delete the associated non-API-key secret from the OS keyring.</param>
|
||||
/// <returns>Returns true if the configuration was altered during cleanup; otherwise, false.</returns>
|
||||
public static async Task<bool> CleanLeftOverConfigurationObjects<TClass>(
|
||||
PluginConfigurationObjectType configObjectType,
|
||||
Expression<Func<Data, List<TClass>>> configObjectSelection,
|
||||
IList<IAvailablePlugin> availablePlugins,
|
||||
IList<PluginConfigurationObject> configObjectList,
|
||||
SecretStoreType? secretStoreType = null) where TClass : IConfigurationObject
|
||||
SecretStoreType? secretStoreType = null,
|
||||
bool deleteSecret = false) where TClass : IConfigurationObject
|
||||
{
|
||||
var configuredObjects = configObjectSelection.Compile()(SETTINGS_MANAGER.ConfigurationData);
|
||||
var leftOverObjects = new List<TClass>();
|
||||
@@ -220,7 +303,15 @@ public sealed record PluginConfigurationObject
|
||||
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)
|
||||
if(deleteSecret && item is ISecretId regularSecretId)
|
||||
{
|
||||
var deleteResult = await RUST_SERVICE.DeleteSecret(regularSecretId, secretStoreType ?? SecretStoreType.DATA_SOURCE);
|
||||
if (deleteResult.Success)
|
||||
LOG.LogInformation($"Successfully deleted secret for removed enterprise object '{item.Name}' from the OS keyring.");
|
||||
else
|
||||
LOG.LogWarning($"Failed to delete secret for removed enterprise object '{item.Name}' from the OS keyring: {deleteResult.Issue}");
|
||||
}
|
||||
else if(secretStoreType is not null && item is ISecretId secretId)
|
||||
{
|
||||
var deleteResult = await RUST_SERVICE.DeleteAPIKey(secretId, secretStoreType.Value);
|
||||
if (deleteResult.Success)
|
||||
|
||||
@@ -174,6 +174,10 @@ public static partial class PluginFactory
|
||||
if(await PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.EMBEDDING_PROVIDER, x => x.EmbeddingProviders, AVAILABLE_PLUGINS, configObjectList, SecretStoreType.EMBEDDING_PROVIDER))
|
||||
wasConfigurationChanged = true;
|
||||
|
||||
// Check data sources:
|
||||
if(await PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.DATA_SOURCE, x => x.DataSources, AVAILABLE_PLUGINS, configObjectList, SecretStoreType.DATA_SOURCE, deleteSecret: true))
|
||||
wasConfigurationChanged = true;
|
||||
|
||||
// Check chat templates:
|
||||
if(await PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.CHAT_TEMPLATE, x => x.ChatTemplates, AVAILABLE_PLUGINS, configObjectList))
|
||||
wasConfigurationChanged = true;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
namespace AIStudio.Tools;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the type of secret store used for API keys.
|
||||
/// Represents the type of secret store used for API keys and other secrets.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Different provider types use different prefixes for storing API keys.
|
||||
/// Different provider and secret types use different prefixes for storing secrets.
|
||||
/// This prevents collisions when the same instance name is used across
|
||||
/// different provider types (e.g., LLM, Embedding, Transcription).
|
||||
/// </remarks>
|
||||
@@ -29,4 +29,9 @@ public enum SecretStoreType
|
||||
/// Image provider secrets. Uses the "image::" prefix.
|
||||
/// </summary>
|
||||
IMAGE_PROVIDER,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Data source secrets. Uses the "data-source::" prefix.
|
||||
/// </summary>
|
||||
DATA_SOURCE,
|
||||
}
|
||||
@@ -9,12 +9,14 @@ public static class SecretStoreTypeExtensions
|
||||
/// LLM_PROVIDER uses the legacy "provider" prefix for backward compatibility.
|
||||
/// </remarks>
|
||||
/// <param name="type">The SecretStoreType enum value.</param>
|
||||
/// <returns>>The corresponding prefix string.</returns>
|
||||
/// <returns>The corresponding prefix string.</returns>
|
||||
public static string Prefix(this SecretStoreType type) => type switch
|
||||
{
|
||||
SecretStoreType.LLM_PROVIDER => "provider",
|
||||
SecretStoreType.EMBEDDING_PROVIDER => "embedding",
|
||||
SecretStoreType.TRANSCRIPTION_PROVIDER => "transcription",
|
||||
SecretStoreType.IMAGE_PROVIDER => "image",
|
||||
SecretStoreType.DATA_SOURCE => "data-source",
|
||||
|
||||
_ => "provider",
|
||||
};
|
||||
|
||||
@@ -200,7 +200,7 @@ public sealed class EnterpriseEnvironmentService(ILogger<EnterpriseEnvironmentSe
|
||||
{
|
||||
logger.LogInformation("The enterprise encryption secret changed. Refreshing the enterprise encryption service and reloading plugins.");
|
||||
PluginFactory.InitializeEnterpriseEncryption(enterpriseEncryptionSecret);
|
||||
await this.RemoveEnterpriseManagedApiKeysAsync();
|
||||
await this.RemoveEnterpriseManagedSecretsAsync();
|
||||
await PluginFactory.LoadAll();
|
||||
}
|
||||
|
||||
@@ -249,34 +249,36 @@ public sealed class EnterpriseEnvironmentService(ILogger<EnterpriseEnvironmentSe
|
||||
return serverUrl.Trim().TrimEnd('/');
|
||||
}
|
||||
|
||||
private async Task RemoveEnterpriseManagedApiKeysAsync()
|
||||
private async Task RemoveEnterpriseManagedSecretsAsync()
|
||||
{
|
||||
var secretTargets = GetEnterpriseManagedSecretTargets();
|
||||
if (secretTargets.Count == 0)
|
||||
{
|
||||
logger.LogInformation("No enterprise-managed API keys are currently known in the settings. No keyring cleanup is required.");
|
||||
logger.LogInformation("No enterprise-managed secrets are currently known in the settings. No keyring cleanup is required.");
|
||||
return;
|
||||
}
|
||||
|
||||
logger.LogInformation("Removing {SecretCount} enterprise-managed API key(s) from the OS keyring after an enterprise encryption secret change.", secretTargets.Count);
|
||||
logger.LogInformation("Removing {SecretCount} enterprise-managed secret(s) from the OS keyring after an enterprise encryption secret change.", secretTargets.Count);
|
||||
foreach (var target in secretTargets)
|
||||
{
|
||||
try
|
||||
{
|
||||
var deleteResult = await rustService.DeleteAPIKey(target, target.StoreType);
|
||||
var deleteResult = target.StoreType is SecretStoreType.DATA_SOURCE
|
||||
? await rustService.DeleteSecret(target, target.StoreType)
|
||||
: await rustService.DeleteAPIKey(target, target.StoreType);
|
||||
if (deleteResult.Success)
|
||||
{
|
||||
if (deleteResult.WasEntryFound)
|
||||
logger.LogInformation("Successfully deleted enterprise-managed API key '{SecretName}' from the OS keyring.", target.SecretName);
|
||||
logger.LogInformation("Successfully deleted enterprise-managed secret '{SecretName}' from the OS keyring.", target.SecretName);
|
||||
else
|
||||
logger.LogInformation("Enterprise-managed API key '{SecretName}' was already absent from the OS keyring.", target.SecretName);
|
||||
logger.LogInformation("Enterprise-managed secret '{SecretName}' was already absent from the OS keyring.", target.SecretName);
|
||||
}
|
||||
else
|
||||
logger.LogWarning("Failed to delete enterprise-managed API key '{SecretName}' from the OS keyring: {Issue}", target.SecretName, deleteResult.Issue);
|
||||
logger.LogWarning("Failed to delete enterprise-managed secret '{SecretName}' from the OS keyring: {Issue}", target.SecretName, deleteResult.Issue);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogWarning(e, "Failed to delete enterprise-managed API key '{SecretName}' from the OS keyring.", target.SecretName);
|
||||
logger.LogWarning(e, "Failed to delete enterprise-managed secret '{SecretName}' from the OS keyring.", target.SecretName);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -289,6 +291,7 @@ public sealed class EnterpriseEnvironmentService(ILogger<EnterpriseEnvironmentSe
|
||||
AddEnterpriseManagedSecretTargets(configurationData.Providers, SecretStoreType.LLM_PROVIDER, secretTargets);
|
||||
AddEnterpriseManagedSecretTargets(configurationData.EmbeddingProviders, SecretStoreType.EMBEDDING_PROVIDER, secretTargets);
|
||||
AddEnterpriseManagedSecretTargets(configurationData.TranscriptionProviders, SecretStoreType.TRANSCRIPTION_PROVIDER, secretTargets);
|
||||
AddEnterpriseManagedSecretTargets(configurationData.DataSources.OfType<IExternalDataSource>(), SecretStoreType.DATA_SOURCE, secretTargets);
|
||||
|
||||
return secretTargets.ToList();
|
||||
}
|
||||
|
||||
@@ -32,4 +32,35 @@ public sealed partial class RustService
|
||||
this.userLanguageLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string> ReadUserName(bool forceRequest = false)
|
||||
{
|
||||
if (!forceRequest && !string.IsNullOrWhiteSpace(this.cachedUserName))
|
||||
return this.cachedUserName;
|
||||
|
||||
await this.userNameLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
if (!forceRequest && !string.IsNullOrWhiteSpace(this.cachedUserName))
|
||||
return this.cachedUserName;
|
||||
|
||||
var response = await this.http.GetAsync("/system/username");
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
this.logger!.LogError($"Failed to read the user name from Rust: '{response.StatusCode}'");
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var userName = (await response.Content.ReadAsStringAsync()).Trim();
|
||||
if (string.IsNullOrWhiteSpace(userName))
|
||||
return string.Empty;
|
||||
|
||||
this.cachedUserName = userName;
|
||||
return userName;
|
||||
}
|
||||
finally
|
||||
{
|
||||
this.userNameLock.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,26 +4,34 @@ namespace AIStudio.Tools.Services;
|
||||
|
||||
public sealed partial class RustService
|
||||
{
|
||||
private static string SecretKey(ISecretId secretId, SecretStoreType storeType) => $"{storeType.Prefix()}::{secretId.SecretId}::{secretId.SecretName}";
|
||||
|
||||
private static string LegacySecretKey(ISecretId secretId) => $"secret::{secretId.SecretId}::{secretId.SecretName}";
|
||||
|
||||
/// <summary>
|
||||
/// Try to get the secret data for the given secret ID.
|
||||
/// </summary>
|
||||
/// <param name="secretId">The secret ID to get the data for.</param>
|
||||
/// <param name="storeType">The secret store type.</param>
|
||||
/// <param name="isTrying">Indicates if we are trying to get the data. In that case, we don't log errors.</param>
|
||||
/// <returns>The requested secret.</returns>
|
||||
public async Task<RequestedSecret> GetSecret(ISecretId secretId, bool isTrying = false)
|
||||
public async Task<RequestedSecret> GetSecret(ISecretId secretId, SecretStoreType storeType, bool isTrying = false)
|
||||
{
|
||||
var secretRequest = new SelectSecretRequest($"secret::{secretId.SecretId}::{secretId.SecretName}", Environment.UserName, isTrying);
|
||||
var result = await this.http.PostAsJsonAsync("/secrets/get", secretRequest, this.jsonRustSerializerOptions);
|
||||
if (!result.IsSuccessStatusCode)
|
||||
var secretKey = SecretKey(secretId, storeType);
|
||||
var secret = await this.GetSecretByKey(secretKey, isTrying || storeType is SecretStoreType.DATA_SOURCE);
|
||||
if (secret.Success || storeType is not SecretStoreType.DATA_SOURCE)
|
||||
return secret;
|
||||
|
||||
var legacySecretKey = LegacySecretKey(secretId);
|
||||
var legacySecret = await this.GetSecretByKey(legacySecretKey, isTrying: true);
|
||||
if (legacySecret.Success)
|
||||
{
|
||||
if(!isTrying)
|
||||
this.logger!.LogError($"Failed to get the secret data for secret ID '{secretId.SecretId}' due to an API issue: '{result.StatusCode}'");
|
||||
return new RequestedSecret(false, new EncryptedText(string.Empty), TB("Failed to get the secret data due to an API issue."));
|
||||
this.logger!.LogDebug($"Successfully retrieved the legacy data source secret for '{legacySecretKey}'.");
|
||||
return legacySecret;
|
||||
}
|
||||
|
||||
var secret = await result.Content.ReadFromJsonAsync<RequestedSecret>(this.jsonRustSerializerOptions);
|
||||
|
||||
if (!secret.Success && !isTrying)
|
||||
this.logger!.LogError($"Failed to get the secret data for secret ID '{secretId.SecretId}': '{secret.Issue}'");
|
||||
this.logger!.LogError($"Failed to get the secret data for '{secretKey}': '{secret.Issue}'");
|
||||
|
||||
return secret;
|
||||
}
|
||||
@@ -33,21 +41,26 @@ public sealed partial class RustService
|
||||
/// </summary>
|
||||
/// <param name="secretId">The secret ID to store the data for.</param>
|
||||
/// <param name="secretData">The data to store.</param>
|
||||
/// <param name="storeType">The secret store type.</param>
|
||||
/// <returns>The store secret response.</returns>
|
||||
public async Task<StoreSecretResponse> SetSecret(ISecretId secretId, string secretData)
|
||||
public async Task<StoreSecretResponse> SetSecret(ISecretId secretId, string secretData, SecretStoreType storeType)
|
||||
{
|
||||
var secretKey = SecretKey(secretId, storeType);
|
||||
var encryptedSecret = await this.encryptor!.Encrypt(secretData);
|
||||
var request = new StoreSecretRequest($"secret::{secretId.SecretId}::{secretId.SecretName}", Environment.UserName, encryptedSecret);
|
||||
var request = new StoreSecretRequest(secretKey, Environment.UserName, encryptedSecret);
|
||||
var result = await this.http.PostAsJsonAsync("/secrets/store", request, this.jsonRustSerializerOptions);
|
||||
if (!result.IsSuccessStatusCode)
|
||||
{
|
||||
this.logger!.LogError($"Failed to store the secret data for secret ID '{secretId.SecretId}' due to an API issue: '{result.StatusCode}'");
|
||||
return new StoreSecretResponse(false, TB("Failed to get the secret data due to an API issue."));
|
||||
this.logger!.LogError($"Failed to store the secret data for '{secretKey}' due to an API issue: '{result.StatusCode}'");
|
||||
return new StoreSecretResponse(false, TB("Failed to store the secret data due to an API issue."));
|
||||
}
|
||||
|
||||
var state = await result.Content.ReadFromJsonAsync<StoreSecretResponse>(this.jsonRustSerializerOptions);
|
||||
if (!state.Success)
|
||||
this.logger!.LogError($"Failed to store the secret data for secret ID '{secretId.SecretId}': '{state.Issue}'");
|
||||
this.logger!.LogError($"Failed to store the secret data for '{secretKey}': '{state.Issue}'");
|
||||
|
||||
if (state.Success && storeType is SecretStoreType.DATA_SOURCE)
|
||||
await this.DeleteSecretByKey(LegacySecretKey(secretId));
|
||||
|
||||
return state;
|
||||
}
|
||||
@@ -56,20 +69,48 @@ public sealed partial class RustService
|
||||
/// Tries to delete the secret data for the given secret ID.
|
||||
/// </summary>
|
||||
/// <param name="secretId">The secret ID to delete the data for.</param>
|
||||
/// <param name="storeType">The secret store type.</param>
|
||||
/// <returns>The delete secret response.</returns>
|
||||
public async Task<DeleteSecretResponse> DeleteSecret(ISecretId secretId)
|
||||
public async Task<DeleteSecretResponse> DeleteSecret(ISecretId secretId, SecretStoreType storeType)
|
||||
{
|
||||
var request = new SelectSecretRequest($"secret::{secretId.SecretId}::{secretId.SecretName}", Environment.UserName, false);
|
||||
var deleteResult = await this.DeleteSecretByKey(SecretKey(secretId, storeType));
|
||||
if (storeType is not SecretStoreType.DATA_SOURCE || !deleteResult.Success)
|
||||
return deleteResult;
|
||||
|
||||
var legacyDeleteResult = await this.DeleteSecretByKey(LegacySecretKey(secretId));
|
||||
if (!legacyDeleteResult.Success)
|
||||
return legacyDeleteResult;
|
||||
|
||||
return deleteResult with { WasEntryFound = deleteResult.WasEntryFound || legacyDeleteResult.WasEntryFound };
|
||||
}
|
||||
|
||||
private async Task<RequestedSecret> GetSecretByKey(string secretKey, bool isTrying)
|
||||
{
|
||||
var secretRequest = new SelectSecretRequest(secretKey, Environment.UserName, isTrying);
|
||||
var result = await this.http.PostAsJsonAsync("/secrets/get", secretRequest, this.jsonRustSerializerOptions);
|
||||
if (!result.IsSuccessStatusCode)
|
||||
{
|
||||
if(!isTrying)
|
||||
this.logger!.LogError($"Failed to get the secret data for '{secretKey}' due to an API issue: '{result.StatusCode}'");
|
||||
return new RequestedSecret(false, new EncryptedText(string.Empty), TB("Failed to get the secret data due to an API issue."));
|
||||
}
|
||||
|
||||
return await result.Content.ReadFromJsonAsync<RequestedSecret>(this.jsonRustSerializerOptions);
|
||||
}
|
||||
|
||||
private async Task<DeleteSecretResponse> DeleteSecretByKey(string secretKey)
|
||||
{
|
||||
var request = new SelectSecretRequest(secretKey, Environment.UserName, false);
|
||||
var result = await this.http.PostAsJsonAsync("/secrets/delete", request, this.jsonRustSerializerOptions);
|
||||
if (!result.IsSuccessStatusCode)
|
||||
{
|
||||
this.logger!.LogError($"Failed to delete the secret data for secret ID '{secretId.SecretId}' due to an API issue: '{result.StatusCode}'");
|
||||
this.logger!.LogError($"Failed to delete the secret data for '{secretKey}' due to an API issue: '{result.StatusCode}'");
|
||||
return new DeleteSecretResponse{Success = false, WasEntryFound = false, Issue = TB("Failed to delete the secret data due to an API issue.")};
|
||||
}
|
||||
|
||||
var state = await result.Content.ReadFromJsonAsync<DeleteSecretResponse>(this.jsonRustSerializerOptions);
|
||||
if (!state.Success)
|
||||
this.logger!.LogError($"Failed to delete the secret data for secret ID '{secretId.SecretId}': '{state.Issue}'");
|
||||
this.logger!.LogError($"Failed to delete the secret data for '{secretKey}': '{state.Issue}'");
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ public sealed partial class RustService : BackgroundService
|
||||
|
||||
private readonly HttpClient http;
|
||||
private readonly SemaphoreSlim userLanguageLock = new(1, 1);
|
||||
private readonly SemaphoreSlim userNameLock = new(1, 1);
|
||||
|
||||
private readonly JsonSerializerOptions jsonRustSerializerOptions = new()
|
||||
{
|
||||
@@ -31,6 +32,7 @@ public sealed partial class RustService : BackgroundService
|
||||
private ILogger<RustService>? logger;
|
||||
private Encryption? encryptor;
|
||||
private string? cachedUserLanguage;
|
||||
private string? cachedUserName;
|
||||
|
||||
private readonly string apiPort;
|
||||
private readonly string certificateFingerprint;
|
||||
@@ -91,6 +93,7 @@ public sealed partial class RustService : BackgroundService
|
||||
{
|
||||
this.http.Dispose();
|
||||
this.userLanguageLock.Dispose();
|
||||
this.userNameLock.Dispose();
|
||||
base.Dispose();
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user