fix(core): prevent duplicate management mapping during startup

- Preserve hashed management keys written by the v8 core
- Avoid rewriting hashed keys with the GUI plaintext value
- Prevent duplicate top-level management mappings

Fixes #351
This commit is contained in:
lzt404 committed 2026-10-06 13:41:35 +08:00
1 parent 1d64a4535f
commit 317c6df98f
2 files changed
+61 -6

No files matched your search

+24 -6
View File
@@ -621,12 +621,30 @@ pub(crate) fn apply_gui_managed_settings(
&["observability", "logs", "request-log"],
serde_norway::Value::Bool(config.request_log),
)?;
changed |= set_core_yaml_schema_value(
document,
&["remote-management", "secret-key"],
&["management", "secret-key"],
serde_norway::Value::String(config.management_secret_key.clone()),
)?;
// v8 kernels persist this credential as a bcrypt/argon2 hash after a
// successful start. It is already the canonical value the kernel
// expects; replacing it with the GUI plaintext needlessly enters the
// format-preserving YAML editor and can produce a duplicate top-level
// `management` mapping for some valid config layouts.
let stored_management_key = document
.as_mapping()
.and_then(|root| nested_yaml_value(root, &["management", "secret-key"]))
.or_else(|| {
document.as_mapping().and_then(|root| {
nested_yaml_value(root, &["remote-management", "secret-key"])
})
});
if !stored_management_key
.and_then(serde_norway::Value::as_str)
.is_some_and(is_hashed_management_secret_key)
{
changed |= set_core_yaml_schema_value(
document,
&["remote-management", "secret-key"],
&["management", "secret-key"],
serde_norway::Value::String(config.management_secret_key.clone()),
)?;
}
changed |= set_core_yaml_nested_value(
document,
"plugins",
+37
View File
@@ -1556,6 +1556,43 @@ fn v8_core_config_reads_and_writes_canonical_nested_fields() {
}
}
#[test]
fn v8_inline_management_hash_is_preserved_without_duplicate_section() {
let input = "config-version: 8\nserver: {host: 127.0.0.1, port: 8317}\nmanagement: {secret-key: $2a$10$abcdefghijklmnopqrstuuuuuuuuuuuuuuuuuuuuuuuuuuuuu}\naccess: {api-keys: [client-key]}\nrequests: {proxy-url: direct}\n";
let mut config = GuiConfigFile::default();
config.management_secret_key = "plaintext-management-key".into();
let updated = apply_gui_managed_settings(input, &config)
.unwrap_or_else(|error| panic!("management key update failed: {error}"));
let parsed = serde_norway::from_str::<serde_norway::Value>(&updated).unwrap();
assert_eq!(parsed["management"]["secret-key"], "$2a$10$abcdefghijklmnopqrstuuuuuuuuuuuuuuuuuuuuuuuuuuuuu");
}
#[test]
fn v8_multiline_management_hash_with_comments_is_preserved() {
let input = "# generated by CLIProxyAPI\n# keep this comment\nconfig-version: 8\nserver:\n host: 127.0.0.1\n port: 8317\nmanagement:\n secret-key: $2a$10$abcdefghijklmnopqrstuuuuuuuuuuuuuuuuuuuuuuuuuuuuu\n allow-remote: false\naccess:\n api-keys:\n - client-key\nrequests:\n proxy-url: direct\n";
let mut config = GuiConfigFile::default();
config.management_secret_key = "plaintext-management-key".into();
let updated = apply_gui_managed_settings(input, &config)
.unwrap_or_else(|error| panic!("management key update failed: {error}"));
let parsed = serde_norway::from_str::<serde_norway::Value>(&updated).unwrap();
assert_eq!(parsed["management"]["secret-key"], "$2a$10$abcdefghijklmnopqrstuuuuuuuuuuuuuuuuuuuuuuuuuuuuu");
assert_eq!(parsed["management"]["allow-remote"], false);
}
#[test]
fn gui_start_preserves_hashed_v8_management_key() {
let hash = "$2a$10$abcdefghijklmnopqrstuuuuuuuuuuuuuuuuuuuuuuuuuuuuu";
let input = format!(
"config-version: 8\nserver: {{host: 127.0.0.1, port: 8317}}\nmanagement: {{secret-key: {hash}}}\naccess: {{api-keys: [client-key]}}\nrequests: {{proxy-url: direct}}\n"
);
let mut config = GuiConfigFile::default();
config.management_secret_key = "plaintext-management-key".into();
let updated = apply_gui_managed_settings(&input, &config).unwrap();
let parsed = serde_norway::from_str::<serde_norway::Value>(&updated).unwrap();
assert_eq!(parsed["management"]["secret-key"], hash);
assert!(!updated.contains("plaintext-management-key"));
}
#[test]
fn v8_omitted_fields_use_runtime_defaults_instead_of_gui_presets() {
let settings = core_config_settings_from_value(