Use system keyring service for password auth

This commit is contained in:
Scriptim 2020-11-04 00:18:27 +01:00
parent f4abe3197c
commit b174b5020a
No known key found for this signature in database
GPG key ID: 94CAB459397A9309
4 changed files with 107 additions and 5 deletions

View file

@ -5,6 +5,12 @@ General authenticators useful in many situations
import getpass
from typing import Optional, Tuple
# optional import for KeyringAuthenticator
try:
# pylint: disable=import-error
import keyring
except ModuleNotFoundError:
pass
class TfaAuthenticator:
# pylint: disable=too-few-public-methods
@ -123,3 +129,82 @@ 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

@ -9,7 +9,7 @@ from typing import Optional
import bs4
import requests
from ..authenticators import TfaAuthenticator, UserPassAuthenticator
from ..authenticators import TfaAuthenticator, UserPassAuthenticator, KeyringAuthenticator
from ..utils import soupify
LOGGER = logging.getLogger(__name__)
@ -129,3 +129,13 @@ class KitShibbolethAuthenticator(IliasAuthenticator):
@staticmethod
def _tfa_required(soup: bs4.BeautifulSoup) -> bool:
return soup.find(id="j_tokenNumber") is not None
class KeyringKitShibbolethAuthenticator(KitShibbolethAuthenticator):
"""
Authenticate via KIT's shibboleth system using the system keyring service.
"""
def __init__(self, username: Optional[str] = None, password: Optional[str] = None) -> None:
super().__init__()
self._auth = KeyringAuthenticator("KIT ILIAS Shibboleth", username, password)

View file

@ -11,7 +11,8 @@ from urllib.parse import urlparse
from PFERD import Pferd
from PFERD.cookie_jar import CookieJar
from PFERD.ilias import (IliasCrawler, IliasElementType,
KitShibbolethAuthenticator)
KitShibbolethAuthenticator,
KeyringKitShibbolethAuthenticator)
from PFERD.utils import to_path
@ -20,6 +21,7 @@ def main() -> None:
parser.add_argument("--test-run", action="store_true")
parser.add_argument('-c', '--cookies', nargs='?', default=None, help="File to store cookies in")
parser.add_argument('--no-videos', nargs='?', default=None, help="Don't download videos")
parser.add_argument("-k", "--keyring", action="store_true", help="Use the system keyring service for authentication")
parser.add_argument('url', help="URL to the course page")
parser.add_argument('folder', nargs='?', default=None, help="Folder to put stuff into")
args = parser.parse_args()
@ -28,7 +30,8 @@ def main() -> None:
cookie_jar = CookieJar(to_path(args.cookies) if args.cookies else None)
session = cookie_jar.create_session()
authenticator = KitShibbolethAuthenticator()
authenticator = (KeyringKitShibbolethAuthenticator() if args.keyring
else KitShibbolethAuthenticator())
crawler = IliasCrawler(url.scheme + '://' + url.netloc, session,
authenticator, lambda x, y: True)
@ -55,11 +58,14 @@ def main() -> None:
pferd.enable_logging()
# fetch
(username, password) = authenticator._auth.get_credentials()
pferd.ilias_kit_folder(
target=folder,
full_url=args.url,
cookies=args.cookies,
dir_filter=dir_filter
dir_filter=dir_filter,
username=username,
password=password
)