mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-05-22 13:52:15 +00:00
Improved ERI server export flow
This commit is contained in:
parent
d2b92784f5
commit
1c4f10ffdd
@ -1,52 +0,0 @@
|
||||
@using AIStudio.Tools.ERIClient.DataModel
|
||||
@inherits MSGComponentBase
|
||||
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
<MudText Typo="Typo.body1" Class="mb-3">
|
||||
@string.Format(T("Export the ERI v1 data source '{0}' as Lua code for a configuration plugin."), this.DataSource.Name)
|
||||
</MudText>
|
||||
|
||||
@if (this.NeedsSecret())
|
||||
{
|
||||
@if (!this.HasConfiguredSecret)
|
||||
{
|
||||
<MudAlert Severity="Severity.Warning" Class="mb-3">
|
||||
@T("No secret is configured for this ERI data source. The export will contain a placeholder that you need to replace manually.")
|
||||
</MudAlert>
|
||||
}
|
||||
else if (!this.CanEncryptSecret)
|
||||
{
|
||||
<MudAlert Severity="Severity.Warning" Class="mb-3">
|
||||
@T("No enterprise encryption secret is configured. The export will contain a placeholder that you need to replace manually.")
|
||||
</MudAlert>
|
||||
}
|
||||
|
||||
<MudTextSwitch @bind-Value="@this.includeSecret"
|
||||
Disabled="@(!this.CanIncludeSecret)"
|
||||
Label="@this.GetIncludeSecretLabel()"
|
||||
LabelOn="@this.GetIncludeSecretLabelOn()"
|
||||
LabelOff="@this.GetIncludeSecretLabelOff()" />
|
||||
}
|
||||
|
||||
@if (this.DataSource.AuthMethod is AuthMethod.USERNAME_PASSWORD)
|
||||
{
|
||||
<MudSelect @bind-Value="@this.usernamePasswordMode" Text="@this.GetUsernamePasswordModeText()" Label="@T("Username and password mode")" Class="mt-3 mb-3" OpenIcon="@Icons.Material.Filled.ExpandMore" AdornmentColor="Color.Info" Adornment="Adornment.Start">
|
||||
@foreach (var mode in this.availableUsernamePasswordModes)
|
||||
{
|
||||
<MudSelectItem Value="@mode">
|
||||
@this.GetUsernamePasswordModeText(mode)
|
||||
</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="@this.Cancel" Variant="Variant.Filled">
|
||||
@T("Cancel")
|
||||
</MudButton>
|
||||
<MudButton OnClick="@this.Export" Variant="Variant.Filled" Color="Color.Primary">
|
||||
@T("Export")
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
@ -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)));
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
using AIStudio.Settings.DataModel;
|
||||
|
||||
namespace AIStudio.Dialogs;
|
||||
|
||||
public readonly record struct DataSourceERIV1ExportDialogResult(bool IncludeSecret, DataSourceERIUsernamePasswordMode UsernamePasswordMode);
|
||||
@ -0,0 +1,26 @@
|
||||
@inherits MSGComponentBase
|
||||
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
<MudText Typo="Typo.body1" Class="mb-3">
|
||||
@string.Format(T("How should AI Studio export the username and password configuration for the ERI v1 data source '{0}'?"), this.DataSource.Name)
|
||||
</MudText>
|
||||
|
||||
<MudSelect @bind-Value="@this.usernamePasswordMode" Text="@this.GetUsernamePasswordModeText()" Label="@T("Username and password mode")" Class="mt-3 mb-3" OpenIcon="@Icons.Material.Filled.ExpandMore" AdornmentColor="Color.Info" Adornment="Adornment.Start">
|
||||
@foreach (var mode in this.availableUsernamePasswordModes)
|
||||
{
|
||||
<MudSelectItem Value="@mode">
|
||||
@this.GetUsernamePasswordModeText(mode)
|
||||
</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="@this.Cancel" Variant="Variant.Filled">
|
||||
@T("Cancel")
|
||||
</MudButton>
|
||||
<MudButton OnClick="@this.Export" Variant="Variant.Filled" Color="Color.Primary">
|
||||
@T("Export")
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
@ -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)));
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
using AIStudio.Settings.DataModel;
|
||||
|
||||
namespace AIStudio.Dialogs;
|
||||
|
||||
public readonly record struct DataSourceERIV1UsernamePasswordExportDialogResult(DataSourceERIUsernamePasswordMode UsernamePasswordMode);
|
||||
@ -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<DataSourceERIV1ExportDialog>
|
||||
if (!secretResponse.Success)
|
||||
{
|
||||
{ x => x.DataSource, eriDataSource },
|
||||
{ x => x.HasConfiguredSecret, secretResponse.Success },
|
||||
{ x => x.CanEncryptSecret, canEncryptSecret },
|
||||
};
|
||||
|
||||
var dialogReference = await this.DialogService.ShowAsync<DataSourceERIV1ExportDialog>(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<ConfirmDialog>
|
||||
{
|
||||
{ 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<ConfirmDialog>(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<DataSourceERIV1UsernamePasswordExportDialog>
|
||||
{
|
||||
{ x => x.DataSource, eriDataSource },
|
||||
};
|
||||
|
||||
var dialogReference = await this.DialogService.ShowAsync<DataSourceERIV1UsernamePasswordExportDialog>(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;
|
||||
|
||||
|
||||
@ -277,9 +277,8 @@ public readonly record struct DataSourceERI_V1 : IERIDataSource
|
||||
/// </summary>
|
||||
/// <param name="encryptedSecret">Optional encrypted token or password to include in the export.</param>
|
||||
/// <param name="usernamePasswordMode">The organization-managed username/password mode to export.</param>
|
||||
/// <param name="useSecretPlaceholder">Whether to include a placeholder for the encrypted secret.</param>
|
||||
/// <returns>A Lua configuration section string.</returns>
|
||||
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:<base64-encoded encrypted token>", 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:<base64-encoded encrypted password>", 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)}",
|
||||
""";
|
||||
}
|
||||
|
||||
|
||||
@ -18,24 +18,16 @@ public sealed partial class RustService
|
||||
public async Task<RequestedSecret> 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<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;
|
||||
}
|
||||
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<RequestedSecret> GetLegacySecret(ISecretId secretId, bool isTrying)
|
||||
private async Task<RequestedSecret> 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)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user