diff --git a/google/auth/compute_engine/credentials.py b/google/auth/compute_engine/credentials.py index 74f12e7cc..65b3233f2 100644 --- a/google/auth/compute_engine/credentials.py +++ b/google/auth/compute_engine/credentials.py @@ -83,6 +83,10 @@ def __init__( self._scopes = scopes self._default_scopes = default_scopes self._universe_domain_cached = False + # If a specific service account email (i.e., not "default") is provided at + # initialization, we can skip fetching the full details from the metadata + # server. + self._service_account_info_cached = service_account_email != "default" if universe_domain: self._universe_domain = universe_domain self._universe_domain_cached = True @@ -90,6 +94,27 @@ def __init__( def _metric_header_for_usage(self): return metrics.CRED_TYPE_SA_MDS + def _retrieve_info(self): + """Retrieve information about the service account. + Updates the scopes and retrieves the full service account email. + """ + if self._service_account_info_cached: + return + + from google.auth.transport import requests as google_auth_requests + + request = google_auth_requests.Request() + info = _metadata.get_service_account_info( + request, service_account=self._service_account_email + ) + + self._service_account_email = info["email"] + + # Don't override scopes requested by the user. + if self._scopes is None: + self._scopes = info["scopes"] + self._service_account_info_cached = True + def refresh(self, request): """Refresh the access token and scopes. @@ -115,13 +140,17 @@ def refresh(self, request): def service_account_email(self): """The service account email. - .. note:: This is not guaranteed to be set until :meth:`refresh` has been - called. + .. note:: + Accessing this property for the first time may trigger a network + request to the metadata server to retrieve the default service + account email. """ + self._retrieve_info() return self._service_account_email @property def requires_scopes(self): + self._retrieve_info() return not self._scopes @property diff --git a/system_tests/secrets.tar.enc b/system_tests/secrets.tar.enc index 9a188ee8e..e5abdc202 100644 Binary files a/system_tests/secrets.tar.enc and b/system_tests/secrets.tar.enc differ diff --git a/tests/compute_engine/test_credentials.py b/tests/compute_engine/test_credentials.py index 8485ece4b..4468acb4c 100644 --- a/tests/compute_engine/test_credentials.py +++ b/tests/compute_engine/test_credentials.py @@ -72,14 +72,8 @@ def credentials_fixture(self): universe_domain=FAKE_UNIVERSE_DOMAIN, ) - def test_get_cred_info(self): - assert self.credentials.get_cred_info() == { - "credential_source": "metadata server", - "credential_type": "VM credentials", - "principal": "default", - } - def test_default_state(self): + self.credentials._service_account_info_cached = True assert not self.credentials.valid # Expiration hasn't been set yet assert not self.credentials.expired @@ -93,6 +87,37 @@ def test_default_state(self): assert self.credentials._universe_domain == "googleapis.com" assert not self.credentials._universe_domain_cached + @mock.patch("google.auth.compute_engine._metadata.get", autospec=True) + def test_get_cred_info(self, get): + get.side_effect = [ + {"email": FAKE_SERVICE_ACCOUNT_EMAIL, "scopes": ["one", "two"]} + ] + assert self.credentials.get_cred_info() == { + "credential_source": "metadata server", + "credential_type": "VM credentials", + "principal": FAKE_SERVICE_ACCOUNT_EMAIL, + } + + @mock.patch("google.auth.compute_engine._metadata.get", autospec=True) + def test_service_account_email_success(self, get): + get.side_effect = [ + {"email": "service-account@example.com", "scopes": ["one", "two"]} + ] + + assert not self.credentials._service_account_info_cached + assert self.credentials.service_account_email == "service-account@example.com" + assert self.credentials._service_account_info_cached + + @mock.patch("google.auth.compute_engine._metadata.get", autospec=True) + def test_service_account_require_scope_success(self, get): + get.side_effect = [ + {"email": "service-account@example.com", "scopes": ["one", "two"]} + ] + + assert not self.credentials._service_account_info_cached + assert not self.credentials.requires_scopes + assert self.credentials._service_account_info_cached + @mock.patch( "google.auth._helpers.utcnow", return_value=datetime.datetime.min + _helpers.REFRESH_THRESHOLD, @@ -108,10 +133,6 @@ def test_refresh_success(self, get, utcnow): assert self.credentials.token == "token" assert self.credentials.expiry == (utcnow() + datetime.timedelta(seconds=500)) - # Check the credential info - assert self.credentials.service_account_email == "default" - assert self.credentials._scopes is None - # Check that the credentials are valid (have a token and are not # expired) assert self.credentials.valid @@ -137,10 +158,6 @@ def test_refresh_success_with_scopes(self, get, utcnow, mock_metrics_header_valu assert self.credentials.token == "token" assert self.credentials.expiry == (utcnow() + datetime.timedelta(seconds=500)) - # Check the credential info - assert self.credentials.service_account_email == "default" - assert self.credentials._scopes == scopes - # Check that the credentials are valid (have a token and are not # expired) assert self.credentials.valid