Refactor secret management to include SecretStoreType for data sources

This commit is contained in:
Thorsten Sommer 2026-05-17 14:28:14 +02:00
parent e9fa4187fc
commit cea7f09653
Signed by untrusted user who does not match committer: tsommer
GPG Key ID: 371BBA77A02C0108
18 changed files with 182 additions and 143 deletions

View File

@ -116,7 +116,7 @@ public partial class DataSourceERI_V1Dialog : MSGComponentBase, ISecretId
if (this.dataAuthMethod is AuthMethod.TOKEN or AuthMethod.USERNAME_PASSWORD) if (this.dataAuthMethod is AuthMethod.TOKEN or AuthMethod.USERNAME_PASSWORD)
{ {
// Load the secret: // Load the secret:
var requestedSecret = await this.RustService.GetSecret(this); var requestedSecret = await this.RustService.GetSecret(this, SecretStoreType.DATA_SOURCE);
if (requestedSecret.Success) if (requestedSecret.Success)
this.dataSecret = await requestedSecret.Secret.Decrypt(this.encryption); this.dataSecret = await requestedSecret.Secret.Decrypt(this.encryption);
else else
@ -324,7 +324,7 @@ public partial class DataSourceERI_V1Dialog : MSGComponentBase, ISecretId
if (!string.IsNullOrWhiteSpace(this.dataSecret)) if (!string.IsNullOrWhiteSpace(this.dataSecret))
{ {
// Store the secret in the OS secure storage: // Store the secret in the OS secure storage:
var storeResponse = await this.RustService.SetSecret(this, this.dataSecret); var storeResponse = await this.RustService.SetSecret(this, this.dataSecret, SecretStoreType.DATA_SOURCE);
if (!storeResponse.Success) if (!storeResponse.Success)
{ {
this.dataSecretStorageIssue = string.Format(T("Failed to store the auth. secret in the operating system. The message was: {0}. Please try again."), storeResponse.Issue); this.dataSecretStorageIssue = string.Format(T("Failed to store the auth. secret in the operating system. The message was: {0}. Please try again."), storeResponse.Issue);

View File

@ -180,7 +180,7 @@ public partial class SettingsDialogDataSources : SettingsDialogBase
// All other auth methods require a secret, which we need to delete now: // All other auth methods require a secret, which we need to delete now:
else else
{ {
var deleteSecretResponse = await this.RustService.DeleteSecret(externalDataSource); var deleteSecretResponse = await this.RustService.DeleteSecret(externalDataSource, SecretStoreType.DATA_SOURCE);
if (deleteSecretResponse.Success) if (deleteSecretResponse.Success)
applyChanges = true; applyChanges = true;
} }

View File

@ -2,7 +2,6 @@
using AIStudio.Assistants.ERI; using AIStudio.Assistants.ERI;
using AIStudio.Chat; using AIStudio.Chat;
using AIStudio.Tools;
using AIStudio.Tools.ERIClient; using AIStudio.Tools.ERIClient;
using AIStudio.Tools.ERIClient.DataModel; using AIStudio.Tools.ERIClient.DataModel;
using AIStudio.Tools.PluginSystem; using AIStudio.Tools.PluginSystem;
@ -313,7 +312,8 @@ public readonly record struct DataSourceERI_V1 : IERIDataSource
PendingEnterpriseSecrets.Add(new( PendingEnterpriseSecrets.Add(new(
$"{ISecretId.ENTERPRISE_KEY_PREFIX}::{dataSource.Id}", $"{ISecretId.ENTERPRISE_KEY_PREFIX}::{dataSource.Id}",
dataSource.Name, dataSource.Name,
decryptedSecret)); decryptedSecret,
SecretStoreType.DATA_SOURCE));
LOGGER.LogDebug($"Successfully decrypted the {secretFieldName} for data source {idx}. It will be stored in the OS keyring. (Plugin ID: {configPluginId})"); LOGGER.LogDebug($"Successfully decrypted the {secretFieldName} for data source {idx}. It will be stored in the OS keyring. (Plugin ID: {configPluginId})");
return true; return true;
} }

View File

@ -16,21 +16,6 @@ namespace AIStudio.Settings;
[JsonDerivedType(typeof(DataSourceERI_V1), nameof(DataSourceType.ERI_V1))] [JsonDerivedType(typeof(DataSourceERI_V1), nameof(DataSourceType.ERI_V1))]
public interface IDataSource : IConfigurationObject public interface IDataSource : IConfigurationObject
{ {
/// <summary>
/// The number of the data source.
/// </summary>
public uint Num { get; init; }
/// <summary>
/// The unique identifier of the data source.
/// </summary>
public string Id { get; init; }
/// <summary>
/// The name of the data source.
/// </summary>
public string Name { get; init; }
/// <summary> /// <summary>
/// Which type of data source is this? /// Which type of data source is this?
/// </summary> /// </summary>
@ -41,16 +26,6 @@ public interface IDataSource : IConfigurationObject
/// </summary> /// </summary>
public DataSourceSecurity SecurityPolicy { get; init; } public DataSourceSecurity SecurityPolicy { get; init; }
/// <summary>
/// Is this data source an enterprise configuration?
/// </summary>
public bool IsEnterpriseConfiguration { get; init; }
/// <summary>
/// The ID of the enterprise configuration plugin.
/// </summary>
public Guid EnterpriseConfigurationPluginId { get; init; }
/// <summary> /// <summary>
/// The maximum number of matches to return when retrieving data from the ERI server. /// The maximum number of matches to return when retrieving data from the ERI server.
/// </summary> /// </summary>

View File

@ -1,4 +1,5 @@
using AIStudio.Assistants.ERI; using AIStudio.Assistants.ERI;
using AIStudio.Settings.DataModel;
using AIStudio.Tools.ERIClient.DataModel; using AIStudio.Tools.ERIClient.DataModel;
namespace AIStudio.Settings; namespace AIStudio.Settings;

View File

@ -7,7 +7,7 @@ public interface IExternalDataSource : IDataSource, ISecretId
#region Implementation of ISecretId #region Implementation of ISecretId
[JsonIgnore] [JsonIgnore]
string ISecretId.SecretId => this.IsEnterpriseConfiguration ? $"{ISecretId.ENTERPRISE_KEY_PREFIX}::{this.Id}" : this.Id; string ISecretId.SecretId => this.IsEnterpriseConfiguration ? $"{ENTERPRISE_KEY_PREFIX}::{this.Id}" : this.Id;
[JsonIgnore] [JsonIgnore]
string ISecretId.SecretName => this.Name; string ISecretId.SecretName => this.Name;

View File

@ -119,7 +119,7 @@ public class ERIClientV1(IERIDataSource dataSource) : ERIClientBase(dataSource),
string password; string password;
if (string.IsNullOrWhiteSpace(temporarySecret)) if (string.IsNullOrWhiteSpace(temporarySecret))
{ {
var passwordResponse = await rustService.GetSecret(this.DataSource); var passwordResponse = await rustService.GetSecret(this.DataSource, SecretStoreType.DATA_SOURCE);
if (!passwordResponse.Success) if (!passwordResponse.Success)
{ {
return new() return new()
@ -173,7 +173,7 @@ public class ERIClientV1(IERIDataSource dataSource) : ERIClientBase(dataSource),
string token; string token;
if (string.IsNullOrWhiteSpace(temporarySecret)) if (string.IsNullOrWhiteSpace(temporarySecret))
{ {
var tokenResponse = await rustService.GetSecret(this.DataSource); var tokenResponse = await rustService.GetSecret(this.DataSource, SecretStoreType.DATA_SOURCE);
if (!tokenResponse.Success) if (!tokenResponse.Success)
{ {
return new() return new()

View File

@ -14,80 +14,3 @@ public sealed record PendingEnterpriseApiKey(
string SecretName, string SecretName,
string ApiKey, string ApiKey,
SecretStoreType StoreType); 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;
}
}
}
/// <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>
public sealed record PendingEnterpriseSecret(
string SecretId,
string SecretName,
string SecretData);
/// <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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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);

View File

@ -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;
}
}
}

View File

@ -62,7 +62,7 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
try try
{ {
var secretId = new TemporarySecretId(pendingSecret.SecretId, pendingSecret.SecretName); var secretId = new TemporarySecretId(pendingSecret.SecretId, pendingSecret.SecretName);
var result = await rustService.SetSecret(secretId, pendingSecret.SecretData); var result = await rustService.SetSecret(secretId, pendingSecret.SecretData, pendingSecret.StoreType);
if (result.Success) if (result.Success)
LOG.LogDebug($"Successfully stored enterprise secret for '{pendingSecret.SecretName}' in the OS keyring."); LOG.LogDebug($"Successfully stored enterprise secret for '{pendingSecret.SecretName}' in the OS keyring.");

View File

@ -252,6 +252,7 @@ public sealed record PluginConfigurationObject
/// <param name="availablePlugins">A list of currently available plugins.</param> /// <param name="availablePlugins">A list of currently available plugins.</param>
/// <param name="configObjectList">A list of all existing configuration objects.</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="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> /// <returns>Returns true if the configuration was altered during cleanup; otherwise, false.</returns>
public static async Task<bool> CleanLeftOverConfigurationObjects<TClass>( public static async Task<bool> CleanLeftOverConfigurationObjects<TClass>(
PluginConfigurationObjectType configObjectType, PluginConfigurationObjectType configObjectType,
@ -304,7 +305,7 @@ public sealed record PluginConfigurationObject
// Delete the API key from the OS keyring if the removed object has one: // Delete the API key from the OS keyring if the removed object has one:
if(deleteSecret && item is ISecretId regularSecretId) if(deleteSecret && item is ISecretId regularSecretId)
{ {
var deleteResult = await RUST_SERVICE.DeleteSecret(regularSecretId); var deleteResult = await RUST_SERVICE.DeleteSecret(regularSecretId, secretStoreType ?? SecretStoreType.DATA_SOURCE);
if (deleteResult.Success) if (deleteResult.Success)
LOG.LogInformation($"Successfully deleted secret for removed enterprise object '{item.Name}' from the OS keyring."); LOG.LogInformation($"Successfully deleted secret for removed enterprise object '{item.Name}' from the OS keyring.");
else else

View File

@ -175,7 +175,7 @@ public static partial class PluginFactory
wasConfigurationChanged = true; wasConfigurationChanged = true;
// Check data sources: // Check data sources:
if(await PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.DATA_SOURCE, x => x.DataSources, AVAILABLE_PLUGINS, configObjectList, deleteSecret: true)) if(await PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.DATA_SOURCE, x => x.DataSources, AVAILABLE_PLUGINS, configObjectList, SecretStoreType.DATA_SOURCE, deleteSecret: true))
wasConfigurationChanged = true; wasConfigurationChanged = true;
// Check chat templates: // Check chat templates:

View File

@ -1,10 +1,10 @@
namespace AIStudio.Tools; namespace AIStudio.Tools;
/// <summary> /// <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> /// </summary>
/// <remarks> /// <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 /// This prevents collisions when the same instance name is used across
/// different provider types (e.g., LLM, Embedding, Transcription). /// different provider types (e.g., LLM, Embedding, Transcription).
/// </remarks> /// </remarks>
@ -29,4 +29,9 @@ public enum SecretStoreType
/// Image provider secrets. Uses the "image::" prefix. /// Image provider secrets. Uses the "image::" prefix.
/// </summary> /// </summary>
IMAGE_PROVIDER, IMAGE_PROVIDER,
/// <summary>
/// Data source secrets. Uses the "data-source::" prefix.
/// </summary>
DATA_SOURCE,
} }

View File

@ -9,12 +9,14 @@ public static class SecretStoreTypeExtensions
/// LLM_PROVIDER uses the legacy "provider" prefix for backward compatibility. /// LLM_PROVIDER uses the legacy "provider" prefix for backward compatibility.
/// </remarks> /// </remarks>
/// <param name="type">The SecretStoreType enum value.</param> /// <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 public static string Prefix(this SecretStoreType type) => type switch
{ {
SecretStoreType.LLM_PROVIDER => "provider", SecretStoreType.LLM_PROVIDER => "provider",
SecretStoreType.EMBEDDING_PROVIDER => "embedding", SecretStoreType.EMBEDDING_PROVIDER => "embedding",
SecretStoreType.TRANSCRIPTION_PROVIDER => "transcription", SecretStoreType.TRANSCRIPTION_PROVIDER => "transcription",
SecretStoreType.IMAGE_PROVIDER => "image",
SecretStoreType.DATA_SOURCE => "data-source",
_ => "provider", _ => "provider",
}; };

View File

@ -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."); logger.LogInformation("The enterprise encryption secret changed. Refreshing the enterprise encryption service and reloading plugins.");
PluginFactory.InitializeEnterpriseEncryption(enterpriseEncryptionSecret); PluginFactory.InitializeEnterpriseEncryption(enterpriseEncryptionSecret);
await this.RemoveEnterpriseManagedApiKeysAsync(); await this.RemoveEnterpriseManagedSecretsAsync();
await PluginFactory.LoadAll(); await PluginFactory.LoadAll();
} }
@ -249,36 +249,36 @@ public sealed class EnterpriseEnvironmentService(ILogger<EnterpriseEnvironmentSe
return serverUrl.Trim().TrimEnd('/'); return serverUrl.Trim().TrimEnd('/');
} }
private async Task RemoveEnterpriseManagedApiKeysAsync() private async Task RemoveEnterpriseManagedSecretsAsync()
{ {
var secretTargets = GetEnterpriseManagedSecretTargets(); var secretTargets = GetEnterpriseManagedSecretTargets();
if (secretTargets.Count == 0) 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; 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) foreach (var target in secretTargets)
{ {
try try
{ {
var deleteResult = target.StoreType is { } storeType var deleteResult = target.StoreType is SecretStoreType.DATA_SOURCE
? await rustService.DeleteAPIKey(target, storeType) ? await rustService.DeleteSecret(target, target.StoreType)
: await rustService.DeleteSecret(target); : await rustService.DeleteAPIKey(target, target.StoreType);
if (deleteResult.Success) if (deleteResult.Success)
{ {
if (deleteResult.WasEntryFound) 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 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 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) 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);
} }
} }
} }
@ -291,14 +291,14 @@ public sealed class EnterpriseEnvironmentService(ILogger<EnterpriseEnvironmentSe
AddEnterpriseManagedSecretTargets(configurationData.Providers, SecretStoreType.LLM_PROVIDER, secretTargets); AddEnterpriseManagedSecretTargets(configurationData.Providers, SecretStoreType.LLM_PROVIDER, secretTargets);
AddEnterpriseManagedSecretTargets(configurationData.EmbeddingProviders, SecretStoreType.EMBEDDING_PROVIDER, secretTargets); AddEnterpriseManagedSecretTargets(configurationData.EmbeddingProviders, SecretStoreType.EMBEDDING_PROVIDER, secretTargets);
AddEnterpriseManagedSecretTargets(configurationData.TranscriptionProviders, SecretStoreType.TRANSCRIPTION_PROVIDER, secretTargets); AddEnterpriseManagedSecretTargets(configurationData.TranscriptionProviders, SecretStoreType.TRANSCRIPTION_PROVIDER, secretTargets);
AddEnterpriseManagedSecretTargets(configurationData.DataSources.OfType<IExternalDataSource>(), null, secretTargets); AddEnterpriseManagedSecretTargets(configurationData.DataSources.OfType<IExternalDataSource>(), SecretStoreType.DATA_SOURCE, secretTargets);
return secretTargets.ToList(); return secretTargets.ToList();
} }
private static void AddEnterpriseManagedSecretTargets<TSecret>( private static void AddEnterpriseManagedSecretTargets<TSecret>(
IEnumerable<TSecret> secrets, IEnumerable<TSecret> secrets,
SecretStoreType? storeType, SecretStoreType storeType,
ISet<EnterpriseSecretTarget> secretTargets) where TSecret : ISecretId, IConfigurationObject ISet<EnterpriseSecretTarget> secretTargets) where TSecret : ISecretId, IConfigurationObject
{ {
foreach (var secret in secrets) foreach (var secret in secrets)

View File

@ -4,26 +4,42 @@ namespace AIStudio.Tools.Services;
public sealed partial class RustService 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> /// <summary>
/// Try to get the secret data for the given secret ID. /// Try to get the secret data for the given secret ID.
/// </summary> /// </summary>
/// <param name="secretId">The secret ID to get the data for.</param> /// <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> /// <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> /// <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 secretKey = SecretKey(secretId, storeType);
var secretRequest = new SelectSecretRequest(secretKey, Environment.UserName, isTrying);
var result = await this.http.PostAsJsonAsync("/secrets/get", secretRequest, this.jsonRustSerializerOptions); var result = await this.http.PostAsJsonAsync("/secrets/get", secretRequest, this.jsonRustSerializerOptions);
if (!result.IsSuccessStatusCode) if (!result.IsSuccessStatusCode)
{ {
if(!isTrying) if(!isTrying)
this.logger!.LogError($"Failed to get the secret data for secret ID '{secretId.SecretId}' due to an API issue: '{result.StatusCode}'"); 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 new RequestedSecret(false, new EncryptedText(string.Empty), TB("Failed to get the secret data due to an API issue."));
} }
var secret = await result.Content.ReadFromJsonAsync<RequestedSecret>(this.jsonRustSerializerOptions); var secret = await result.Content.ReadFromJsonAsync<RequestedSecret>(this.jsonRustSerializerOptions);
if (!secret.Success && storeType is SecretStoreType.DATA_SOURCE)
{
var legacySecret = await this.GetLegacySecret(secretId, isTrying: true);
if (legacySecret.Success)
{
this.logger!.LogDebug($"Successfully retrieved the legacy data source secret for '{LegacySecretKey(secretId)}'.");
return legacySecret;
}
}
if (!secret.Success && !isTrying) 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; return secret;
} }
@ -33,21 +49,26 @@ public sealed partial class RustService
/// </summary> /// </summary>
/// <param name="secretId">The secret ID to store the data for.</param> /// <param name="secretId">The secret ID to store the data for.</param>
/// <param name="secretData">The data to store.</param> /// <param name="secretData">The data to store.</param>
/// <param name="storeType">The secret store type.</param>
/// <returns>The store secret response.</returns> /// <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 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); var result = await this.http.PostAsJsonAsync("/secrets/store", request, this.jsonRustSerializerOptions);
if (!result.IsSuccessStatusCode) if (!result.IsSuccessStatusCode)
{ {
this.logger!.LogError($"Failed to store the secret data for secret ID '{secretId.SecretId}' due to an API issue: '{result.StatusCode}'"); 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 get the secret data due to an API issue.")); 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); var state = await result.Content.ReadFromJsonAsync<StoreSecretResponse>(this.jsonRustSerializerOptions);
if (!state.Success) 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.DeleteSecretKey(secretId, LegacySecretKey(secretId));
return state; return state;
} }
@ -56,20 +77,49 @@ public sealed partial class RustService
/// Tries to delete the secret data for the given secret ID. /// Tries to delete the secret data for the given secret ID.
/// </summary> /// </summary>
/// <param name="secretId">The secret ID to delete the data for.</param> /// <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> /// <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.DeleteSecretKey(secretId, SecretKey(secretId, storeType));
if (storeType is not SecretStoreType.DATA_SOURCE || !deleteResult.Success)
return deleteResult;
var legacyDeleteResult = await this.DeleteSecretKey(secretId, LegacySecretKey(secretId));
if (!legacyDeleteResult.Success)
return legacyDeleteResult;
return deleteResult with { WasEntryFound = deleteResult.WasEntryFound || legacyDeleteResult.WasEntryFound };
}
private async Task<RequestedSecret> GetLegacySecret(ISecretId secretId, bool isTrying)
{
var secretKey = LegacySecretKey(secretId);
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> DeleteSecretKey(ISecretId secretId, string secretKey)
{
var request = new SelectSecretRequest(secretKey, Environment.UserName, false);
var result = await this.http.PostAsJsonAsync("/secrets/delete", request, this.jsonRustSerializerOptions); var result = await this.http.PostAsJsonAsync("/secrets/delete", request, this.jsonRustSerializerOptions);
if (!result.IsSuccessStatusCode) 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.")}; 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); var state = await result.Content.ReadFromJsonAsync<DeleteSecretResponse>(this.jsonRustSerializerOptions);
if (!state.Success) 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; return state;
} }