mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-04 16:49:40 +00:00
Allow external HTTP root certificates to be configured by a policy file (#805)
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 }}) (-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
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 / 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 }}) (-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
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
This commit is contained in:
1 parent
5272895441
commit
0ea63a16c0
4 files changed
+296
-31
No files matched your search
+34
-13
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user