Skip to content

Commit fdf9912

Browse files
authored
Merge pull request #23 from KGergo88/22-add-a-feature-that-enables-users-not-to-enter-the-same-password-multiple-times
Implemented the prompt credential provider feature
2 parents 52ec833 + 82251bd commit fdf9912

4 files changed

Lines changed: 56 additions & 8 deletions

File tree

README.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,14 +59,24 @@ For example this repository will have the implicit id `Documents`:
5959
Passwords of the repositories can be provided in the following ways:
6060
- During runtime in the terminal<br>
6161
If no passwords are provided in the playbook, the program will ask for them.
62-
If the `--no-interaction` switch is active, the program will fail if it needs to ask for the password.
6362
- Plain text in the playbook<br>
6463
For this add the `password` field to your repository object with the password:
6564
`"password": "my_plaintext_password"`
6665
- Via environment variables<br>
6766
For this add the `password` field to your repository object that
6867
defines the name of the environment variable that stores the password:
6968
`"password": "env:MY_RESTIC_PASSWORD_ENV_VAR"`
69+
- Via the prompt credential provider<br>
70+
For this add the `password` field to your repository object that
71+
defines the name of the credential that shall be used for the repository:
72+
`"password": "prompt:my_credential"`
73+
The program will ask for the password during runtime in the terminal and then store it
74+
for the duration of the playbook execution. If another repository references the same credential,
75+
the program will use the stored password and not ask for it again.
76+
This method is useful if you have multiple repositories that have the same password.
77+
You can define multiple unique credentials if not all repositories use the same password.
78+
79+
*Note: If the `--no-interaction` switch is active, the program will fail if it needs to ask for a password.*
7080

7181
The program will pass the passwords to the restic backend via temporarily setting environment variables:
7282
- `RESTIC_PASSWORD` — password for the target repository
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import getpass
2+
3+
4+
class PromptCredentialProvider:
5+
"""
6+
Credential provider that asks the user for credentials if they are not yet stored.
7+
"""
8+
# pylint: disable=too-few-public-methods
9+
# There are no more methods needed for this class at the moment.
10+
def __init__(self) -> None:
11+
self.__credentials: dict[str, str] = {}
12+
13+
def get_credential(self, credential_name: str) -> str:
14+
"""
15+
Returns the credential if exists, asks the user for the password via getpass.
16+
The credential entered will be stored and returned in the future without another prompt.
17+
"""
18+
if credential_name in self.__credentials:
19+
return self.__credentials[credential_name]
20+
21+
credential = getpass.getpass(f"Enter credentials for \"{credential_name}\": ")
22+
self.__credentials[credential_name] = credential
23+
24+
return credential

source/backup_automation/restic/restic_playbook_format.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ class ResticPlaybookFormat(PlaybookFormat):
1414
REPOSITORIES_URI_KEY = "uri"
1515
REPOSITORIES_PASSWORD_KEY = "password"
1616
REPOSITORIES_PASSWORD_VALUE_ENV_PREFIX = "env:"
17+
REPOSITORIES_PASSWORD_VALUE_PROMPT_PREFIX = "prompt:"
1718
STEPS_KEY = "steps"
1819
STEPS_COMMAND_KEY = "command"
1920
STEPS_COMMAND_VALUE_BACKUP = "backup"

source/backup_automation/restic/restic_playbook_parser.py

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from backup_automation.playbook import Playbook
66
from backup_automation.playbook_parser import PlaybookParser, PlaybookParserSettings
7+
from backup_automation.prompt_credential_provider import PromptCredentialProvider
78
from backup_automation.restic.restic_backend import ResticBackend
89
from backup_automation.restic.restic_playbook import ResticPlaybook
910
from backup_automation.restic.restic_playbook_exception import ResticPlaybookException
@@ -31,6 +32,7 @@ def __init__(self,
3132
self.__repositories: dict[str, ResticRepository] = {}
3233
self.__steps: list[ResticPlaybookStep] = []
3334
self.__format = ResticPlaybookFormat()
35+
self.__prompt_credential_provider = PromptCredentialProvider()
3436

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

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

123-
if not password_value.lower().startswith(self.__format.REPOSITORIES_PASSWORD_VALUE_ENV_PREFIX):
124-
return password_value
126+
# Environment password provided in the playbook
127+
if password_value.lower().startswith(self.__format.REPOSITORIES_PASSWORD_VALUE_ENV_PREFIX):
128+
password_environment_variable = password_value[len(self.__format.REPOSITORIES_PASSWORD_VALUE_ENV_PREFIX):]
129+
if password_environment_variable not in os.environ:
130+
raise ResticPlaybookException(f"Environment variable \"{password_environment_variable}\""
131+
f" for repository \"{repository_id}\" is not defined!")
132+
return os.environ[password_environment_variable]
125133

126-
password_environment_variable = password_value[len(self.__format.REPOSITORIES_PASSWORD_VALUE_ENV_PREFIX):]
127-
if password_environment_variable not in os.environ:
128-
raise ResticPlaybookException(f"Environment variable \"{password_environment_variable}\""
129-
f" for repository \"{repository_id}\" is not defined!")
130-
return os.environ[password_environment_variable]
134+
# Prompt password provided in the playbook
135+
if password_value.lower().startswith(self.__format.REPOSITORIES_PASSWORD_VALUE_PROMPT_PREFIX):
136+
if self.__no_interaction:
137+
raise ResticPlaybookException(f"Repository \"{repository_id}\" requested the prompt credential provider,"
138+
f" which cannot be used in the no-interaction mode.")
139+
credential_name = password_value[len(self.__format.REPOSITORIES_PASSWORD_VALUE_PROMPT_PREFIX):]
140+
return self.__prompt_credential_provider.get_credential(credential_name)
141+
142+
# Plain text password provided in the playbook
143+
return password_value
131144

132145
def __parse_steps_json(self, steps_json: JsonList) -> None:
133146
step_parser = ResticPlaybookStepParser(self.__backend, self.__repository_lookup)

0 commit comments

Comments
 (0)