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:
@@ -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 => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 */
|
||||
|
||||
Reference in New Issue
Block a user