Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix restoring unencrypted backup in corner case #5600

Merged
merged 7 commits into from
Feb 4, 2025

Conversation

agners
Copy link
Member

@agners agners commented Feb 4, 2025

Proposed change

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.

Type of change

  • Dependency upgrade
  • Bugfix (non-breaking change which fixes an issue)
  • New feature (which adds functionality to the supervisor)
  • Breaking change (fix/feature causing existing functionality to break)
  • Code quality improvements to existing code or addition of tests

Additional information

  • This PR fixes or closes issue: fixes #
  • This PR is related to issue:
  • Link to documentation pull request:
  • Link to cli pull request:
  • Link to client library pull request:

Checklist

  • The code change is tested and works locally.
  • Local tests pass. Your PR cannot be merged unless tests pass
  • There is no commented out code in this PR.
  • I have followed the development checklist
  • The code has been formatted using Ruff (ruff format supervisor tests)
  • Tests have been added to verify that the new code works.

If API endpoints or add-on configuration are added/changed:

Summary by CodeRabbit

  • New Features

    • Streamlined the backup restoration process for enhanced reliability.
    • Improved password handling to clearly distinguish between protected and unprotected backups.
    • Focused restoration on essential components by removing redundant Docker configuration recovery.
  • Tests

    • Introduced comprehensive tests to ensure consistent behavior when restoring both encrypted and unencrypted backups.

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.
@agners agners added the bugfix A bug fix label Feb 4, 2025
Copy link
Contributor

coderabbitai bot commented Feb 4, 2025

Warning

Rate limit exceeded

@agners has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 14 minutes and 25 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 315f667 and 9225c60.

📒 Files selected for processing (1)
  • tests/api/test_backups.py (2 hunks)
📝 Walkthrough

Walkthrough

The changes update the backup restoration process and its associated test coverage. In the backup manager module, the internal password handling method has been renamed from _validate_location_password to _set_location_password, with modified behavior that assigns a None password when a location is not protected. Additionally, the restoration method no longer restores Docker configuration, focusing solely on folders, Home Assistant, and add-ons. A new test has been introduced to verify backup restorations involving both encrypted and unencrypted backups, ensuring the system correctly handles transitions between different encryption states.

Changes

File Change Summary
supervisor/.../manager.py Renamed _validate_location_password to _set_location_password with updated logic to set password to None for unprotected locations; removed Docker config restoration in _do_restore; updated do_restore_full and do_restore_partial to use the new method.
tests/.../test_backups.py Added new test function test_restore_backup_unencrypted_after_encrypted to verify the restoration process with both encrypted and unencrypted backups.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant BackupManager
    participant Backup

    Client->>BackupManager: Initiate do_restore_full/partial(backup, password, location)
    BackupManager->>BackupManager: _set_location_password(backup, password, location)
    alt Location is protected
        BackupManager->>Backup: Validate and set provided password
    else Not Protected
        BackupManager->>Backup: Set password to None
    end
    BackupManager->>Backup: Execute restore process (restore folders, Home Assistant, add-ons)
    BackupManager-->>Client: Return restoration result
Loading

Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR. (Beta)
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai or @coderabbitai title anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
supervisor/backups/manager.py (1)

730-731: Log ignored password for unprotected backups

Currently, the code discards any user-supplied password by unconditionally setting it to None. This is correct for an unprotected backup. However, to improve clarity and user awareness, you may consider logging a warning or debug message if the user provided a password that’s being ignored:

 else:
+        if password:
+            _LOGGER.warning(
+                "Ignoring user-provided password for unprotected backup %s",
+                backup.slug
+            )
         backup.set_password(None)
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 58df655 and e4591b5.

📒 Files selected for processing (1)
  • supervisor/backups/manager.py (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (4)
  • GitHub Check: Build armv7 supervisor
  • GitHub Check: Build armhf supervisor
  • GitHub Check: Build aarch64 supervisor
  • GitHub Check: Run tests Python 3.13.1

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
tests/api/test_backups.py (1)

958-1011: LGTM! Test thoroughly validates the bugfix.

The test effectively validates the fix for restoring unencrypted backups after encrypted ones by:

  1. Testing encrypted backup restore with password
  2. Testing unencrypted backup restore without password
  3. Testing encrypted backup restore with password again

Consider adding assertions to verify the backup's encryption state before each restore to make the test more robust:

     # Restore encrypted backup
     (test_file := coresys.config.path_ssl / "test.txt").touch()
+    assert backup.all_locations[None]["protected"] is True
     resp = await api_client.post(
         f"/backups/{backup.slug}/restore/partial",
         json={"location": None, "password": "test", "folders": ["ssl"]},
     )

     # Restore unencrypted backup
     test_file.touch()
+    assert backup.all_locations[".cloud_backup"]["protected"] is False
     resp = await api_client.post(
         f"/backups/{backup.slug}/restore/partial",
         json={"location": ".cloud_backup", "folders": ["ssl"]},
     )
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e4591b5 and a32a68b.

📒 Files selected for processing (1)
  • tests/api/test_backups.py (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (4)
  • GitHub Check: Build armv7 supervisor
  • GitHub Check: Build armhf supervisor
  • GitHub Check: Build aarch64 supervisor
  • GitHub Check: Run tests Python 3.13.1

@@ -727,6 +727,8 @@ async def _validate_location_password(
raise BackupInvalidError(
f"Invalid password for backup {backup.slug}", _LOGGER.error
)
else:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be a finally? Otherwise we leave the password around in cache if validate_password raises

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should not trust whatever is left around.

This else relates to if backup.all_locations[location][ATTR_PROTECTED]:: This way we set/clear the password on every invocation of _validate_location_password. We probably should rename _validate_location_password to _set_location_password or something, since it does set the password...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh I get it now. The collapsed view just showed the raise and then else, I thought it was a try - else, not an if - else. My bad, looks good 👍

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
supervisor/backups/backup.py (1)

300-327: Consider documenting the Docker configuration precedence.

The consolidate method now gives precedence to the Docker configuration from the newly reloaded backup. While this change is correct and aligns with the PR objectives, it would be helpful to document this behavior in the method's docstring to make the precedence rules clear for future maintainers.

Apply this diff to add documentation:

    def consolidate(self, backup: Self) -> None:
-        """Consolidate two backups with same slug in different locations."""
+        """Consolidate two backups with same slug in different locations.
+        
+        When consolidating backups, the Docker configuration from the newly reloaded
+        backup takes precedence over the existing configuration.
+        """
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a32a68b and 9b47a1c.

📒 Files selected for processing (2)
  • supervisor/backups/backup.py (1 hunks)
  • supervisor/backups/manager.py (5 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • supervisor/backups/manager.py
⏰ Context from checks skipped due to timeout of 90000ms (5)
  • GitHub Check: Build i386 supervisor
  • GitHub Check: Build armv7 supervisor
  • GitHub Check: Build armhf supervisor
  • GitHub Check: Build aarch64 supervisor
  • GitHub Check: Run tests Python 3.13.1
🔇 Additional comments (1)
supervisor/backups/backup.py (1)

325-326: LGTM! Docker configuration update aligns with PR objectives.

The added code ensures that the Docker configuration from the newly reloaded backup is used during consolidation, which helps fix the issue with restoring unencrypted backups after encrypted ones by ensuring the correct Docker configuration is maintained.

f"/backups/{backup.slug}/restore/partial",
json={"location": None, "password": "test", "folders": ["ssl"]},
)
assert resp.status == 200
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this actually exposes another issue: The Docker configuration (registry credentials) are stored as part of the metadata. Since we consolidate metadata, we might use the "wrong" metadata on restore. E.g. if a encrypted and unencrypted backup is consolidated, we might try to decrypt a unencrypted password (which usually fails, and as the reason for the test failure), or use the encrypted password without decrypting it (which actually goes through unnoticed).

I propose to simply drop the Docker configuration feature from the backup for now. We've intend to move the config to a supervisor.tar.gz specific file anyways.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, this likely will lead to a bigger discussion. Let's separate the bugfix for the Docker config in a separate PR. I made this PR to be about the original issue only.

@agners agners force-pushed the fix-restoring-unencrypted-backup branch from a39c085 to 315f667 Compare February 4, 2025 16:30
@agners agners requested a review from mdegat01 February 4, 2025 16:32
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
tests/api/test_backups.py (1)

982-1016: Enhance test coverage with additional assertions.

While the test effectively verifies the basic restore functionality, consider adding assertions to:

  1. Verify the backup's content after each restore
  2. Validate the encryption state remains consistent
  3. Check that the password cache is properly cleared between restores

Example additions:

 assert resp.status == 200
 body = await resp.json()
 assert body["result"] == "ok"
 assert not test_file.is_file()
+# Verify backup content and state
+assert backup.homeassistant is not None
+assert backup.protected is True
+assert backup.supervisor_version is not None
+# Verify password cache is cleared
+assert backup._password is None
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a39c085 and 315f667.

📒 Files selected for processing (1)
  • tests/api/test_backups.py (2 hunks)
🧰 Additional context used
🪛 GitHub Check: Check pylint
tests/api/test_backups.py

[warning] 976-976:
W0511: TODO: There is a bug in the restore code that causes the restore to fail (fixme)

⏰ Context from checks skipped due to timeout of 90000ms (4)
  • GitHub Check: Build armv7 supervisor
  • GitHub Check: Build armhf supervisor
  • GitHub Check: Build aarch64 supervisor
  • GitHub Check: Run tests Python 3.13.1
🔇 Additional comments (2)
tests/api/test_backups.py (2)

958-974: LGTM! Well-structured test setup.

The test setup effectively creates a mixed encryption scenario by copying encrypted and unencrypted backup fixtures to different locations and verifies the initial state.


976-980: Track and address the Docker registry configuration bug.

The workaround using MagicMock allows the test to proceed, but the underlying issue should be tracked and fixed:

  • Bug: Docker registry configuration restore fails when backup locations have mixed encryption states
  • Current workaround: Mock the restore_dockerconfig
  • Proposed solution: Move Docker configuration to a separate supervisor.tar.gz file

Would you like me to:

  1. Create an issue to track this bug?
  2. Help implement the solution to move Docker configuration to a separate file?
🧰 Tools
🪛 GitHub Check: Check pylint

[warning] 976-976:
W0511: TODO: There is a bug in the restore code that causes the restore to fail (fixme)

@agners agners merged commit 9164d35 into main Feb 4, 2025
20 checks passed
@agners agners deleted the fix-restoring-unencrypted-backup branch February 4, 2025 16:53
@github-actions github-actions bot locked and limited conversation to collaborators Feb 6, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants