Use system keyring service for password auth

This commit is contained in:
Scriptim 2020-11-04 00:18:27 +01:00 committed by I-Al-Istannen
parent 75471c46d1
commit 83ea15ee83
7 changed files with 125 additions and 10 deletions

View File

@ -1,4 +1,8 @@
<<<<<<< HEAD
Copyright 2019-2020 Garmelon, I-Al-Istannen, danstooamerican, pavelzw, TheChristophe
=======
Copyright 2019-2020 Garmelon, I-Al-Istannen, danstooamerican, pavelzw, Scriptim
>>>>>>> f89226c (Use system keyring service for password auth)
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in

View File

@ -3,8 +3,19 @@ General authenticators useful in many situations
"""
import getpass
import logging
from typing import Optional, Tuple
from .logging import PrettyLogger
LOGGER = logging.getLogger(__name__)
PRETTY = PrettyLogger(LOGGER)
try:
import keyring
except ImportError:
PRETTY.warning("Keyring module not found, KeyringAuthenticator won't work!")
class TfaAuthenticator:
# pylint: disable=too-few-public-methods
@ -123,3 +134,81 @@ class UserPassAuthenticator:
if self._given_username is not None and self._given_password is not None:
self._given_username = None
self._given_password = None
class KeyringAuthenticator(UserPassAuthenticator):
"""
An authenticator for username-password combinations that stores the
password using the system keyring service and prompts the user for missing
information.
"""
def get_credentials(self) -> Tuple[str, str]:
"""
Returns a tuple (username, password). Prompts user for username or
password when necessary.
"""
if self._username is None and self._given_username is not None:
self._username = self._given_username
if self._password is None and self._given_password is not None:
self._password = self._given_password
if self._username is not None and self._password is None:
self._load_password()
if self._username is None or self._password is None:
print(f"Enter credentials ({self._reason})")
username: str
if self._username is None:
username = input("Username: ")
self._username = username
else:
username = self._username
if self._password is None:
self._load_password()
password: str
if self._password is None:
password = getpass.getpass(prompt="Password: ")
self._password = password
self._save_password()
else:
password = self._password
return (username, password)
def _load_password(self) -> None:
"""
Loads the saved password associated with self._username from the system
keyring service (or None if not password has been saved yet) and stores
it in self._password.
"""
self._password = keyring.get_password("pferd-ilias", self._username)
def _save_password(self) -> None:
"""
Saves self._password to the system keyring service and associates it
with self._username.
"""
keyring.set_password("pferd-ilias", self._username, self._password)
def invalidate_credentials(self) -> None:
"""
Marks the credentials as invalid. If only a username was supplied in
the constructor, assumes that the username is valid and only the
password is invalid. If only a password was supplied in the
constructor, assumes that the password is valid and only the username
is invalid. Otherwise, assumes that username and password are both
invalid.
"""
try:
keyring.delete_password("pferd-ilias", self._username)
except keyring.errors.PasswordDeleteError:
pass
super().invalidate_credentials()

View File

@ -2,7 +2,8 @@
Synchronizing files from ILIAS instances (https://www.ilias.de/).
"""
from .authenticators import IliasAuthenticator, KitShibbolethAuthenticator
from .authenticators import (IliasAuthenticator, KitShibbolethAuthenticator,
KeyringKitShibbolethAuthenticator)
from .crawler import (IliasCrawler, IliasCrawlerEntry, IliasDirectoryFilter,
IliasElementType)
from .downloader import (IliasDownloader, IliasDownloadInfo,

View File

@ -37,8 +37,12 @@ class KitShibbolethAuthenticator(IliasAuthenticator):
Authenticate via KIT's shibboleth system.
"""
def __init__(self, username: Optional[str] = None, password: Optional[str] = None) -> None:
self._auth = UserPassAuthenticator("KIT ILIAS Shibboleth", username, password)
def __init__(self, authenticator: Optional[UserPassAuthenticator] = None) -> None:
if authenticator:
self._auth = authenticator
else:
self._auth = UserPassAuthenticator("KIT ILIAS Shibboleth")
self._tfa_auth = TfaAuthenticator("KIT ILIAS Shibboleth")
def authenticate(self, sess: requests.Session) -> None:

View File

@ -1,3 +1,4 @@
requests>=2.21.0
beautifulsoup4>=4.7.1
rich>=2.1.0
keyring>=21.5.0

View File

@ -7,7 +7,8 @@ setup(
install_requires=[
"requests>=2.21.0",
"beautifulsoup4>=4.7.1",
"rich>=2.1.0"
"rich>=2.1.0",
"keyring>=21.5.0"
],
)

View File

@ -8,10 +8,11 @@ import argparse
import logging
import sys
from pathlib import Path, PurePath
from typing import Optional, Tuple
from typing import Optional
from urllib.parse import urlparse
from PFERD import Pferd
from PFERD.authenticators import KeyringAuthenticator, UserPassAuthenticator
from PFERD.cookie_jar import CookieJar
from PFERD.ilias import (IliasCrawler, IliasElementType,
KitShibbolethAuthenticator)
@ -25,9 +26,9 @@ _LOGGER = logging.getLogger("sync_url")
_PRETTY = PrettyLogger(_LOGGER)
def _extract_credentials(file_path: Optional[str]) -> Tuple[Optional[str], Optional[str]]:
def _extract_credentials(file_path: Optional[str]) -> UserPassAuthenticator:
if not file_path:
return (None, None)
return UserPassAuthenticator("KIT ILIAS Shibboleth", None, None)
if not Path(file_path).exists():
_PRETTY.error("Credential file does not exist")
@ -39,7 +40,7 @@ def _extract_credentials(file_path: Optional[str]) -> Tuple[Optional[str], Optio
name = read_name if read_name else None
password = read_password[0] if read_password else None
return (name, password)
return UserPassAuthenticator("KIT ILIAS Shibboleth", username=name, password=password)
def _resolve_remote_first(_path: PurePath, _conflict: ConflictType) -> FileConflictResolution:
@ -71,6 +72,8 @@ def main() -> None:
parser.add_argument('--credential-file', nargs='?', default=None,
help="Path to a file containing credentials for Ilias. The file must have "
"one line in the following format: '<user>:<password>'")
parser.add_argument("-k", "--keyring", action="store_true",
help="Use the system keyring service for authentication")
parser.add_argument('--no-videos', nargs='?', default=None, help="Don't download videos")
parser.add_argument('--local-first', action="store_true",
help="Don't prompt for confirmation, keep existing files")
@ -85,10 +88,21 @@ def main() -> None:
cookie_jar = CookieJar(to_path(args.cookies) if args.cookies else None)
session = cookie_jar.create_session()
username, password = _extract_credentials(args.credential_file)
authenticator = KitShibbolethAuthenticator(username=username, password=password)
if args.keyring:
if not args.username:
_PRETTY.error("Keyring auth selected but no --username passed!")
return
inner_auth: UserPassAuthenticator = KeyringAuthenticator(
"KIT ILIAS Shibboleth", username=args.username, password=args.password
)
else:
inner_auth = _extract_credentials(args.credential_file)
username, password = inner_auth.get_credentials()
authenticator = KitShibbolethAuthenticator(inner_auth)
url = urlparse(args.url)
crawler = IliasCrawler(url.scheme + '://' + url.netloc, session,
authenticator, lambda x, y: True)
@ -125,6 +139,7 @@ def main() -> None:
file_confilict_resolver = resolve_prompt_user
pferd.enable_logging()
# fetch
pferd.ilias_kit_folder(
target=target,