Skip to content

Commit 9164d35

Browse files
authored
Fix restoring unencrypted backup in corner case (#5600)
* Fix restoring unencrypted backup in corner case If a backup has a encrypted and unencrypted location, and the encrypted location is beeing restored first, the encryption key is still cached. When the user restores the unencrypted backup next, it will fail because the Supervisor tries to use encryption key still. * Add integration test for restoring backups with and without encryption * Rename _validate_location_password to _set_location_password * Reload backup metadata from restore location * Revert "Reload backup metadata from restore location" This reverts commit 9b47a1c. * Make pytest work/punt the ball on docker config restore issue * Address pylint error
1 parent 58df655 commit 9164d35

File tree

2 files changed

+68
-4
lines changed

2 files changed

+68
-4
lines changed

supervisor/backups/manager.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -708,7 +708,7 @@ async def _do_restore(
708708
_job_override__cleanup=False
709709
)
710710

711-
async def _validate_location_password(
711+
async def _set_location_password(
712712
self,
713713
backup: Backup,
714714
password: str | None = None,
@@ -727,6 +727,8 @@ async def _validate_location_password(
727727
raise BackupInvalidError(
728728
f"Invalid password for backup {backup.slug}", _LOGGER.error
729729
)
730+
else:
731+
backup.set_password(None)
730732

731733
@Job(
732734
name=JOB_FULL_RESTORE,
@@ -756,7 +758,7 @@ async def do_restore_full(
756758
f"{backup.slug} is only a partial backup!", _LOGGER.error
757759
)
758760

759-
await self._validate_location_password(backup, password, location)
761+
await self._set_location_password(backup, password, location)
760762

761763
if backup.supervisor_version > self.sys_supervisor.version:
762764
raise BackupInvalidError(
@@ -821,7 +823,7 @@ async def do_restore_partial(
821823
folder_list.remove(FOLDER_HOMEASSISTANT)
822824
homeassistant = True
823825

824-
await self._validate_location_password(backup, password, location)
826+
await self._set_location_password(backup, password, location)
825827

826828
if backup.homeassistant is None and homeassistant:
827829
raise BackupInvalidError(

tests/api/test_backups.py

+63-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from pathlib import Path, PurePath
55
from shutil import copy
66
from typing import Any
7-
from unittest.mock import ANY, AsyncMock, PropertyMock, patch
7+
from unittest.mock import ANY, AsyncMock, MagicMock, PropertyMock, patch
88

99
from aiohttp import MultipartWriter
1010
from aiohttp.test_utils import TestClient
@@ -955,6 +955,68 @@ async def test_restore_backup_from_location(
955955
assert test_file.is_file()
956956

957957

958+
@pytest.mark.usefixtures("tmp_supervisor_data")
959+
async def test_restore_backup_unencrypted_after_encrypted(
960+
api_client: TestClient,
961+
coresys: CoreSys,
962+
):
963+
"""Test restoring an unencrypted backup after an encrypted backup and vis-versa."""
964+
enc_tar = copy(get_fixture_path("test_consolidate.tar"), coresys.config.path_backup)
965+
unc_tar = copy(
966+
get_fixture_path("test_consolidate_unc.tar"), coresys.config.path_core_backup
967+
)
968+
await coresys.backups.reload()
969+
970+
backup = coresys.backups.get("d9c48f8b")
971+
assert backup.all_locations == {
972+
None: {"path": Path(enc_tar), "protected": True},
973+
".cloud_backup": {"path": Path(unc_tar), "protected": False},
974+
}
975+
976+
# pylint: disable=fixme
977+
# TODO: There is a bug in the restore code that causes the restore to fail
978+
# if the backup contains a Docker registry configuration and one location
979+
# is encrypted and the other is not (just like our test fixture).
980+
# We punt the ball on this one for this PR since this is a rare edge case.
981+
backup.restore_dockerconfig = MagicMock()
982+
983+
coresys.core.state = CoreState.RUNNING
984+
coresys.hardware.disk.get_disk_free_space = lambda x: 5000
985+
986+
# Restore encrypted backup
987+
(test_file := coresys.config.path_ssl / "test.txt").touch()
988+
resp = await api_client.post(
989+
f"/backups/{backup.slug}/restore/partial",
990+
json={"location": None, "password": "test", "folders": ["ssl"]},
991+
)
992+
assert resp.status == 200
993+
body = await resp.json()
994+
assert body["result"] == "ok"
995+
assert not test_file.is_file()
996+
997+
# Restore unencrypted backup
998+
test_file.touch()
999+
resp = await api_client.post(
1000+
f"/backups/{backup.slug}/restore/partial",
1001+
json={"location": ".cloud_backup", "folders": ["ssl"]},
1002+
)
1003+
assert resp.status == 200
1004+
body = await resp.json()
1005+
assert body["result"] == "ok"
1006+
assert not test_file.is_file()
1007+
1008+
# Restore encrypted backup
1009+
test_file.touch()
1010+
resp = await api_client.post(
1011+
f"/backups/{backup.slug}/restore/partial",
1012+
json={"location": None, "password": "test", "folders": ["ssl"]},
1013+
)
1014+
assert resp.status == 200
1015+
body = await resp.json()
1016+
assert body["result"] == "ok"
1017+
assert not test_file.is_file()
1018+
1019+
9581020
@pytest.mark.parametrize(
9591021
("backup_type", "postbody"), [("partial", {"homeassistant": True}), ("full", {})]
9601022
)

0 commit comments

Comments
 (0)