fix(settings): a plugin's password is sealed at rest, and a blank box keeps it (#5671)
CI / lint (push) Successful in 2s
CI / integration (push) Successful in 1m13s
CI / unit (push) Successful in 1m27s
CI / publish (push) Successful in 1m2s

The UniFi password sat in plugin.unifi in plain text: SECRET_KEYS covers
whole settings, and a plugin's settings are one dict. Saving the plugin's
settings page also wiped it, since the password box is never filled back
in and its blank value was stored.

- A plugin field whose name holds password, secret or token is a
  credential (is_plugin_secret). set_setting seals it inside the dict,
  every read opens it, startup seals any plaintext left from before, and
  the undecryptable-secrets banner names it as plugin.<name>.<field>.
- The settings page uses the same test for its password boxes, and a blank
  box keeps the saved value.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 07:06:31 -04:00
co-authored by Claude Opus 5.5
parent 002181253e
commit eea29123f9
6 changed files with 191 additions and 3 deletions
+7
View File
@@ -15,6 +15,7 @@ from steward.core.settings import (
get_stored_plugin_names, find_orphaned_plugins,
to_smtp_cfg, to_webhook_cfg, to_ansible_cfg, to_plugins_cfg,
to_oidc_cfg, to_ldap_cfg, to_thresholds_cfg, to_access_cfg, DEFAULTS, seal_source_tokens,
is_plugin_secret,
)
settings_bp = Blueprint("settings", __name__, url_prefix="/settings")
@@ -830,6 +831,12 @@ async def save_plugin_detail(name: str):
old_enabled = to_plugins_cfg(old_settings).get(name, {}).get("enabled", False)
plugin_cfg = _build_plugin_cfg_from_form(plugin, form)
# A secret is never sent back to the browser, so its box arrives blank
# unless someone typed a new one: blank keeps the saved value.
old_cfg = to_plugins_cfg(old_settings).get(name, {})
for field, value in list(plugin_cfg.items()):
if is_plugin_secret(field) and value == "" and old_cfg.get(field):
plugin_cfg[field] = old_cfg[field]
async with current_app.db_sessionmaker() as db:
async with db.begin():