mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-03-29 17:31:37 +00:00
Improve enterprise config to handle encryption-only sources
This commit is contained in:
parent
422b31b3f4
commit
c1b4f19893
@ -23,13 +23,15 @@ So that MindWork AI Studio knows where to load which configuration, this informa
|
||||
|
||||
### Source order and fallback behavior
|
||||
|
||||
AI Studio does **not** merge the registry, policy files, and environment variables. Instead, it checks them in order and uses the **first source that contains at least one valid enterprise configuration**:
|
||||
AI Studio does **not** merge the registry, policy files, and environment variables. Instead, it checks them in order:
|
||||
|
||||
- **Windows:** Registry -> Policy files -> Environment variables
|
||||
- **Linux:** Policy files -> Environment variables
|
||||
- **macOS:** Policy files -> Environment variables
|
||||
|
||||
The encryption secret follows the same rule. It is only used from the same source that provided the active enterprise configurations.
|
||||
For enterprise configurations, AI Studio uses the **first source that contains at least one valid enterprise configuration**.
|
||||
|
||||
For the encryption secret, AI Studio uses the **first source that contains a non-empty encryption secret**, even if that source does not contain any enterprise configuration IDs or server URLs. This allows secret-only setups during migration or on machines that only need encrypted API key support.
|
||||
|
||||
### Multiple configurations (recommended)
|
||||
|
||||
@ -226,7 +228,7 @@ You can include encrypted API keys in your configuration plugins for cloud provi
|
||||
In AI Studio, enable the "Show administration settings" toggle in the app settings. Then click the "Generate encryption secret and copy to clipboard" button in the "Enterprise Administration" section. This generates a cryptographically secure 256-bit key and copies it to your clipboard as a base64 string.
|
||||
|
||||
2. **Deploy the encryption secret:**
|
||||
Distribute the secret to all client machines using the same source you use for the enterprise configurations:
|
||||
Distribute the secret to all client machines using any supported enterprise source. The secret can be deployed on its own, even when no enterprise configuration IDs or server URLs are defined on that machine:
|
||||
- Windows Registry / GPO: `HKEY_CURRENT_USER\Software\github\MindWork AI Studio\Enterprise IT\config_encryption_secret`
|
||||
- Policy file: `config_encryption_secret.yaml`
|
||||
- Environment fallback: `MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_ENCRYPTION_SECRET`
|
||||
|
||||
@ -197,7 +197,7 @@ struct EnterpriseSourceData {
|
||||
#[get("/system/enterprise/config/id")]
|
||||
pub fn read_enterprise_env_config_id(_token: APIToken) -> String {
|
||||
debug!("Trying to read the effective enterprise configuration ID.");
|
||||
resolve_effective_enterprise_source()
|
||||
resolve_effective_enterprise_config_source()
|
||||
.configs
|
||||
.into_iter()
|
||||
.next()
|
||||
@ -208,7 +208,7 @@ pub fn read_enterprise_env_config_id(_token: APIToken) -> String {
|
||||
#[get("/system/enterprise/config/server")]
|
||||
pub fn read_enterprise_env_config_server_url(_token: APIToken) -> String {
|
||||
debug!("Trying to read the effective enterprise configuration server URL.");
|
||||
resolve_effective_enterprise_source()
|
||||
resolve_effective_enterprise_config_source()
|
||||
.configs
|
||||
.into_iter()
|
||||
.next()
|
||||
@ -219,21 +219,27 @@ pub fn read_enterprise_env_config_server_url(_token: APIToken) -> String {
|
||||
#[get("/system/enterprise/config/encryption_secret")]
|
||||
pub fn read_enterprise_env_config_encryption_secret(_token: APIToken) -> String {
|
||||
debug!("Trying to read the effective enterprise configuration encryption secret.");
|
||||
resolve_effective_enterprise_source().encryption_secret
|
||||
resolve_effective_enterprise_secret_source().encryption_secret
|
||||
}
|
||||
|
||||
/// Returns all enterprise configurations from the effective source.
|
||||
#[get("/system/enterprise/configs")]
|
||||
pub fn read_enterprise_configs(_token: APIToken) -> Json<Vec<EnterpriseConfig>> {
|
||||
info!("Trying to read the effective enterprise configurations.");
|
||||
Json(resolve_effective_enterprise_source().configs)
|
||||
Json(resolve_effective_enterprise_config_source().configs)
|
||||
}
|
||||
|
||||
fn resolve_effective_enterprise_source() -> EnterpriseSourceData {
|
||||
select_effective_enterprise_source(gather_enterprise_sources())
|
||||
fn resolve_effective_enterprise_config_source() -> EnterpriseSourceData {
|
||||
select_effective_enterprise_config_source(gather_enterprise_sources())
|
||||
}
|
||||
|
||||
fn select_effective_enterprise_source(sources: Vec<EnterpriseSourceData>) -> EnterpriseSourceData {
|
||||
fn resolve_effective_enterprise_secret_source() -> EnterpriseSourceData {
|
||||
select_effective_enterprise_secret_source(gather_enterprise_sources())
|
||||
}
|
||||
|
||||
fn select_effective_enterprise_config_source(
|
||||
sources: Vec<EnterpriseSourceData>,
|
||||
) -> EnterpriseSourceData {
|
||||
for source in sources {
|
||||
if !source.configs.is_empty() {
|
||||
info!("Using enterprise configuration source '{}'.", source.source_name);
|
||||
@ -247,6 +253,22 @@ fn select_effective_enterprise_source(sources: Vec<EnterpriseSourceData>) -> Ent
|
||||
EnterpriseSourceData::default()
|
||||
}
|
||||
|
||||
fn select_effective_enterprise_secret_source(
|
||||
sources: Vec<EnterpriseSourceData>,
|
||||
) -> EnterpriseSourceData {
|
||||
for source in sources {
|
||||
if !source.encryption_secret.is_empty() {
|
||||
info!("Using enterprise encryption-secret source '{}'.", source.source_name);
|
||||
return source;
|
||||
}
|
||||
|
||||
info!("Enterprise encryption-secret source '{}' did not provide a usable secret.", source.source_name);
|
||||
}
|
||||
|
||||
info!("No enterprise source provided an enterprise encryption secret.");
|
||||
EnterpriseSourceData::default()
|
||||
}
|
||||
|
||||
fn gather_enterprise_sources() -> Vec<EnterpriseSourceData> {
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(target_os = "windows")] {
|
||||
@ -624,7 +646,8 @@ fn normalize_enterprise_config_id(value: &str) -> Option<String> {
|
||||
mod tests {
|
||||
use super::{
|
||||
linux_policy_directories_from_xdg, load_policy_values_from_directories,
|
||||
normalize_locale_tag, parse_enterprise_source_values, select_effective_enterprise_source,
|
||||
normalize_locale_tag, parse_enterprise_source_values,
|
||||
select_effective_enterprise_config_source, select_effective_enterprise_secret_source,
|
||||
EnterpriseConfig, EnterpriseSourceData,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
@ -736,8 +759,8 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_effective_enterprise_source_uses_first_source_with_configs_only() {
|
||||
let selected = select_effective_enterprise_source(vec![
|
||||
fn select_effective_enterprise_config_source_uses_first_source_with_configs_only() {
|
||||
let selected = select_effective_enterprise_config_source(vec![
|
||||
EnterpriseSourceData {
|
||||
source_name: String::from("registry"),
|
||||
configs: vec![EnterpriseConfig {
|
||||
@ -761,6 +784,85 @@ mod tests {
|
||||
assert_eq!(selected.configs.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_effective_enterprise_secret_source_allows_secret_only_source() {
|
||||
let selected = select_effective_enterprise_secret_source(vec![
|
||||
EnterpriseSourceData {
|
||||
source_name: String::from("policy files"),
|
||||
configs: Vec::new(),
|
||||
encryption_secret: String::from("POLICY-SECRET"),
|
||||
},
|
||||
EnterpriseSourceData {
|
||||
source_name: String::from("environment"),
|
||||
configs: vec![EnterpriseConfig {
|
||||
id: String::from(TEST_ID_B),
|
||||
server_url: String::from("https://env.example.org"),
|
||||
}],
|
||||
encryption_secret: String::new(),
|
||||
},
|
||||
]);
|
||||
|
||||
assert_eq!(selected.source_name, "policy files");
|
||||
assert_eq!(selected.encryption_secret, "POLICY-SECRET");
|
||||
assert!(selected.configs.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_effective_enterprise_secret_source_falls_back_independently_from_configs() {
|
||||
let selected = select_effective_enterprise_secret_source(vec![
|
||||
EnterpriseSourceData {
|
||||
source_name: String::from("registry"),
|
||||
configs: vec![EnterpriseConfig {
|
||||
id: TEST_ID_A.to_lowercase(),
|
||||
server_url: String::from("https://registry.example.org"),
|
||||
}],
|
||||
encryption_secret: String::new(),
|
||||
},
|
||||
EnterpriseSourceData {
|
||||
source_name: String::from("environment"),
|
||||
configs: Vec::new(),
|
||||
encryption_secret: String::from("ENV-SECRET"),
|
||||
},
|
||||
]);
|
||||
|
||||
assert_eq!(selected.source_name, "environment");
|
||||
assert_eq!(selected.encryption_secret, "ENV-SECRET");
|
||||
assert!(selected.configs.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_effective_enterprise_secret_source_ignores_empty_secrets() {
|
||||
let selected = select_effective_enterprise_secret_source(vec![
|
||||
EnterpriseSourceData {
|
||||
source_name: String::from("policy files"),
|
||||
configs: Vec::new(),
|
||||
encryption_secret: String::new(),
|
||||
},
|
||||
EnterpriseSourceData {
|
||||
source_name: String::from("environment"),
|
||||
configs: Vec::new(),
|
||||
encryption_secret: String::from("VALID-SECRET"),
|
||||
},
|
||||
]);
|
||||
|
||||
assert_eq!(selected.source_name, "environment");
|
||||
assert_eq!(selected.encryption_secret, "VALID-SECRET");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_enterprise_source_values_supports_secret_without_configs() {
|
||||
let mut values = HashMap::new();
|
||||
values.insert(
|
||||
String::from("config_encryption_secret"),
|
||||
String::from(" SECRET-ONLY "),
|
||||
);
|
||||
|
||||
let source = parse_enterprise_source_values("environment variables", &values);
|
||||
|
||||
assert!(source.configs.is_empty());
|
||||
assert_eq!(source.encryption_secret, "SECRET-ONLY");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn linux_policy_directories_from_xdg_preserves_order_and_falls_back() {
|
||||
assert_eq!(
|
||||
@ -869,6 +971,23 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_policy_values_from_directories_supports_secret_only_policy_files() {
|
||||
let directory = tempdir().unwrap();
|
||||
|
||||
fs::write(
|
||||
directory.path().join("config_encryption_secret.yaml"),
|
||||
"config_encryption_secret: \"POLICY-SECRET\"",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let values = load_policy_values_from_directories(&[directory.path().to_path_buf()]);
|
||||
let source = parse_enterprise_source_values("policy files", &values);
|
||||
|
||||
assert!(source.configs.is_empty());
|
||||
assert_eq!(source.encryption_secret, "POLICY-SECRET");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_policy_values_from_directories_ignores_invalid_and_incomplete_files() {
|
||||
let directory = tempdir().unwrap();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user