diff --git a/leverage/modules/auth.py b/leverage/modules/auth.py index 62daf33..8a4b74e 100644 --- a/leverage/modules/auth.py +++ b/leverage/modules/auth.py @@ -1,10 +1,11 @@ import time -from configparser import NoSectionError, NoOptionError from pathlib import Path +from configparser import NoSectionError, NoOptionError -import boto3 import hcl2 +import boto3 from configupdater import ConfigUpdater +from botocore.exceptions import ClientError from leverage import logger from leverage._utils import key_finder, ExitError, get_or_create_section @@ -40,7 +41,7 @@ def get_layer_profile(raw_profile: str, config_updater: ConfigUpdater, tf_profil except NoSectionError: raise ExitError(40, f"Missing {sso_profile} permission for account {account_name}.") - # if we are processing a profile from a different layer, we need to built it + # if we are processing a profile from a different layer, we need to build it layer_profile = layer_profile or f"{project}-{account_name}-{sso_role.lower()}" return account_id, account_name, sso_role, layer_profile @@ -104,7 +105,7 @@ def refresh_layer_credentials(cli): expiration = int(config_updater.get(f"profile {layer_profile}", "expiration").value) / 1000 except (NoSectionError, NoOptionError): # first time using this profile, skip into the credential's retrieval step - logger.debug(f"No cached credentials found.") + logger.debug("No cached credentials found.") else: # we reduce the validity 30 minutes, to avoid expiration over long-standing tasks renewal = time.time() + (30 * 60) @@ -117,11 +118,20 @@ def refresh_layer_credentials(cli): # retrieve credentials logger.debug(f"Retrieving role credentials for {sso_role}...") - credentials = client.get_role_credentials( - roleName=sso_role, - accountId=account_id, - accessToken=cli.get_sso_access_token(), - )["roleCredentials"] + try: + credentials = client.get_role_credentials( + roleName=sso_role, + accountId=account_id, + accessToken=cli.get_sso_access_token(), + )["roleCredentials"] + except ClientError as error: + if error.response["Error"]["Code"] in ("AccessDeniedException", "ForbiddenException"): + raise ExitError( + 40, + f"User does not have permission to assume role [bold]{sso_role}[/bold]" + " in this account.\nPlease check with your administrator or try" + " running [bold]leverage aws configure sso[/bold].", + ) # update expiration on aws//config logger.info(f"Writing {layer_profile} profile") diff --git a/tests/test_modules/test_auth.py b/tests/test_modules/test_auth.py index b295d95..0692f98 100644 --- a/tests/test_modules/test_auth.py +++ b/tests/test_modules/test_auth.py @@ -3,6 +3,7 @@ from unittest.mock import Mock, MagicMock, PropertyMock import pytest +from botocore.exceptions import ClientError from configupdater import ConfigUpdater from leverage._utils import ExitError @@ -249,8 +250,8 @@ def test_refresh_layer_credentials_still_valid(mock_open, mock_boto, sso_contain @mock.patch("leverage.modules.auth.update_config_section") @mock.patch("builtins.open", side_effect=open_side_effect) -@mock.patch("boto3.client", return_value=b3_client) @mock.patch("time.time", new=Mock(return_value=1705859000)) +@mock.patch("boto3.client", return_value=b3_client) @mock.patch("pathlib.Path.touch", new=Mock()) def test_refresh_layer_credentials(mock_boto, mock_open, mock_update_conf, sso_container, propagate_logs): refresh_layer_credentials(sso_container) @@ -265,3 +266,24 @@ def test_refresh_layer_credentials(mock_boto, mock_open, mock_update_conf, sso_c "aws_secret_access_key": "secret-key", "aws_session_token": "session-token", } + + +@mock.patch("leverage.modules.auth.update_config_section") +@mock.patch("builtins.open", side_effect=open_side_effect) +@mock.patch("time.time", new=Mock(return_value=1705859000)) +@mock.patch("pathlib.Path.touch", new=Mock()) +@pytest.mark.parametrize( + "error", + [ + ClientError({"Error": {"Code": "AccessDeniedException", "Message": "No access"}}, "GetRoleCredentials"), + ClientError({"Error": {"Code": "ForbiddenException", "Message": "No access"}}, "GetRoleCredentials"), + ], +) +def test_refresh_layer_credentials_no_access(mock_update_conf, mock_open, sso_container, error): + with mock.patch("boto3.client") as mocked_client: + mocked_client_obj = MagicMock() + mocked_client_obj.get_role_credentials.side_effect = error + mocked_client.return_value = mocked_client_obj + + with pytest.raises(ExitError): + refresh_layer_credentials(sso_container)