diff --git a/src/openhuman/config/ops.rs b/src/openhuman/config/ops.rs index 380b697b0..2bb2b727d 100644 --- a/src/openhuman/config/ops.rs +++ b/src/openhuman/config/ops.rs @@ -418,6 +418,9 @@ pub struct RuntimeFlagsOut { pub log_prompts: bool, } +const BROWSER_ALLOW_ALL_ENV: &str = "OPENHUMAN_BROWSER_ALLOW_ALL"; +const BROWSER_ALLOW_ALL_RPC_ENABLE_ENV: &str = "OPENHUMAN_BROWSER_ALLOW_ALL_RPC_ENABLE"; + /// Returns a full configuration snapshot for the UI. pub async fn get_config_snapshot(config: &Config) -> Result, String> { let snapshot = snapshot_config_json(config)?; @@ -932,13 +935,14 @@ pub async fn workspace_onboarding_flag_resolve( /// Returns the current state of runtime-only flags. pub fn get_runtime_flags() -> RpcOutcome { - RpcOutcome::single_log( - RuntimeFlagsOut { - browser_allow_all: env_flag_enabled("OPENHUMAN_BROWSER_ALLOW_ALL"), - log_prompts: env_flag_enabled("OPENHUMAN_LOG_PROMPTS"), - }, - "runtime flags read", - ) + RpcOutcome::single_log(runtime_flags(), "runtime flags read") +} + +fn runtime_flags() -> RuntimeFlagsOut { + RuntimeFlagsOut { + browser_allow_all: env_flag_enabled(BROWSER_ALLOW_ALL_ENV), + log_prompts: env_flag_enabled("OPENHUMAN_LOG_PROMPTS"), + } } /// Updates the `OPENHUMAN_BROWSER_ALLOW_ALL` environment flag. @@ -951,18 +955,32 @@ pub fn get_runtime_flags() -> RpcOutcome { /// /// `is_private_host` checks still apply to the resolved IP, so this /// flag does not unlock loopback / RFC1918 destinations. -pub fn set_browser_allow_all(enabled: bool) -> RpcOutcome { - let was_enabled = env_flag_enabled("OPENHUMAN_BROWSER_ALLOW_ALL"); - if enabled { - std::env::set_var("OPENHUMAN_BROWSER_ALLOW_ALL", "1"); - } else { - std::env::remove_var("OPENHUMAN_BROWSER_ALLOW_ALL"); +pub fn set_browser_allow_all(enabled: bool) -> Result, String> { + if enabled && !env_flag_enabled(BROWSER_ALLOW_ALL_RPC_ENABLE_ENV) { + tracing::warn!( + "[SECURITY] refused browser allow-all enable via RPC: \ + set {BROWSER_ALLOW_ALL_ENV}=1 at startup or explicitly set \ + {BROWSER_ALLOW_ALL_RPC_ENABLE_ENV}=1 before using the runtime toggle" + ); + return Err(format!( + "Refusing to enable {BROWSER_ALLOW_ALL_ENV} via RPC. Start OpenHuman with \ + {BROWSER_ALLOW_ALL_ENV}=1, or set {BROWSER_ALLOW_ALL_RPC_ENABLE_ENV}=1 for an \ + explicit operator-approved runtime override." + )); } - let now_enabled = env_flag_enabled("OPENHUMAN_BROWSER_ALLOW_ALL"); - let flags = RuntimeFlagsOut { - browser_allow_all: now_enabled, - log_prompts: env_flag_enabled("OPENHUMAN_LOG_PROMPTS"), - }; + + let was_enabled = env_flag_enabled(BROWSER_ALLOW_ALL_ENV); + if enabled { + unsafe { + std::env::set_var(BROWSER_ALLOW_ALL_ENV, "1"); + } + } else { + unsafe { + std::env::remove_var(BROWSER_ALLOW_ALL_ENV); + } + } + let flags = runtime_flags(); + let now_enabled = flags.browser_allow_all; if was_enabled != now_enabled { if now_enabled { @@ -984,7 +1002,7 @@ pub fn set_browser_allow_all(enabled: bool) -> RpcOutcome { } else { "[SECURITY] browser allow-all flag set to disabled" }; - RpcOutcome::single_log(flags, log_msg) + Ok(RpcOutcome::single_log(flags, log_msg)) } /// Checks if a specific onboarding flag file exists in the workspace. diff --git a/src/openhuman/config/ops_tests.rs b/src/openhuman/config/ops_tests.rs index 46f1f93e3..2d1bdc069 100644 --- a/src/openhuman/config/ops_tests.rs +++ b/src/openhuman/config/ops_tests.rs @@ -149,30 +149,47 @@ fn get_runtime_flags_reads_env_overrides() { } #[test] -fn set_browser_allow_all_toggles_env_var() { +fn set_browser_allow_all_rejects_enable_without_operator_override() { let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner()); - let before = std::env::var("OPENHUMAN_BROWSER_ALLOW_ALL").ok(); + let before = std::env::var(BROWSER_ALLOW_ALL_ENV).ok(); + let before_override = std::env::var(BROWSER_ALLOW_ALL_RPC_ENABLE_ENV).ok(); - let _ = set_browser_allow_all(true); - assert!(env_flag_enabled("OPENHUMAN_BROWSER_ALLOW_ALL")); + unsafe { + std::env::remove_var(BROWSER_ALLOW_ALL_ENV); + std::env::remove_var(BROWSER_ALLOW_ALL_RPC_ENABLE_ENV); + } - let _ = set_browser_allow_all(false); - assert!(!env_flag_enabled("OPENHUMAN_BROWSER_ALLOW_ALL")); + let err = set_browser_allow_all(true).expect_err("runtime enable should require override"); + assert!( + err.contains("Refusing to enable OPENHUMAN_BROWSER_ALLOW_ALL via RPC"), + "unexpected error: {err}" + ); + assert!(!env_flag_enabled(BROWSER_ALLOW_ALL_ENV)); unsafe { match before { - Some(v) => std::env::set_var("OPENHUMAN_BROWSER_ALLOW_ALL", v), - None => std::env::remove_var("OPENHUMAN_BROWSER_ALLOW_ALL"), + Some(v) => std::env::set_var(BROWSER_ALLOW_ALL_ENV, v), + None => std::env::remove_var(BROWSER_ALLOW_ALL_ENV), + } + match before_override { + Some(v) => std::env::set_var(BROWSER_ALLOW_ALL_RPC_ENABLE_ENV, v), + None => std::env::remove_var(BROWSER_ALLOW_ALL_RPC_ENABLE_ENV), } } } #[test] -fn set_browser_allow_all_emits_security_audit_log() { +fn set_browser_allow_all_toggles_env_var_when_operator_override_is_set() { let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner()); - let before = std::env::var("OPENHUMAN_BROWSER_ALLOW_ALL").ok(); + let before = std::env::var(BROWSER_ALLOW_ALL_ENV).ok(); + let before_override = std::env::var(BROWSER_ALLOW_ALL_RPC_ENABLE_ENV).ok(); - let enable_outcome = set_browser_allow_all(true); + unsafe { + std::env::remove_var(BROWSER_ALLOW_ALL_ENV); + std::env::set_var(BROWSER_ALLOW_ALL_RPC_ENABLE_ENV, "1"); + } + + let enable_outcome = set_browser_allow_all(true).expect("override should allow runtime enable"); assert_eq!(enable_outcome.logs.len(), 1); let enable_log = &enable_outcome.logs[0]; assert!( @@ -184,8 +201,9 @@ fn set_browser_allow_all_emits_security_audit_log() { "enable log should mention enabled state: {enable_log}" ); assert!(enable_outcome.value.browser_allow_all); + assert!(env_flag_enabled(BROWSER_ALLOW_ALL_ENV)); - let disable_outcome = set_browser_allow_all(false); + let disable_outcome = set_browser_allow_all(false).expect("runtime disable should always work"); assert_eq!(disable_outcome.logs.len(), 1); let disable_log = &disable_outcome.logs[0]; assert!( @@ -197,11 +215,49 @@ fn set_browser_allow_all_emits_security_audit_log() { "disable log should mention disabled state: {disable_log}" ); assert!(!disable_outcome.value.browser_allow_all); + assert!(!env_flag_enabled(BROWSER_ALLOW_ALL_ENV)); unsafe { match before { - Some(v) => std::env::set_var("OPENHUMAN_BROWSER_ALLOW_ALL", v), - None => std::env::remove_var("OPENHUMAN_BROWSER_ALLOW_ALL"), + Some(v) => std::env::set_var(BROWSER_ALLOW_ALL_ENV, v), + None => std::env::remove_var(BROWSER_ALLOW_ALL_ENV), + } + match before_override { + Some(v) => std::env::set_var(BROWSER_ALLOW_ALL_RPC_ENABLE_ENV, v), + None => std::env::remove_var(BROWSER_ALLOW_ALL_RPC_ENABLE_ENV), + } + } +} + +#[test] +fn set_browser_allow_all_disable_does_not_require_operator_override() { + let _g = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner()); + let before = std::env::var(BROWSER_ALLOW_ALL_ENV).ok(); + let before_override = std::env::var(BROWSER_ALLOW_ALL_RPC_ENABLE_ENV).ok(); + + unsafe { + std::env::set_var(BROWSER_ALLOW_ALL_ENV, "1"); + std::env::remove_var(BROWSER_ALLOW_ALL_RPC_ENABLE_ENV); + } + + let disable_outcome = + set_browser_allow_all(false).expect("runtime disable should not require override"); + assert!( + disable_outcome.logs[0].contains("[SECURITY]"), + "disable log should be audit-tagged: {:?}", + disable_outcome.logs + ); + assert!(!disable_outcome.value.browser_allow_all); + assert!(!env_flag_enabled(BROWSER_ALLOW_ALL_ENV)); + + unsafe { + match before { + Some(v) => std::env::set_var(BROWSER_ALLOW_ALL_ENV, v), + None => std::env::remove_var(BROWSER_ALLOW_ALL_ENV), + } + match before_override { + Some(v) => std::env::set_var(BROWSER_ALLOW_ALL_RPC_ENABLE_ENV, v), + None => std::env::remove_var(BROWSER_ALLOW_ALL_RPC_ENABLE_ENV), } } } diff --git a/src/openhuman/config/schemas.rs b/src/openhuman/config/schemas.rs index af1869436..d8aa47085 100644 --- a/src/openhuman/config/schemas.rs +++ b/src/openhuman/config/schemas.rs @@ -591,11 +591,11 @@ pub fn schemas(function: &str) -> ControllerSchema { "set_browser_allow_all" => ControllerSchema { namespace: "config", function: "set_browser_allow_all", - description: "Set OPENHUMAN_BROWSER_ALLOW_ALL runtime flag.", + description: "Disable browser allow-all mode, or enable it only when operator opt-in is present.", inputs: vec![FieldSchema { name: "enabled", ty: TypeSchema::Bool, - comment: "Whether to enable browser allow-all mode.", + comment: "Whether to enable browser allow-all mode. Runtime enable is refused unless OPENHUMAN_BROWSER_ALLOW_ALL_RPC_ENABLE=1.", required: true, }], outputs: vec![FieldSchema { @@ -1067,7 +1067,7 @@ fn handle_resolve_api_url(_params: Map) -> ControllerFuture { fn handle_set_browser_allow_all(params: Map) -> ControllerFuture { Box::pin(async move { let payload = deserialize_params::(params)?; - to_json(config_rpc::set_browser_allow_all(payload.enabled)) + to_json(config_rpc::set_browser_allow_all(payload.enabled)?) }) }