Allow external HTTP root certificates to be configured by a policy file (#805)
Some checks are pending
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions

This commit is contained in:
Thorsten Sommer 2026-06-11 11:37:40 +02:00 committed by GitHub
parent 5272895441
commit 0ea63a16c0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 296 additions and 31 deletions

View File

@ -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<string> ReadAllowedHostPatterns(string? envAllowedHosts)
private static IReadOnlyList<string> ReadAllowedHostPatternsFromDelimitedValue(string allowedHosts)
{
IEnumerable<string> 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<string> ReadAllowedHostPatterns(IEnumerable<string> rawPatterns)
{
var patterns = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var rawPattern in rawPatterns)
{

View File

@ -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

View File

@ -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<R: tauri::Runtime>(app_handle: tauri::AppHandle<R>) {
let secret_key_salt = BASE64_STANDARD.encode(ENCRYPTION.secret_key_salt);
let api_port = *API_SERVER_PORT;
let dotnet_server_environment: HashMap<String, String> = HashMap::from_iter([
let mut dotnet_server_environment: HashMap<String, String> = 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();

View File

@ -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<Vec<EnterpriseCon
Json(resolve_effective_enterprise_config_source().configs)
}
pub fn resolve_external_http_custom_root_certificate_policy() -> 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<String, String>) -> Option<ExternalHttpCustomRootCertificatePolicy> {
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<String> {
Some(String::from(trimmed))
}
fn parse_policy_boolean_value(raw_value: &str) -> Option<bool> {
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<String> {
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();