Skip to content

Commit

Permalink
feat: ADC can load an impersonated service account credentials. (#962)
Browse files Browse the repository at this point in the history
* 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 <partheniou@google.com>
  • Loading branch information
liuchaoren and parthea authored Jan 29, 2022
1 parent 83b20f0 commit 52c8ef9
Show file tree
Hide file tree
Showing 3 changed files with 89 additions and 1 deletion.
20 changes: 19 additions & 1 deletion google/auth/impersonated_credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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__(
Expand All @@ -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.
Expand Down
54 changes: 54 additions & 0 deletions tests/test__default.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 16 additions & 0 deletions tests/test_impersonated_credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
):
Expand Down

0 comments on commit 52c8ef9

Please sign in to comment.