Skip to content

Commit

Permalink
add success message for login and a max number of retry attempts
Browse files Browse the repository at this point in the history
  • Loading branch information
travishathaway committed Sep 29, 2023
1 parent 05b19bc commit 570a632
Show file tree
Hide file tree
Showing 5 changed files with 73 additions and 12 deletions.
21 changes: 19 additions & 2 deletions conda_auth/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,16 @@

from .condarc import CondaRC, CondaRCError
from .constants import OAUTH2_NAME, HTTP_BASIC_AUTH_NAME
from .exceptions import CondaAuthError
from .exceptions import CondaAuthError, InvalidCredentialsError
from .handlers import AuthManager, oauth2_manager, basic_auth_manager

AUTH_MANAGER_MAPPING = {
OAUTH2_NAME: oauth2_manager,
HTTP_BASIC_AUTH_NAME: basic_auth_manager,
}
SUCCESSFUL_LOGIN_MESSAGE = "Successfully logged in"
SUCCESSFUL_LOGOUT_MESSAGE = "Successfully logged out"
MAX_LOGIN_ATTEMPTS = 3


def parse_channel(ctx, param, value):
Expand Down Expand Up @@ -89,7 +92,19 @@ def login(channel: Channel, **kwargs):
settings.update(kwargs)

auth_type, auth_manager = get_auth_manager(settings)
username = auth_manager.authenticate(channel, settings)
attempts = 0

while True:
try:
username = auth_manager.authenticate(channel, settings)
break
except InvalidCredentialsError as exc:
auth_manager.remove_channel_cache(channel.canonical_name)
attempts += 1
if attempts >= MAX_LOGIN_ATTEMPTS:
raise CondaAuthError(f"Max attempts reached; {exc}")

click.echo(click.style(SUCCESSFUL_LOGIN_MESSAGE, fg="green"))

try:
condarc = CondaRC()
Expand All @@ -113,3 +128,5 @@ def logout(channel: Channel):
settings["type"] = settings["auth"]
auth_type, auth_manager = get_auth_manager(settings)
auth_manager.remove_secret(channel, settings)

click.echo(click.style(SUCCESSFUL_LOGOUT_MESSAGE, fg="green"))
4 changes: 4 additions & 0 deletions conda_auth/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,7 @@

class CondaAuthError(CondaError):
"""Custom error for the conda-auth plugin"""


class InvalidCredentialsError(CondaAuthError):
"""Error raised when credentials are invalid"""
20 changes: 17 additions & 3 deletions conda_auth/handlers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from conda.gateways.connection.session import CondaSession
from conda.models.channel import Channel

from ..exceptions import CondaAuthError
from ..exceptions import InvalidCredentialsError

INVALID_CREDENTIALS_ERROR_MESSAGE = "Provided credentials are not correct."

Expand Down Expand Up @@ -41,7 +41,12 @@ def hook_action(self, command: str) -> None:
self.authenticate(channel, settings)

def authenticate(self, channel: Channel, settings: Mapping[str, str]) -> str:
"""Used to retrieve credentials and store them on the ``cache`` property"""
"""
Used to retrieve credentials and store them on the ``cache`` property
This method returns a "username" because this property could have been retrieved
via user input while calling ``fetch_secret``.
"""
extra_params = {
param: settings.get(param) for param in self.get_config_parameters()
}
Expand Down Expand Up @@ -88,6 +93,15 @@ def get_secret(self, channel_name: str) -> tuple[str | None, str | None]:

return secrets

def remove_channel_cache(self, channel_name: str) -> None:
"""
Removes the cached secret for the given channel name
"""
try:
del self._cache[channel_name]
except KeyError:
pass

@abstractmethod
def _fetch_secret(
self, channel: Channel, settings: Mapping[str, str | None]
Expand Down Expand Up @@ -149,4 +163,4 @@ def verify_credentials(channel: Channel, auth_cls: type) -> None:
else:
error_message = str(exc)

raise CondaAuthError(error_message)
raise InvalidCredentialsError(error_message)
36 changes: 31 additions & 5 deletions tests/cli/test_login.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from conda_auth.cli import group
from conda_auth.cli import group, SUCCESSFUL_LOGIN_MESSAGE
from conda_auth.condarc import CondaRCError
from conda_auth.constants import HTTP_BASIC_AUTH_NAME
from conda_auth.exceptions import CondaAuthError
from conda_auth.exceptions import CondaAuthError, InvalidCredentialsError
from conda_auth.handlers.base import INVALID_CREDENTIALS_ERROR_MESSAGE


def test_login_no_options_basic_auth(mocker, runner, session, keyring, condarc):
Expand All @@ -24,7 +25,7 @@ def test_login_no_options_basic_auth(mocker, runner, session, keyring, condarc):
result = runner.invoke(group, ["login", channel_name])

assert result.exit_code == 0
assert result.output == ""
assert SUCCESSFUL_LOGIN_MESSAGE in result.output


def test_login_with_options_basic_auth(mocker, runner, session, keyring, condarc):
Expand All @@ -44,7 +45,7 @@ def test_login_with_options_basic_auth(mocker, runner, session, keyring, condarc
)

assert result.exit_code == 0
assert result.output == ""
assert SUCCESSFUL_LOGIN_MESSAGE in result.output


def test_login_with_invalid_auth_type(mocker, runner, session, keyring, condarc):
Expand Down Expand Up @@ -87,7 +88,7 @@ def test_login_with_non_existent_channel(mocker, runner, session, keyring, conda
result = runner.invoke(group, ["login", channel_name], input="user")

assert result.exit_code == 0
assert result.output == ""
assert SUCCESSFUL_LOGIN_MESSAGE in result.output


def test_login_succeeds_error_returned_when_updating_condarc(
Expand All @@ -114,3 +115,28 @@ def test_login_succeeds_error_returned_when_updating_condarc(

assert exc_type == CondaAuthError
assert "Could not save file" == exception.message


def test_login_exceed_max_login_retries(mocker, runner, session, keyring, condarc):
"""
Test the case where the login runs successfully but an error is returned when trying to update
the condarc file.
"""
channel_name = "tester"

# setup mocks
mocker.patch("conda_auth.cli.context")
mock_manager = mocker.patch("conda_auth.cli.get_auth_manager")
mock_type = "http-basic"
mock_auth_manager = mocker.MagicMock()
mock_auth_manager.authenticate.side_effect = InvalidCredentialsError(
INVALID_CREDENTIALS_ERROR_MESSAGE
)
mock_manager.return_value = (mock_type, mock_auth_manager)

# run command
result = runner.invoke(group, ["login", channel_name], input="user")
exc_type, exception, _ = result.exc_info

assert exc_type == CondaAuthError
assert "Max attempts reached" in exception.message
4 changes: 2 additions & 2 deletions tests/cli/test_logout.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from conda_auth.cli import group
from conda_auth.cli import group, SUCCESSFUL_LOGOUT_MESSAGE
from conda_auth.constants import HTTP_BASIC_AUTH_NAME, PLUGIN_NAME
from conda_auth.exceptions import CondaAuthError

Expand All @@ -21,7 +21,7 @@ def test_logout_of_active_session(mocker, runner, keyring):
# run command
result = runner.invoke(group, ["logout", channel_name])

assert result.output == ""
assert SUCCESSFUL_LOGOUT_MESSAGE in result.output
assert result.exit_code == 0

# Make sure the delete password call was invoked correctly
Expand Down

0 comments on commit 570a632

Please sign in to comment.