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

This commit is contained in:
Thorsten Sommer authored and GitHub committed 2026-05-18 16:26:51 +02:00
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();
}