Add a keyring authenticator

This commit is contained in:
I-Al-Istannen 2021-05-23 19:44:12 +02:00
parent 2fdf24495b
commit 6e9f8fd391
4 changed files with 69 additions and 0 deletions

View File

@ -154,6 +154,15 @@ This authenticator prompts the user on the console for a two-factor
authentication token. The token is provided as password and it is not cached.
This authenticator does not support usernames.
### The `keyring` authenticator
This authenticator uses the system keyring to store passwords. It expects a
username in the config and will prompt *once* for the password. After that it
receives the password from the system keyring.
- `username`: The username. (Required)
- `keyring_name`: The service name PFERD uses for storing credentials. (Optional)
## Transformation rules
Transformation rules are rules for renaming and excluding files and directories.

View File

@ -3,6 +3,7 @@ from typing import Callable, Dict
from ..config import Config
from .authenticator import Authenticator, AuthSection
from .keyring_authenticator import KeyringAuthenticator, KeyringAuthSection
from .simple import SimpleAuthenticator, SimpleAuthSection
from .tfa import TfaAuthenticator
@ -17,4 +18,6 @@ AUTHENTICATORS: Dict[str, AuthConstructor] = {
SimpleAuthenticator(n, SimpleAuthSection(s), c),
"tfa": lambda n, s, c:
TfaAuthenticator(n, AuthSection(s), c),
"keyring": lambda n, s, c:
KeyringAuthenticator(n, KeyringAuthSection(s), c)
}

View File

@ -0,0 +1,56 @@
from typing import Optional, Tuple
import keyring
from ..config import Config
from ..logging import log
from ..utils import agetpass
from ..version import NAME
from .authenticator import Authenticator, AuthException, AuthSection
class KeyringAuthSection(AuthSection):
def username(self) -> str:
name = self.s.get("username")
if name is None:
self.missing_value("username")
return name
def keyring_name(self) -> str:
return self.s.get("keyring_name", fallback=NAME)
class KeyringAuthenticator(Authenticator):
def __init__(
self,
name: str,
section: KeyringAuthSection,
config: Config,
) -> None:
super().__init__(name, section, config)
self._username = section.username()
self._password: Optional[str] = None
self._keyring_name = section.keyring_name()
async def credentials(self) -> Tuple[str, str]:
if self._password is not None:
return self._username, self._password
password = keyring.get_password(self._keyring_name, self._username)
if not password:
async with log.exclusive_output():
password = await agetpass("Password: ")
keyring.set_password(self._keyring_name, self._username, password)
self._password = password
return self._username, password
def invalidate_credentials(self) -> None:
self.invalidate_password()
def invalidate_password(self) -> None:
raise AuthException("Invalid password")

View File

@ -9,6 +9,7 @@ install_requires =
aiohttp>=3.7.4.post0
beautifulsoup4>=4.9.3
rich>=10.1.0
keyring>=23.0.1
[options.entry_points]
console_scripts =