Improved the enterprise configuration details on the information page

This commit is contained in:
Thorsten Sommer 2026-05-31 12:02:26 +02:00
parent f42598791f
commit 39d77d53d9
Signed by untrusted user who does not match committer: tsommer
GPG Key ID: 371BBA77A02C0108
11 changed files with 292 additions and 156 deletions

View File

@ -6046,9 +6046,15 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1137744461"] = "ID mismatch: the
-- This is a private AI Studio installation. It runs without an enterprise configuration.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1209549230"] = "This is a private AI Studio installation. It runs without an enterprise configuration."
-- Copies the configuration origin to the clipboard
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T125850635"] = "Copies the configuration origin to the clipboard"
-- Unknown configuration plugin
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1290340974"] = "Unknown configuration plugin"
-- Copies the configuration slot to the clipboard
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1347508205"] = "Copies the configuration slot to the clipboard"
-- This library is used to read PDF files. This is necessary, e.g., for using PDFs as a data source for a chat.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1388816916"] = "This library is used to read PDF files. This is necessary, e.g., for using PDFs as a data source for a chat."
@ -6145,6 +6151,12 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2371107659"] = "installation pro
-- Installed Pandoc version: Pandoc is not installed or not available.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2374031539"] = "Installed Pandoc version: Pandoc is not installed or not available."
-- Configuration origin:
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2435772109"] = "Configuration origin:"
-- Configuration slot:
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T254943559"] = "Configuration slot:"
-- This library is used to determine the language of the operating system. This is necessary to set the language of the user interface.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2557014401"] = "This library is used to determine the language of the operating system. This is necessary to set the language of the user interface."
@ -6199,6 +6211,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2868174483"] = "The .NET backend
-- AI Studio runs with an enterprise configuration and configuration servers. The configuration plugins are not yet available.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2924964415"] = "AI Studio runs with an enterprise configuration and configuration servers. The configuration plugins are not yet available."
-- Copies the configuration source to the clipboard
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2929232062"] = "Copies the configuration source to the clipboard"
-- Changelog
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3017574265"] = "Changelog"
@ -6259,6 +6274,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3722989559"] = "This library is
-- Username provided by the OS
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3764549776"] = "Username provided by the OS"
-- Configuration source:
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3801531724"] = "Configuration source:"
-- this version does not met the requirements
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3813932670"] = "this version does not met the requirements"

View File

@ -89,18 +89,7 @@
{
<ConfigPluginInfoCard HeaderIcon="@Icons.Material.Filled.HourglassBottom"
HeaderText="@T("Waiting for the configuration plugin...")"
Items="@([
new ConfigInfoRowItem(Icons.Material.Filled.ArrowRightAlt,
$"{T("Enterprise configuration ID:")} {env.ConfigurationId}",
env.ConfigurationId.ToString(),
T("Copies the config ID to the clipboard")),
new ConfigInfoRowItem(Icons.Material.Filled.ArrowRightAlt,
$"{T("Configuration server:")} {env.ConfigurationServerUrl}",
env.ConfigurationServerUrl,
T("Copies the server URL to the clipboard"),
"margin-top: 4px;")
])"/>
Items="@this.BuildEnterpriseConfigurationItems(env)"/>
}
<EncryptionSecretInfo IsConfigured="@(PluginFactory.EnterpriseEncryption?.IsAvailable is true)"
@ -130,41 +119,13 @@
{
<ConfigPluginInfoCard HeaderIcon="@Icons.Material.Filled.HourglassBottom"
HeaderText="@T("Waiting for the configuration plugin...")"
Items="@([
new ConfigInfoRowItem(Icons.Material.Filled.ArrowRightAlt,
$"{T("Enterprise configuration ID:")} {env.ConfigurationId}",
env.ConfigurationId.ToString(),
T("Copies the config ID to the clipboard")),
new ConfigInfoRowItem(Icons.Material.Filled.ArrowRightAlt,
$"{T("Configuration server:")} {env.ConfigurationServerUrl}",
env.ConfigurationServerUrl,
T("Copies the server URL to the clipboard"),
"margin-top: 4px;")
])"/>
Items="@this.BuildEnterpriseConfigurationItems(env)"/>
continue;
}
<ConfigPluginInfoCard HeaderIcon="@Icons.Material.Filled.Extension"
HeaderText="@matchingPlugin.Name"
Items="@([
new ConfigInfoRowItem(Icons.Material.Filled.ArrowRightAlt,
$"{T("Enterprise configuration ID:")} {env.ConfigurationId}",
env.ConfigurationId.ToString(),
T("Copies the config ID to the clipboard")),
new ConfigInfoRowItem(Icons.Material.Filled.ArrowRightAlt,
$"{T("Configuration server:")} {env.ConfigurationServerUrl}",
env.ConfigurationServerUrl,
T("Copies the server URL to the clipboard"),
"margin-top: 4px;"),
new ConfigInfoRowItem(Icons.Material.Filled.ArrowRightAlt,
$"{T("Configuration plugin ID:")} {matchingPlugin.Id}",
matchingPlugin.Id.ToString(),
T("Copies the configuration plugin ID to the clipboard"),
"margin-top: 4px;")
])"
Items="@this.BuildEnterpriseConfigurationItems(env, matchingPlugin)"
ShowWarning="@this.IsManagedConfigurationIdMismatch(matchingPlugin, env.ConfigurationId)"
WarningText="@T("ID mismatch: the plugin ID differs from the enterprise configuration ID.")"/>
}

View File

@ -324,6 +324,55 @@ public partial class Information : MSGComponentBase
?? this.configPlugins.FirstOrDefault(plugin => plugin.ManagedConfigurationId is null && plugin.Id == configurationId);
}
private IReadOnlyList<ConfigInfoRowItem> BuildEnterpriseConfigurationItems(EnterpriseEnvironment environment, IAvailablePlugin? plugin = null)
{
var items = new List<ConfigInfoRowItem>
{
new(Icons.Material.Filled.ArrowRightAlt,
$"{T("Enterprise configuration ID:")} {environment.ConfigurationId}",
environment.ConfigurationId.ToString(),
T("Copies the config ID to the clipboard")),
new(Icons.Material.Filled.ArrowRightAlt,
$"{T("Configuration server:")} {environment.ConfigurationServerUrl}",
environment.ConfigurationServerUrl,
T("Copies the server URL to the clipboard"),
"margin-top: 4px;"),
new(Icons.Material.Filled.ArrowRightAlt,
$"{T("Configuration source:")} {environment.Source}",
environment.Source,
T("Copies the configuration source to the clipboard"),
"margin-top: 4px;"),
};
if (!string.IsNullOrWhiteSpace(environment.SourceDetail))
{
items.Add(new ConfigInfoRowItem(Icons.Material.Filled.ArrowRightAlt,
$"{T("Configuration origin:")} {environment.SourceDetail}",
environment.SourceDetail,
T("Copies the configuration origin to the clipboard"),
"margin-top: 4px;"));
}
items.Add(new ConfigInfoRowItem(Icons.Material.Filled.ArrowRightAlt,
$"{T("Configuration slot:")} {environment.Slot}",
environment.Slot,
T("Copies the configuration slot to the clipboard"),
"margin-top: 4px;"));
if (plugin is not null)
{
items.Add(new ConfigInfoRowItem(Icons.Material.Filled.ArrowRightAlt,
$"{T("Configuration plugin ID:")} {plugin.Id}",
plugin.Id.ToString(),
T("Copies the configuration plugin ID to the clipboard"),
"margin-top: 4px;"));
}
return items;
}
private bool IsManagedConfigurationIdMismatch(IAvailablePlugin plugin, Guid configurationId)
{
return plugin.ManagedConfigurationId == configurationId && plugin.Id != configurationId;

View File

@ -6048,9 +6048,15 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1137744461"] = "ID-Konflikt: Die
-- This is a private AI Studio installation. It runs without an enterprise configuration.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1209549230"] = "Dies ist eine private AI Studio-Installation. Sie läuft ohne Unternehmenskonfiguration."
-- Copies the configuration origin to the clipboard
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T125850635"] = "Kopiert den Ursprung der Konfiguration in die Zwischenablage"
-- Unknown configuration plugin
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1290340974"] = "Unbekanntes Konfigurations-Plugin"
-- Copies the configuration slot to the clipboard
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1347508205"] = "Kopiert den Slot der Konfiguration in die Zwischenablage"
-- This library is used to read PDF files. This is necessary, e.g., for using PDFs as a data source for a chat.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1388816916"] = "Diese Bibliothek wird verwendet, um PDF-Dateien zu lesen. Das ist zum Beispiel notwendig, um PDFs als Datenquelle für einen Chat zu nutzen."
@ -6147,6 +6153,12 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2371107659"] = "Installation vom
-- Installed Pandoc version: Pandoc is not installed or not available.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2374031539"] = "Installierte Pandoc-Version: Pandoc ist nicht installiert oder nicht verfügbar."
-- Configuration origin:
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2435772109"] = "Ursprung der Konfiguration:"
-- Configuration slot:
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T254943559"] = "Slot der Konfiguration:"
-- This library is used to determine the language of the operating system. This is necessary to set the language of the user interface.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2557014401"] = "Diese Bibliothek wird verwendet, um die Sprache des Betriebssystems zu erkennen. Dies ist notwendig, um die Sprache der Benutzeroberfläche einzustellen."
@ -6201,6 +6213,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2868174483"] = "Das .NET-Backend
-- AI Studio runs with an enterprise configuration and configuration servers. The configuration plugins are not yet available.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2924964415"] = "AI Studio wird mit Unternehmenskonfigurationen und Konfigurationsservern betrieben. Die Konfigurations-Plugins sind noch nicht verfügbar."
-- Copies the configuration source to the clipboard
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2929232062"] = "Kopiert die Quelle der Konfiguration in die Zwischenablage"
-- Changelog
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3017574265"] = "Änderungsprotokoll"
@ -6261,6 +6276,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3722989559"] = "Diese Bibliothek
-- Username provided by the OS
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3764549776"] = "Vom Betriebssystem bereitgestellter Benutzername"
-- Configuration source:
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3801531724"] = "Quelle der Konfiguration:"
-- this version does not met the requirements
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3813932670"] = "diese Version erfüllt die Anforderungen nicht"

View File

@ -6048,9 +6048,15 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1137744461"] = "ID mismatch: the
-- This is a private AI Studio installation. It runs without an enterprise configuration.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1209549230"] = "This is a private AI Studio installation. It runs without an enterprise configuration."
-- Copies the configuration origin to the clipboard
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T125850635"] = "Copies the configuration origin to the clipboard"
-- Unknown configuration plugin
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1290340974"] = "Unknown configuration plugin"
-- Copies the configuration slot to the clipboard
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1347508205"] = "Copies the configuration slot to the clipboard"
-- This library is used to read PDF files. This is necessary, e.g., for using PDFs as a data source for a chat.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1388816916"] = "This library is used to read PDF files. This is necessary, e.g., for using PDFs as a data source for a chat."
@ -6147,6 +6153,12 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2371107659"] = "installation pro
-- Installed Pandoc version: Pandoc is not installed or not available.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2374031539"] = "Installed Pandoc version: Pandoc is not installed or not available."
-- Configuration origin:
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2435772109"] = "Configuration origin:"
-- Configuration slot:
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T254943559"] = "Configuration slot:"
-- This library is used to determine the language of the operating system. This is necessary to set the language of the user interface.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2557014401"] = "This library is used to determine the language of the operating system. This is necessary to set the language of the user interface."
@ -6201,6 +6213,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2868174483"] = "The .NET backend
-- AI Studio runs with an enterprise configuration and configuration servers. The configuration plugins are not yet available.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2924964415"] = "AI Studio runs with an enterprise configuration and configuration servers. The configuration plugins are not yet available."
-- Copies the configuration source to the clipboard
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2929232062"] = "Copies the configuration source to the clipboard"
-- Changelog
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3017574265"] = "Changelog"
@ -6261,6 +6276,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3722989559"] = "This library is
-- Username provided by the OS
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3764549776"] = "Username provided by the OS"
-- Configuration source:
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3801531724"] = "Configuration source:"
-- this version does not met the requirements
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3813932670"] = "this version does not met the requirements"

View File

@ -2,7 +2,7 @@ using System.Net.Http.Headers;
namespace AIStudio.Tools;
public readonly record struct EnterpriseEnvironment(string ConfigurationServerUrl, Guid ConfigurationId, EntityTagHeaderValue? ETag)
public readonly record struct EnterpriseEnvironment(string ConfigurationServerUrl, Guid ConfigurationId, string Source, string SourceDetail, string Slot, EntityTagHeaderValue? ETag)
{
public bool IsActive => !string.IsNullOrWhiteSpace(this.ConfigurationServerUrl) && this.ConfigurationId != Guid.Empty;
}

View File

@ -1,3 +1,3 @@
namespace AIStudio.Tools.Rust;
public sealed record EnterpriseConfig(string Id, string ServerUrl);
public sealed record EnterpriseConfig(string Id, string ServerUrl, string Source, string SourceDetail, string Slot);

View File

@ -14,7 +14,7 @@ public sealed class EnterpriseEnvironmentService(ILogger<EnterpriseEnvironmentSe
private static EnterpriseSecretSnapshot CURRENT_SECRET_SNAPSHOT;
private readonly record struct EnterpriseEnvironmentSnapshot(Guid ConfigurationId, string ConfigurationServerUrl, string? ETag);
private readonly record struct EnterpriseEnvironmentSnapshot(Guid ConfigurationId, string ConfigurationServerUrl, string Source, string SourceDetail, string Slot, string? ETag);
private readonly record struct EnterpriseSecretSnapshot(bool HasSecret, string Fingerprint);
@ -224,6 +224,9 @@ public sealed class EnterpriseEnvironmentService(ILogger<EnterpriseEnvironmentSe
.Select(environment => new EnterpriseEnvironmentSnapshot(
environment.ConfigurationId,
NormalizeServerUrl(environment.ConfigurationServerUrl),
environment.Source,
environment.SourceDetail,
environment.Slot,
environment.ETag?.ToString()))
.OrderBy(environment => environment.ConfigurationId)
.ToList();

View File

@ -47,7 +47,7 @@ public sealed partial class RustService
foreach (var config in configs)
{
if (Guid.TryParse(config.Id, out var id))
environments.Add(new EnterpriseEnvironment(config.ServerUrl, id, null));
environments.Add(new EnterpriseEnvironment(config.ServerUrl, id, config.Source, config.SourceDetail, config.Slot, null));
else
this.logger!.LogWarning($"Skipping enterprise config with invalid ID: '{config.Id}'.");
}

View File

@ -1,2 +1,3 @@
# v26.6.1, build 241 (2026-06-xx xx:xx UTC)
- Added support for up to 100,000 enterprise configuration slots, using fixed-width slot names such as `config_00000` while keeping the existing first ten slot names compatible.
- Added support for up to 100 thousand enterprise configuration slots, using fixed-width slot names such as `config_00000` while keeping the existing first ten slot names compatible.
- Improved the enterprise configuration details on the information page by showing where each configuration comes from and which configuration slot was used.

View File

@ -196,8 +196,53 @@ pub async fn read_user_language(_token: APIToken) -> String {
pub struct EnterpriseConfig {
pub id: String,
pub server_url: String,
pub source: String,
pub source_detail: String,
pub slot: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct EnterpriseSourceValue {
value: String,
source_detail: String,
}
impl EnterpriseSourceValue {
fn new(value: String, source_detail: String) -> Self {
Self {
value,
source_detail,
}
}
}
trait EnterpriseSourceValueAccess {
fn value(&self) -> &str;
fn source_detail(&self) -> &str;
}
impl EnterpriseSourceValueAccess for EnterpriseSourceValue {
fn value(&self) -> &str {
&self.value
}
fn source_detail(&self) -> &str {
&self.source_detail
}
}
impl EnterpriseSourceValueAccess for String {
fn value(&self) -> &str {
self
}
fn source_detail(&self) -> &str {
""
}
}
type EnterpriseSourceValues = HashMap<String, EnterpriseSourceValue>;
#[derive(Clone, Debug, Default, PartialEq, Eq)]
struct EnterpriseSourceData {
source_name: String,
@ -301,7 +346,7 @@ fn load_registry_enterprise_source() -> EnterpriseSourceData {
info!(r"Trying to read enterprise configuration metadata from 'HKEY_CURRENT_USER\{}'.", ENTERPRISE_REGISTRY_KEY_PATH);
let mut values = HashMap::new();
let mut values = EnterpriseSourceValues::new();
let key = match CURRENT_USER.open(ENTERPRISE_REGISTRY_KEY_PATH) {
Ok(key) => key,
Err(_) => {
@ -322,7 +367,7 @@ fn load_registry_enterprise_source() -> EnterpriseSourceData {
match String::try_from(value) {
Ok(value) => {
values.insert(source_key_name, value);
values.insert(source_key_name, EnterpriseSourceValue::new(value, String::new()));
},
Err(error) => {
@ -359,16 +404,26 @@ fn load_policy_file_enterprise_source() -> EnterpriseSourceData {
fn load_environment_enterprise_source() -> EnterpriseSourceData {
info!("Trying to read enterprise configuration metadata from environment variables.");
let mut values = HashMap::new();
let mut values = EnterpriseSourceValues::new();
for (env_name, value) in env::vars() {
if let Some(source_key_name) = enterprise_environment_key_name(&env_name) {
values.insert(source_key_name, value);
let source_detail = enterprise_environment_source_detail(&source_key_name, &env_name);
values.insert(source_key_name, EnterpriseSourceValue::new(value, source_detail));
}
}
parse_enterprise_source_values("environment variables", &values)
}
fn enterprise_environment_source_detail(source_key_name: &str, env_name: &str) -> String {
if source_key_name == "config_id"
|| enterprise_source_key_suffix(source_key_name, ENTERPRISE_CONFIG_ID_KEY_PREFIX).is_some() {
String::from(env_name)
} else {
String::new()
}
}
fn enterprise_environment_key_name(env_name: &str) -> Option<String> {
if enterprise_env_key_equals(env_name, ENTERPRISE_ENV_CONFIGS) {
return Some(String::from("configs"));
@ -472,8 +527,8 @@ fn linux_policy_directories_from_xdg(xdg_config_dirs: Option<&str>) -> Vec<PathB
directories
}
fn load_policy_values_from_directories(directories: &[PathBuf]) -> HashMap<String, String> {
let mut values = HashMap::new();
fn load_policy_values_from_directories(directories: &[PathBuf]) -> EnterpriseSourceValues {
let mut values = EnterpriseSourceValues::new();
for directory in directories {
info!("Checking enterprise policy directory '{}'.", directory.display());
let entries = match fs::read_dir(directory) {
@ -504,12 +559,17 @@ fn load_policy_values_from_directories(directories: &[PathBuf]) -> HashMap<Strin
let path = entry.path();
if let Some(config_values) = read_policy_yaml_mapping(&path) {
let source_detail = path
.canonicalize()
.unwrap_or_else(|_| path.clone())
.to_string_lossy()
.into_owned();
if let Some(id) = config_values.get("id") {
insert_first_non_empty_value(&mut values, &format!("config_id{suffix}"), id);
insert_first_non_empty_value(&mut values, &format!("config_id{suffix}"), id, &source_detail);
}
if let Some(server_url) = config_values.get("server_url") {
insert_first_non_empty_value(&mut values, &format!("config_server_url{suffix}"), server_url);
insert_first_non_empty_value(&mut values, &format!("config_server_url{suffix}"), server_url, &source_detail);
}
}
}
@ -517,7 +577,7 @@ fn load_policy_values_from_directories(directories: &[PathBuf]) -> HashMap<Strin
let secret_path = directory.join(ENTERPRISE_POLICY_SECRET_FILE_NAME);
if let Some(secret_values) = read_policy_yaml_mapping(&secret_path)
&& let Some(secret) = secret_values.get("config_encryption_secret") {
insert_first_non_empty_value(&mut values, "config_encryption_secret", secret);
insert_first_non_empty_value(&mut values, "config_encryption_secret", secret, "");
}
}
@ -615,9 +675,11 @@ fn parse_policy_yaml_value(raw_value: &str) -> Option<String> {
Some(String::from(trimmed))
}
fn insert_first_non_empty_value(values: &mut HashMap<String, String>, key: &str, raw_value: &str) {
fn insert_first_non_empty_value(values: &mut EnterpriseSourceValues, key: &str, raw_value: &str, source_detail: &str) {
if let Some(value) = normalize_enterprise_value(raw_value) {
values.entry(String::from(key)).or_insert(value);
values
.entry(String::from(key))
.or_insert_with(|| EnterpriseSourceValue::new(value, String::from(source_detail)));
}
}
@ -664,7 +726,7 @@ fn is_enterprise_slot_suffix(suffix: &str) -> bool {
&& suffix.parse::<u32>().is_ok_and(|index| index <= ENTERPRISE_CONFIG_SLOT_MAX)
}
fn collect_enterprise_config_slots(values: &HashMap<String, String>) -> Vec<String> {
fn collect_enterprise_config_slots<T: EnterpriseSourceValueAccess>(values: &HashMap<String, T>) -> Vec<String> {
let mut slots = HashSet::new();
for key_name in values.keys() {
if let Some(suffix) = enterprise_source_key_suffix(key_name, ENTERPRISE_CONFIG_ID_KEY_PREFIX)
@ -701,21 +763,20 @@ fn enterprise_slot_width_rank(suffix: &str) -> u8 {
}
}
fn indexed_enterprise_source_value<'a>(
values: &'a HashMap<String, String>,
fn indexed_enterprise_source_value<'a, T: EnterpriseSourceValueAccess>(
values: &'a HashMap<String, T>,
prefix: &str,
suffix: &str,
) -> Option<&'a str> {
) -> Option<&'a T> {
let separated_key = format!("{prefix}_{suffix}");
values
.get(&separated_key)
.or_else(|| values.get(&format!("{prefix}{suffix}")))
.map(String::as_str)
}
fn parse_enterprise_source_values(
fn parse_enterprise_source_values<T: EnterpriseSourceValueAccess>(
source_name: &str,
values: &HashMap<String, String>,
values: &HashMap<String, T>,
) -> EnterpriseSourceData {
let mut configs = Vec::new();
let mut seen_ids = HashSet::new();
@ -733,7 +794,7 @@ fn parse_enterprise_source_values(
if let Some(combined) = values
.get("configs")
.and_then(|value| normalize_enterprise_value(value))
.and_then(|value| normalize_enterprise_value(value.value()))
{
add_combined_enterprise_configs(source_name, &combined, &mut configs, &mut seen_ids);
}
@ -741,15 +802,15 @@ fn parse_enterprise_source_values(
add_enterprise_config_pair(
source_name,
"legacy single configuration",
values.get("config_id").map(String::as_str),
values.get("config_server_url").map(String::as_str),
values.get("config_id"),
values.get("config_server_url"),
&mut configs,
&mut seen_ids,
);
let encryption_secret = values
.get("config_encryption_secret")
.and_then(|value| normalize_enterprise_value(value))
.and_then(|value| normalize_enterprise_value(value.value()))
.unwrap_or_default();
EnterpriseSourceData {
@ -761,26 +822,32 @@ fn parse_enterprise_source_values(
fn add_enterprise_config_pair(
source_name: &str,
context: &str,
raw_id: Option<&str>,
raw_server_url: Option<&str>,
slot: &str,
raw_id: Option<&impl EnterpriseSourceValueAccess>,
raw_server_url: Option<&impl EnterpriseSourceValueAccess>,
configs: &mut Vec<EnterpriseConfig>,
seen_ids: &mut HashSet<String>,
) {
let id = raw_id.and_then(normalize_enterprise_config_id);
let server_url = raw_server_url.and_then(normalize_enterprise_value);
let id = raw_id.and_then(|value| normalize_enterprise_config_id(value.value()));
let server_url = raw_server_url.and_then(|value| normalize_enterprise_value(value.value()));
match (id, server_url) {
(Some(id), Some(server_url)) => {
if seen_ids.insert(id.clone()) {
configs.push(EnterpriseConfig { id, server_url });
configs.push(EnterpriseConfig {
id,
server_url,
source: String::from(source_name),
source_detail: raw_id.map(|value| String::from(value.source_detail())).unwrap_or_default(),
slot: String::from(slot),
});
} else {
info!("Ignoring duplicate enterprise configuration '{}' from {} in '{}'.", id, source_name, context);
info!("Ignoring duplicate enterprise configuration '{}' from {} in '{}'.", id, source_name, slot);
}
}
(Some(_), None) | (None, Some(_)) => {
warn!("Ignoring incomplete enterprise configuration from {} in '{}'.", source_name, context);
warn!("Ignoring incomplete enterprise configuration from {} in '{}'.", source_name, slot);
}
(None, None) => {}
@ -804,11 +871,13 @@ fn add_combined_enterprise_configs(
continue;
};
let id = EnterpriseSourceValue::new(String::from(raw_id), String::new());
let server_url = EnterpriseSourceValue::new(String::from(raw_server_url), String::new());
add_enterprise_config_pair(
source_name,
&format!("combined legacy entry {}", index + 1),
Some(raw_id),
Some(raw_server_url),
Some(&id),
Some(&server_url),
configs,
seen_ids,
);
@ -835,7 +904,7 @@ mod tests {
linux_policy_directories_from_xdg, load_policy_values_from_directories,
normalize_locale_tag, parse_enterprise_source_values,
select_effective_enterprise_config_source, select_effective_enterprise_secret_source,
EnterpriseConfig, EnterpriseSourceData,
EnterpriseConfig, EnterpriseSourceData, EnterpriseSourceValue, EnterpriseSourceValues,
};
use std::collections::HashMap;
use std::fs;
@ -846,6 +915,30 @@ mod tests {
const TEST_ID_B: &str = "a1b2c3d4-e5f6-7890-abcd-ef1234567890";
const TEST_ID_C: &str = "11111111-2222-3333-4444-555555555555";
fn enterprise_config(
id: &str,
server_url: &str,
source: &str,
source_detail: &str,
slot: &str,
) -> EnterpriseConfig {
EnterpriseConfig {
id: String::from(id),
server_url: String::from(server_url),
source: String::from(source),
source_detail: String::from(source_detail),
slot: String::from(slot),
}
}
fn policy_path(path: PathBuf) -> String {
path
.canonicalize()
.unwrap_or(path)
.to_string_lossy()
.into_owned()
}
#[test]
fn normalize_locale_tag_supports_common_linux_formats() {
assert_eq!(
@ -897,18 +990,9 @@ mod tests {
assert_eq!(
source.configs,
vec![
EnterpriseConfig {
id: String::from("9072b77d-ca81-40da-be6a-861da525ef7b"),
server_url: String::from("https://indexed.example.org"),
},
EnterpriseConfig {
id: String::from(TEST_ID_B),
server_url: String::from("https://combined.example.org"),
},
EnterpriseConfig {
id: String::from(TEST_ID_C),
server_url: String::from("https://legacy.example.org"),
},
enterprise_config("9072b77d-ca81-40da-be6a-861da525ef7b", "https://indexed.example.org", "test", "", "indexed slot 0"),
enterprise_config(TEST_ID_B, "https://combined.example.org", "test", "", "combined legacy entry 2"),
enterprise_config(TEST_ID_C, "https://legacy.example.org", "test", "", "legacy single configuration"),
]
);
assert_eq!(source.encryption_secret, "secret");
@ -933,14 +1017,8 @@ mod tests {
assert_eq!(
source.configs,
vec![
EnterpriseConfig {
id: String::from("9072b77d-ca81-40da-be6a-861da525ef7b"),
server_url: String::from("https://slot0.example.org"),
},
EnterpriseConfig {
id: String::from(TEST_ID_B),
server_url: String::from("https://slot4.example.org"),
},
enterprise_config("9072b77d-ca81-40da-be6a-861da525ef7b", "https://slot0.example.org", "test", "", "indexed slot 0"),
enterprise_config(TEST_ID_B, "https://slot4.example.org", "test", "", "indexed slot 4"),
]
);
}
@ -964,14 +1042,8 @@ mod tests {
assert_eq!(
source.configs,
vec![
EnterpriseConfig {
id: String::from("9072b77d-ca81-40da-be6a-861da525ef7b"),
server_url: String::from("https://slot0.example.org"),
},
EnterpriseConfig {
id: String::from(TEST_ID_B),
server_url: String::from("https://slot10503.example.org"),
},
enterprise_config("9072b77d-ca81-40da-be6a-861da525ef7b", "https://slot0.example.org", "test", "", "indexed slot 00000"),
enterprise_config(TEST_ID_B, "https://slot10503.example.org", "test", "", "indexed slot 10503"),
]
);
}
@ -995,14 +1067,8 @@ mod tests {
assert_eq!(
source.configs,
vec![
EnterpriseConfig {
id: String::from("9072b77d-ca81-40da-be6a-861da525ef7b"),
server_url: String::from("https://padded.example.org"),
},
EnterpriseConfig {
id: String::from(TEST_ID_B),
server_url: String::from("https://legacy-slot.example.org"),
},
enterprise_config("9072b77d-ca81-40da-be6a-861da525ef7b", "https://padded.example.org", "test", "", "indexed slot 00001"),
enterprise_config(TEST_ID_B, "https://legacy-slot.example.org", "test", "", "indexed slot 1"),
]
);
}
@ -1030,10 +1096,7 @@ mod tests {
assert_eq!(
source.configs,
vec![EnterpriseConfig {
id: String::from("9072b77d-ca81-40da-be6a-861da525ef7b"),
server_url: String::from("https://valid.example.org"),
}]
vec![enterprise_config("9072b77d-ca81-40da-be6a-861da525ef7b", "https://valid.example.org", "test", "", "indexed slot 99999")]
);
}
@ -1057,6 +1120,35 @@ mod tests {
);
}
#[test]
fn parse_enterprise_source_values_keeps_environment_id_variable_as_source_detail() {
let mut values = EnterpriseSourceValues::new();
values.insert(
String::from("config_id00000"),
EnterpriseSourceValue::new(
String::from(TEST_ID_A),
String::from("MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_ID_00000"),
),
);
values.insert(
String::from("config_server_url00000"),
EnterpriseSourceValue::new(String::from("https://env.example.org"), String::new()),
);
let source = parse_enterprise_source_values("environment variables", &values);
assert_eq!(
source.configs,
vec![enterprise_config(
"9072b77d-ca81-40da-be6a-861da525ef7b",
"https://env.example.org",
"environment variables",
"MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_ID_00000",
"indexed slot 00000"
)]
);
}
#[test]
fn enterprise_policy_file_slot_suffix_accepts_valid_slot_file_names() {
assert_eq!(enterprise_policy_file_slot_suffix("config0.yaml"), Some("0"));
@ -1077,18 +1169,12 @@ mod tests {
let selected = select_effective_enterprise_config_source(vec![
EnterpriseSourceData {
source_name: String::from("registry"),
configs: vec![EnterpriseConfig {
id: TEST_ID_A.to_lowercase(),
server_url: String::from("https://registry.example.org"),
}],
configs: vec![enterprise_config(&TEST_ID_A.to_lowercase(), "https://registry.example.org", "registry", "", "indexed slot 0")],
encryption_secret: String::new(),
},
EnterpriseSourceData {
source_name: String::from("environment"),
configs: vec![EnterpriseConfig {
id: String::from(TEST_ID_B),
server_url: String::from("https://env.example.org"),
}],
configs: vec![enterprise_config(TEST_ID_B, "https://env.example.org", "environment", "", "indexed slot 0")],
encryption_secret: String::from("ENV-SECRET"),
},
]);
@ -1108,10 +1194,7 @@ mod tests {
},
EnterpriseSourceData {
source_name: String::from("environment"),
configs: vec![EnterpriseConfig {
id: String::from(TEST_ID_B),
server_url: String::from("https://env.example.org"),
}],
configs: vec![enterprise_config(TEST_ID_B, "https://env.example.org", "environment", "", "indexed slot 0")],
encryption_secret: String::new(),
},
]);
@ -1126,10 +1209,7 @@ mod tests {
let selected = select_effective_enterprise_secret_source(vec![
EnterpriseSourceData {
source_name: String::from("registry"),
configs: vec![EnterpriseConfig {
id: TEST_ID_A.to_lowercase(),
server_url: String::from("https://registry.example.org"),
}],
configs: vec![enterprise_config(&TEST_ID_A.to_lowercase(), "https://registry.example.org", "registry", "", "indexed slot 0")],
encryption_secret: String::new(),
},
EnterpriseSourceData {
@ -1235,19 +1315,19 @@ mod tests {
]);
assert_eq!(
values.get("config_id0").map(String::as_str),
values.get("config_id0").map(|value| value.value.as_str()),
Some("9072b77d-ca81-40da-be6a-861da525ef7b")
);
assert_eq!(
values.get("config_server_url0").map(String::as_str),
values.get("config_server_url0").map(|value| value.value.as_str()),
Some("https://org.example.org")
);
assert_eq!(
values.get("config_id1").map(String::as_str),
values.get("config_id1").map(|value| value.value.as_str()),
Some("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb")
);
assert_eq!(
values.get("config_encryption_secret").map(String::as_str),
values.get("config_encryption_secret").map(|value| value.value.as_str()),
Some("SECRET-A")
);
}
@ -1273,14 +1353,8 @@ mod tests {
assert_eq!(
source.configs,
vec![
EnterpriseConfig {
id: String::from("9072b77d-ca81-40da-be6a-861da525ef7b"),
server_url: String::from("https://slot0.example.org"),
},
EnterpriseConfig {
id: String::from(TEST_ID_B),
server_url: String::from("https://slot4.example.org"),
},
enterprise_config("9072b77d-ca81-40da-be6a-861da525ef7b", "https://slot0.example.org", "policy files", &policy_path(directory.path().join("config0.yaml")), "indexed slot 0"),
enterprise_config(TEST_ID_B, "https://slot4.example.org", "policy files", &policy_path(directory.path().join("config4.yaml")), "indexed slot 4"),
]
);
}
@ -1311,14 +1385,8 @@ mod tests {
assert_eq!(
source.configs,
vec![
EnterpriseConfig {
id: String::from("9072b77d-ca81-40da-be6a-861da525ef7b"),
server_url: String::from("https://slot0.example.org"),
},
EnterpriseConfig {
id: String::from(TEST_ID_B),
server_url: String::from("https://slot10503.example.org"),
},
enterprise_config("9072b77d-ca81-40da-be6a-861da525ef7b", "https://slot0.example.org", "policy files", &policy_path(directory.path().join("config_00000.yaml")), "indexed slot 00000"),
enterprise_config(TEST_ID_B, "https://slot10503.example.org", "policy files", &policy_path(directory.path().join("config_10503.yaml")), "indexed slot 10503"),
]
);
}