fix(settings): a plugin's password is sealed at rest, and a blank box keeps it (#5671)
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:
@@ -0,0 +1,34 @@
|
||||
"""A plugin's credentials are sealed inside its settings dict (#5671): the
|
||||
UniFi password was stored in plain text, so a backup would have carried it."""
|
||||
from steward.core.crypto import init_crypto, is_encrypted
|
||||
from steward.core.settings import _decode, is_plugin_secret, seal_plugin_secrets
|
||||
|
||||
|
||||
def setup_function():
|
||||
init_crypto("unit-test-secret-key")
|
||||
|
||||
|
||||
def test_which_fields_are_credentials():
|
||||
assert is_plugin_secret("password") and is_plugin_secret("client_secret")
|
||||
assert is_plugin_secret("api_token")
|
||||
assert not is_plugin_secret("username") and not is_plugin_secret("host")
|
||||
|
||||
|
||||
def test_sealing_touches_only_plaintext_secrets_and_reading_opens_them():
|
||||
cfg = {"enabled": True, "host": "https://10.0.0.1", "username": "admin",
|
||||
"password": "hunter2", "poll_interval_seconds": 60}
|
||||
sealed, n = seal_plugin_secrets(cfg)
|
||||
assert n == 1 and is_encrypted(sealed["password"])
|
||||
assert {k: v for k, v in sealed.items() if k != "password"} == {
|
||||
k: v for k, v in cfg.items() if k != "password"}
|
||||
again, n2 = seal_plugin_secrets(sealed) # idempotent
|
||||
assert n2 == 0 and again == sealed
|
||||
assert _decode(sealed, "plugin.unifi") == cfg
|
||||
assert seal_plugin_secrets({"password": ""}) == ({"password": ""}, 0)
|
||||
assert seal_plugin_secrets("not a dict") == ("not a dict", 0)
|
||||
|
||||
|
||||
def test_only_plugin_settings_are_opened():
|
||||
# A dict under a non-plugin key is left alone: only plugin.* is opened.
|
||||
sealed, _ = seal_plugin_secrets({"password": "x"})
|
||||
assert _decode(sealed, "thresholds.cpu") == sealed
|
||||
Reference in New Issue
Block a user