From a1ba0f55156240830bd0c9d799640d96dbf947c1 Mon Sep 17 00:00:00 2001 From: Sai Sunder Srinivasan Date: Fri, 7 Oct 2022 04:37:08 +0000 Subject: [PATCH 1/2] feat: Introduce the functionality to override token_uri in credentials --- google/auth/compute_engine/credentials.py | 27 +++++++++++++- google/auth/credentials.py | 15 ++++++++ google/auth/external_account.py | 26 +++++++++++++- google/oauth2/credentials.py | 17 +++++++++ google/oauth2/service_account.py | 38 ++++++++++++++++++-- tests/compute_engine/test_credentials.py | 44 +++++++++++++++++++++++ tests/oauth2/test_credentials.py | 12 +++++++ tests/oauth2/test_service_account.py | 14 ++++++++ tests/test_external_account.py | 27 ++++++++++++++ 9 files changed, 216 insertions(+), 4 deletions(-) diff --git a/google/auth/compute_engine/credentials.py b/google/auth/compute_engine/credentials.py index 59b48dae6..e97fabea9 100644 --- a/google/auth/compute_engine/credentials.py +++ b/google/auth/compute_engine/credentials.py @@ -154,7 +154,11 @@ def with_scopes(self, scopes, default_scopes=None): _DEFAULT_TOKEN_URI = "https://www.googleapis.com/oauth2/v4/token" -class IDTokenCredentials(credentials.CredentialsWithQuotaProject, credentials.Signing): +class IDTokenCredentials( + credentials.CredentialsWithQuotaProject, + credentials.Signing, + credentials.CredentialsWithTokenUri, +): """Open ID Connect ID Token-based service account credentials. These credentials relies on the default service account of a GCE instance. @@ -302,6 +306,27 @@ def with_quota_project(self, quota_project_id): quota_project_id=quota_project_id, ) + @_helpers.copy_docstring(credentials.CredentialsWithTokenUri) + def with_token_uri(self, token_uri): + + # since the signer is already instantiated, + # the request is not needed + if self._use_metadata_identity_endpoint: + raise ValueError( + "If use_metadata_identity_endpoint is set, token_uri" " must not be set" + ) + else: + return self.__class__( + None, + service_account_email=self._service_account_email, + token_uri=token_uri, + target_audience=self._target_audience, + additional_claims=self._additional_claims.copy(), + signer=self.signer, + use_metadata_identity_endpoint=False, + quota_project_id=self.quota_project_id, + ) + def _make_authorization_grant_assertion(self): """Create the OAuth 2.0 assertion. This assertion is used during the OAuth 2.0 grant to acquire an diff --git a/google/auth/credentials.py b/google/auth/credentials.py index 004fde9c2..2735892d4 100644 --- a/google/auth/credentials.py +++ b/google/auth/credentials.py @@ -150,6 +150,21 @@ def with_quota_project(self, quota_project_id): raise NotImplementedError("This credential does not support quota project.") +class CredentialsWithTokenUri(Credentials): + """Abstract base for credentials supporting ``with_token_uri`` factory""" + + def with_token_uri(self, token_uri): + """Returns a copy of these credentials with a modified token uri. + + Args: + token_uri (str): The uri to use for fetching/exchanging tokens + + Returns: + google.oauth2.credentials.Credentials: A new credentials instance. + """ + raise NotImplementedError("This credential does not use token uri.") + + class AnonymousCredentials(Credentials): """Credentials that do not provide any authentication information. diff --git a/google/auth/external_account.py b/google/auth/external_account.py index eb216fb72..c1ba5efa0 100644 --- a/google/auth/external_account.py +++ b/google/auth/external_account.py @@ -55,7 +55,11 @@ @six.add_metaclass(abc.ABCMeta) -class Credentials(credentials.Scoped, credentials.CredentialsWithQuotaProject): +class Credentials( + credentials.Scoped, + credentials.CredentialsWithQuotaProject, + credentials.CredentialsWithTokenUri, +): """Base class for all external account credentials. This is used to instantiate Credentials for exchanging external account @@ -382,6 +386,26 @@ def with_quota_project(self, quota_project_id): d.pop("workforce_pool_user_project") return self.__class__(**d) + @_helpers.copy_docstring(credentials.CredentialsWithTokenUri) + def with_token_uri(self, token_uri): + d = dict( + audience=self._audience, + subject_token_type=self._subject_token_type, + token_url=token_uri, + credential_source=self._credential_source, + service_account_impersonation_url=self._service_account_impersonation_url, + service_account_impersonation_options=self._service_account_impersonation_options, + client_id=self._client_id, + client_secret=self._client_secret, + quota_project_id=self._quota_project_id, + scopes=self._scopes, + default_scopes=self._default_scopes, + workforce_pool_user_project=self._workforce_pool_user_project, + ) + if not self.is_workforce_pool: + d.pop("workforce_pool_user_project") + return self.__class__(**d) + def _initialize_impersonated_credentials(self): """Generates an impersonated credentials. diff --git a/google/oauth2/credentials.py b/google/oauth2/credentials.py index 4cc502fea..8f1c3dda4 100644 --- a/google/oauth2/credentials.py +++ b/google/oauth2/credentials.py @@ -254,6 +254,23 @@ def with_quota_project(self, quota_project_id): enable_reauth_refresh=self._enable_reauth_refresh, ) + @_helpers.copy_docstring(credentials.CredentialsWithTokenUri) + def with_token_uri(self, token_uri): + + return self.__class__( + self.token, + refresh_token=self.refresh_token, + id_token=self.id_token, + token_uri=token_uri, + client_id=self.client_id, + client_secret=self.client_secret, + scopes=self.scopes, + default_scopes=self.default_scopes, + quota_project_id=self.quota_project_id, + rapt_token=self.rapt_token, + enable_reauth_refresh=self._enable_reauth_refresh, + ) + @_helpers.copy_docstring(credentials.Credentials) def refresh(self, request): scopes = self._scopes if self._scopes is not None else self._default_scopes diff --git a/google/oauth2/service_account.py b/google/oauth2/service_account.py index 5c4f340fa..0989750db 100644 --- a/google/oauth2/service_account.py +++ b/google/oauth2/service_account.py @@ -84,7 +84,10 @@ class Credentials( - credentials.Signing, credentials.Scoped, credentials.CredentialsWithQuotaProject + credentials.Signing, + credentials.Scoped, + credentials.CredentialsWithQuotaProject, + credentials.CredentialsWithTokenUri, ): """Service account credentials @@ -364,6 +367,22 @@ def with_quota_project(self, quota_project_id): always_use_jwt_access=self._always_use_jwt_access, ) + @_helpers.copy_docstring(credentials.CredentialsWithTokenUri) + def with_token_uri(self, token_uri): + + return self.__class__( + self._signer, + service_account_email=self._service_account_email, + default_scopes=self._default_scopes, + scopes=self._scopes, + token_uri=token_uri, + subject=self._subject, + project_id=self._project_id, + quota_project_id=self._quota_project_id, + additional_claims=self._additional_claims.copy(), + always_use_jwt_access=self._always_use_jwt_access, + ) + def _make_authorization_grant_assertion(self): """Create the OAuth 2.0 assertion. @@ -455,7 +474,11 @@ def signer_email(self): return self._service_account_email -class IDTokenCredentials(credentials.Signing, credentials.CredentialsWithQuotaProject): +class IDTokenCredentials( + credentials.Signing, + credentials.CredentialsWithQuotaProject, + credentials.CredentialsWithTokenUri, +): """Open ID Connect ID Token-based service account credentials. These credentials are largely similar to :class:`.Credentials`, but instead @@ -627,6 +650,17 @@ def with_quota_project(self, quota_project_id): quota_project_id=quota_project_id, ) + @_helpers.copy_docstring(credentials.CredentialsWithTokenUri) + def with_token_uri(self, token_uri): + return self.__class__( + self._signer, + service_account_email=self._service_account_email, + token_uri=token_uri, + target_audience=self._target_audience, + additional_claims=self._additional_claims.copy(), + quota_project_id=self._quota_project_id, + ) + def _make_authorization_grant_assertion(self): """Create the OAuth 2.0 assertion. diff --git a/tests/compute_engine/test_credentials.py b/tests/compute_engine/test_credentials.py index ebce176e8..6a2f8cc20 100644 --- a/tests/compute_engine/test_credentials.py +++ b/tests/compute_engine/test_credentials.py @@ -483,6 +483,50 @@ def test_with_quota_project(self, sign, get, utcnow): # Check that the signer have been initialized with a Request object assert isinstance(self.credentials._signer._request, transport.Request) + @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) + def test_with_token_uri(self, sign, get, utcnow): + get.side_effect = [ + {"email": "service-account@example.com", "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", + token_uri="http://xyz.com", + ) + assert self.credentials._token_uri == "http://xyz.com" + creds_with_token_uri = self.credentials.with_token_uri("http://abc.com") + assert creds_with_token_uri._token_uri == "http://abc.com" + + @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) + def test_with_token_uri_exception(self, sign, get, utcnow): + get.side_effect = [ + {"email": "service-account@example.com", "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", + use_metadata_identity_endpoint=True, + ) + assert self.credentials._token_uri is None + with pytest.raises(ValueError): + self.credentials.with_token_uri("http://abc.com") + @responses.activate def test_with_quota_project_integration(self): """ Test that it is possible to refresh credentials diff --git a/tests/oauth2/test_credentials.py b/tests/oauth2/test_credentials.py index e5f71def0..c8301078d 100644 --- a/tests/oauth2/test_credentials.py +++ b/tests/oauth2/test_credentials.py @@ -701,6 +701,18 @@ def test_with_quota_project(self): creds.apply(headers) assert "x-goog-user-project" in headers + def test_with_token_uri(self): + info = AUTH_USER_INFO.copy() + + creds = credentials.Credentials.from_authorized_user_info(info) + new_token_uri = "https://oauth2-eu.googleapis.com/token" + + assert creds._token_uri == credentials._GOOGLE_OAUTH2_TOKEN_ENDPOINT + + creds_with_new_token_uri = creds.with_token_uri(new_token_uri) + + assert creds_with_new_token_uri._token_uri == new_token_uri + def test_from_authorized_user_info(self): info = AUTH_USER_INFO.copy() diff --git a/tests/oauth2/test_service_account.py b/tests/oauth2/test_service_account.py index 1d1438485..4bd194b35 100644 --- a/tests/oauth2/test_service_account.py +++ b/tests/oauth2/test_service_account.py @@ -155,6 +155,13 @@ def test_with_quota_project(self): new_credentials.apply(hdrs, token="tok") assert "x-goog-user-project" in hdrs + def test_with_token_uri(self): + credentials = self.make_credentials() + new_token_uri = "https://example2.com/oauth2/token" + assert credentials._token_uri == self.TOKEN_URI + creds_with_new_token_uri = credentials.with_token_uri(new_token_uri) + assert creds_with_new_token_uri._token_uri == new_token_uri + def test__with_always_use_jwt_access(self): credentials = self.make_credentials() assert not credentials._always_use_jwt_access @@ -464,6 +471,13 @@ def test_with_quota_project(self): new_credentials = credentials.with_quota_project("project-foo") assert new_credentials._quota_project_id == "project-foo" + def test_with_token_uri(self): + credentials = self.make_credentials() + new_token_uri = "https://example2.com/oauth2/token" + assert credentials._token_uri == self.TOKEN_URI + creds_with_new_token_uri = credentials.with_token_uri(new_token_uri) + assert creds_with_new_token_uri._token_uri == new_token_uri + def test__make_authorization_grant_assertion(self): credentials = self.make_credentials() token = credentials._make_authorization_grant_assertion() diff --git a/tests/test_external_account.py b/tests/test_external_account.py index 920aa34ea..468152e05 100644 --- a/tests/test_external_account.py +++ b/tests/test_external_account.py @@ -542,6 +542,33 @@ def test_with_scopes_full_options_propagated(self): workforce_pool_user_project=None, ) + def test_with_token_uri(self): + credentials = self.make_credentials() + new_token_uri = "https://eu-sts.googleapis.com/v1/token" + + assert credentials._token_url == self.TOKEN_URL + + creds_with_new_token_uri = credentials.with_token_uri(new_token_uri) + + assert creds_with_new_token_uri._token_url == new_token_uri + + def test_with_token_uri_workforce_pool(self): + credentials = self.make_workforce_pool_credentials( + workforce_pool_user_project=self.WORKFORCE_POOL_USER_PROJECT + ) + + new_token_uri = "https://eu-sts.googleapis.com/v1/token" + + assert credentials._token_url == self.TOKEN_URL + + creds_with_new_token_uri = credentials.with_token_uri(new_token_uri) + + assert creds_with_new_token_uri._token_url == new_token_uri + assert ( + creds_with_new_token_uri.info.get("workforce_pool_user_project") + == self.WORKFORCE_POOL_USER_PROJECT + ) + def test_with_quota_project(self): credentials = self.make_credentials() From 15f41ea76da4fad9e383fd5dcaf1213339e525d8 Mon Sep 17 00:00:00 2001 From: Sai Sunder Srinivasan Date: Fri, 7 Oct 2022 04:41:18 +0000 Subject: [PATCH 2/2] update rt --- system_tests/secrets.tar.enc | Bin 10324 -> 10324 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/system_tests/secrets.tar.enc b/system_tests/secrets.tar.enc index 78f7254cb92a2cbd1bdb0b492bb9168b9fe9ff68..37bd829047df8f49c333ff8429aba105b6256177 100644 GIT binary patch literal 10324 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 m(FEmGkUwT^F)4XOoasezkp