Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: ADC can load an impersonated service account credentials. #962

Merged
merged 3 commits into from
Jan 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 == "[email protected]"
assert credentials._delegates == ["[email protected]"]
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