feat: add quota project to base credentials class (#546)
diff --git a/tests/compute_engine/test_credentials.py b/tests/compute_engine/test_credentials.py
index 363dc9d..4ee6536 100644
--- a/tests/compute_engine/test_credentials.py
+++ b/tests/compute_engine/test_credentials.py
@@ -59,6 +59,8 @@
assert not self.credentials.requires_scopes
# Service account email hasn't been populated
assert self.credentials.service_account_email == "default"
+ # No quota project
+ assert not self.credentials._quota_project_id
@mock.patch(
"google.auth._helpers.utcnow",
@@ -131,6 +133,11 @@
# Credentials should now be valid.
assert self.credentials.valid
+ def test_with_quota_project(self):
+ quota_project_creds = self.credentials.with_quota_project("project-foo")
+
+ assert quota_project_creds._quota_project_id == "project-foo"
+
class TestIDTokenCredentials(object):
credentials = None
@@ -154,6 +161,8 @@
# Signer is initialized
assert self.credentials.signer
assert self.credentials.signer_email == "[email protected]"
+ # No quota project
+ assert not self.credentials._quota_project_id
@mock.patch(
"google.auth._helpers.utcnow",
@@ -394,6 +403,121 @@
)
@mock.patch("google.auth.compute_engine._metadata.get", autospec=True)
@mock.patch("google.auth.iam.Signer.sign", autospec=True)
+ def test_with_quota_project(self, sign, get, utcnow):
+ get.side_effect = [
+ {"email": "[email protected]", "scopes": ["one", "two"]}
+ ]
+ sign.side_effect = [b"signature"]
+
+ request = mock.create_autospec(transport.Request, instance=True)
+ self.credentials = credentials.IDTokenCredentials(
+ request=request, target_audience="https://audience.com"
+ )
+ self.credentials = self.credentials.with_quota_project("project-foo")
+
+ assert self.credentials._quota_project_id == "project-foo"
+
+ # Generate authorization grant:
+ token = self.credentials._make_authorization_grant_assertion()
+ payload = jwt.decode(token, verify=False)
+
+ # The JWT token signature is 'signature' encoded in base 64:
+ assert token.endswith(b".c2lnbmF0dXJl")
+
+ # Check that the credentials have the token and proper expiration
+ assert payload == {
+ "aud": "https://www.googleapis.com/oauth2/v4/token",
+ "exp": 3600,
+ "iat": 0,
+ "iss": "[email protected]",
+ "target_audience": "https://audience.com",
+ }
+
+ # Check that the signer have been initialized with a Request object
+ assert isinstance(self.credentials._signer._request, transport.Request)
+
+ @responses.activate
+ def test_with_quota_project_integration(self):
+ """ Test that it is possible to refresh credentials
+ generated from `with_quota_project`.
+
+ Instead of mocking the methods, the HTTP responses
+ have been mocked.
+ """
+
+ # mock information about credentials
+ responses.add(
+ responses.GET,
+ "http://metadata.google.internal/computeMetadata/v1/instance/"
+ "service-accounts/default/?recursive=true",
+ status=200,
+ content_type="application/json",
+ json={
+ "scopes": "email",
+ "email": "[email protected]",
+ "aliases": ["default"],
+ },
+ )
+
+ # mock token for credentials
+ responses.add(
+ responses.GET,
+ "http://metadata.google.internal/computeMetadata/v1/instance/"
+ "service-accounts/[email protected]/token",
+ status=200,
+ content_type="application/json",
+ json={
+ "access_token": "some-token",
+ "expires_in": 3210,
+ "token_type": "Bearer",
+ },
+ )
+
+ # mock sign blob endpoint
+ signature = base64.b64encode(b"some-signature").decode("utf-8")
+ responses.add(
+ responses.POST,
+ "https://iamcredentials.googleapis.com/v1/projects/-/"
+ "serviceAccounts/[email protected]:signBlob?alt=json",
+ status=200,
+ content_type="application/json",
+ json={"keyId": "some-key-id", "signedBlob": signature},
+ )
+
+ id_token = "{}.{}.{}".format(
+ base64.b64encode(b'{"some":"some"}').decode("utf-8"),
+ base64.b64encode(b'{"exp": 3210}').decode("utf-8"),
+ base64.b64encode(b"token").decode("utf-8"),
+ )
+
+ # mock id token endpoint
+ responses.add(
+ responses.POST,
+ "https://www.googleapis.com/oauth2/v4/token",
+ status=200,
+ content_type="application/json",
+ json={"id_token": id_token, "expiry": 3210},
+ )
+
+ self.credentials = credentials.IDTokenCredentials(
+ request=requests.Request(),
+ service_account_email="[email protected]",
+ target_audience="https://audience.com",
+ )
+
+ self.credentials = self.credentials.with_quota_project("project-foo")
+
+ self.credentials.refresh(requests.Request())
+
+ assert self.credentials.token is not None
+ assert self.credentials._quota_project_id == "project-foo"
+
+ @mock.patch(
+ "google.auth._helpers.utcnow",
+ return_value=datetime.datetime.utcfromtimestamp(0),
+ )
+ @mock.patch("google.auth.compute_engine._metadata.get", autospec=True)
+ @mock.patch("google.auth.iam.Signer.sign", autospec=True)
@mock.patch("google.oauth2._client.id_token_jwt_grant", autospec=True)
def test_refresh_success(self, id_token_jwt_grant, sign, get, utcnow):
get.side_effect = [
@@ -551,6 +675,23 @@
@mock.patch(
"google.auth.compute_engine._metadata.get_service_account_info", autospec=True
)
+ def test_id_token_with_quota_project(self, get_service_account_info):
+ get_service_account_info.return_value = {"email": "[email protected]"}
+
+ cred = credentials.IDTokenCredentials(
+ mock.Mock(), "audience", use_metadata_identity_endpoint=True
+ )
+ cred = cred.with_quota_project("project-foo")
+
+ assert cred._quota_project_id == "project-foo"
+ assert cred._use_metadata_identity_endpoint
+ assert cred._signer is None
+ assert cred._token_uri is None
+ assert cred._service_account_email == "[email protected]"
+
+ @mock.patch(
+ "google.auth.compute_engine._metadata.get_service_account_info", autospec=True
+ )
@mock.patch("google.auth.compute_engine._metadata.get", autospec=True)
def test_invalid_id_token_from_metadata(self, get, get_service_account_info):
get.return_value = "invalid_id_token"
diff --git a/tests/oauth2/test_credentials.py b/tests/oauth2/test_credentials.py
index 78b1012..69d9fbc 100644
--- a/tests/oauth2/test_credentials.py
+++ b/tests/oauth2/test_credentials.py
@@ -454,6 +454,13 @@
cred.refresh(None)
assert cred.token == "access_token"
+ def test_with_quota_project(self):
+ cred = credentials.UserAccessTokenCredentials()
+ quota_project_cred = cred.with_quota_project("project-foo")
+
+ assert quota_project_cred._quota_project_id == "project-foo"
+ assert quota_project_cred._account == cred._account
+
@mock.patch(
"google.oauth2.credentials.UserAccessTokenCredentials.apply", autospec=True
)
diff --git a/tests/oauth2/test_service_account.py b/tests/oauth2/test_service_account.py
index 457d472..7f27dad 100644
--- a/tests/oauth2/test_service_account.py
+++ b/tests/oauth2/test_service_account.py
@@ -291,6 +291,11 @@
new_credentials = credentials.with_target_audience("https://new.example.com")
assert new_credentials._target_audience == "https://new.example.com"
+ def test_with_quota_project(self):
+ credentials = self.make_credentials()
+ new_credentials = credentials.with_quota_project("project-foo")
+ assert new_credentials._quota_project_id == "project-foo"
+
def test__make_authorization_grant_assertion(self):
credentials = self.make_credentials()
token = credentials._make_authorization_grant_assertion()
diff --git a/tests/test__default.py b/tests/test__default.py
index 3c87b35..0665efa 100644
--- a/tests/test__default.py
+++ b/tests/test__default.py
@@ -21,6 +21,7 @@
from google.auth import _default
from google.auth import app_engine
from google.auth import compute_engine
+from google.auth import credentials
from google.auth import environment_vars
from google.auth import exceptions
from google.oauth2 import service_account
@@ -48,9 +49,12 @@
with open(SERVICE_ACCOUNT_FILE) as fh:
SERVICE_ACCOUNT_FILE_DATA = json.load(fh)
+MOCK_CREDENTIALS = mock.Mock(spec=credentials.Credentials)
+MOCK_CREDENTIALS.with_quota_project.return_value = MOCK_CREDENTIALS
+
LOAD_FILE_PATCH = mock.patch(
"google.auth._default.load_credentials_from_file",
- return_value=(mock.sentinel.credentials, mock.sentinel.project_id),
+ return_value=(MOCK_CREDENTIALS, mock.sentinel.project_id),
autospec=True,
)
@@ -136,6 +140,16 @@
assert credentials.scopes == ["https://www.google.com/calendar/feeds"]
+def test_load_credentials_from_file_authorized_user_cloud_sdk_with_quota_project():
+ credentials, project_id = _default.load_credentials_from_file(
+ AUTHORIZED_USER_CLOUD_SDK_FILE, quota_project_id="project-foo"
+ )
+
+ assert isinstance(credentials, google.oauth2.credentials.Credentials)
+ assert project_id is None
+ assert credentials.quota_project_id == "project-foo"
+
+
def test_load_credentials_from_file_service_account():
credentials, project_id = _default.load_credentials_from_file(SERVICE_ACCOUNT_FILE)
assert isinstance(credentials, service_account.Credentials)
@@ -173,19 +187,19 @@
credentials, project_id = _default._get_explicit_environ_credentials()
- assert credentials is mock.sentinel.credentials
+ assert credentials is MOCK_CREDENTIALS
assert project_id is mock.sentinel.project_id
load.assert_called_with("filename")
@LOAD_FILE_PATCH
def test__get_explicit_environ_credentials_no_project_id(load, monkeypatch):
- load.return_value = mock.sentinel.credentials, None
+ load.return_value = MOCK_CREDENTIALS, None
monkeypatch.setenv(environment_vars.CREDENTIALS, "filename")
credentials, project_id = _default._get_explicit_environ_credentials()
- assert credentials is mock.sentinel.credentials
+ assert credentials is MOCK_CREDENTIALS
assert project_id is None
@@ -198,7 +212,7 @@
credentials, project_id = _default._get_gcloud_sdk_credentials()
- assert credentials is mock.sentinel.credentials
+ assert credentials is MOCK_CREDENTIALS
assert project_id is mock.sentinel.project_id
load.assert_called_with(SERVICE_ACCOUNT_FILE)
@@ -226,11 +240,11 @@
def test__get_gcloud_sdk_credentials_project_id(load, unused_isfile, get_project_id):
# Don't return a project ID from load file, make the function check
# the Cloud SDK project.
- load.return_value = mock.sentinel.credentials, None
+ load.return_value = MOCK_CREDENTIALS, None
credentials, project_id = _default._get_gcloud_sdk_credentials()
- assert credentials == mock.sentinel.credentials
+ assert credentials == MOCK_CREDENTIALS
assert project_id == mock.sentinel.project_id
assert get_project_id.called
@@ -241,11 +255,11 @@
def test__get_gcloud_sdk_credentials_no_project_id(load, unused_isfile, get_project_id):
# Don't return a project ID from load file, make the function check
# the Cloud SDK project.
- load.return_value = mock.sentinel.credentials, None
+ load.return_value = MOCK_CREDENTIALS, None
credentials, project_id = _default._get_gcloud_sdk_credentials()
- assert credentials == mock.sentinel.credentials
+ assert credentials == MOCK_CREDENTIALS
assert project_id is None
assert get_project_id.called
@@ -351,58 +365,58 @@
@mock.patch(
"google.auth._default._get_explicit_environ_credentials",
- return_value=(mock.sentinel.credentials, mock.sentinel.project_id),
+ return_value=(MOCK_CREDENTIALS, mock.sentinel.project_id),
autospec=True,
)
def test_default_early_out(unused_get):
- assert _default.default() == (mock.sentinel.credentials, mock.sentinel.project_id)
+ assert _default.default() == (MOCK_CREDENTIALS, mock.sentinel.project_id)
@mock.patch(
"google.auth._default._get_explicit_environ_credentials",
- return_value=(mock.sentinel.credentials, mock.sentinel.project_id),
+ return_value=(MOCK_CREDENTIALS, mock.sentinel.project_id),
autospec=True,
)
def test_default_explict_project_id(unused_get, monkeypatch):
monkeypatch.setenv(environment_vars.PROJECT, "explicit-env")
- assert _default.default() == (mock.sentinel.credentials, "explicit-env")
+ assert _default.default() == (MOCK_CREDENTIALS, "explicit-env")
@mock.patch(
"google.auth._default._get_explicit_environ_credentials",
- return_value=(mock.sentinel.credentials, mock.sentinel.project_id),
+ return_value=(MOCK_CREDENTIALS, mock.sentinel.project_id),
autospec=True,
)
def test_default_explict_legacy_project_id(unused_get, monkeypatch):
monkeypatch.setenv(environment_vars.LEGACY_PROJECT, "explicit-env")
- assert _default.default() == (mock.sentinel.credentials, "explicit-env")
+ assert _default.default() == (MOCK_CREDENTIALS, "explicit-env")
@mock.patch("logging.Logger.warning", autospec=True)
@mock.patch(
"google.auth._default._get_explicit_environ_credentials",
- return_value=(mock.sentinel.credentials, None),
+ return_value=(MOCK_CREDENTIALS, None),
autospec=True,
)
@mock.patch(
"google.auth._default._get_gcloud_sdk_credentials",
- return_value=(mock.sentinel.credentials, None),
+ return_value=(MOCK_CREDENTIALS, None),
autospec=True,
)
@mock.patch(
"google.auth._default._get_gae_credentials",
- return_value=(mock.sentinel.credentials, None),
+ return_value=(MOCK_CREDENTIALS, None),
autospec=True,
)
@mock.patch(
"google.auth._default._get_gce_credentials",
- return_value=(mock.sentinel.credentials, None),
+ return_value=(MOCK_CREDENTIALS, None),
autospec=True,
)
def test_default_without_project_id(
unused_gce, unused_gae, unused_sdk, unused_explicit, logger_warning
):
- assert _default.default() == (mock.sentinel.credentials, None)
+ assert _default.default() == (MOCK_CREDENTIALS, None)
logger_warning.assert_called_with(mock.ANY, mock.ANY, mock.ANY)
@@ -433,10 +447,14 @@
@mock.patch(
"google.auth._default._get_explicit_environ_credentials",
- return_value=(mock.sentinel.credentials, mock.sentinel.project_id),
+ return_value=(MOCK_CREDENTIALS, mock.sentinel.project_id),
autospec=True,
)
[email protected]("google.auth.credentials.with_scopes_if_required", autospec=True)
[email protected](
+ "google.auth.credentials.with_scopes_if_required",
+ return_value=MOCK_CREDENTIALS,
+ autospec=True,
+)
def test_default_scoped(with_scopes, unused_get):
scopes = ["one", "two"]
@@ -444,12 +462,12 @@
assert credentials == with_scopes.return_value
assert project_id == mock.sentinel.project_id
- with_scopes.assert_called_once_with(mock.sentinel.credentials, scopes)
+ with_scopes.assert_called_once_with(MOCK_CREDENTIALS, scopes)
@mock.patch(
"google.auth._default._get_explicit_environ_credentials",
- return_value=(mock.sentinel.credentials, mock.sentinel.project_id),
+ return_value=(MOCK_CREDENTIALS, mock.sentinel.project_id),
autospec=True,
)
def test_default_no_app_engine_compute_engine_module(unused_get):
@@ -463,7 +481,4 @@
with mock.patch.dict("sys.modules"):
sys.modules["google.auth.compute_engine"] = None
sys.modules["google.auth.app_engine"] = None
- assert _default.default() == (
- mock.sentinel.credentials,
- mock.sentinel.project_id,
- )
+ assert _default.default() == (MOCK_CREDENTIALS, mock.sentinel.project_id)
diff --git a/tests/test_app_engine.py b/tests/test_app_engine.py
index 9dfdfa6..846d314 100644
--- a/tests/test_app_engine.py
+++ b/tests/test_app_engine.py
@@ -102,6 +102,7 @@
# Scopes are required
assert not credentials.scopes
assert credentials.requires_scopes
+ assert not credentials.quota_project_id
def test_with_scopes(self, app_identity):
credentials = app_engine.Credentials()
@@ -114,6 +115,16 @@
assert scoped_credentials.has_scopes(["email"])
assert not scoped_credentials.requires_scopes
+ def test_with_quota_project(self, app_identity):
+ credentials = app_engine.Credentials()
+
+ assert not credentials.scopes
+ assert not credentials.quota_project_id
+
+ quota_project_creds = credentials.with_quota_project("project-foo")
+
+ assert quota_project_creds.quota_project_id == "project-foo"
+
def test_service_account_email_implicit(self, app_identity):
app_identity.get_service_account_name.return_value = (
mock.sentinel.service_account_email
diff --git a/tests/test_credentials.py b/tests/test_credentials.py
index 16ddd9b..2023fac 100644
--- a/tests/test_credentials.py
+++ b/tests/test_credentials.py
@@ -24,6 +24,9 @@
def refresh(self, request):
self.token = request
+ def with_quota_project(self, quota_project_id):
+ raise NotImplementedError()
+
def test_credentials_constructor():
credentials = CredentialsImpl()
@@ -112,6 +115,12 @@
assert headers == {}
+def test_anonymous_credentials_with_quota_project():
+ with pytest.raises(ValueError):
+ anon = credentials.AnonymousCredentials()
+ anon.with_quota_project("project-foo")
+
+
class ReadOnlyScopedCredentialsImpl(credentials.ReadOnlyScoped, CredentialsImpl):
@property
def requires_scopes(self):
diff --git a/tests/test_iam.py b/tests/test_iam.py
index ea7d08a..e20eeba 100644
--- a/tests/test_iam.py
+++ b/tests/test_iam.py
@@ -50,6 +50,9 @@
def refresh(self, request):
pass
+ def with_quota_project(self, quota_project_id):
+ raise NotImplementedError()
+
return CredentialsImpl()
diff --git a/tests/test_impersonated_credentials.py b/tests/test_impersonated_credentials.py
index e0b5b11..46850a0 100644
--- a/tests/test_impersonated_credentials.py
+++ b/tests/test_impersonated_credentials.py
@@ -311,6 +311,12 @@
signature = credentials.sign_bytes(b"signed bytes")
assert signature == b"signature"
+ def test_with_quota_project(self):
+ credentials = self.make_credentials()
+
+ quota_project_creds = credentials.with_quota_project("project-foo")
+ assert quota_project_creds._quota_project_id == "project-foo"
+
def test_id_token_success(
self, mock_donor_credentials, mock_authorizedsession_idtoken
):
@@ -435,3 +441,32 @@
id_creds.refresh(request)
assert id_creds.token == ID_TOKEN_DATA
+
+ def test_id_token_with_quota_project(
+ self, mock_donor_credentials, mock_authorizedsession_idtoken
+ ):
+ credentials = self.make_credentials(lifetime=None)
+ token = "token"
+ target_audience = "https://foo.bar"
+
+ expire_time = (
+ _helpers.utcnow().replace(microsecond=0) + datetime.timedelta(seconds=500)
+ ).isoformat("T") + "Z"
+ response_body = {"accessToken": token, "expireTime": expire_time}
+
+ request = self.make_request(
+ data=json.dumps(response_body), status=http_client.OK
+ )
+
+ credentials.refresh(request)
+
+ assert credentials.valid
+ assert not credentials.expired
+
+ id_creds = impersonated_credentials.IDTokenCredentials(
+ credentials, target_audience=target_audience
+ )
+ id_creds = id_creds.with_quota_project("project-foo")
+ id_creds.refresh(request)
+
+ assert id_creds.quota_project_id == "project-foo"
diff --git a/tests/test_jwt.py b/tests/test_jwt.py
index 488aee4..7aa031e 100644
--- a/tests/test_jwt.py
+++ b/tests/test_jwt.py
@@ -363,6 +363,18 @@
assert new_credentials._subject == self.credentials._subject
assert new_credentials._audience == new_audience
assert new_credentials._additional_claims == self.credentials._additional_claims
+ assert new_credentials._quota_project_id == self.credentials._quota_project_id
+
+ def test_with_quota_project(self):
+ quota_project_id = "project-foo"
+
+ new_credentials = self.credentials.with_quota_project(quota_project_id)
+ assert new_credentials._signer == self.credentials._signer
+ assert new_credentials._issuer == self.credentials._issuer
+ assert new_credentials._subject == self.credentials._subject
+ assert new_credentials._audience == self.credentials._audience
+ assert new_credentials._additional_claims == self.credentials._additional_claims
+ assert new_credentials._quota_project_id == quota_project_id
def test_sign_bytes(self):
to_sign = b"123"
@@ -507,6 +519,16 @@
assert new_credentials._subject == self.credentials._subject
assert new_credentials._additional_claims == new_claims
+ def test_with_quota_project(self):
+ quota_project_id = "project-foo"
+ new_credentials = self.credentials.with_quota_project(quota_project_id)
+
+ assert new_credentials._signer == self.credentials._signer
+ assert new_credentials._issuer == self.credentials._issuer
+ assert new_credentials._subject == self.credentials._subject
+ assert new_credentials._additional_claims == self.credentials._additional_claims
+ assert new_credentials._quota_project_id == quota_project_id
+
def test_sign_bytes(self):
to_sign = b"123"
signature = self.credentials.sign_bytes(to_sign)
diff --git a/tests/transport/test_grpc.py b/tests/transport/test_grpc.py
index c3da76d..ef2e2e2 100644
--- a/tests/transport/test_grpc.py
+++ b/tests/transport/test_grpc.py
@@ -52,6 +52,9 @@
def refresh(self, request):
self.token += "1"
+ def with_quota_project(self, quota_project_id):
+ raise NotImplementedError()
+
class TestAuthMetadataPlugin(object):
def test_call_no_refresh(self):
diff --git a/tests/transport/test_requests.py b/tests/transport/test_requests.py
index 77e1527..7ac55ce 100644
--- a/tests/transport/test_requests.py
+++ b/tests/transport/test_requests.py
@@ -109,6 +109,9 @@
def refresh(self, request):
self.token += "1"
+ def with_quota_project(self, quota_project_id):
+ raise NotImplementedError()
+
class TimeTickCredentialsStub(CredentialsStub):
"""Credentials that spend some (mocked) time when refreshing a token."""
diff --git a/tests/transport/test_urllib3.py b/tests/transport/test_urllib3.py
index 1a1c0a1..3158b92 100644
--- a/tests/transport/test_urllib3.py
+++ b/tests/transport/test_urllib3.py
@@ -65,6 +65,9 @@
def refresh(self, request):
self.token += "1"
+ def with_quota_project(self, quota_project_id):
+ raise NotImplementedError()
+
class HttpStub(object):
def __init__(self, responses, headers=None):