mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-05-20 09:52:14 +00:00
Added export option for ERI server data sources for admins
This commit is contained in:
parent
266f89c834
commit
29f80c8b15
@ -0,0 +1,52 @@
|
||||
@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>
|
||||
@ -0,0 +1,83 @@
|
||||
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)));
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
using AIStudio.Settings.DataModel;
|
||||
|
||||
namespace AIStudio.Dialogs;
|
||||
|
||||
public readonly record struct DataSourceERIV1ExportDialogResult(bool IncludeSecret, DataSourceERIUsernamePasswordMode UsernamePasswordMode);
|
||||
@ -49,6 +49,12 @@
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.Edit" OnClick="() => this.EditDataSource(context)">
|
||||
@T("Edit")
|
||||
</MudButton>
|
||||
@if (this.SettingsManager.ConfigurationData.App.ShowAdminSettings && context is DataSourceERI_V1)
|
||||
{
|
||||
<MudTooltip Text="@T("Export configuration")">
|
||||
<MudIconButton Variant="Variant.Filled" Color="Color.Info" Icon="@Icons.Material.Filled.Dataset" OnClick="() => this.ExportDataSource(context)"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Error" StartIcon="@Icons.Material.Filled.Delete" OnClick="() => this.DeleteDataSource(context)">
|
||||
@T("Delete")
|
||||
</MudButton>
|
||||
|
||||
@ -1,11 +1,17 @@
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Settings.DataModel;
|
||||
using AIStudio.Tools.ERIClient.DataModel;
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Dialogs.Settings;
|
||||
|
||||
public partial class SettingsDialogDataSources : SettingsDialogBase
|
||||
{
|
||||
[Inject]
|
||||
private ISnackbar Snackbar { get; init; } = null!;
|
||||
|
||||
private string GetEmbeddingName(IDataSource dataSource)
|
||||
{
|
||||
if(dataSource is IInternalDataSource internalDataSource)
|
||||
@ -86,6 +92,69 @@ public partial class SettingsDialogDataSources : SettingsDialogBase
|
||||
await this.SettingsManager.StoreSettings();
|
||||
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
|
||||
}
|
||||
|
||||
private async Task ExportDataSource(IDataSource dataSource)
|
||||
{
|
||||
if (!this.SettingsManager.ConfigurationData.App.ShowAdminSettings)
|
||||
return;
|
||||
|
||||
if (dataSource is not DataSourceERI_V1 eriDataSource)
|
||||
return;
|
||||
|
||||
if (eriDataSource.AuthMethod is AuthMethod.KERBEROS)
|
||||
{
|
||||
await this.DialogService.ShowMessageBox(
|
||||
T("Export ERI Data Source"),
|
||||
T("Kerberos/SSO ERI data sources cannot be exported yet. Please configure them manually in the configuration plugin."),
|
||||
T("Close"));
|
||||
return;
|
||||
}
|
||||
|
||||
var needsSecret = eriDataSource.AuthMethod is AuthMethod.TOKEN or AuthMethod.USERNAME_PASSWORD;
|
||||
if (!needsSecret)
|
||||
{
|
||||
var publicLuaCode = eriDataSource.ExportAsConfigurationSection();
|
||||
if (!string.IsNullOrWhiteSpace(publicLuaCode))
|
||||
await this.RustService.CopyText2Clipboard(this.Snackbar, publicLuaCode);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var secretResponse = await this.RustService.GetSecret(eriDataSource, SecretStoreType.DATA_SOURCE, isTrying: true);
|
||||
var canEncryptSecret = PluginFactory.EnterpriseEncryption?.IsAvailable == true;
|
||||
|
||||
var dialogParameters = new DialogParameters<DataSourceERIV1ExportDialog>
|
||||
{
|
||||
{ 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)
|
||||
return;
|
||||
|
||||
string? encryptedSecret = null;
|
||||
if (exportResult.IncludeSecret && secretResponse.Success)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
var luaCode = eriDataSource.ExportAsConfigurationSection(
|
||||
encryptedSecret,
|
||||
exportResult.UsernamePasswordMode,
|
||||
needsSecret && string.IsNullOrWhiteSpace(encryptedSecret));
|
||||
if (string.IsNullOrWhiteSpace(luaCode))
|
||||
return;
|
||||
|
||||
await this.RustService.CopyText2Clipboard(this.Snackbar, luaCode);
|
||||
}
|
||||
|
||||
private async Task EditDataSource(IDataSource dataSource)
|
||||
{
|
||||
|
||||
@ -272,6 +272,63 @@ public readonly record struct DataSourceERI_V1 : IERIDataSource
|
||||
return TryQueueEnterpriseSecret(idx, table, configPluginId, dataSource);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exports the ERI v1 data source configuration as a Lua configuration section.
|
||||
/// </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)
|
||||
{
|
||||
var secretLine = string.Empty;
|
||||
var usernamePasswordModeLine = string.Empty;
|
||||
var usernameLine = string.Empty;
|
||||
|
||||
switch (this.AuthMethod)
|
||||
{
|
||||
case AuthMethod.TOKEN:
|
||||
secretLine = CreateSecretLine("Token", encryptedSecret, "ENC:v1:<base64-encoded encrypted token>", useSecretPlaceholder);
|
||||
break;
|
||||
|
||||
case AuthMethod.USERNAME_PASSWORD:
|
||||
if (usernamePasswordMode is DataSourceERIUsernamePasswordMode.USER_MANAGED)
|
||||
usernamePasswordMode = DataSourceERIUsernamePasswordMode.OS_USERNAME_SHARED_PASSWORD;
|
||||
|
||||
usernamePasswordModeLine = $"""
|
||||
["UsernamePasswordMode"] = "{usernamePasswordMode}",
|
||||
""";
|
||||
|
||||
if (usernamePasswordMode is DataSourceERIUsernamePasswordMode.SHARED_USERNAME_AND_PASSWORD)
|
||||
{
|
||||
var username = string.IsNullOrWhiteSpace(this.Username) ? "<shared username>" : this.Username;
|
||||
usernameLine = $"""
|
||||
["Username"] = "{LuaTools.EscapeLuaString(username)}",
|
||||
""";
|
||||
}
|
||||
|
||||
secretLine = CreateSecretLine("Password", encryptedSecret, "ENC:v1:<base64-encoded encrypted password>", useSecretPlaceholder);
|
||||
break;
|
||||
}
|
||||
|
||||
return $$"""
|
||||
CONFIG["DATA_SOURCES"][#CONFIG["DATA_SOURCES"]+1] = {
|
||||
["Id"] = "{{Guid.NewGuid().ToString()}}",
|
||||
["Name"] = "{{LuaTools.EscapeLuaString(this.Name)}}",
|
||||
["Type"] = "ERI_V1",
|
||||
["Hostname"] = "{{LuaTools.EscapeLuaString(this.Hostname)}}",
|
||||
["Port"] = {{this.Port}},
|
||||
["AuthMethod"] = "{{this.AuthMethod}}",
|
||||
{{usernamePasswordModeLine}}
|
||||
{{usernameLine}}
|
||||
{{secretLine}}
|
||||
["SecurityPolicy"] = "{{this.SecurityPolicy}}",
|
||||
["SelectedRetrievalId"] = "{{LuaTools.EscapeLuaString(this.SelectedRetrievalId)}}",
|
||||
["MaxMatches"] = {{this.MaxMatches}},
|
||||
}
|
||||
""";
|
||||
}
|
||||
|
||||
private static bool TryQueueEnterpriseSecret(int idx, LuaTable table, Guid configPluginId, DataSourceERI_V1 dataSource)
|
||||
{
|
||||
var secretFieldName = dataSource.AuthMethod switch
|
||||
@ -318,6 +375,22 @@ public readonly record struct DataSourceERI_V1 : IERIDataSource
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string CreateSecretLine(string fieldName, string? encryptedSecret, string placeholder, bool useSecretPlaceholder)
|
||||
{
|
||||
var secret = !string.IsNullOrWhiteSpace(encryptedSecret)
|
||||
? encryptedSecret
|
||||
: useSecretPlaceholder
|
||||
? placeholder
|
||||
: string.Empty;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(secret))
|
||||
return string.Empty;
|
||||
|
||||
return $"""
|
||||
["{fieldName}"] = "{LuaTools.EscapeLuaString(secret)}",
|
||||
""";
|
||||
}
|
||||
|
||||
private static string CleanHostname(string hostname)
|
||||
{
|
||||
var cleanedHostname = hostname.Trim();
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
# v26.5.5, build 240 (2026-05-xx xx:xx UTC)
|
||||
- Released the voice recording and transcription for all users. You no longer need to enable a preview feature to configure transcription providers, select a transcription provider, or use dictation.
|
||||
- Added support for organization-managed ERI servers in configuration plugins, so admins can preconfigure external data sources for users.
|
||||
- Added an export option for ERI server data sources, so admins can create configuration plugin snippets without writing the Lua code manually.
|
||||
- Added the username to the information page to make organization support easier when users share their screen.
|
||||
- Improved the app's security foundation with major modernization of the native runtime and its internal communication layer. This work is mostly invisible during everyday use, but it replaces older components that no longer received the security updates we require. We also continued updating security-sensitive dependencies so AI Studio stays on a healthier, better maintained base.
|
||||
- Improved the Pandoc management and detection process to make it more reliable.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user