Refuse a key that is not on a line of its own, name the sharers of an include that gets old_key

This commit is contained in:
J. Nick Koston
2026-10-13 09:55:05 -10:00
parent afbd2ab7b1
commit c2fbcf0f24
2 changed files with 37 additions and 11 deletions
+19 -10
View File
@@ -41,14 +41,22 @@ class KeyEdit(LineEdit):
def _key_blocks(raw: ConfigType, key: str) -> list[ConfigType]:
"""The api and esphome ota ``encryption:`` mappings whose key is ``key``."""
blocks = [raw.get(CONF_API) or {}, *esphome_ota_items(raw)]
# A bare ota block gets the api key at validation; it has no key line
return [
block[CONF_ENCRYPTION]
for block in blocks
if (node := static_encryption_key(block)) is not None
and str(node) == key
and source_of(block[CONF_ENCRYPTION], CONF_KEY) is not None
]
found = []
for block in blocks:
if (node := static_encryption_key(block)) is None or str(node) != key:
continue
encryption = block[CONF_ENCRYPTION]
if source_of(encryption, CONF_KEY) is not None:
found.append(encryption)
elif hasattr(next(k for k in encryption if k == CONF_KEY), "esp_range"):
# Read from a file but not on a line of its own, as with a merge
# key; only a key validation added, which has no range at all,
# is passed over (a bare ota block takes the api key)
raise EsphomeError(
f"The '{CONF_KEY}' of an '{CONF_ENCRYPTION}:' block is not on a "
"line of its own; edit the key by hand"
)
return found
_INDENT_RE = re.compile(r"\s*")
@@ -165,8 +173,8 @@ def with_sharers(edits: list[KeyEdit]) -> list[str]:
f"{path} includes {match['target']} through a substitution"
)
for edit in edits:
if edit.insert_after:
continue
if edit.insert_after and edit.secret:
continue # a new secrets line has no users yet
ref = (
_secret_use_re(edit.secret) if edit.secret else _file_use_re(edit.path.name)
)
@@ -223,6 +231,7 @@ def _secret_insert(secrets_path: Path, after: str, name: str, value: str) -> Key
m.string,
f'{_indent(m.string)}{name}: "{value}"',
insert_after=True,
secret=name,
)
+18 -1
View File
@@ -1074,10 +1074,27 @@ def test_a_key_merged_from_an_anchor_is_refused(tmp_path: Path) -> None:
f'named: &named\n key: "{OLD_KEY}"\n\napi:\n encryption:\n <<: *named\n\n'
"ota:\n - platform: esphome\n encryption:\n",
)
with pytest.raises(EsphomeError, match="was not found on a line of the yaml"):
with pytest.raises(EsphomeError, match="not on a line of its own"):
locate_key_edits(OLD_KEY, NEW_KEY)
def test_old_key_inserted_into_a_shared_include_names_the_sharers(
tmp_path: Path,
) -> None:
"""The ota block lives in an include another device loads too, so the
inserted old_key reaches that device at its next install."""
(tmp_path / "ota.yaml").write_bytes(
f'platform: esphome\nencryption:\n key: "{OLD_KEY}"\n'.encode()
)
(tmp_path / "other.yaml").write_bytes(b"ota:\n - !include ota.yaml\n")
_setup(tmp_path, "esphome:\n name: test\n\nota:\n - !include ota.yaml\n")
edits = _edits()
assert [(e.insert_after, [p.name for p in e.shared_with]) for e in edits] == [
(False, ["other.yaml"]),
(True, ["other.yaml"]),
]
def test_key_overridden_on_top_of_a_package_block(tmp_path: Path) -> None:
"""The device yaml's line wins the merge and is the one rewritten."""
from esphome.config_helpers import merge_config