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>
35 lines
1.5 KiB
Python
35 lines
1.5 KiB
Python
"""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
|