diff --git a/app/MindWork AI Studio/Tools/ExternalHttpClientTimeout.cs b/app/MindWork AI Studio/Tools/ExternalHttpClientTimeout.cs index de13f40d..1181cb40 100644 --- a/app/MindWork AI Studio/Tools/ExternalHttpClientTimeout.cs +++ b/app/MindWork AI Studio/Tools/ExternalHttpClientTimeout.cs @@ -19,6 +19,10 @@ public static class ExternalHttpClientTimeout private const string ENV_CUSTOM_ROOT_CERTIFICATES_ENABLED = "MINDWORK_AI_STUDIO_EXTERNAL_HTTP_CUSTOM_ROOT_CERTIFICATES_ENABLED"; private const string ENV_CUSTOM_ROOT_CERTIFICATE_BUNDLE_PATH = "MINDWORK_AI_STUDIO_EXTERNAL_HTTP_CUSTOM_ROOT_CERTIFICATE_BUNDLE_PATH"; private const string ENV_CUSTOM_ROOT_CERTIFICATE_ALLOWED_HOSTS = "MINDWORK_AI_STUDIO_EXTERNAL_HTTP_CUSTOM_ROOT_CERTIFICATE_ALLOWED_HOSTS"; + private const string ENV_POLICY_CUSTOM_ROOT_CERTIFICATES_CONFIGURED = "AI_STUDIO_EXTERNAL_HTTP_CUSTOM_ROOT_CERTIFICATES_POLICY_CONFIGURED"; + private const string ENV_POLICY_CUSTOM_ROOT_CERTIFICATES_ENABLED = "AI_STUDIO_EXTERNAL_HTTP_CUSTOM_ROOT_CERTIFICATES_ENABLED"; + private const string ENV_POLICY_CUSTOM_ROOT_CERTIFICATE_BUNDLE_PATH = "AI_STUDIO_EXTERNAL_HTTP_CUSTOM_ROOT_CERTIFICATE_BUNDLE_PATH"; + private const string ENV_POLICY_CUSTOM_ROOT_CERTIFICATE_ALLOWED_HOSTS = "AI_STUDIO_EXTERNAL_HTTP_CUSTOM_ROOT_CERTIFICATE_ALLOWED_HOSTS"; // id-kp-serverAuth: Extended Key Usage for TLS server authentication. // See RFC 5280, section 4.2.1.12: https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.12 @@ -123,29 +127,43 @@ public static class ExternalHttpClientTimeout private static CustomRootCertificateConfiguration ReadCustomRootCertificateConfiguration() { + if (TryParseBooleanEnvironmentValue(Environment.GetEnvironmentVariable(ENV_POLICY_CUSTOM_ROOT_CERTIFICATES_CONFIGURED), out var policyConfigured) && policyConfigured) + { + var policyEnabled = TryParseBooleanEnvironmentValue(Environment.GetEnvironmentVariable(ENV_POLICY_CUSTOM_ROOT_CERTIFICATES_ENABLED), out var parsedPolicyEnabled) && parsedPolicyEnabled; + var policyBundlePath = Environment.GetEnvironmentVariable(ENV_POLICY_CUSTOM_ROOT_CERTIFICATE_BUNDLE_PATH)?.Trim() ?? string.Empty; + var policyAllowedHosts = Environment.GetEnvironmentVariable(ENV_POLICY_CUSTOM_ROOT_CERTIFICATE_ALLOWED_HOSTS) ?? string.Empty; + return new(policyEnabled, policyBundlePath, ReadAllowedHostPatternsFromDelimitedValue(policyAllowedHosts), TB("policy files")); + } + var envEnabled = Environment.GetEnvironmentVariable(ENV_CUSTOM_ROOT_CERTIFICATES_ENABLED); var envBundlePath = Environment.GetEnvironmentVariable(ENV_CUSTOM_ROOT_CERTIFICATE_BUNDLE_PATH); var envAllowedHosts = Environment.GetEnvironmentVariable(ENV_CUSTOM_ROOT_CERTIFICATE_ALLOWED_HOSTS); + if (!string.IsNullOrWhiteSpace(envEnabled) || !string.IsNullOrWhiteSpace(envBundlePath) || !string.IsNullOrWhiteSpace(envAllowedHosts)) + { + var enabled = TryParseBooleanEnvironmentValue(envEnabled, out var parsedEnvEnabled) + ? parsedEnvEnabled + : SettingsManagerAccess.ConfigurationData.App.ExternalHttpCustomRootCertificatesEnabled; - var enabled = TryParseBooleanEnvironmentValue(envEnabled, out var parsedEnvEnabled) - ? parsedEnvEnabled - : SettingsManagerAccess.ConfigurationData.App.ExternalHttpCustomRootCertificatesEnabled; + var bundlePath = !string.IsNullOrWhiteSpace(envBundlePath) + ? envBundlePath.Trim() + : SettingsManagerAccess.ConfigurationData.App.ExternalHttpCustomRootCertificateBundlePath.Trim(); - var bundlePath = !string.IsNullOrWhiteSpace(envBundlePath) - ? envBundlePath.Trim() - : SettingsManagerAccess.ConfigurationData.App.ExternalHttpCustomRootCertificateBundlePath.Trim(); + var allowedHostPatterns = !string.IsNullOrWhiteSpace(envAllowedHosts) + ? ReadAllowedHostPatternsFromDelimitedValue(envAllowedHosts) + : ReadAllowedHostPatterns(SettingsManagerAccess.ConfigurationData.App.ExternalHttpCustomRootCertificateAllowedHosts); - var allowedHostPatterns = ReadAllowedHostPatterns(envAllowedHosts); - var source = ReadCustomRootCertificateConfigurationSource(envEnabled, envBundlePath, envAllowedHosts); + return new(enabled, bundlePath, allowedHostPatterns, TB("environment variables")); + } - return new(enabled, bundlePath, allowedHostPatterns, source); + return new( + SettingsManagerAccess.ConfigurationData.App.ExternalHttpCustomRootCertificatesEnabled, + SettingsManagerAccess.ConfigurationData.App.ExternalHttpCustomRootCertificateBundlePath.Trim(), + ReadAllowedHostPatterns(SettingsManagerAccess.ConfigurationData.App.ExternalHttpCustomRootCertificateAllowedHosts), + ReadCustomRootCertificateSettingsSource()); } - private static string ReadCustomRootCertificateConfigurationSource(string? envEnabled, string? envBundlePath, string? envAllowedHosts) + private static string ReadCustomRootCertificateSettingsSource() { - if (!string.IsNullOrWhiteSpace(envEnabled) || !string.IsNullOrWhiteSpace(envBundlePath) || !string.IsNullOrWhiteSpace(envAllowedHosts)) - return TB("environment variables"); - var enabledIsManaged = ManagedConfiguration.TryGet(x => x.App, x => x.ExternalHttpCustomRootCertificatesEnabled, out var enabledMeta) && enabledMeta.IsLocked; var bundlePathIsManaged = ManagedConfiguration.TryGet(x => x.App, x => x.ExternalHttpCustomRootCertificateBundlePath, out var bundlePathMeta) && bundlePathMeta.IsLocked; var allowedHostsIsManaged = ManagedConfiguration.TryGet(x => x.App, x => x.ExternalHttpCustomRootCertificateAllowedHosts, out var allowedHostsMeta) && allowedHostsMeta.IsLocked; @@ -154,12 +172,13 @@ public static class ExternalHttpClientTimeout : TB("app settings"); } - private static IReadOnlyList ReadAllowedHostPatterns(string? envAllowedHosts) + private static IReadOnlyList ReadAllowedHostPatternsFromDelimitedValue(string allowedHosts) { - IEnumerable rawPatterns = !string.IsNullOrWhiteSpace(envAllowedHosts) - ? envAllowedHosts.Split([';', ','], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) - : SettingsManagerAccess.ConfigurationData.App.ExternalHttpCustomRootCertificateAllowedHosts; + return ReadAllowedHostPatterns(allowedHosts.Split([';', ','], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)); + } + private static IReadOnlyList ReadAllowedHostPatterns(IEnumerable rawPatterns) + { var patterns = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (var rawPattern in rawPatterns) { diff --git a/documentation/Enterprise IT.md b/documentation/Enterprise IT.md index 3d7a9c1b..d5f626d9 100644 --- a/documentation/Enterprise IT.md +++ b/documentation/Enterprise IT.md @@ -129,6 +129,18 @@ Optional encryption secret file: config_encryption_secret: "BASE64..." ``` +Optional custom root certificate policy file: + +- `external_http_custom_root_certificates.yaml` + +```yaml +enabled: true +bundle_path: "/app/etc/MindWorkAI/company-root-cas.pem" +allowed_hosts: "*.intra.example.org;eri.example.org" +``` + +When this file exists and contains a valid `enabled` value, it takes precedence over the custom root certificate environment variables described below. This is useful for Flatpak deployments because a Flatpak provisioning extension can provide the policy file and the PEM bundle together. Set `enabled: false` to explicitly disable additional root certificates and ignore lower-priority environment variables. + ### Environment variable example If you need the fallback environment-variable format, configure the values like this: @@ -172,7 +184,18 @@ If your organization uses private root CAs, place a PEM bundle with the required -----END CERTIFICATE----- ``` -For the first enterprise configuration download, configure these environment variables before AI Studio starts: +For Flatpak deployments, the recommended approach is to provide an enterprise policy file through the Flatpak provisioning extension: + +```yaml +# /app/etc/MindWorkAI/external_http_custom_root_certificates.yaml +enabled: true +bundle_path: "/app/etc/MindWorkAI/company-root-cas.pem" +allowed_hosts: "*.intra.example.org;eri.example.org" +``` + +Place the PEM bundle at the configured path inside the sandbox, for example, through the same provisioning extension. This allows AI Studio to use the additional root certificates during the first enterprise configuration download. + +As a fallback, you can configure these environment variables before AI Studio starts: ```bash MINDWORK_AI_STUDIO_EXTERNAL_HTTP_CUSTOM_ROOT_CERTIFICATES_ENABLED=true diff --git a/runtime/src/dotnet.rs b/runtime/src/dotnet.rs index c5158e13..f269c3ee 100644 --- a/runtime/src/dotnet.rs +++ b/runtime/src/dotnet.rs @@ -13,7 +13,13 @@ use crate::runtime_api_token::API_TOKEN; use crate::app_window::change_location_to; use crate::runtime_certificate::CERTIFICATE_FINGERPRINT; use crate::encryption::ENCRYPTION; -use crate::environment::{is_dev, DATA_DIRECTORY}; +use crate::environment::{ + is_dev, resolve_external_http_custom_root_certificate_policy, DATA_DIRECTORY, + DOTNET_ENV_CUSTOM_ROOT_CERTIFICATE_ALLOWED_HOSTS, + DOTNET_ENV_CUSTOM_ROOT_CERTIFICATE_BUNDLE_PATH, + DOTNET_ENV_CUSTOM_ROOT_CERTIFICATE_POLICY_CONFIGURED, + DOTNET_ENV_CUSTOM_ROOT_CERTIFICATES_ENABLED, +}; use crate::network::get_available_port; use crate::runtime_api::API_SERVER_PORT; use crate::stale_process_cleanup::{kill_stale_process, log_potential_stale_process}; @@ -93,6 +99,20 @@ pub async fn dotnet_port(_token: APIToken) -> String { format!("{dotnet_server_port}") } +fn external_http_custom_root_certificate_policy_environment() -> Vec<(String, String)> { + let policy = resolve_external_http_custom_root_certificate_policy(); + if !policy.is_configured { + return Vec::new(); + } + + vec![ + (String::from(DOTNET_ENV_CUSTOM_ROOT_CERTIFICATE_POLICY_CONFIGURED), String::from("true")), + (String::from(DOTNET_ENV_CUSTOM_ROOT_CERTIFICATES_ENABLED), policy.enabled.to_string()), + (String::from(DOTNET_ENV_CUSTOM_ROOT_CERTIFICATE_BUNDLE_PATH), policy.bundle_path), + (String::from(DOTNET_ENV_CUSTOM_ROOT_CERTIFICATE_ALLOWED_HOSTS), policy.allowed_hosts), + ] +} + /// Creates the startup environment file for the .NET server in the development /// environment. The file is created in the root directory of the repository. /// Creating that env file on a production environment would be a security @@ -113,18 +133,18 @@ pub fn create_startup_env_file() { warn!(Source = "Bootloader .NET"; "Development environment detected; create the startup env file at '../startup.env'."); let env_file_path = std::path::PathBuf::from("..").join("startup.env"); let mut env_file = std::fs::File::create(env_file_path).unwrap(); - let env_file_content = format!( - "AI_STUDIO_SECRET_PASSWORD={secret_password}\n\ - AI_STUDIO_SECRET_KEY_SALT={secret_key_salt}\n\ - AI_STUDIO_CERTIFICATE_FINGERPRINT={cert_fingerprint}\n\ - AI_STUDIO_API_PORT={api_port}\n\ - AI_STUDIO_API_TOKEN={api_token}", + let mut env_file_lines = vec![ + format!("AI_STUDIO_SECRET_PASSWORD={secret_password}"), + format!("AI_STUDIO_SECRET_KEY_SALT={secret_key_salt}"), + format!("AI_STUDIO_CERTIFICATE_FINGERPRINT={}", CERTIFICATE_FINGERPRINT.get().unwrap()), + format!("AI_STUDIO_API_PORT={api_port}"), + format!("AI_STUDIO_API_TOKEN={}", API_TOKEN.to_hex_text()), + ]; + for (key, value) in external_http_custom_root_certificate_policy_environment() { + env_file_lines.push(format!("{key}={value}")); + } - cert_fingerprint = CERTIFICATE_FINGERPRINT.get().unwrap(), - api_token = API_TOKEN.to_hex_text() - ); - - std::io::Write::write_all(&mut env_file, env_file_content.as_bytes()).unwrap(); + std::io::Write::write_all(&mut env_file, env_file_lines.join("\n").as_bytes()).unwrap(); info!(Source = "Bootloader .NET"; "The startup env file was created successfully."); } @@ -136,13 +156,14 @@ pub fn start_dotnet_server(app_handle: tauri::AppHandle) { let secret_key_salt = BASE64_STANDARD.encode(ENCRYPTION.secret_key_salt); let api_port = *API_SERVER_PORT; - let dotnet_server_environment: HashMap = HashMap::from_iter([ + let mut dotnet_server_environment: HashMap = HashMap::from_iter([ (String::from("AI_STUDIO_SECRET_PASSWORD"), secret_password), (String::from("AI_STUDIO_SECRET_KEY_SALT"), secret_key_salt), (String::from("AI_STUDIO_CERTIFICATE_FINGERPRINT"), CERTIFICATE_FINGERPRINT.get().unwrap().to_string()), (String::from("AI_STUDIO_API_PORT"), format!("{api_port}")), (String::from("AI_STUDIO_API_TOKEN"), API_TOKEN.to_hex_text().to_string()), ]); + dotnet_server_environment.extend(external_http_custom_root_certificate_policy_environment()); info!("Try to start the .NET server..."); let server_spawn_clone = DOTNET_SERVER.clone(); diff --git a/runtime/src/environment.rs b/runtime/src/environment.rs index 400b2fa8..e3f1955c 100644 --- a/runtime/src/environment.rs +++ b/runtime/src/environment.rs @@ -21,6 +21,12 @@ const ENTERPRISE_CONFIG_SERVER_URL_KEY_PREFIX: &str = "config_server_url"; const ENTERPRISE_REGISTRY_KEY_PATH: &str = r"Software\github\MindWork AI Studio\Enterprise IT"; const ENTERPRISE_POLICY_SECRET_FILE_NAME: &str = "config_encryption_secret.yaml"; +const EXTERNAL_HTTP_CUSTOM_ROOT_CERTIFICATE_POLICY_FILE_NAME: &str = "external_http_custom_root_certificates.yaml"; + +pub const DOTNET_ENV_CUSTOM_ROOT_CERTIFICATE_POLICY_CONFIGURED: &str = "AI_STUDIO_EXTERNAL_HTTP_CUSTOM_ROOT_CERTIFICATES_POLICY_CONFIGURED"; +pub const DOTNET_ENV_CUSTOM_ROOT_CERTIFICATES_ENABLED: &str = "AI_STUDIO_EXTERNAL_HTTP_CUSTOM_ROOT_CERTIFICATES_ENABLED"; +pub const DOTNET_ENV_CUSTOM_ROOT_CERTIFICATE_BUNDLE_PATH: &str = "AI_STUDIO_EXTERNAL_HTTP_CUSTOM_ROOT_CERTIFICATE_BUNDLE_PATH"; +pub const DOTNET_ENV_CUSTOM_ROOT_CERTIFICATE_ALLOWED_HOSTS: &str = "AI_STUDIO_EXTERNAL_HTTP_CUSTOM_ROOT_CERTIFICATE_ALLOWED_HOSTS"; #[cfg(any(target_os = "linux", test))] const FLATPAK_ENTERPRISE_POLICY_DIRECTORY: &str = "/app/etc/MindWorkAI"; @@ -257,6 +263,15 @@ pub struct EnterpriseConfig { pub slot: String, } +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct ExternalHttpCustomRootCertificatePolicy { + pub is_configured: bool, + pub enabled: bool, + pub bundle_path: String, + pub allowed_hosts: String, + pub source_detail: String, +} + #[derive(Clone, Debug, PartialEq, Eq)] struct EnterpriseSourceValue { value: String, @@ -337,6 +352,10 @@ pub async fn read_enterprise_configs(_token: APIToken) -> Json ExternalHttpCustomRootCertificatePolicy { + load_external_http_custom_root_certificate_policy_from_directories(&enterprise_policy_directories()) +} + fn resolve_effective_enterprise_config_source() -> EnterpriseSourceData { select_effective_enterprise_config_source(gather_enterprise_sources()) } @@ -646,6 +665,54 @@ fn load_policy_values_from_directories(directories: &[PathBuf]) -> EnterpriseSou values } +fn load_external_http_custom_root_certificate_policy_from_directories(directories: &[PathBuf]) -> ExternalHttpCustomRootCertificatePolicy { + for directory in directories { + let path = directory.join(EXTERNAL_HTTP_CUSTOM_ROOT_CERTIFICATE_POLICY_FILE_NAME); + let Some(values) = read_policy_yaml_mapping(&path) else { + continue; + }; + + if let Some(policy) = parse_external_http_custom_root_certificate_policy(&path, &values) { + info!("Using external HTTP custom root certificate policy from '{}'.", policy.source_detail); + return policy; + } + } + + ExternalHttpCustomRootCertificatePolicy::default() +} + +fn parse_external_http_custom_root_certificate_policy(path: &Path, values: &HashMap) -> Option { + let Some(raw_enabled) = values.get("enabled") else { + warn!("Ignoring external HTTP custom root certificate policy '{}': missing 'enabled'.", path.display()); + return None; + }; + + let Some(enabled) = parse_policy_boolean_value(raw_enabled) else { + warn!("Ignoring external HTTP custom root certificate policy '{}': invalid 'enabled' value.", path.display()); + return None; + }; + + let source_detail = path + .canonicalize() + .unwrap_or_else(|_| path.to_path_buf()) + .to_string_lossy() + .into_owned(); + + Some(ExternalHttpCustomRootCertificatePolicy { + is_configured: true, + enabled, + bundle_path: values + .get("bundle_path") + .and_then(|value| normalize_enterprise_value(value)) + .unwrap_or_default(), + allowed_hosts: values + .get("allowed_hosts") + .and_then(|value| normalize_enterprise_value(value)) + .unwrap_or_default(), + source_detail, + }) +} + fn enterprise_policy_file_slot_suffix(file_name: &str) -> Option<&str> { let suffix = file_name .strip_prefix("config")? @@ -737,6 +804,25 @@ fn parse_policy_yaml_value(raw_value: &str) -> Option { Some(String::from(trimmed)) } +fn parse_policy_boolean_value(raw_value: &str) -> Option { + let normalized = raw_value.trim(); + if normalized.eq_ignore_ascii_case("true") + || normalized == "1" + || normalized.eq_ignore_ascii_case("yes") + || normalized.eq_ignore_ascii_case("on") { + return Some(true); + } + + if normalized.eq_ignore_ascii_case("false") + || normalized == "0" + || normalized.eq_ignore_ascii_case("no") + || normalized.eq_ignore_ascii_case("off") { + return Some(false); + } + + None +} + 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 @@ -963,10 +1049,12 @@ fn normalize_enterprise_config_id(value: &str) -> Option { mod tests { use super::{ enterprise_environment_key_name, enterprise_policy_file_slot_suffix, + load_external_http_custom_root_certificate_policy_from_directories, 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, EnterpriseSourceValue, EnterpriseSourceValues, + ExternalHttpCustomRootCertificatePolicy, }; use std::collections::HashMap; use std::fs; @@ -1490,6 +1578,120 @@ mod tests { assert_eq!(source.encryption_secret, "POLICY-SECRET"); } + #[test] + fn load_external_http_custom_root_certificate_policy_uses_first_valid_directory() { + let directory_a = tempdir().unwrap(); + let directory_b = tempdir().unwrap(); + + fs::write( + directory_a.path().join("external_http_custom_root_certificates.yaml"), + "enabled: true\nbundle_path: \"/app/etc/MindWorkAI/company-a.pem\"\nallowed_hosts: \"*.a.example.org;eri.a.example.org\"", + ) + .unwrap(); + fs::write( + directory_b.path().join("external_http_custom_root_certificates.yaml"), + "enabled: true\nbundle_path: \"/app/etc/MindWorkAI/company-b.pem\"\nallowed_hosts: \"*.b.example.org\"", + ) + .unwrap(); + + let policy = load_external_http_custom_root_certificate_policy_from_directories(&[ + directory_a.path().to_path_buf(), + directory_b.path().to_path_buf(), + ]); + + assert_eq!( + policy, + ExternalHttpCustomRootCertificatePolicy { + is_configured: true, + enabled: true, + bundle_path: String::from("/app/etc/MindWorkAI/company-a.pem"), + allowed_hosts: String::from("*.a.example.org;eri.a.example.org"), + source_detail: policy_path(directory_a.path().join("external_http_custom_root_certificates.yaml")), + } + ); + } + + #[test] + fn load_external_http_custom_root_certificate_policy_allows_disabled_policy_to_win() { + let directory_a = tempdir().unwrap(); + let directory_b = tempdir().unwrap(); + + fs::write( + directory_a.path().join("external_http_custom_root_certificates.yaml"), + "enabled: false", + ) + .unwrap(); + fs::write( + directory_b.path().join("external_http_custom_root_certificates.yaml"), + "enabled: true\nbundle_path: \"/app/etc/MindWorkAI/company-b.pem\"\nallowed_hosts: \"*.b.example.org\"", + ) + .unwrap(); + + let policy = load_external_http_custom_root_certificate_policy_from_directories(&[ + directory_a.path().to_path_buf(), + directory_b.path().to_path_buf(), + ]); + + assert_eq!( + policy, + ExternalHttpCustomRootCertificatePolicy { + is_configured: true, + enabled: false, + bundle_path: String::new(), + allowed_hosts: String::new(), + source_detail: policy_path(directory_a.path().join("external_http_custom_root_certificates.yaml")), + } + ); + } + + #[test] + fn load_external_http_custom_root_certificate_policy_skips_invalid_files() { + let directory_a = tempdir().unwrap(); + let directory_b = tempdir().unwrap(); + + fs::write( + directory_a.path().join("external_http_custom_root_certificates.yaml"), + "enabled: maybe\nbundle_path: \"/app/etc/MindWorkAI/ignored.pem\"", + ) + .unwrap(); + fs::write( + directory_b.path().join("external_http_custom_root_certificates.yaml"), + "enabled: yes\nbundle_path: \"/app/etc/MindWorkAI/company-b.pem\"\nallowed_hosts: \"*.b.example.org,eri.b.example.org\"", + ) + .unwrap(); + + let policy = load_external_http_custom_root_certificate_policy_from_directories(&[ + directory_a.path().to_path_buf(), + directory_b.path().to_path_buf(), + ]); + + assert_eq!( + policy, + ExternalHttpCustomRootCertificatePolicy { + is_configured: true, + enabled: true, + bundle_path: String::from("/app/etc/MindWorkAI/company-b.pem"), + allowed_hosts: String::from("*.b.example.org,eri.b.example.org"), + source_detail: policy_path(directory_b.path().join("external_http_custom_root_certificates.yaml")), + } + ); + } + + #[test] + fn load_external_http_custom_root_certificate_policy_requires_enabled_key() { + let directory = tempdir().unwrap(); + + fs::write( + directory.path().join("external_http_custom_root_certificates.yaml"), + "bundle_path: \"/app/etc/MindWorkAI/company.pem\"\nallowed_hosts: \"*.example.org\"", + ) + .unwrap(); + + let policy = load_external_http_custom_root_certificate_policy_from_directories(&[directory.path().to_path_buf()]); + + assert_eq!(policy, ExternalHttpCustomRootCertificatePolicy::default()); + } + #[test] fn load_policy_values_from_directories_ignores_invalid_and_incomplete_files() { let directory = tempdir().unwrap();