From da277483bf993303c99d36286c1cfdc1a6fb6e2f Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Tue, 17 Feb 2026 08:52:31 +0100 Subject: [PATCH] feat: show all settings in YAML UI and protect from empty overwrites (#7976) - Show custom_instance_pg_databases, ducklake_settings, ducklake_user_pg_pwd and rsa_keys in frontend YAML editor (remove from excludedKeys) - Redact sensitive values: add ducklake_user_pg_pwd and rsa_keys to sensitiveKeys, add custom_instance_pg_databases.user_pwd to nestedSensitiveFields - Remove rsa_keys from HIDDEN_SETTINGS so it appears in YAML export - Hide automate_username_creation from export (add to HIDDEN_SETTINGS) - Add ducklake_user_pg_pwd and rsa_keys to SENSITIVE_SETTINGS for log redaction - Generalize empty/null protection for all PROTECTED_SETTINGS: operator diff skips empty values when DB has existing data, direct API rejects delete/empty for protected settings Co-authored-by: Claude Opus 4.6 --- backend/windmill-api-settings/src/lib.rs | 23 ++++++++----------- .../windmill-common/src/instance_config.rs | 11 ++++++--- .../lib/components/InstanceSettings.svelte | 19 ++++++++------- 3 files changed, 27 insertions(+), 26 deletions(-) diff --git a/backend/windmill-api-settings/src/lib.rs b/backend/windmill-api-settings/src/lib.rs index 3f46ee94b2..a36ca3a467 100644 --- a/backend/windmill-api-settings/src/lib.rs +++ b/backend/windmill-api-settings/src/lib.rs @@ -286,24 +286,21 @@ pub async fn set_global_setting_internal( run_setting_pre_write_hook(db, &key, &value).await?; - if key == "jwt_secret" { - match &value { - serde_json::Value::Null | serde_json::Value::String(_) - if value.as_str().map_or(true, |s| s.is_empty()) => - { - return Err(error::Error::BadRequest( - "jwt_secret cannot be set to empty or null".to_string(), - )); - } - _ => {} - } - } - match value { serde_json::Value::Null => { + if instance_config::PROTECTED_SETTINGS.contains(&key.as_str()) { + return Err(error::Error::BadRequest( + format!("{key} is a protected setting and cannot be deleted"), + )); + } delete_global_setting(db, &key).await?; } serde_json::Value::String(x) if x.is_empty() => { + if instance_config::PROTECTED_SETTINGS.contains(&key.as_str()) { + return Err(error::Error::BadRequest( + format!("{key} is a protected setting and cannot be set to empty"), + )); + } delete_global_setting(db, &key).await?; } v => { diff --git a/backend/windmill-common/src/instance_config.rs b/backend/windmill-common/src/instance_config.rs index 0e2fe5ad72..3d0384c8f2 100644 --- a/backend/windmill-common/src/instance_config.rs +++ b/backend/windmill-common/src/instance_config.rs @@ -766,14 +766,16 @@ pub const PROTECTED_SETTINGS: &[&str] = &[ /// Note: jwt_secret is intentionally NOT hidden — it is included in YAML exports so that /// operators can set it via ConfigMap. It is protected from deletion (PROTECTED_SETTINGS) /// and from being set to empty/null, and its value is partially redacted in log output. -pub const HIDDEN_SETTINGS: &[&str] = &["uid", "rsa_keys", "min_keep_alive_version"]; +pub const HIDDEN_SETTINGS: &[&str] = &["uid", "min_keep_alive_version", "automate_username_creation"]; /// Top-level settings whose entire value is sensitive and must be fully redacted in logs. const SENSITIVE_SETTINGS: &[&str] = &[ "jwt_secret", + "rsa_keys", "scim_token", "hub_api_secret", "license_key", + "ducklake_user_pg_pwd", "pip_index_url", "pip_extra_index_url", "npm_config_registry", @@ -930,9 +932,12 @@ pub fn diff_global_settings( let mut previous_values = BTreeMap::new(); let mut unchanged_count: usize = 0; for (key, desired_value) in desired { - if key == "jwt_secret" && is_empty_or_null(desired_value) { + if PROTECTED_SETTINGS.contains(&key.as_str()) + && is_empty_or_null(desired_value) + && current.contains_key(key) + { tracing::warn!( - "Skipping jwt_secret update: value must not be empty or null" + "Skipping {key} update: protected setting cannot be overwritten with empty/null value" ); continue; } diff --git a/frontend/src/lib/components/InstanceSettings.svelte b/frontend/src/lib/components/InstanceSettings.svelte index 8c8d3c6f28..da0539d8a9 100644 --- a/frontend/src/lib/components/InstanceSettings.svelte +++ b/frontend/src/lib/components/InstanceSettings.svelte @@ -550,26 +550,25 @@ const SENSITIVE_UNCHANGED = '__SENSITIVE_AND_UNCHANGED__' - const sensitiveKeys: Set = new Set( - [...Object.values(settings), scimSamlSetting] + const sensitiveKeys: Set = new Set([ + ...[...Object.values(settings), scimSamlSetting] .flatMap((s) => Object.values(s)) .filter((s) => s.fieldType === 'password' || s.fieldType === 'license_key') - .map((s) => s.key) - ) + .map((s) => s.key), + 'ducklake_user_pg_pwd', + 'rsa_keys' + ]) // Settings that should never appear in YAML export/import - const excludedKeys: Set = new Set([ - 'custom_instance_pg_databases', - 'ducklake_settings', - 'ducklake_user_pg_pwd' - ]) + const excludedKeys: Set = new Set([]) // Nested fields inside object-valued settings that contain secrets. // Each entry maps a top-level key to its sensitive sub-field names. const nestedSensitiveFields: Record = { smtp_settings: ['smtp_password'], secret_backend: ['token'], - object_store_cache_config: ['secret_key', 'serviceAccountKey'] + object_store_cache_config: ['secret_key', 'serviceAccountKey'], + custom_instance_pg_databases: ['user_pwd'] } /** Returns SENSITIVE_UNCHANGED if the value is non-empty and matches the initial */