Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,14 +59,24 @@ For example this repository will have the implicit id `Documents`:
Passwords of the repositories can be provided in the following ways:
- During runtime in the terminal<br>
If no passwords are provided in the playbook, the program will ask for them.
If the `--no-interaction` switch is active, the program will fail if it needs to ask for the password.
- Plain text in the playbook<br>
For this add the `password` field to your repository object with the password:
`"password": "my_plaintext_password"`
- Via environment variables<br>
For this add the `password` field to your repository object that
defines the name of the environment variable that stores the password:
`"password": "env:MY_RESTIC_PASSWORD_ENV_VAR"`
- Via the prompt credential provider<br>
For this add the `password` field to your repository object that
defines the name of the credential that shall be used for the repository:
`"password": "prompt:my_credential"`
The program will ask for the password during runtime in the terminal and then store it
for the duration of the playbook execution. If another repository references the same credential,
the program will use the stored password and not ask for it again.
This method is useful if you have multiple repositories that have the same password.
You can define multiple unique credentials if not all repositories use the same password.

*Note: If the `--no-interaction` switch is active, the program will fail if it needs to ask for a password.*

The program will pass the passwords to the restic backend via temporarily setting environment variables:
- `RESTIC_PASSWORD` — password for the target repository
Expand Down
24 changes: 24 additions & 0 deletions source/backup_automation/prompt_credential_provider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import getpass


class PromptCredentialProvider:
"""
Credential provider that asks the user for credentials if they are not yet stored.
"""
# pylint: disable=too-few-public-methods
# There are no more methods needed for this class at the moment.
def __init__(self) -> None:
self.__credentials: dict[str, str] = {}

def get_credential(self, credential_name: str) -> str:
"""
Returns the credential if exists, asks the user for the password via getpass.
The credential entered will be stored and returned in the future without another prompt.
"""
if credential_name in self.__credentials:
return self.__credentials[credential_name]

credential = getpass.getpass(f"Enter credentials for \"{credential_name}\": ")
self.__credentials[credential_name] = credential

return credential
1 change: 1 addition & 0 deletions source/backup_automation/restic/restic_playbook_format.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class ResticPlaybookFormat(PlaybookFormat):
REPOSITORIES_URI_KEY = "uri"
REPOSITORIES_PASSWORD_KEY = "password"
REPOSITORIES_PASSWORD_VALUE_ENV_PREFIX = "env:"
REPOSITORIES_PASSWORD_VALUE_PROMPT_PREFIX = "prompt:"
STEPS_KEY = "steps"
STEPS_COMMAND_KEY = "command"
STEPS_COMMAND_VALUE_BACKUP = "backup"
Expand Down
27 changes: 20 additions & 7 deletions source/backup_automation/restic/restic_playbook_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from backup_automation.playbook import Playbook
from backup_automation.playbook_parser import PlaybookParser, PlaybookParserSettings
from backup_automation.prompt_credential_provider import PromptCredentialProvider
from backup_automation.restic.restic_backend import ResticBackend
from backup_automation.restic.restic_playbook import ResticPlaybook
from backup_automation.restic.restic_playbook_exception import ResticPlaybookException
Expand Down Expand Up @@ -31,6 +32,7 @@ def __init__(self,
self.__repositories: dict[str, ResticRepository] = {}
self.__steps: list[ResticPlaybookStep] = []
self.__format = ResticPlaybookFormat()
self.__prompt_credential_provider = PromptCredentialProvider()

def parse(self, playbook_path: pathlib.Path) -> Playbook:
"""
Expand Down Expand Up @@ -115,19 +117,30 @@ def __parse_repositories_json(self, repositories_json: JsonList) -> None:
self.__repositories[repository_id] = repository

def __resolve_repository_password(self, repository_id: str, password_value: str | None) -> str:
# No password provided in the playbook
if not password_value:
if self.__no_interaction:
raise ResticPlaybookException(f"No password was provided for repository \"{repository_id}\"")
return getpass.getpass(f"Enter password for restic repository \"{repository_id}\": ")

if not password_value.lower().startswith(self.__format.REPOSITORIES_PASSWORD_VALUE_ENV_PREFIX):
return password_value
# Environment password provided in the playbook
if password_value.lower().startswith(self.__format.REPOSITORIES_PASSWORD_VALUE_ENV_PREFIX):
password_environment_variable = password_value[len(self.__format.REPOSITORIES_PASSWORD_VALUE_ENV_PREFIX):]
if password_environment_variable not in os.environ:
raise ResticPlaybookException(f"Environment variable \"{password_environment_variable}\""
f" for repository \"{repository_id}\" is not defined!")
return os.environ[password_environment_variable]

password_environment_variable = password_value[len(self.__format.REPOSITORIES_PASSWORD_VALUE_ENV_PREFIX):]
if password_environment_variable not in os.environ:
raise ResticPlaybookException(f"Environment variable \"{password_environment_variable}\""
f" for repository \"{repository_id}\" is not defined!")
return os.environ[password_environment_variable]
# Prompt password provided in the playbook
if password_value.lower().startswith(self.__format.REPOSITORIES_PASSWORD_VALUE_PROMPT_PREFIX):
if self.__no_interaction:
raise ResticPlaybookException(f"Repository \"{repository_id}\" requested the prompt credential provider,"
f" which cannot be used in the no-interaction mode.")
credential_name = password_value[len(self.__format.REPOSITORIES_PASSWORD_VALUE_PROMPT_PREFIX):]
return self.__prompt_credential_provider.get_credential(credential_name)

# Plain text password provided in the playbook
return password_value

def __parse_steps_json(self, steps_json: JsonList) -> None:
step_parser = ResticPlaybookStepParser(self.__backend, self.__repository_lookup)
Expand Down
Loading