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 <noreply@anthropic.com>
This commit is contained in:
Ruben Fiszel
2026-02-17 08:52:31 +01:00
committed by GitHub
parent 69c0900e3b
commit da277483bf
3 changed files with 27 additions and 26 deletions

View File

@@ -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 => {

View File

@@ -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;
}

View File

@@ -550,26 +550,25 @@
const SENSITIVE_UNCHANGED = '__SENSITIVE_AND_UNCHANGED__'
const sensitiveKeys: Set<string> = new Set(
[...Object.values(settings), scimSamlSetting]
const sensitiveKeys: Set<string> = 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<string> = new Set([
'custom_instance_pg_databases',
'ducklake_settings',
'ducklake_user_pg_pwd'
])
const excludedKeys: Set<string> = 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<string, string[]> = {
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 */