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 import getpass
from typing import Optional, Tuple from typing import Optional, Tuple
# optional import for KeyringAuthenticator
try:
# pylint: disable=import-error
import keyring
except ModuleNotFoundError:
pass
class TfaAuthenticator: class TfaAuthenticator:
# pylint: disable=too-few-public-methods # 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: if self._given_username is not None and self._given_password is not None:
self._given_username = None self._given_username = None
self._given_password = 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/). 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, from .crawler import (IliasCrawler, IliasCrawlerEntry, IliasDirectoryFilter,
IliasElementType) IliasElementType)
from .downloader import (IliasDownloader, IliasDownloadInfo, from .downloader import (IliasDownloader, IliasDownloadInfo,

View file

@ -9,7 +9,7 @@ from typing import Optional
import bs4 import bs4
import requests import requests
from ..authenticators import TfaAuthenticator, UserPassAuthenticator from ..authenticators import TfaAuthenticator, UserPassAuthenticator, KeyringAuthenticator
from ..utils import soupify from ..utils import soupify
LOGGER = logging.getLogger(__name__) LOGGER = logging.getLogger(__name__)
@ -129,3 +129,13 @@ class KitShibbolethAuthenticator(IliasAuthenticator):
@staticmethod @staticmethod
def _tfa_required(soup: bs4.BeautifulSoup) -> bool: def _tfa_required(soup: bs4.BeautifulSoup) -> bool:
return soup.find(id="j_tokenNumber") is not None 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 import Pferd
from PFERD.cookie_jar import CookieJar from PFERD.cookie_jar import CookieJar
from PFERD.ilias import (IliasCrawler, IliasElementType, from PFERD.ilias import (IliasCrawler, IliasElementType,
KitShibbolethAuthenticator) KitShibbolethAuthenticator,
KeyringKitShibbolethAuthenticator)
from PFERD.utils import to_path from PFERD.utils import to_path
@ -20,6 +21,7 @@ def main() -> None:
parser.add_argument("--test-run", action="store_true") 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('-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('--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('url', help="URL to the course page")
parser.add_argument('folder', nargs='?', default=None, help="Folder to put stuff into") parser.add_argument('folder', nargs='?', default=None, help="Folder to put stuff into")
args = parser.parse_args() args = parser.parse_args()
@ -28,7 +30,8 @@ def main() -> None:
cookie_jar = CookieJar(to_path(args.cookies) if args.cookies else None) cookie_jar = CookieJar(to_path(args.cookies) if args.cookies else None)
session = cookie_jar.create_session() session = cookie_jar.create_session()
authenticator = KitShibbolethAuthenticator() authenticator = (KeyringKitShibbolethAuthenticator() if args.keyring
else KitShibbolethAuthenticator())
crawler = IliasCrawler(url.scheme + '://' + url.netloc, session, crawler = IliasCrawler(url.scheme + '://' + url.netloc, session,
authenticator, lambda x, y: True) authenticator, lambda x, y: True)
@ -55,11 +58,14 @@ def main() -> None:
pferd.enable_logging() pferd.enable_logging()
# fetch # fetch
(username, password) = authenticator._auth.get_credentials()
pferd.ilias_kit_folder( pferd.ilias_kit_folder(
target=folder, target=folder,
full_url=args.url, full_url=args.url,
cookies=args.cookies, cookies=args.cookies,
dir_filter=dir_filter dir_filter=dir_filter,
username=username,
password=password
) )