From 52c8ef90058120d7d04d3d201adc111664be526c Mon Sep 17 00:00:00 2001 From: Chaoren Date: Fri, 28 Jan 2022 20:10:48 -0500 Subject: [PATCH] feat: ADC can load an impersonated service account credentials. (#962) * Make the impersonated credentials to be Scoped so default func will add scopes. * Add tests for impersonated service account credentials. Co-authored-by: Anthonios Partheniou --- google/auth/impersonated_credentials.py | 20 ++++++++- tests/test__default.py | 54 +++++++++++++++++++++++++ tests/test_impersonated_credentials.py | 16 ++++++++ 3 files changed, 89 insertions(+), 1 deletion(-) diff --git a/google/auth/impersonated_credentials.py b/google/auth/impersonated_credentials.py index 80d6fdfdc..48acd1bb3 100644 --- a/google/auth/impersonated_credentials.py +++ b/google/auth/impersonated_credentials.py @@ -120,7 +120,9 @@ def _make_iam_token_request( six.raise_from(new_exc, caught_exc) -class Credentials(credentials.CredentialsWithQuotaProject, credentials.Signing): +class Credentials( + credentials.Scoped, credentials.CredentialsWithQuotaProject, credentials.Signing +): """This module defines impersonated credentials which are essentially impersonated identities. @@ -309,6 +311,10 @@ def service_account_email(self): def signer(self): return self + @property + def requires_scopes(self): + return not self._target_scopes + @_helpers.copy_docstring(credentials.CredentialsWithQuotaProject) def with_quota_project(self, quota_project_id): return self.__class__( @@ -321,6 +327,18 @@ def with_quota_project(self, quota_project_id): iam_endpoint_override=self._iam_endpoint_override, ) + @_helpers.copy_docstring(credentials.Scoped) + def with_scopes(self, scopes, default_scopes=None): + return self.__class__( + self._source_credentials, + target_principal=self._target_principal, + target_scopes=scopes or default_scopes, + delegates=self._delegates, + lifetime=self._lifetime, + quota_project_id=self._quota_project_id, + iam_endpoint_override=self._iam_endpoint_override, + ) + class IDTokenCredentials(credentials.CredentialsWithQuotaProject): """Open ID Connect ID Token-based service account credentials. diff --git a/tests/test__default.py b/tests/test__default.py index 1c27ac11c..35a5dc021 100644 --- a/tests/test__default.py +++ b/tests/test__default.py @@ -1089,6 +1089,60 @@ def test_default_no_warning_with_quota_project_id_for_user_creds(get_adc_path): credentials, project_id = _default.default(quota_project_id="project-foo") +@mock.patch( + "google.auth._cloud_sdk.get_application_default_credentials_path", autospec=True +) +def test_default_impersonated_service_account(get_adc_path): + get_adc_path.return_value = IMPERSONATED_SERVICE_ACCOUNT_AUTHORIZED_USER_SOURCE_FILE + + credentials, _ = _default.default() + + assert isinstance(credentials, impersonated_credentials.Credentials) + assert isinstance( + credentials._source_credentials, google.oauth2.credentials.Credentials + ) + assert credentials.service_account_email == "service-account-target@example.com" + assert credentials._delegates == ["service-account-delegate@example.com"] + assert not credentials._quota_project_id + assert not credentials._target_scopes + + +@mock.patch( + "google.auth._cloud_sdk.get_application_default_credentials_path", autospec=True +) +def test_default_impersonated_service_account_set_scopes(get_adc_path): + get_adc_path.return_value = IMPERSONATED_SERVICE_ACCOUNT_AUTHORIZED_USER_SOURCE_FILE + scopes = ["scope1", "scope2"] + + credentials, _ = _default.default(scopes=scopes) + assert credentials._target_scopes == scopes + + +@mock.patch( + "google.auth._cloud_sdk.get_application_default_credentials_path", autospec=True +) +def test_default_impersonated_service_account_set_default_scopes(get_adc_path): + get_adc_path.return_value = IMPERSONATED_SERVICE_ACCOUNT_AUTHORIZED_USER_SOURCE_FILE + default_scopes = ["scope1", "scope2"] + + credentials, _ = _default.default(default_scopes=default_scopes) + assert credentials._target_scopes == default_scopes + + +@mock.patch( + "google.auth._cloud_sdk.get_application_default_credentials_path", autospec=True +) +def test_default_impersonated_service_account_set_both_scopes_and_default_scopes( + get_adc_path +): + get_adc_path.return_value = IMPERSONATED_SERVICE_ACCOUNT_AUTHORIZED_USER_SOURCE_FILE + scopes = ["scope1", "scope2"] + default_scopes = ["scope3", "scope4"] + + credentials, _ = _default.default(scopes=scopes, default_scopes=default_scopes) + assert credentials._target_scopes == scopes + + def test__get_api_key_credentials_no_env_var(): cred, project_id = _default._get_api_key_credentials(quota_project_id="project-foo") assert cred is None diff --git a/tests/test_impersonated_credentials.py b/tests/test_impersonated_credentials.py index 58d159a59..f65fb7541 100644 --- a/tests/test_impersonated_credentials.py +++ b/tests/test_impersonated_credentials.py @@ -399,6 +399,22 @@ def test_with_quota_project_iam_endpoint_override( request_kwargs = request.call_args[1] assert request_kwargs["url"] == self.IAM_ENDPOINT_OVERRIDE + def test_with_scopes(self): + credentials = self.make_credentials() + credentials._target_scopes = [] + assert credentials.requires_scopes is True + credentials = credentials.with_scopes(["fake_scope1", "fake_scope2"]) + assert credentials.requires_scopes is False + assert credentials._target_scopes == ["fake_scope1", "fake_scope2"] + + def test_with_scopes_provide_default_scopes(self): + credentials = self.make_credentials() + credentials._target_scopes = [] + credentials = credentials.with_scopes( + ["fake_scope1"], default_scopes=["fake_scope2"] + ) + assert credentials._target_scopes == ["fake_scope1"] + def test_id_token_success( self, mock_donor_credentials, mock_authorizedsession_idtoken ):