Skip to content

Commit

Permalink
Allow to disable autologin and keyring in CLI (#85)
Browse files Browse the repository at this point in the history
  • Loading branch information
psrok1 authored Jul 21, 2023
1 parent b9fe609 commit 5a44505
Show file tree
Hide file tree
Showing 5 changed files with 126 additions and 56 deletions.
40 changes: 27 additions & 13 deletions mwdblib/api/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,21 @@ class APIClient:
mwdb.api.delete(f'object/{sha256}')
"""

def __init__(self, _auth_token: Optional[str] = None, **api_options: Any) -> None:
def __init__(
self,
_auth_token: Optional[str] = None,
autologin: bool = True,
**api_options: Any,
) -> None:
self.options: APIClientOptions = APIClientOptions(**api_options)
self.auth_token: Optional[JWTAuthToken] = None

# These state variables will be filled after
# successful authentication
self.username: Optional[str] = None
self.password: Optional[str] = None
self.api_key: Optional[str] = None

self._server_metadata: Optional[dict] = None

self.session: requests.Session = requests.Session()
Expand All @@ -92,10 +104,12 @@ def __init__(self, _auth_token: Optional[str] = None, **api_options: Any) -> Non

if _auth_token:
self.set_auth_token(_auth_token)
if self.options.api_key:
self.set_api_key(self.options.api_key)
elif self.options.username and self.options.password:
self.login(self.options.username, self.options.password)

if autologin:
if self.options.api_key:
self.set_api_key(self.options.api_key)
elif self.options.username and self.options.password:
self.login(self.options.username, self.options.password)

@property
def server_metadata(self) -> dict:
Expand Down Expand Up @@ -150,9 +164,9 @@ def login(self, username: str, password: str) -> None:
"auth/login", json={"login": username, "password": password}, noauth=True
)["token"]
self.set_auth_token(token)
# Store credentials in API options
self.options.username = username
self.options.password = password
# Store credentials in API state
self.username = username
self.password = password

def set_api_key(self, api_key: str) -> None:
"""
Expand All @@ -165,10 +179,10 @@ def set_api_key(self, api_key: str) -> None:
:param api_key: API key to set
"""
self.set_auth_token(api_key)
# Store credentials in API options
self.options.api_key = api_key
# Store credentials in API state
self.api_key = api_key
if self.auth_token is not None:
self.options.username = self.auth_token.username
self.username = self.auth_token.username

def logout(self) -> None:
"""
Expand Down Expand Up @@ -253,10 +267,10 @@ def request(
# Forget current auth_key
self.logout()
# If no password set: re-raise
if self.options.password is None:
if self.username is None or self.password is None:
raise
# Try to log in
self.login(self.options.username, self.options.password)
self.login(self.username, self.password)
# Retry failed request...
except LimitExceededError as e:
if not self.options.obey_ratelimiter:
Expand Down
27 changes: 20 additions & 7 deletions mwdblib/api/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,14 +158,14 @@ def __init__(
f"mwdb:{self.api_url}", self.username
)

def clear_stored_credentials(self, config_writeback: bool = True) -> None:
def clear_stored_credentials(self, config_writeback: bool = True) -> bool:
"""
Clears stored credentials in configuration for current user.
Used by ``mwdb logout`` CLI command.
"""
if not self.username:
return
return False
# Remove credentials from keyring
if self.use_keyring:
try:
Expand All @@ -188,15 +188,21 @@ def clear_stored_credentials(self, config_writeback: bool = True) -> None:
if config_writeback and self.config_path:
with self.config_path.open("w") as f:
self.config_parser.write(f)
return True

def store_credentials(self) -> None:
def store_credentials(
self, username: Optional[str], password: Optional[str], api_key: Optional[str]
) -> bool:
"""
Stores current credentials in configuration for current user.
Used by ``mwdb login`` CLI command.
"""
if not self.username or (not self.api_key and not self.password):
return
if not username or (not api_key and not password):
return False
self.username = username
self.password = password
self.api_key = api_key
# Clear currently stored credentials
self.clear_stored_credentials(config_writeback=False)
# Ensure that 'mwdb' section exists in configuration
Expand All @@ -215,16 +221,23 @@ def store_credentials(self) -> None:
keyring.set_password(
f"mwdb-apikey:{self.api_url}", self.username, self.api_key
)
else:
elif self.password:
keyring.set_password(
f"mwdb:{self.api_url}", self.username, self.password
)
else:
raise RuntimeError("Implementation error: no api_key nor password")
self.config_parser.set(instance_section, "use_keyring", "1")
else:
if self.api_key:
self.config_parser.set(instance_section, "api_key", self.api_key)
else:
elif self.password:
self.config_parser.set(instance_section, "password", self.password)
else:
raise RuntimeError("Implementation error: no api_key nor password")
self.config_parser.set(instance_section, "use_keyring", "0")
# Perform configuration writeback
if self.config_path:
with self.config_path.open("w") as f:
self.config_parser.write(f)
return True
36 changes: 31 additions & 5 deletions mwdblib/cli/login.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@
@click.option(
"--password", "-p", type=str, default=None, help="MWDB password (default: ask)"
)
@click.option(
"--use-keyring/--no-keyring",
default=None,
help="Don't use keyring, store credentials in plaintext",
)
@click.option("--via-api-key", "-A", is_flag=True, help="Use API key provided by stdin")
@click.option(
"--api-key",
Expand All @@ -19,18 +24,22 @@
default=None,
help="API key token (default: password-based authentication)",
)
@pass_mwdb
@pass_mwdb(autologin=False)
@click.pass_context
def login_command(ctx, mwdb, username, password, via_api_key, api_key):
def login_command(ctx, mwdb, username, password, use_keyring, via_api_key, api_key):
"""Store credentials for MWDB authentication"""
if via_api_key:
api_key = click.prompt("Provide your API key token", hide_input=True)

if api_key is None:
if username is None:
username = click.prompt("Username")
if password is None:
password = click.prompt("Password", hide_input=True)

if use_keyring is not None:
mwdb.api.options.use_keyring = use_keyring

try:
# Try to use credentials
if api_key is None:
Expand All @@ -42,11 +51,28 @@ def login_command(ctx, mwdb, username, password, via_api_key, api_key):
except (InvalidCredentialsError, NotAuthenticatedError) as e:
click.echo("Error: Login failed - {}".format(str(e)), err=True)
ctx.abort()
mwdb.api.options.store_credentials()
mwdb.api.options.store_credentials(username, password, api_key)
if not mwdb.api.options.use_keyring:
click.echo(
f"Warning! Your password is stored in plaintext in "
f"{mwdb.api.options.config_path}. Use --use-keyring to store "
f"credentials in keyring (if available on your system).",
err=True,
)
click.echo(
f"Logged in successfully to {mwdb.api.options.api_url} "
f"as {mwdb.api.logged_user}",
err=True,
)


@main.command("logout")
@pass_mwdb
@pass_mwdb(autologin=False)
def logout_command(mwdb):
"""Reset stored credentials"""
mwdb.api.options.clear_stored_credentials()
if mwdb.api.options.clear_stored_credentials():
click.echo(f"Logged out successfully from {mwdb.api.options.api_url}", err=True)
else:
click.echo(
f"Error: user already logged out from {mwdb.api.options.api_url}!", err=True
)
74 changes: 43 additions & 31 deletions mwdblib/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,37 +8,49 @@
from ..exc import MWDBError, NotAuthenticatedError


def pass_mwdb(fn):
@click.option("--api-url", type=str, default=None, help="URL to MWDB instance API")
@click.option(
"--config-path", type=str, default=None, help="Alternative configuration path"
)
@functools.wraps(fn)
def wrapper(*args, **kwargs):
ctx = get_current_context()
mwdb_options = {}
api_url = kwargs.pop("api_url")
if api_url:
mwdb_options["api_url"] = api_url
config_path = kwargs.pop("config_path")
if config_path:
mwdb_options["config_path"] = config_path
mwdb = MWDB(**mwdb_options)
try:
return fn(mwdb=mwdb, *args, **kwargs)
except NotAuthenticatedError:
click.echo(
"Error: Not authenticated. Use `mwdb login` first to set credentials.",
err=True,
)
ctx.abort()
except MWDBError as error:
click.echo(
"{}: {}".format(error.__class__.__name__, error.args[0]), err=True
)
ctx.abort()

return wrapper
def pass_mwdb(*fn, autologin=True):
def uses_mwdb(fn):
@click.option(
"--api-url", type=str, default=None, help="URL to MWDB instance API"
)
@click.option(
"--config-path",
type=str,
default=None,
help="Alternative configuration path",
)
@functools.wraps(fn)
def wrapper(*args, **kwargs):
ctx = get_current_context()
mwdb_options = {}
api_url = kwargs.pop("api_url")
if api_url:
mwdb_options["api_url"] = api_url
config_path = kwargs.pop("config_path")
if config_path:
mwdb_options["config_path"] = config_path
mwdb = MWDB(autologin=autologin, **mwdb_options)
try:
return fn(mwdb=mwdb, *args, **kwargs)
except NotAuthenticatedError:
click.echo(
"Error: Not authenticated. Use `mwdb login` first "
"to set credentials.",
err=True,
)
ctx.abort()
except MWDBError as error:
click.echo(
"{}: {}".format(error.__class__.__name__, error.args[0]), err=True
)
ctx.abort()

return wrapper

if fn:
return uses_mwdb(fn[0])
else:
return uses_mwdb


@click.group()
Expand Down
5 changes: 5 additions & 0 deletions mwdblib/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ class MWDB:
:param api_key: MWDB API key
:param username: MWDB account username
:param password: MWDB account password
:param autologin: Login automatically using credentials stored in configuration
or provided in arguments (default: True)
:param verify_ssl: Verify SSL certificate correctness (default: True)
:param obey_ratelimiter: If ``False``, HTTP 429 errors will cause an exception
like all other error codes.
Expand Down Expand Up @@ -78,6 +80,9 @@ class MWDB:
Added ``use_keyring``, ``emit_warnings`` and ``config_path`` options.
``username`` and ``password`` can be passed directly to the constructor.
.. versionadded:: 4.4.0
Added ``autologin`` option.
Usage example:
.. code-block:: python
Expand Down

0 comments on commit 5a44505

Please sign in to comment.