Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Disable autologin and keyring in CLI #85

Merged
merged 2 commits into from
Jul 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading