feat: Add custom scopes for access tokens from the metadata service (#633)
This works for App Engine, Cloud Run and Flex. On Compute Engine you
can request custom scopes, but they are ignored.
Co-authored-by: Tres Seaver <tseaver@palladion.com>
Co-authored-by: Bu Sun Kim <8822365+busunkim96@users.noreply.github.com>
diff --git a/google/auth/_default.py b/google/auth/_default.py
index de81c5b..4377893 100644
--- a/google/auth/_default.py
+++ b/google/auth/_default.py
@@ -274,10 +274,11 @@
gcloud config set project
3. If the application is running in the `App Engine standard environment`_
- then the credentials and project ID from the `App Identity Service`_
- are used.
- 4. If the application is running in `Compute Engine`_ or the
- `App Engine flexible environment`_ then the credentials and project ID
+ (first generation) then the credentials and project ID from the
+ `App Identity Service`_ are used.
+ 4. If the application is running in `Compute Engine`_ or `Cloud Run`_ or
+ the `App Engine flexible environment`_ or the `App Engine standard
+ environment`_ (second generation) then the credentials and project ID
are obtained from the `Metadata Service`_.
5. If no credentials are found,
:class:`~google.auth.exceptions.DefaultCredentialsError` will be raised.
@@ -293,6 +294,7 @@
/appengine/flexible
.. _Metadata Service: https://cloud.google.com/compute/docs\
/storing-retrieving-metadata
+ .. _Cloud Run: https://cloud.google.com/run
Example::
diff --git a/google/auth/_default_async.py b/google/auth/_default_async.py
index 3347fbf..1a725af 100644
--- a/google/auth/_default_async.py
+++ b/google/auth/_default_async.py
@@ -187,10 +187,11 @@
gcloud config set project
3. If the application is running in the `App Engine standard environment`_
- then the credentials and project ID from the `App Identity Service`_
- are used.
- 4. If the application is running in `Compute Engine`_ or the
- `App Engine flexible environment`_ then the credentials and project ID
+ (first generation) then the credentials and project ID from the
+ `App Identity Service`_ are used.
+ 4. If the application is running in `Compute Engine`_ or `Cloud Run`_ or
+ the `App Engine flexible environment`_ or the `App Engine standard
+ environment`_ (second generation) then the credentials and project ID
are obtained from the `Metadata Service`_.
5. If no credentials are found,
:class:`~google.auth.exceptions.DefaultCredentialsError` will be raised.
@@ -206,6 +207,7 @@
/appengine/flexible
.. _Metadata Service: https://cloud.google.com/compute/docs\
/storing-retrieving-metadata
+ .. _Cloud Run: https://cloud.google.com/run
Example::
diff --git a/google/auth/compute_engine/_metadata.py b/google/auth/compute_engine/_metadata.py
index 94e4ffb..5687a42 100644
--- a/google/auth/compute_engine/_metadata.py
+++ b/google/auth/compute_engine/_metadata.py
@@ -234,7 +234,7 @@
return get(request, path, params={"recursive": "true"})
-def get_service_account_token(request, service_account="default"):
+def get_service_account_token(request, service_account="default", scopes=None):
"""Get the OAuth 2.0 access token for a service account.
Args:
@@ -243,7 +243,8 @@
service_account (str): The string 'default' or a service account email
address. The determines which service account for which to acquire
an access token.
-
+ scopes (Optional[Union[str, List[str]]]): Optional string or list of
+ strings with auth scopes.
Returns:
Union[str, datetime]: The access token and its expiration.
@@ -251,9 +252,15 @@
google.auth.exceptions.TransportError: if an error occurred while
retrieving metadata.
"""
- token_json = get(
- request, "instance/service-accounts/{0}/token".format(service_account)
- )
+ if scopes:
+ if not isinstance(scopes, str):
+ scopes = ",".join(scopes)
+ params = {"scopes": scopes}
+ else:
+ params = None
+
+ path = "instance/service-accounts/{0}/token".format(service_account)
+ token_json = get(request, path, params=params)
token_expiry = _helpers.utcnow() + datetime.timedelta(
seconds=token_json["expires_in"]
)
diff --git a/google/auth/compute_engine/credentials.py b/google/auth/compute_engine/credentials.py
index 8a41ffc..4ac6c8c 100644
--- a/google/auth/compute_engine/credentials.py
+++ b/google/auth/compute_engine/credentials.py
@@ -32,29 +32,28 @@
from google.oauth2 import _client
-class Credentials(credentials.ReadOnlyScoped, credentials.CredentialsWithQuotaProject):
+class Credentials(credentials.Scoped, credentials.CredentialsWithQuotaProject):
"""Compute Engine Credentials.
These credentials use the Google Compute Engine metadata server to obtain
- OAuth 2.0 access tokens associated with the instance's service account.
+ OAuth 2.0 access tokens associated with the instance's service account,
+ and are also used for Cloud Run, Flex and App Engine (except for the Python
+ 2.7 runtime).
For more information about Compute Engine authentication, including how
to configure scopes, see the `Compute Engine authentication
documentation`_.
- .. note:: Compute Engine instances can be created with scopes and therefore
- these credentials are considered to be 'scoped'. However, you can
- not use :meth:`~google.auth.credentials.ScopedCredentials.with_scopes`
- because it is not possible to change the scopes that the instance
- has. Also note that
- :meth:`~google.auth.credentials.ScopedCredentials.has_scopes` will not
- work until the credentials have been refreshed.
+ .. note:: On Compute Engine the metadata server ignores requested scopes.
+ On Cloud Run, Flex and App Engine the server honours requested scopes.
.. _Compute Engine authentication documentation:
https://cloud.google.com/compute/docs/authentication#using
"""
- def __init__(self, service_account_email="default", quota_project_id=None):
+ def __init__(
+ self, service_account_email="default", quota_project_id=None, scopes=None
+ ):
"""
Args:
service_account_email (str): The service account email to use, or
@@ -66,6 +65,7 @@
super(Credentials, self).__init__()
self._service_account_email = service_account_email
self._quota_project_id = quota_project_id
+ self._scopes = scopes
def _retrieve_info(self, request):
"""Retrieve information about the service account.
@@ -81,7 +81,10 @@
)
self._service_account_email = info["email"]
- self._scopes = info["scopes"]
+
+ # Don't override scopes requested by the user.
+ if self._scopes is None:
+ self._scopes = info["scopes"]
def refresh(self, request):
"""Refresh the access token and scopes.
@@ -98,7 +101,9 @@
try:
self._retrieve_info(request)
self.token, self.expiry = _metadata.get_service_account_token(
- request, service_account=self._service_account_email
+ request,
+ service_account=self._service_account_email,
+ scopes=self._scopes,
)
except exceptions.TransportError as caught_exc:
new_exc = exceptions.RefreshError(caught_exc)
@@ -115,14 +120,25 @@
@property
def requires_scopes(self):
- """False: Compute Engine credentials can not be scoped."""
- return False
+ return not self._scopes
@_helpers.copy_docstring(credentials.CredentialsWithQuotaProject)
def with_quota_project(self, quota_project_id):
return self.__class__(
service_account_email=self._service_account_email,
quota_project_id=quota_project_id,
+ scopes=self._scopes,
+ )
+
+ @_helpers.copy_docstring(credentials.Scoped)
+ def with_scopes(self, scopes):
+ # Compute Engine credentials can not be scoped (the metadata service
+ # ignores the scopes parameter). App Engine, Cloud Run and Flex support
+ # requesting scopes.
+ return self.__class__(
+ scopes=scopes,
+ service_account_email=self._service_account_email,
+ quota_project_id=self._quota_project_id,
)