From 1c4f10ffdd65df981691770966dec85125f30edc Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 17 May 2026 21:12:25 +0200 Subject: [PATCH] Improved ERI server export flow --- .../Dialogs/DataSourceERIV1ExportDialog.razor | 52 ------------ .../DataSourceERIV1ExportDialog.razor.cs | 83 ------------------- .../DataSourceERIV1ExportDialogResult.cs | 5 -- ...rceERIV1UsernamePasswordExportDialog.razor | 26 ++++++ ...ERIV1UsernamePasswordExportDialog.razor.cs | 37 +++++++++ ...ERIV1UsernamePasswordExportDialogResult.cs | 5 ++ .../SettingsDialogDataSources.razor.cs | 73 +++++++++++----- .../Settings/DataModel/DataSourceERI_V1.cs | 19 ++--- .../Tools/Services/RustService.Secrets.cs | 29 +++---- 9 files changed, 136 insertions(+), 193 deletions(-) delete mode 100644 app/MindWork AI Studio/Dialogs/DataSourceERIV1ExportDialog.razor delete mode 100644 app/MindWork AI Studio/Dialogs/DataSourceERIV1ExportDialog.razor.cs delete mode 100644 app/MindWork AI Studio/Dialogs/DataSourceERIV1ExportDialogResult.cs create mode 100644 app/MindWork AI Studio/Dialogs/DataSourceERIV1UsernamePasswordExportDialog.razor create mode 100644 app/MindWork AI Studio/Dialogs/DataSourceERIV1UsernamePasswordExportDialog.razor.cs create mode 100644 app/MindWork AI Studio/Dialogs/DataSourceERIV1UsernamePasswordExportDialogResult.cs diff --git a/app/MindWork AI Studio/Dialogs/DataSourceERIV1ExportDialog.razor b/app/MindWork AI Studio/Dialogs/DataSourceERIV1ExportDialog.razor deleted file mode 100644 index 2d79acb6..00000000 --- a/app/MindWork AI Studio/Dialogs/DataSourceERIV1ExportDialog.razor +++ /dev/null @@ -1,52 +0,0 @@ -@using AIStudio.Tools.ERIClient.DataModel -@inherits MSGComponentBase - - - - - @string.Format(T("Export the ERI v1 data source '{0}' as Lua code for a configuration plugin."), this.DataSource.Name) - - - @if (this.NeedsSecret()) - { - @if (!this.HasConfiguredSecret) - { - - @T("No secret is configured for this ERI data source. The export will contain a placeholder that you need to replace manually.") - - } - else if (!this.CanEncryptSecret) - { - - @T("No enterprise encryption secret is configured. The export will contain a placeholder that you need to replace manually.") - - } - - - } - - @if (this.DataSource.AuthMethod is AuthMethod.USERNAME_PASSWORD) - { - - @foreach (var mode in this.availableUsernamePasswordModes) - { - - @this.GetUsernamePasswordModeText(mode) - - } - - } - - - - @T("Cancel") - - - @T("Export") - - - \ No newline at end of file diff --git a/app/MindWork AI Studio/Dialogs/DataSourceERIV1ExportDialog.razor.cs b/app/MindWork AI Studio/Dialogs/DataSourceERIV1ExportDialog.razor.cs deleted file mode 100644 index 905e8fc0..00000000 --- a/app/MindWork AI Studio/Dialogs/DataSourceERIV1ExportDialog.razor.cs +++ /dev/null @@ -1,83 +0,0 @@ -using AIStudio.Components; -using AIStudio.Settings.DataModel; -using AIStudio.Tools.ERIClient.DataModel; - -using Microsoft.AspNetCore.Components; - -namespace AIStudio.Dialogs; - -public partial class DataSourceERIV1ExportDialog : MSGComponentBase -{ - [CascadingParameter] - private IMudDialogInstance MudDialog { get; set; } = null!; - - [Parameter] - public DataSourceERI_V1 DataSource { get; set; } - - [Parameter] - public bool HasConfiguredSecret { get; set; } - - [Parameter] - public bool CanEncryptSecret { get; set; } - - private readonly DataSourceERIUsernamePasswordMode[] availableUsernamePasswordModes = - [ - DataSourceERIUsernamePasswordMode.OS_USERNAME_SHARED_PASSWORD, - DataSourceERIUsernamePasswordMode.SHARED_USERNAME_AND_PASSWORD - ]; - - private bool includeSecret; - private DataSourceERIUsernamePasswordMode usernamePasswordMode = DataSourceERIUsernamePasswordMode.OS_USERNAME_SHARED_PASSWORD; - - private bool CanIncludeSecret => this.HasConfiguredSecret && this.CanEncryptSecret; - - #region Overrides of ComponentBase - - protected override async Task OnInitializedAsync() - { - this.includeSecret = this.CanIncludeSecret; - await base.OnInitializedAsync(); - } - - #endregion - - private bool NeedsSecret() => this.DataSource.AuthMethod is AuthMethod.TOKEN or AuthMethod.USERNAME_PASSWORD; - - private string GetIncludeSecretLabel() => this.DataSource.AuthMethod switch - { - AuthMethod.TOKEN => T("Include the configured access token in the export?"), - AuthMethod.USERNAME_PASSWORD => T("Include the configured password in the export?"), - - _ => T("Include the configured secret in the export?"), - }; - - private string GetIncludeSecretLabelOn() => this.DataSource.AuthMethod switch - { - AuthMethod.TOKEN => T("Yes, export the encrypted access token"), - AuthMethod.USERNAME_PASSWORD => T("Yes, export the encrypted password"), - - _ => T("Yes, export the encrypted secret"), - }; - - private string GetIncludeSecretLabelOff() => this.DataSource.AuthMethod switch - { - AuthMethod.TOKEN => T("No, use a token placeholder"), - AuthMethod.USERNAME_PASSWORD => T("No, use a password placeholder"), - - _ => T("No, use a placeholder"), - }; - - private string GetUsernamePasswordModeText() => this.GetUsernamePasswordModeText(this.usernamePasswordMode); - - private string GetUsernamePasswordModeText(DataSourceERIUsernamePasswordMode mode) => mode switch - { - DataSourceERIUsernamePasswordMode.OS_USERNAME_SHARED_PASSWORD => T("Read each user's username from the operating system and share one password"), - DataSourceERIUsernamePasswordMode.SHARED_USERNAME_AND_PASSWORD => T("Use the same username and password for all users"), - - _ => T("User-managed username and password"), - }; - - private void Cancel() => this.MudDialog.Cancel(); - - private void Export() => this.MudDialog.Close(DialogResult.Ok(new DataSourceERIV1ExportDialogResult(this.includeSecret, this.usernamePasswordMode))); -} \ No newline at end of file diff --git a/app/MindWork AI Studio/Dialogs/DataSourceERIV1ExportDialogResult.cs b/app/MindWork AI Studio/Dialogs/DataSourceERIV1ExportDialogResult.cs deleted file mode 100644 index eca1eac9..00000000 --- a/app/MindWork AI Studio/Dialogs/DataSourceERIV1ExportDialogResult.cs +++ /dev/null @@ -1,5 +0,0 @@ -using AIStudio.Settings.DataModel; - -namespace AIStudio.Dialogs; - -public readonly record struct DataSourceERIV1ExportDialogResult(bool IncludeSecret, DataSourceERIUsernamePasswordMode UsernamePasswordMode); \ No newline at end of file diff --git a/app/MindWork AI Studio/Dialogs/DataSourceERIV1UsernamePasswordExportDialog.razor b/app/MindWork AI Studio/Dialogs/DataSourceERIV1UsernamePasswordExportDialog.razor new file mode 100644 index 00000000..088be8ea --- /dev/null +++ b/app/MindWork AI Studio/Dialogs/DataSourceERIV1UsernamePasswordExportDialog.razor @@ -0,0 +1,26 @@ +@inherits MSGComponentBase + + + + + @string.Format(T("How should AI Studio export the username and password configuration for the ERI v1 data source '{0}'?"), this.DataSource.Name) + + + + @foreach (var mode in this.availableUsernamePasswordModes) + { + + @this.GetUsernamePasswordModeText(mode) + + } + + + + + @T("Cancel") + + + @T("Export") + + + \ No newline at end of file diff --git a/app/MindWork AI Studio/Dialogs/DataSourceERIV1UsernamePasswordExportDialog.razor.cs b/app/MindWork AI Studio/Dialogs/DataSourceERIV1UsernamePasswordExportDialog.razor.cs new file mode 100644 index 00000000..cf0ec960 --- /dev/null +++ b/app/MindWork AI Studio/Dialogs/DataSourceERIV1UsernamePasswordExportDialog.razor.cs @@ -0,0 +1,37 @@ +using AIStudio.Components; +using AIStudio.Settings.DataModel; + +using Microsoft.AspNetCore.Components; + +namespace AIStudio.Dialogs; + +public partial class DataSourceERIV1UsernamePasswordExportDialog : MSGComponentBase +{ + [CascadingParameter] + private IMudDialogInstance MudDialog { get; set; } = null!; + + [Parameter] + public DataSourceERI_V1 DataSource { get; set; } + + private readonly DataSourceERIUsernamePasswordMode[] availableUsernamePasswordModes = + [ + DataSourceERIUsernamePasswordMode.OS_USERNAME_SHARED_PASSWORD, + DataSourceERIUsernamePasswordMode.SHARED_USERNAME_AND_PASSWORD + ]; + + private DataSourceERIUsernamePasswordMode usernamePasswordMode = DataSourceERIUsernamePasswordMode.OS_USERNAME_SHARED_PASSWORD; + + private string GetUsernamePasswordModeText() => this.GetUsernamePasswordModeText(this.usernamePasswordMode); + + private string GetUsernamePasswordModeText(DataSourceERIUsernamePasswordMode mode) => mode switch + { + DataSourceERIUsernamePasswordMode.OS_USERNAME_SHARED_PASSWORD => T("Read each user's username from the operating system and share one password"), + DataSourceERIUsernamePasswordMode.SHARED_USERNAME_AND_PASSWORD => T("Use the same username and password for all users"), + + _ => T("User-managed username and password"), + }; + + private void Cancel() => this.MudDialog.Cancel(); + + private void Export() => this.MudDialog.Close(DialogResult.Ok(new DataSourceERIV1UsernamePasswordExportDialogResult(this.usernamePasswordMode))); +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Dialogs/DataSourceERIV1UsernamePasswordExportDialogResult.cs b/app/MindWork AI Studio/Dialogs/DataSourceERIV1UsernamePasswordExportDialogResult.cs new file mode 100644 index 00000000..907f920e --- /dev/null +++ b/app/MindWork AI Studio/Dialogs/DataSourceERIV1UsernamePasswordExportDialogResult.cs @@ -0,0 +1,5 @@ +using AIStudio.Settings.DataModel; + +namespace AIStudio.Dialogs; + +public readonly record struct DataSourceERIV1UsernamePasswordExportDialogResult(DataSourceERIUsernamePasswordMode UsernamePasswordMode); \ No newline at end of file diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogDataSources.razor.cs b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogDataSources.razor.cs index f904f33f..ff706363 100644 --- a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogDataSources.razor.cs +++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogDataSources.razor.cs @@ -121,35 +121,66 @@ public partial class SettingsDialogDataSources : SettingsDialogBase } var secretResponse = await this.RustService.GetSecret(eriDataSource, SecretStoreType.DATA_SOURCE, isTrying: true); - var canEncryptSecret = PluginFactory.EnterpriseEncryption?.IsAvailable == true; - - var dialogParameters = new DialogParameters + if (!secretResponse.Success) { - { x => x.DataSource, eriDataSource }, - { x => x.HasConfiguredSecret, secretResponse.Success }, - { x => x.CanEncryptSecret, canEncryptSecret }, - }; - - var dialogReference = await this.DialogService.ShowAsync(T("Export ERI Data Source"), dialogParameters, DialogOptions.FULLSCREEN); - var dialogResult = await dialogReference.Result; - if (dialogResult is null || dialogResult.Canceled || dialogResult.Data is not DataSourceERIV1ExportDialogResult exportResult) + await this.DialogService.ShowMessageBox( + T("Export ERI Data Source"), + string.Format(T("Cannot export this ERI data source because no authentication secret is configured. The issue was: {0}"), secretResponse.Issue), + T("Close")); return; + } - string? encryptedSecret = null; - if (exportResult.IncludeSecret && secretResponse.Success) + var encryption = PluginFactory.EnterpriseEncryption; + if (encryption?.IsAvailable != true) { - var decryptedSecret = await secretResponse.Secret.Decrypt(Program.ENCRYPTION); - var encryption = PluginFactory.EnterpriseEncryption; - if (encryption?.TryEncrypt(decryptedSecret, out var encrypted) == true) - encryptedSecret = encrypted; - else - this.Snackbar.Add(T("Cannot export the encrypted secret. A placeholder will be used instead."), Severity.Warning); + await this.DialogService.ShowMessageBox( + T("Export ERI Data Source"), + T("Cannot export this ERI data source because no enterprise encryption secret is configured."), + T("Close")); + return; + } + + var usernamePasswordMode = DataSourceERIUsernamePasswordMode.USER_MANAGED; + if (eriDataSource.AuthMethod is AuthMethod.TOKEN) + { + var dialogParameters = new DialogParameters + { + { x => x.Message, T("This ERI data source has an access token configured. Do you want to include the encrypted access token in the export? Note: The recipient will need the same encryption secret to use the access token.") }, + }; + + var dialogReference = await this.DialogService.ShowAsync(T("Export Access Token?"), dialogParameters, DialogOptions.FULLSCREEN); + var dialogResult = await dialogReference.Result; + if (dialogResult is null || dialogResult.Canceled) + return; + } + else if (eriDataSource.AuthMethod is AuthMethod.USERNAME_PASSWORD) + { + var dialogParameters = new DialogParameters + { + { x => x.DataSource, eriDataSource }, + }; + + var dialogReference = await this.DialogService.ShowAsync(T("Export ERI Data Source"), dialogParameters, DialogOptions.FULLSCREEN); + var dialogResult = await dialogReference.Result; + if (dialogResult is null || dialogResult.Canceled || dialogResult.Data is not DataSourceERIV1UsernamePasswordExportDialogResult exportResult) + return; + + usernamePasswordMode = exportResult.UsernamePasswordMode; + } + + var decryptedSecret = await secretResponse.Secret.Decrypt(Program.ENCRYPTION); + if (!encryption.TryEncrypt(decryptedSecret, out var encryptedSecret)) + { + await this.DialogService.ShowMessageBox( + T("Export ERI Data Source"), + T("Cannot export this ERI data source because the authentication secret could not be encrypted."), + T("Close")); + return; } var luaCode = eriDataSource.ExportAsConfigurationSection( encryptedSecret, - exportResult.UsernamePasswordMode, - needsSecret && string.IsNullOrWhiteSpace(encryptedSecret)); + usernamePasswordMode); if (string.IsNullOrWhiteSpace(luaCode)) return; diff --git a/app/MindWork AI Studio/Settings/DataModel/DataSourceERI_V1.cs b/app/MindWork AI Studio/Settings/DataModel/DataSourceERI_V1.cs index 59ecdc62..cd254751 100644 --- a/app/MindWork AI Studio/Settings/DataModel/DataSourceERI_V1.cs +++ b/app/MindWork AI Studio/Settings/DataModel/DataSourceERI_V1.cs @@ -277,9 +277,8 @@ public readonly record struct DataSourceERI_V1 : IERIDataSource /// /// Optional encrypted token or password to include in the export. /// The organization-managed username/password mode to export. - /// Whether to include a placeholder for the encrypted secret. /// A Lua configuration section string. - public string ExportAsConfigurationSection(string? encryptedSecret = null, DataSourceERIUsernamePasswordMode usernamePasswordMode = DataSourceERIUsernamePasswordMode.USER_MANAGED, bool useSecretPlaceholder = false) + public string ExportAsConfigurationSection(string? encryptedSecret = null, DataSourceERIUsernamePasswordMode usernamePasswordMode = DataSourceERIUsernamePasswordMode.USER_MANAGED) { var secretLine = string.Empty; var usernamePasswordModeLine = string.Empty; @@ -288,7 +287,7 @@ public readonly record struct DataSourceERI_V1 : IERIDataSource switch (this.AuthMethod) { case AuthMethod.TOKEN: - secretLine = CreateSecretLine("Token", encryptedSecret, "ENC:v1:", useSecretPlaceholder); + secretLine = CreateSecretLine("Token", encryptedSecret); break; case AuthMethod.USERNAME_PASSWORD: @@ -307,7 +306,7 @@ public readonly record struct DataSourceERI_V1 : IERIDataSource """; } - secretLine = CreateSecretLine("Password", encryptedSecret, "ENC:v1:", useSecretPlaceholder); + secretLine = CreateSecretLine("Password", encryptedSecret); break; } @@ -375,19 +374,13 @@ public readonly record struct DataSourceERI_V1 : IERIDataSource return true; } - private static string CreateSecretLine(string fieldName, string? encryptedSecret, string placeholder, bool useSecretPlaceholder) + private static string CreateSecretLine(string fieldName, string? encryptedSecret) { - var secret = !string.IsNullOrWhiteSpace(encryptedSecret) - ? encryptedSecret - : useSecretPlaceholder - ? placeholder - : string.Empty; - - if (string.IsNullOrWhiteSpace(secret)) + if (string.IsNullOrWhiteSpace(encryptedSecret)) return string.Empty; return $""" - ["{fieldName}"] = "{LuaTools.EscapeLuaString(secret)}", + ["{fieldName}"] = "{LuaTools.EscapeLuaString(encryptedSecret)}", """; } diff --git a/app/MindWork AI Studio/Tools/Services/RustService.Secrets.cs b/app/MindWork AI Studio/Tools/Services/RustService.Secrets.cs index de5a3db4..36ed6b6b 100644 --- a/app/MindWork AI Studio/Tools/Services/RustService.Secrets.cs +++ b/app/MindWork AI Studio/Tools/Services/RustService.Secrets.cs @@ -18,24 +18,16 @@ public sealed partial class RustService public async Task GetSecret(ISecretId secretId, SecretStoreType storeType, bool isTrying = false) { var secretKey = SecretKey(secretId, storeType); - var secretRequest = new SelectSecretRequest(secretKey, Environment.UserName, isTrying); - var result = await this.http.PostAsJsonAsync("/secrets/get", secretRequest, this.jsonRustSerializerOptions); - if (!result.IsSuccessStatusCode) + 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 '{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.")); - } - - var secret = await result.Content.ReadFromJsonAsync(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; - } + this.logger!.LogDebug($"Successfully retrieved the legacy data source secret for '{legacySecretKey}'."); + return legacySecret; } if (!secret.Success && !isTrying) @@ -92,9 +84,8 @@ public sealed partial class RustService return deleteResult with { WasEntryFound = deleteResult.WasEntryFound || legacyDeleteResult.WasEntryFound }; } - private async Task GetLegacySecret(ISecretId secretId, bool isTrying) + private async Task GetSecretByKey(string secretKey, 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)