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

Check password early on backup restore #5519

Merged
merged 1 commit into from
Dec 31, 2024
Merged

Conversation

agners
Copy link
Member

@agners agners commented Dec 31, 2024

Proposed change

Introduce a validate password method which only peaks into the archive to validate the password before starting the actual restore process. This makes sure that a wrong password returns an error even when restoring the backup in background.

This implements what has been added in Core with home-assistant/core#133647.

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

    • Added an asynchronous password validation method for backups.
    • Enhanced backup password management functionality.
  • Bug Fixes

    • Improved error handling for backup password validation.
    • Refined backup restoration process with more precise password checks.
  • Refactor

    • Updated password validation mechanism in backup and restore processes.
    • Modified test cases to align with new password validation approach.

@agners agners added the new-feature A new feature label Dec 31, 2024
Copy link

coderabbitai bot commented Dec 31, 2024

📝 Walkthrough

Walkthrough

The changes focus on enhancing the password validation mechanism for backups in the Home Assistant Supervisor. The Backup class in backup.py now includes a new asynchronous validate_password method and modifies the set_password method to handle password setting more flexibly. Corresponding updates in the backup manager and test files adjust the password validation process, separating password setting from validation and improving error handling for backup restoration scenarios.

Changes

File Changes
supervisor/backups/backup.py - Updated set_password method signature to accept `str
supervisor/backups/manager.py - Refined password validation in do_restore_full and do_restore_partial methods
- Separated password setting and validation steps
- Improved error handling for invalid passwords
tests/api/test_backups.py - Modified mocking of password validation method from set_password to validate_password
tests/backups/test_manager.py - Updated test cases to use validate_password instead of set_password for password validation

Sequence Diagram

sequenceDiagram
    participant Backup
    participant BackupManager
    participant BackupFile

    Backup->>BackupFile: set_password(password)
    Backup->>BackupFile: validate_password()
    alt Password Valid
        BackupFile-->>Backup: Return True
        Backup-->>BackupManager: Proceed with restore
    else Password Invalid
        BackupFile-->>Backup: Return False
        Backup-->>BackupManager: Raise BackupInvalidError
    end
Loading

The sequence diagram illustrates the new password validation flow, showing how the Backup class now separates password setting and validation, with explicit error handling when an invalid password is detected.

Tip

CodeRabbit's docstrings feature is now available as part of our Early Access Program! Simply use the command @coderabbitai generate docstrings to have CodeRabbit automatically generate docstrings for your pull request. We would love to hear your feedback on Discord.


📜 Recent review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 5e4c7a7 and 56df84a.

📒 Files selected for processing (4)
  • supervisor/backups/backup.py (2 hunks)
  • supervisor/backups/manager.py (2 hunks)
  • tests/api/test_backups.py (1 hunks)
  • tests/backups/test_manager.py (9 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • tests/api/test_backups.py
  • supervisor/backups/manager.py
  • tests/backups/test_manager.py
🔇 Additional comments (2)
supervisor/backups/backup.py (2)

299-305: LGTM! Clean separation of password setting from validation.

The refactored set_password method now has a single responsibility of managing the password state, which aligns well with the SOLID principles.


338-378: Consider adding timeout protection for password validation.

The password validation implementation looks solid with proper error handling and logging. However, since this involves file I/O operations, consider adding a timeout to prevent hanging in case of slow disk operations.

 async def validate_password(self) -> bool:
     """Validate backup password.

     Returns false only when the password is known to be wrong.
     """
-    return await self.sys_run_in_executor(_validate_file)
+    try:
+        return await asyncio.wait_for(
+            self.sys_run_in_executor(_validate_file),
+            timeout=30  # 30 seconds timeout
+        )
+    except asyncio.TimeoutError:
+        _LOGGER.warning("Password validation timed out")
+        return True  # Default to allowing restore attempt on timeout

Let's verify the usage of this new method:


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

@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: 1

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

299-305: Clarify the return type in the docstring or type hints.
Although the signature was changed from returning a bool to returning None, consider updating documentation to reflect that it always returns None to avoid confusion for callers.

 def set_password(self, password: str | None) -> None:
-    """Set the password for an existing backup."""
+    """Set the password for an existing backup (returns 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 5783834 and e48f467.

📒 Files selected for processing (4)
  • supervisor/backups/backup.py (2 hunks)
  • supervisor/backups/manager.py (2 hunks)
  • tests/api/test_backups.py (1 hunks)
  • tests/backups/test_manager.py (2 hunks)
🧰 Additional context used
🪛 Ruff (0.8.2)
supervisor/backups/backup.py

339-339: First line should end with a period

Add period

(D400)


339-339: First line should end with a period, question mark, or exclamation point

Add closing punctuation

(D415)

🪛 GitHub Check: Check ruff
supervisor/backups/backup.py

[failure] 339-339: Ruff (D400)
supervisor/backups/backup.py:339:9: D400 First line should end with a period


[failure] 339-339: Ruff (D415)
supervisor/backups/backup.py:339:9: D415 First line should end with a period, question mark, or exclamation point

🪛 GitHub Check: Check pylint
supervisor/backups/backup.py

[warning] 363-363:
W0718: Catching too general exception Exception (broad-exception-caught)


[warning] 340-340:
R1710: Either all return statements in a function should return an expression, or none of them should. (inconsistent-return-statements)

🔇 Additional comments (5)
tests/api/test_backups.py (1)

481-481: Good coverage for negative password validation scenario.
Ensuring that the test checks for validate_password returning False validates the error-handling logic.

supervisor/backups/manager.py (2)

690-695: Good use of validate_password in full-restore logic.
This shift to a dedicated password-validation method is clear and improves separation of concerns.


760-765: Proactive password validation for partial restore flow.
Well-integrated approach consistently ensures that all partial restore scenarios are checked before proceeding.

tests/backups/test_manager.py (2)

333-333: Ensure that validate_password is tested thoroughly for negative scenarios.
Transitioning from set_password to validate_password is correct. Confirm that all call sites expecting a boolean now await the new method’s return.


362-362: Consistent negative test logic for partial RESTORE.
Changing from set_password to validate_password keeps the test aligned with the new method signature.

supervisor/backups/backup.py Outdated Show resolved Hide resolved
@agners agners force-pushed the check-backup-password-early branch from e48f467 to e1b3aff Compare December 31, 2024 11:32
Copy link

@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 (2)
supervisor/backups/backup.py (2)

338-342: Enhance docstring with more details.

The docstring should include:

  1. Information about the validation process
  2. Return value explanation
  3. Possible exceptions that could be raised
-    """Validate backup password.
-
-    Returns false only when the password is known to be wrong.
-    """
+    """Validate backup password by attempting to read the archive.
+
+    The method attempts to read the backup archive using the provided password.
+    Returns false only when the password is known to be wrong, true if the
+    password is correct or validation cannot be performed.
+
+    Returns:
+        bool: False if password is incorrect, True otherwise
+    """

374-376: Consider handling specific exceptions.

The broad exception catch could mask important issues. Consider catching specific exceptions that might occur during tar file operations.

-                except Exception:  # noqa: BLE001
+                except (tarfile.HeaderError, OSError) as err:
                     _LOGGER.exception("Unexpected error validating password")
                     return True
🧰 Tools
🪛 GitHub Check: Check pylint

[warning] 374-374:
W0718: Catching too general exception Exception (broad-exception-caught)

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between e48f467 and e1b3aff.

📒 Files selected for processing (4)
  • supervisor/backups/backup.py (2 hunks)
  • supervisor/backups/manager.py (2 hunks)
  • tests/api/test_backups.py (1 hunks)
  • tests/backups/test_manager.py (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/api/test_backups.py
  • supervisor/backups/manager.py
🧰 Additional context used
🪛 GitHub Check: Check pylint
supervisor/backups/backup.py

[warning] 374-374:
W0718: Catching too general exception Exception (broad-exception-caught)

🔇 Additional comments (4)
supervisor/backups/backup.py (2)

299-305: LGTM! Good separation of concerns.

The changes to set_password improve the design by:

  1. Separating password setting from validation
  2. Adding ability to clear password by passing None
  3. Removing boolean return value since the method only sets state

344-377: LGTM! Robust error handling implementation.

The implementation:

  1. Properly handles cases where validation cannot be performed
  2. Uses appropriate logging levels for different scenarios
  3. Follows defensive programming practices
🧰 Tools
🪛 GitHub Check: Check pylint

[warning] 374-374:
W0718: Catching too general exception Exception (broad-exception-caught)

tests/backups/test_manager.py (2)

333-333: LGTM! Test updated to match new validation logic.

The test correctly verifies that backup restoration fails when password validation returns false.


362-362: LGTM! Consistent test coverage for partial backups.

The test maintains parity with full backup testing, ensuring password validation works the same way for partial backups.

@agners agners force-pushed the check-backup-password-early branch from e1b3aff to 5e4c7a7 Compare December 31, 2024 11:36
Copy link

@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 (2)
supervisor/backups/backup.py (2)

339-342: Enhance docstring clarity.

The docstring should clarify that it returns True in case of successful validation or when validation cannot be performed (e.g., unexpected errors).

-    """Validate backup password.
-
-    Returns false only when the password is known to be wrong.
-    """
+    """Validate backup password.
+
+    Returns:
+        bool: False only when the password is known to be wrong.
+             True if password is valid or validation cannot be performed.
+    """

338-378: Add type hints for better code maintainability.

The method would benefit from explicit type hints for the internal function.

     async def validate_password(self) -> bool:
-        def _validate_file() -> bool:
+        def _validate_file() -> bool:
             ending: str = f".tar{'.gz' if self.compressed else ''}"
             test_tar_name: str | None
             test_tar_file: IO[bytes] | 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 e1b3aff and 5e4c7a7.

📒 Files selected for processing (4)
  • supervisor/backups/backup.py (2 hunks)
  • supervisor/backups/manager.py (2 hunks)
  • tests/api/test_backups.py (1 hunks)
  • tests/backups/test_manager.py (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • tests/backups/test_manager.py
  • tests/api/test_backups.py
  • supervisor/backups/manager.py
🔇 Additional comments (2)
supervisor/backups/backup.py (2)

299-305: LGTM! Clean implementation of password handling.

The changes to set_password improve flexibility by allowing password clearing while maintaining clean implementation.


344-377: 🛠️ Refactor suggestion

Add explicit return path and improve error handling.

  1. The _validate_file function needs an explicit return path for the case when no tar file is found and no exceptions occur.
  2. The broad exception catch could mask important issues.
     def _validate_file() -> bool:
         ending = f".tar{'.gz' if self.compressed else ''}"
 
         with tarfile.open(self.tarfile, "r:") as backup:
             test_tar_name = next(
                 (
                     entry.name
                     for entry in backup.getmembers()
                     if entry.name.endswith(ending)
                 ),
                 None,
             )
             if not test_tar_name:
                 _LOGGER.warning("No tar file found to validate password with")
                 return True
 
             test_tar_file = backup.extractfile(test_tar_name)
             try:
                 with SecureTarFile(
                     ending,  # Not used
                     gzip=self.compressed,
                     key=self._key,
                     mode="r",
                     fileobj=test_tar_file,
                 ):
                     # If we can read the tar file, the password is correct
                     return True
             except tarfile.ReadError:
                 _LOGGER.debug("Invalid password")
                 return False
-            except Exception:  # pylint: disable=broad-exception-caught
-                _LOGGER.exception("Unexpected error validating password")
-                return True
+            except (OSError, IOError) as err:
+                _LOGGER.exception("IO error validating password: %s", err)
+                return True
+            except Exception as err:  # pylint: disable=broad-exception-caught
+                _LOGGER.exception("Unexpected error validating password: %s", err)
+                return True

Likely invalid or redundant comment.

Introduce a validate password method which only peaks into the archive
to validate the password before starting the actual restore process.
This makes sure that a wrong password returns an error even when
restoring the backup in background.
@agners agners force-pushed the check-backup-password-early branch from 5e4c7a7 to 56df84a Compare December 31, 2024 12:15
@agners agners merged commit c2f6e31 into main Dec 31, 2024
20 checks passed
@agners agners deleted the check-backup-password-early branch December 31, 2024 12:58
@github-actions github-actions bot locked and limited conversation to collaborators Jan 2, 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