diff --git a/linodecli/configuration/auth.py b/linodecli/configuration/auth.py index 1b633c7bb..f73d89ce5 100644 --- a/linodecli/configuration/auth.py +++ b/linodecli/configuration/auth.py @@ -176,7 +176,14 @@ def _check_full_access(base_url: str, token: str) -> bool: verify=API_CA_PATH, ) - _handle_response_status(result, exit_on_error=True) + # IAM-enrolled users receive a 403 from /profile/grants since that + # endpoint is not accessible to them. Treat 403 as a valid response + # (i.e. not full access) rather than a fatal error. + _handle_response_status( + result, + exit_on_error=True, + status_validator=lambda status: status == 403, + ) return result.status_code == 204 diff --git a/tests/unit/test_configuration.py b/tests/unit/test_configuration.py index 409250330..afe164cab 100644 --- a/tests/unit/test_configuration.py +++ b/tests/unit/test_configuration.py @@ -16,6 +16,7 @@ from linodecli import configuration from linodecli.configuration import ( _bool_input, + _check_full_access, _default_text_input, _default_thing_input, ) @@ -676,3 +677,41 @@ def test_custom_config_path(self, monkeypatch, tmp_path): for i, _ in enumerate(expected_configs): assert expected_configs[i] == configs[i] + + +class TestCheckFullAccess: + """ + Unit tests for _check_full_access + """ + + base_url = "https://linode-test.com" + test_token = "cli-dev-token" + + def test_full_access_returns_true(self): + """ + 204 No Content means the token has full (unrestricted) access. + """ + with requests_mock.Mocker() as m: + m.get(f"{self.base_url}/profile/grants", status_code=204) + assert _check_full_access(self.base_url, self.test_token) is True + + def test_restricted_access_returns_false(self): + """ + 200 with a grants body means the token has restricted access. + """ + with requests_mock.Mocker() as m: + m.get( + f"{self.base_url}/profile/grants", + status_code=200, + json={"linode": []}, + ) + assert _check_full_access(self.base_url, self.test_token) is False + + def test_iam_user_403_returns_false(self): + """ + IAM-enrolled users receive a 403 from /profile/grants. + This should be treated as "not full access" rather than a fatal error. + """ + with requests_mock.Mocker() as m: + m.get(f"{self.base_url}/profile/grants", status_code=403) + assert _check_full_access(self.base_url, self.test_token) is False