diff --git a/supervisor/backups/manager.py b/supervisor/backups/manager.py index e672e5039..4f5d0d812 100644 --- a/supervisor/backups/manager.py +++ b/supervisor/backups/manager.py @@ -708,7 +708,7 @@ class BackupManager(FileConfiguration, JobGroup): _job_override__cleanup=False ) - async def _validate_location_password( + async def _set_location_password( self, backup: Backup, password: str | None = None, @@ -727,6 +727,8 @@ class BackupManager(FileConfiguration, JobGroup): raise BackupInvalidError( f"Invalid password for backup {backup.slug}", _LOGGER.error ) + else: + backup.set_password(None) @Job( name=JOB_FULL_RESTORE, @@ -756,7 +758,7 @@ class BackupManager(FileConfiguration, JobGroup): f"{backup.slug} is only a partial backup!", _LOGGER.error ) - await self._validate_location_password(backup, password, location) + await self._set_location_password(backup, password, location) if backup.supervisor_version > self.sys_supervisor.version: raise BackupInvalidError( @@ -821,7 +823,7 @@ class BackupManager(FileConfiguration, JobGroup): folder_list.remove(FOLDER_HOMEASSISTANT) homeassistant = True - await self._validate_location_password(backup, password, location) + await self._set_location_password(backup, password, location) if backup.homeassistant is None and homeassistant: raise BackupInvalidError( diff --git a/tests/api/test_backups.py b/tests/api/test_backups.py index 8128ad065..bfa24500b 100644 --- a/tests/api/test_backups.py +++ b/tests/api/test_backups.py @@ -4,7 +4,7 @@ import asyncio from pathlib import Path, PurePath from shutil import copy from typing import Any -from unittest.mock import ANY, AsyncMock, PropertyMock, patch +from unittest.mock import ANY, AsyncMock, MagicMock, PropertyMock, patch from aiohttp import MultipartWriter from aiohttp.test_utils import TestClient @@ -955,6 +955,68 @@ async def test_restore_backup_from_location( assert test_file.is_file() +@pytest.mark.usefixtures("tmp_supervisor_data") +async def test_restore_backup_unencrypted_after_encrypted( + api_client: TestClient, + coresys: CoreSys, +): + """Test restoring an unencrypted backup after an encrypted backup and vis-versa.""" + enc_tar = copy(get_fixture_path("test_consolidate.tar"), coresys.config.path_backup) + unc_tar = copy( + get_fixture_path("test_consolidate_unc.tar"), coresys.config.path_core_backup + ) + await coresys.backups.reload() + + backup = coresys.backups.get("d9c48f8b") + assert backup.all_locations == { + None: {"path": Path(enc_tar), "protected": True}, + ".cloud_backup": {"path": Path(unc_tar), "protected": False}, + } + + # pylint: disable=fixme + # TODO: There is a bug in the restore code that causes the restore to fail + # if the backup contains a Docker registry configuration and one location + # is encrypted and the other is not (just like our test fixture). + # We punt the ball on this one for this PR since this is a rare edge case. + backup.restore_dockerconfig = MagicMock() + + coresys.core.state = CoreState.RUNNING + coresys.hardware.disk.get_disk_free_space = lambda x: 5000 + + # Restore encrypted backup + (test_file := coresys.config.path_ssl / "test.txt").touch() + resp = await api_client.post( + f"/backups/{backup.slug}/restore/partial", + json={"location": None, "password": "test", "folders": ["ssl"]}, + ) + assert resp.status == 200 + body = await resp.json() + assert body["result"] == "ok" + assert not test_file.is_file() + + # Restore unencrypted backup + test_file.touch() + resp = await api_client.post( + f"/backups/{backup.slug}/restore/partial", + json={"location": ".cloud_backup", "folders": ["ssl"]}, + ) + assert resp.status == 200 + body = await resp.json() + assert body["result"] == "ok" + assert not test_file.is_file() + + # Restore encrypted backup + test_file.touch() + resp = await api_client.post( + f"/backups/{backup.slug}/restore/partial", + json={"location": None, "password": "test", "folders": ["ssl"]}, + ) + assert resp.status == 200 + body = await resp.json() + assert body["result"] == "ok" + assert not test_file.is_file() + + @pytest.mark.parametrize( ("backup_type", "postbody"), [("partial", {"homeassistant": True}), ("full", {})] )