mirror of
https://github.com/Garmelon/PFERD.git
synced 2023-12-21 10:23:01 +01:00
Use system keyring service for password auth
This commit is contained in:
parent
75471c46d1
commit
83ea15ee83
4
LICENSE
4
LICENSE
@ -1,4 +1,8 @@
|
|||||||
|
<<<<<<< HEAD
|
||||||
Copyright 2019-2020 Garmelon, I-Al-Istannen, danstooamerican, pavelzw, TheChristophe
|
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
|
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
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
@ -3,8 +3,19 @@ General authenticators useful in many situations
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import getpass
|
import getpass
|
||||||
|
import logging
|
||||||
from typing import Optional, Tuple
|
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:
|
class TfaAuthenticator:
|
||||||
# pylint: disable=too-few-public-methods
|
# 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:
|
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()
|
||||||
|
@ -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,
|
||||||
|
@ -37,8 +37,12 @@ class KitShibbolethAuthenticator(IliasAuthenticator):
|
|||||||
Authenticate via KIT's shibboleth system.
|
Authenticate via KIT's shibboleth system.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, username: Optional[str] = None, password: Optional[str] = None) -> None:
|
def __init__(self, authenticator: Optional[UserPassAuthenticator] = None) -> None:
|
||||||
self._auth = UserPassAuthenticator("KIT ILIAS Shibboleth", username, password)
|
if authenticator:
|
||||||
|
self._auth = authenticator
|
||||||
|
else:
|
||||||
|
self._auth = UserPassAuthenticator("KIT ILIAS Shibboleth")
|
||||||
|
|
||||||
self._tfa_auth = TfaAuthenticator("KIT ILIAS Shibboleth")
|
self._tfa_auth = TfaAuthenticator("KIT ILIAS Shibboleth")
|
||||||
|
|
||||||
def authenticate(self, sess: requests.Session) -> None:
|
def authenticate(self, sess: requests.Session) -> None:
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
requests>=2.21.0
|
requests>=2.21.0
|
||||||
beautifulsoup4>=4.7.1
|
beautifulsoup4>=4.7.1
|
||||||
rich>=2.1.0
|
rich>=2.1.0
|
||||||
|
keyring>=21.5.0
|
3
setup.py
3
setup.py
@ -7,7 +7,8 @@ setup(
|
|||||||
install_requires=[
|
install_requires=[
|
||||||
"requests>=2.21.0",
|
"requests>=2.21.0",
|
||||||
"beautifulsoup4>=4.7.1",
|
"beautifulsoup4>=4.7.1",
|
||||||
"rich>=2.1.0"
|
"rich>=2.1.0",
|
||||||
|
"keyring>=21.5.0"
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
27
sync_url.py
27
sync_url.py
@ -8,10 +8,11 @@ import argparse
|
|||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path, PurePath
|
from pathlib import Path, PurePath
|
||||||
from typing import Optional, Tuple
|
from typing import Optional
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from PFERD import Pferd
|
from PFERD import Pferd
|
||||||
|
from PFERD.authenticators import KeyringAuthenticator, UserPassAuthenticator
|
||||||
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)
|
||||||
@ -25,9 +26,9 @@ _LOGGER = logging.getLogger("sync_url")
|
|||||||
_PRETTY = PrettyLogger(_LOGGER)
|
_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:
|
if not file_path:
|
||||||
return (None, None)
|
return UserPassAuthenticator("KIT ILIAS Shibboleth", None, None)
|
||||||
|
|
||||||
if not Path(file_path).exists():
|
if not Path(file_path).exists():
|
||||||
_PRETTY.error("Credential file does not exist")
|
_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
|
name = read_name if read_name else None
|
||||||
password = read_password[0] if read_password 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:
|
def _resolve_remote_first(_path: PurePath, _conflict: ConflictType) -> FileConflictResolution:
|
||||||
@ -71,6 +72,8 @@ def main() -> None:
|
|||||||
parser.add_argument('--credential-file', nargs='?', default=None,
|
parser.add_argument('--credential-file', nargs='?', default=None,
|
||||||
help="Path to a file containing credentials for Ilias. The file must have "
|
help="Path to a file containing credentials for Ilias. The file must have "
|
||||||
"one line in the following format: '<user>:<password>'")
|
"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('--no-videos', nargs='?', default=None, help="Don't download videos")
|
||||||
parser.add_argument('--local-first', action="store_true",
|
parser.add_argument('--local-first', action="store_true",
|
||||||
help="Don't prompt for confirmation, keep existing files")
|
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)
|
cookie_jar = CookieJar(to_path(args.cookies) if args.cookies else None)
|
||||||
session = cookie_jar.create_session()
|
session = cookie_jar.create_session()
|
||||||
|
|
||||||
username, password = _extract_credentials(args.credential_file)
|
if args.keyring:
|
||||||
authenticator = KitShibbolethAuthenticator(username=username, password=password)
|
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)
|
url = urlparse(args.url)
|
||||||
|
|
||||||
crawler = IliasCrawler(url.scheme + '://' + url.netloc, session,
|
crawler = IliasCrawler(url.scheme + '://' + url.netloc, session,
|
||||||
authenticator, lambda x, y: True)
|
authenticator, lambda x, y: True)
|
||||||
|
|
||||||
@ -125,6 +139,7 @@ def main() -> None:
|
|||||||
file_confilict_resolver = resolve_prompt_user
|
file_confilict_resolver = resolve_prompt_user
|
||||||
|
|
||||||
pferd.enable_logging()
|
pferd.enable_logging()
|
||||||
|
|
||||||
# fetch
|
# fetch
|
||||||
pferd.ilias_kit_folder(
|
pferd.ilias_kit_folder(
|
||||||
target=target,
|
target=target,
|
||||||
|
Loading…
Reference in New Issue
Block a user