aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--google/auth/_default.py10
-rw-r--r--google/auth/_default_async.py10
-rw-r--r--google/auth/compute_engine/_metadata.py17
-rw-r--r--google/auth/compute_engine/credentials.py44
-rw-r--r--system_tests/noxfile.py2
-rw-r--r--tests/compute_engine/test_credentials.py51
6 files changed, 104 insertions, 30 deletions
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 @@ def default(scopes=None, request=None, quota_project_id=None):
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 @@ def default(scopes=None, request=None, quota_project_id=None):
/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 @@ def default_async(scopes=None, request=None, quota_project_id=None):
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 @@ def default_async(scopes=None, request=None, quota_project_id=None):
/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 @@ def get_service_account_info(request, service_account="default"):
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 @@ def get_service_account_token(request, service_account="default"):
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 @@ def get_service_account_token(request, service_account="default"):
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.auth.compute_engine import _metadata
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 @@ class Credentials(credentials.ReadOnlyScoped, credentials.CredentialsWithQuotaPr
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 @@ class Credentials(credentials.ReadOnlyScoped, credentials.CredentialsWithQuotaPr
)
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 @@ class Credentials(credentials.ReadOnlyScoped, credentials.CredentialsWithQuotaPr
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 @@ class Credentials(credentials.ReadOnlyScoped, credentials.CredentialsWithQuotaPr
@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,
)
diff --git a/system_tests/noxfile.py b/system_tests/noxfile.py
index 0f852ea..699a1b3 100644
--- a/system_tests/noxfile.py
+++ b/system_tests/noxfile.py
@@ -315,7 +315,7 @@ def default_explicit_service_account_async(session):
session.env[EXPECT_PROJECT_ENV] = "1"
session.install(*(TEST_DEPENDENCIES_SYNC + TEST_DEPENDENCIES_ASYNC))
session.install(LIBRARY_DIR)
- session.run("pytest", "system_tests_async/test_default.py",
+ session.run("pytest", "system_tests_async/test_default.py",
"system_tests_async/test_id_token.py")
diff --git a/tests/compute_engine/test_credentials.py b/tests/compute_engine/test_credentials.py
index 4ee6536..ebe9aa5 100644
--- a/tests/compute_engine/test_credentials.py
+++ b/tests/compute_engine/test_credentials.py
@@ -55,8 +55,8 @@ class TestCredentials(object):
assert not self.credentials.valid
# Expiration hasn't been set yet
assert not self.credentials.expired
- # Scopes aren't needed
- assert not self.credentials.requires_scopes
+ # Scopes are needed
+ assert self.credentials.requires_scopes
# Service account email hasn't been populated
assert self.credentials.service_account_email == "default"
# No quota project
@@ -96,6 +96,45 @@ class TestCredentials(object):
# expired)
assert self.credentials.valid
+ @mock.patch(
+ "google.auth._helpers.utcnow",
+ return_value=datetime.datetime.min + _helpers.CLOCK_SKEW,
+ )
+ @mock.patch("google.auth.compute_engine._metadata.get", autospec=True)
+ def test_refresh_success_with_scopes(self, get, utcnow):
+ get.side_effect = [
+ {
+ # First request is for sevice account info.
+ "email": "service-account@example.com",
+ "scopes": ["one", "two"],
+ },
+ {
+ # Second request is for the token.
+ "access_token": "token",
+ "expires_in": 500,
+ },
+ ]
+
+ # Refresh credentials
+ scopes = ["three", "four"]
+ self.credentials = self.credentials.with_scopes(scopes)
+ self.credentials.refresh(None)
+
+ # Check that the credentials have the token and proper expiration
+ assert self.credentials.token == "token"
+ assert self.credentials.expiry == (utcnow() + datetime.timedelta(seconds=500))
+
+ # Check the credential info
+ assert self.credentials.service_account_email == "service-account@example.com"
+ assert self.credentials._scopes == scopes
+
+ # Check that the credentials are valid (have a token and are not
+ # expired)
+ assert self.credentials.valid
+
+ kwargs = get.call_args[1]
+ assert kwargs == {"params": {"scopes": "three,four"}}
+
@mock.patch("google.auth.compute_engine._metadata.get", autospec=True)
def test_refresh_error(self, get):
get.side_effect = exceptions.TransportError("http error")
@@ -138,6 +177,14 @@ class TestCredentials(object):
assert quota_project_creds._quota_project_id == "project-foo"
+ def test_with_scopes(self):
+ assert self.credentials._scopes is None
+
+ scopes = ["one", "two"]
+ self.credentials = self.credentials.with_scopes(scopes)
+
+ assert self.credentials._scopes == scopes
+
class TestIDTokenCredentials(object):
credentials = None