mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-06-12 04:56:27 +00:00
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
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:
parent
5272895441
commit
0ea63a16c0
@ -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_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_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_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.
|
// 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
|
// 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()
|
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 envEnabled = Environment.GetEnvironmentVariable(ENV_CUSTOM_ROOT_CERTIFICATES_ENABLED);
|
||||||
var envBundlePath = Environment.GetEnvironmentVariable(ENV_CUSTOM_ROOT_CERTIFICATE_BUNDLE_PATH);
|
var envBundlePath = Environment.GetEnvironmentVariable(ENV_CUSTOM_ROOT_CERTIFICATE_BUNDLE_PATH);
|
||||||
var envAllowedHosts = Environment.GetEnvironmentVariable(ENV_CUSTOM_ROOT_CERTIFICATE_ALLOWED_HOSTS);
|
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)
|
var bundlePath = !string.IsNullOrWhiteSpace(envBundlePath)
|
||||||
? parsedEnvEnabled
|
? envBundlePath.Trim()
|
||||||
: SettingsManagerAccess.ConfigurationData.App.ExternalHttpCustomRootCertificatesEnabled;
|
: SettingsManagerAccess.ConfigurationData.App.ExternalHttpCustomRootCertificateBundlePath.Trim();
|
||||||
|
|
||||||
var bundlePath = !string.IsNullOrWhiteSpace(envBundlePath)
|
var allowedHostPatterns = !string.IsNullOrWhiteSpace(envAllowedHosts)
|
||||||
? envBundlePath.Trim()
|
? ReadAllowedHostPatternsFromDelimitedValue(envAllowedHosts)
|
||||||
: SettingsManagerAccess.ConfigurationData.App.ExternalHttpCustomRootCertificateBundlePath.Trim();
|
: ReadAllowedHostPatterns(SettingsManagerAccess.ConfigurationData.App.ExternalHttpCustomRootCertificateAllowedHosts);
|
||||||
|
|
||||||
var allowedHostPatterns = ReadAllowedHostPatterns(envAllowedHosts);
|
return new(enabled, bundlePath, allowedHostPatterns, TB("environment variables"));
|
||||||
var source = ReadCustomRootCertificateConfigurationSource(envEnabled, envBundlePath, envAllowedHosts);
|
}
|
||||||
|
|
||||||
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 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 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;
|
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");
|
: TB("app settings");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IReadOnlyList<string> ReadAllowedHostPatterns(string? envAllowedHosts)
|
private static IReadOnlyList<string> ReadAllowedHostPatternsFromDelimitedValue(string allowedHosts)
|
||||||
{
|
{
|
||||||
IEnumerable<string> rawPatterns = !string.IsNullOrWhiteSpace(envAllowedHosts)
|
return ReadAllowedHostPatterns(allowedHosts.Split([';', ','], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries));
|
||||||
? envAllowedHosts.Split([';', ','], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
}
|
||||||
: SettingsManagerAccess.ConfigurationData.App.ExternalHttpCustomRootCertificateAllowedHosts;
|
|
||||||
|
|
||||||
|
private static IReadOnlyList<string> ReadAllowedHostPatterns(IEnumerable<string> rawPatterns)
|
||||||
|
{
|
||||||
var patterns = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
var patterns = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
foreach (var rawPattern in rawPatterns)
|
foreach (var rawPattern in rawPatterns)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -129,6 +129,18 @@ Optional encryption secret file:
|
|||||||
config_encryption_secret: "BASE64..."
|
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
|
### Environment variable example
|
||||||
|
|
||||||
If you need the fallback environment-variable format, configure the values like this:
|
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-----
|
-----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
|
```bash
|
||||||
MINDWORK_AI_STUDIO_EXTERNAL_HTTP_CUSTOM_ROOT_CERTIFICATES_ENABLED=true
|
MINDWORK_AI_STUDIO_EXTERNAL_HTTP_CUSTOM_ROOT_CERTIFICATES_ENABLED=true
|
||||||
|
|||||||
@ -13,7 +13,13 @@ use crate::runtime_api_token::API_TOKEN;
|
|||||||
use crate::app_window::change_location_to;
|
use crate::app_window::change_location_to;
|
||||||
use crate::runtime_certificate::CERTIFICATE_FINGERPRINT;
|
use crate::runtime_certificate::CERTIFICATE_FINGERPRINT;
|
||||||
use crate::encryption::ENCRYPTION;
|
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::network::get_available_port;
|
||||||
use crate::runtime_api::API_SERVER_PORT;
|
use crate::runtime_api::API_SERVER_PORT;
|
||||||
use crate::stale_process_cleanup::{kill_stale_process, log_potential_stale_process};
|
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}")
|
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
|
/// Creates the startup environment file for the .NET server in the development
|
||||||
/// environment. The file is created in the root directory of the repository.
|
/// environment. The file is created in the root directory of the repository.
|
||||||
/// Creating that env file on a production environment would be a security
|
/// 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'.");
|
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 env_file_path = std::path::PathBuf::from("..").join("startup.env");
|
||||||
let mut env_file = std::fs::File::create(env_file_path).unwrap();
|
let mut env_file = std::fs::File::create(env_file_path).unwrap();
|
||||||
let env_file_content = format!(
|
let mut env_file_lines = vec![
|
||||||
"AI_STUDIO_SECRET_PASSWORD={secret_password}\n\
|
format!("AI_STUDIO_SECRET_PASSWORD={secret_password}"),
|
||||||
AI_STUDIO_SECRET_KEY_SALT={secret_key_salt}\n\
|
format!("AI_STUDIO_SECRET_KEY_SALT={secret_key_salt}"),
|
||||||
AI_STUDIO_CERTIFICATE_FINGERPRINT={cert_fingerprint}\n\
|
format!("AI_STUDIO_CERTIFICATE_FINGERPRINT={}", CERTIFICATE_FINGERPRINT.get().unwrap()),
|
||||||
AI_STUDIO_API_PORT={api_port}\n\
|
format!("AI_STUDIO_API_PORT={api_port}"),
|
||||||
AI_STUDIO_API_TOKEN={api_token}",
|
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(),
|
std::io::Write::write_all(&mut env_file, env_file_lines.join("\n").as_bytes()).unwrap();
|
||||||
api_token = API_TOKEN.to_hex_text()
|
|
||||||
);
|
|
||||||
|
|
||||||
std::io::Write::write_all(&mut env_file, env_file_content.as_bytes()).unwrap();
|
|
||||||
info!(Source = "Bootloader .NET"; "The startup env file was created successfully.");
|
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 secret_key_salt = BASE64_STANDARD.encode(ENCRYPTION.secret_key_salt);
|
||||||
let api_port = *API_SERVER_PORT;
|
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_PASSWORD"), secret_password),
|
||||||
(String::from("AI_STUDIO_SECRET_KEY_SALT"), secret_key_salt),
|
(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_CERTIFICATE_FINGERPRINT"), CERTIFICATE_FINGERPRINT.get().unwrap().to_string()),
|
||||||
(String::from("AI_STUDIO_API_PORT"), format!("{api_port}")),
|
(String::from("AI_STUDIO_API_PORT"), format!("{api_port}")),
|
||||||
(String::from("AI_STUDIO_API_TOKEN"), API_TOKEN.to_hex_text().to_string()),
|
(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...");
|
info!("Try to start the .NET server...");
|
||||||
let server_spawn_clone = DOTNET_SERVER.clone();
|
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_REGISTRY_KEY_PATH: &str = r"Software\github\MindWork AI Studio\Enterprise IT";
|
||||||
|
|
||||||
const ENTERPRISE_POLICY_SECRET_FILE_NAME: &str = "config_encryption_secret.yaml";
|
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))]
|
#[cfg(any(target_os = "linux", test))]
|
||||||
const FLATPAK_ENTERPRISE_POLICY_DIRECTORY: &str = "/app/etc/MindWorkAI";
|
const FLATPAK_ENTERPRISE_POLICY_DIRECTORY: &str = "/app/etc/MindWorkAI";
|
||||||
@ -257,6 +263,15 @@ pub struct EnterpriseConfig {
|
|||||||
pub slot: String,
|
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)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
struct EnterpriseSourceValue {
|
struct EnterpriseSourceValue {
|
||||||
value: String,
|
value: String,
|
||||||
@ -337,6 +352,10 @@ pub async fn read_enterprise_configs(_token: APIToken) -> Json<Vec<EnterpriseCon
|
|||||||
Json(resolve_effective_enterprise_config_source().configs)
|
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 {
|
fn resolve_effective_enterprise_config_source() -> EnterpriseSourceData {
|
||||||
select_effective_enterprise_config_source(gather_enterprise_sources())
|
select_effective_enterprise_config_source(gather_enterprise_sources())
|
||||||
}
|
}
|
||||||
@ -646,6 +665,54 @@ fn load_policy_values_from_directories(directories: &[PathBuf]) -> EnterpriseSou
|
|||||||
values
|
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> {
|
fn enterprise_policy_file_slot_suffix(file_name: &str) -> Option<&str> {
|
||||||
let suffix = file_name
|
let suffix = file_name
|
||||||
.strip_prefix("config")?
|
.strip_prefix("config")?
|
||||||
@ -737,6 +804,25 @@ fn parse_policy_yaml_value(raw_value: &str) -> Option<String> {
|
|||||||
Some(String::from(trimmed))
|
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) {
|
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) {
|
if let Some(value) = normalize_enterprise_value(raw_value) {
|
||||||
values
|
values
|
||||||
@ -963,10 +1049,12 @@ fn normalize_enterprise_config_id(value: &str) -> Option<String> {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::{
|
use super::{
|
||||||
enterprise_environment_key_name, enterprise_policy_file_slot_suffix,
|
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,
|
linux_policy_directories_from_xdg, load_policy_values_from_directories,
|
||||||
normalize_locale_tag, parse_enterprise_source_values,
|
normalize_locale_tag, parse_enterprise_source_values,
|
||||||
select_effective_enterprise_config_source, select_effective_enterprise_secret_source,
|
select_effective_enterprise_config_source, select_effective_enterprise_secret_source,
|
||||||
EnterpriseConfig, EnterpriseSourceData, EnterpriseSourceValue, EnterpriseSourceValues,
|
EnterpriseConfig, EnterpriseSourceData, EnterpriseSourceValue, EnterpriseSourceValues,
|
||||||
|
ExternalHttpCustomRootCertificatePolicy,
|
||||||
};
|
};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
@ -1490,6 +1578,120 @@ mod tests {
|
|||||||
assert_eq!(source.encryption_secret, "POLICY-SECRET");
|
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]
|
#[test]
|
||||||
fn load_policy_values_from_directories_ignores_invalid_and_incomplete_files() {
|
fn load_policy_values_from_directories_ignores_invalid_and_incomplete_files() {
|
||||||
let directory = tempdir().unwrap();
|
let directory = tempdir().unwrap();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user