File manager - Edit - /home/u478019808/domains/bestandroidphones.store/public_html/static/img/logo/tests.tar
Back
test_metrics.py 0000644 00000006260 15025175543 0007636 0 ustar 00 # Copyright 2014 Google Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import platform import mock from google.auth import metrics from google.auth import version def test_add_metric_header(): headers = {} metrics.add_metric_header(headers, None) assert headers == {} headers = {"x-goog-api-client": "foo"} metrics.add_metric_header(headers, "bar") assert headers == {"x-goog-api-client": "foo bar"} headers = {} metrics.add_metric_header(headers, "bar") assert headers == {"x-goog-api-client": "bar"} @mock.patch.object(platform, "python_version", return_value="3.7") def test_versions(mock_python_version): version_save = version.__version__ version.__version__ = "1.1" assert metrics.python_and_auth_lib_version() == "gl-python/3.7 auth/1.1" version.__version__ = version_save @mock.patch( "google.auth.metrics.python_and_auth_lib_version", return_value="gl-python/3.7 auth/1.1", ) def test_metric_values(mock_python_and_auth_lib_version): assert ( metrics.token_request_access_token_mds() == "gl-python/3.7 auth/1.1 auth-request-type/at cred-type/mds" ) assert ( metrics.token_request_id_token_mds() == "gl-python/3.7 auth/1.1 auth-request-type/it cred-type/mds" ) assert ( metrics.token_request_access_token_impersonate() == "gl-python/3.7 auth/1.1 auth-request-type/at cred-type/imp" ) assert ( metrics.token_request_id_token_impersonate() == "gl-python/3.7 auth/1.1 auth-request-type/it cred-type/imp" ) assert ( metrics.token_request_access_token_sa_assertion() == "gl-python/3.7 auth/1.1 auth-request-type/at cred-type/sa" ) assert ( metrics.token_request_id_token_sa_assertion() == "gl-python/3.7 auth/1.1 auth-request-type/it cred-type/sa" ) assert metrics.token_request_user() == "gl-python/3.7 auth/1.1 cred-type/u" assert metrics.mds_ping() == "gl-python/3.7 auth/1.1 auth-request-type/mds" assert metrics.reauth_start() == "gl-python/3.7 auth/1.1 auth-request-type/re-start" assert ( metrics.reauth_continue() == "gl-python/3.7 auth/1.1 auth-request-type/re-cont" ) @mock.patch( "google.auth.metrics.python_and_auth_lib_version", return_value="gl-python/3.7 auth/1.1", ) def test_byoid_metric_header(mock_python_and_auth_lib_version): metrics_options = {} assert ( metrics.byoid_metrics_header(metrics_options) == "gl-python/3.7 auth/1.1 google-byoid-sdk" ) metrics_options["testKey"] = "testValue" assert ( metrics.byoid_metrics_header(metrics_options) == "gl-python/3.7 auth/1.1 google-byoid-sdk testKey/testValue" ) test__service_account_info.py 0000644 00000005072 15025175543 0012516 0 ustar 00 # Copyright 2016 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import json import os import pytest # type: ignore from google.auth import _service_account_info from google.auth import crypt DATA_DIR = os.path.join(os.path.dirname(__file__), "data") SERVICE_ACCOUNT_JSON_FILE = os.path.join(DATA_DIR, "service_account.json") GDCH_SERVICE_ACCOUNT_JSON_FILE = os.path.join(DATA_DIR, "gdch_service_account.json") with open(SERVICE_ACCOUNT_JSON_FILE, "r") as fh: SERVICE_ACCOUNT_INFO = json.load(fh) with open(GDCH_SERVICE_ACCOUNT_JSON_FILE, "r") as fh: GDCH_SERVICE_ACCOUNT_INFO = json.load(fh) def test_from_dict(): signer = _service_account_info.from_dict(SERVICE_ACCOUNT_INFO) assert isinstance(signer, crypt.RSASigner) assert signer.key_id == SERVICE_ACCOUNT_INFO["private_key_id"] def test_from_dict_es256_signer(): signer = _service_account_info.from_dict( GDCH_SERVICE_ACCOUNT_INFO, use_rsa_signer=False ) assert isinstance(signer, crypt.ES256Signer) assert signer.key_id == GDCH_SERVICE_ACCOUNT_INFO["private_key_id"] def test_from_dict_bad_private_key(): info = SERVICE_ACCOUNT_INFO.copy() info["private_key"] = "garbage" with pytest.raises(ValueError) as excinfo: _service_account_info.from_dict(info) assert excinfo.match(r"key") def test_from_dict_bad_format(): with pytest.raises(ValueError) as excinfo: _service_account_info.from_dict({}, require=("meep",)) assert excinfo.match(r"missing fields") def test_from_filename(): info, signer = _service_account_info.from_filename(SERVICE_ACCOUNT_JSON_FILE) for key, value in SERVICE_ACCOUNT_INFO.items(): assert info[key] == value assert isinstance(signer, crypt.RSASigner) assert signer.key_id == SERVICE_ACCOUNT_INFO["private_key_id"] def test_from_filename_es256_signer(): _, signer = _service_account_info.from_filename( GDCH_SERVICE_ACCOUNT_JSON_FILE, use_rsa_signer=False ) assert isinstance(signer, crypt.ES256Signer) assert signer.key_id == GDCH_SERVICE_ACCOUNT_INFO["private_key_id"] test__cloud_sdk.py 0000644 00000013226 15025175543 0010276 0 ustar 00 # Copyright 2016 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import io import json import os import subprocess import sys import mock import pytest # type: ignore from google.auth import _cloud_sdk from google.auth import environment_vars from google.auth import exceptions DATA_DIR = os.path.join(os.path.dirname(__file__), "data") AUTHORIZED_USER_FILE = os.path.join(DATA_DIR, "authorized_user.json") with io.open(AUTHORIZED_USER_FILE, "rb") as fh: AUTHORIZED_USER_FILE_DATA = json.load(fh) SERVICE_ACCOUNT_FILE = os.path.join(DATA_DIR, "service_account.json") with io.open(SERVICE_ACCOUNT_FILE, "rb") as fh: SERVICE_ACCOUNT_FILE_DATA = json.load(fh) @pytest.mark.parametrize( "data, expected_project_id", [(b"example-project\n", "example-project"), (b"", None)], ) def test_get_project_id(data, expected_project_id): check_output_patch = mock.patch( "subprocess.check_output", autospec=True, return_value=data ) with check_output_patch as check_output: project_id = _cloud_sdk.get_project_id() assert project_id == expected_project_id assert check_output.called @mock.patch( "subprocess.check_output", autospec=True, side_effect=subprocess.CalledProcessError(-1, "testing"), ) def test_get_project_id_call_error(check_output): project_id = _cloud_sdk.get_project_id() assert project_id is None assert check_output.called def test__run_subprocess_ignore_stderr(): command = [ sys.executable, "-c", "from __future__ import print_function;" + "import sys;" + "print('error', file=sys.stderr);" + "print('output', file=sys.stdout)", ] # If we ignore stderr, then the output only has stdout output = _cloud_sdk._run_subprocess_ignore_stderr(command) assert output == b"output\n" # If we pipe stderr to stdout, then the output is mixed with stdout and stderr. output = subprocess.check_output(command, stderr=subprocess.STDOUT) assert output == b"output\nerror\n" or output == b"error\noutput\n" @mock.patch("os.name", new="nt") def test_get_project_id_windows(): check_output_patch = mock.patch( "subprocess.check_output", autospec=True, return_value=b"example-project\n" ) with check_output_patch as check_output: project_id = _cloud_sdk.get_project_id() assert project_id == "example-project" assert check_output.called # Make sure the executable is `gcloud.cmd`. args = check_output.call_args[0] command = args[0] executable = command[0] assert executable == "gcloud.cmd" @mock.patch("google.auth._cloud_sdk.get_config_path", autospec=True) def test_get_application_default_credentials_path(get_config_dir): config_path = "config_path" get_config_dir.return_value = config_path credentials_path = _cloud_sdk.get_application_default_credentials_path() assert credentials_path == os.path.join( config_path, _cloud_sdk._CREDENTIALS_FILENAME ) def test_get_config_path_env_var(monkeypatch): config_path_sentinel = "config_path" monkeypatch.setenv(environment_vars.CLOUD_SDK_CONFIG_DIR, config_path_sentinel) config_path = _cloud_sdk.get_config_path() assert config_path == config_path_sentinel @mock.patch("os.path.expanduser") def test_get_config_path_unix(expanduser): expanduser.side_effect = lambda path: path config_path = _cloud_sdk.get_config_path() assert os.path.split(config_path) == ("~/.config", _cloud_sdk._CONFIG_DIRECTORY) @mock.patch("os.name", new="nt") def test_get_config_path_windows(monkeypatch): appdata = "appdata" monkeypatch.setenv(_cloud_sdk._WINDOWS_CONFIG_ROOT_ENV_VAR, appdata) config_path = _cloud_sdk.get_config_path() assert os.path.split(config_path) == (appdata, _cloud_sdk._CONFIG_DIRECTORY) @mock.patch("os.name", new="nt") def test_get_config_path_no_appdata(monkeypatch): monkeypatch.delenv(_cloud_sdk._WINDOWS_CONFIG_ROOT_ENV_VAR, raising=False) monkeypatch.setenv("SystemDrive", "G:") config_path = _cloud_sdk.get_config_path() assert os.path.split(config_path) == ("G:/\\", _cloud_sdk._CONFIG_DIRECTORY) @mock.patch("os.name", new="nt") @mock.patch("subprocess.check_output", autospec=True) def test_get_auth_access_token_windows(check_output): check_output.return_value = b"access_token\n" token = _cloud_sdk.get_auth_access_token() assert token == "access_token" check_output.assert_called_with( ("gcloud.cmd", "auth", "print-access-token"), stderr=subprocess.STDOUT ) @mock.patch("subprocess.check_output", autospec=True) def test_get_auth_access_token_with_account(check_output): check_output.return_value = b"access_token\n" token = _cloud_sdk.get_auth_access_token(account="account") assert token == "access_token" check_output.assert_called_with( ("gcloud", "auth", "print-access-token", "--account=account"), stderr=subprocess.STDOUT, ) @mock.patch("subprocess.check_output", autospec=True) def test_get_auth_access_token_with_exception(check_output): check_output.side_effect = OSError() with pytest.raises(exceptions.UserAccessTokenError): _cloud_sdk.get_auth_access_token(account="account") test_app_engine.py 0000644 00000016377 15025175543 0010307 0 ustar 00 # Copyright 2016 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import datetime import mock import pytest # type: ignore from google.auth import app_engine class _AppIdentityModule(object): """The interface of the App Idenity app engine module. See https://cloud.google.com/appengine/docs/standard/python/refdocs /google.appengine.api.app_identity.app_identity """ def get_application_id(self): raise NotImplementedError() def sign_blob(self, bytes_to_sign, deadline=None): raise NotImplementedError() def get_service_account_name(self, deadline=None): raise NotImplementedError() def get_access_token(self, scopes, service_account_id=None): raise NotImplementedError() @pytest.fixture def app_identity(monkeypatch): """Mocks the app_identity module for google.auth.app_engine.""" app_identity_module = mock.create_autospec(_AppIdentityModule, instance=True) monkeypatch.setattr(app_engine, "app_identity", app_identity_module) yield app_identity_module def test_get_project_id(app_identity): app_identity.get_application_id.return_value = mock.sentinel.project assert app_engine.get_project_id() == mock.sentinel.project @mock.patch.object(app_engine, "app_identity", new=None) def test_get_project_id_missing_apis(): with pytest.raises(EnvironmentError) as excinfo: assert app_engine.get_project_id() assert excinfo.match(r"App Engine APIs are not available") class TestSigner(object): def test_key_id(self, app_identity): app_identity.sign_blob.return_value = ( mock.sentinel.key_id, mock.sentinel.signature, ) signer = app_engine.Signer() assert signer.key_id is None def test_sign(self, app_identity): app_identity.sign_blob.return_value = ( mock.sentinel.key_id, mock.sentinel.signature, ) signer = app_engine.Signer() to_sign = b"123" signature = signer.sign(to_sign) assert signature == mock.sentinel.signature app_identity.sign_blob.assert_called_with(to_sign) class TestCredentials(object): @mock.patch.object(app_engine, "app_identity", new=None) def test_missing_apis(self): with pytest.raises(EnvironmentError) as excinfo: app_engine.Credentials() assert excinfo.match(r"App Engine APIs are not available") def test_default_state(self, app_identity): credentials = app_engine.Credentials() # Not token acquired yet assert not credentials.valid # Expiration hasn't been set yet assert not credentials.expired # Scopes are required assert not credentials.scopes assert not credentials.default_scopes assert credentials.requires_scopes assert not credentials.quota_project_id def test_with_scopes(self, app_identity): credentials = app_engine.Credentials() assert not credentials.scopes assert credentials.requires_scopes scoped_credentials = credentials.with_scopes(["email"]) assert scoped_credentials.has_scopes(["email"]) assert not scoped_credentials.requires_scopes def test_with_default_scopes(self, app_identity): credentials = app_engine.Credentials() assert not credentials.scopes assert not credentials.default_scopes assert credentials.requires_scopes scoped_credentials = credentials.with_scopes( scopes=None, default_scopes=["email"] ) 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 ) credentials = app_engine.Credentials() assert credentials.service_account_email == mock.sentinel.service_account_email assert app_identity.get_service_account_name.called def test_service_account_email_explicit(self, app_identity): credentials = app_engine.Credentials( service_account_id=mock.sentinel.service_account_email ) assert credentials.service_account_email == mock.sentinel.service_account_email assert not app_identity.get_service_account_name.called @mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min) def test_refresh(self, utcnow, app_identity): token = "token" ttl = 643942923 app_identity.get_access_token.return_value = token, ttl credentials = app_engine.Credentials( scopes=["email"], default_scopes=["profile"] ) credentials.refresh(None) app_identity.get_access_token.assert_called_with( credentials.scopes, credentials._service_account_id ) assert credentials.token == token assert credentials.expiry == datetime.datetime(1990, 5, 29, 1, 2, 3) assert credentials.valid assert not credentials.expired @mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min) def test_refresh_with_default_scopes(self, utcnow, app_identity): token = "token" ttl = 643942923 app_identity.get_access_token.return_value = token, ttl credentials = app_engine.Credentials(default_scopes=["email"]) credentials.refresh(None) app_identity.get_access_token.assert_called_with( credentials.default_scopes, credentials._service_account_id ) assert credentials.token == token assert credentials.expiry == datetime.datetime(1990, 5, 29, 1, 2, 3) assert credentials.valid assert not credentials.expired def test_sign_bytes(self, app_identity): app_identity.sign_blob.return_value = ( mock.sentinel.key_id, mock.sentinel.signature, ) credentials = app_engine.Credentials() to_sign = b"123" signature = credentials.sign_bytes(to_sign) assert signature == mock.sentinel.signature app_identity.sign_blob.assert_called_with(to_sign) def test_signer(self, app_identity): credentials = app_engine.Credentials() assert isinstance(credentials.signer, app_engine.Signer) def test_signer_email(self, app_identity): credentials = app_engine.Credentials() assert credentials.signer_email == credentials.service_account_email test_identity_pool.py 0000644 00000145612 15025175543 0011057 0 ustar 00 # Copyright 2020 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import datetime import http.client as http_client import json import os import urllib import mock import pytest # type: ignore from google.auth import _helpers from google.auth import exceptions from google.auth import identity_pool from google.auth import metrics from google.auth import transport CLIENT_ID = "username" CLIENT_SECRET = "password" # Base64 encoding of "username:password". BASIC_AUTH_ENCODING = "dXNlcm5hbWU6cGFzc3dvcmQ=" SERVICE_ACCOUNT_EMAIL = "service-1234@service-name.iam.gserviceaccount.com" SERVICE_ACCOUNT_IMPERSONATION_URL_BASE = ( "https://us-east1-iamcredentials.googleapis.com" ) SERVICE_ACCOUNT_IMPERSONATION_URL_ROUTE = "/v1/projects/-/serviceAccounts/{}:generateAccessToken".format( SERVICE_ACCOUNT_EMAIL ) SERVICE_ACCOUNT_IMPERSONATION_URL = ( SERVICE_ACCOUNT_IMPERSONATION_URL_BASE + SERVICE_ACCOUNT_IMPERSONATION_URL_ROUTE ) QUOTA_PROJECT_ID = "QUOTA_PROJECT_ID" SCOPES = ["scope1", "scope2"] DATA_DIR = os.path.join(os.path.dirname(__file__), "data") SUBJECT_TOKEN_TEXT_FILE = os.path.join(DATA_DIR, "external_subject_token.txt") SUBJECT_TOKEN_JSON_FILE = os.path.join(DATA_DIR, "external_subject_token.json") SUBJECT_TOKEN_FIELD_NAME = "access_token" with open(SUBJECT_TOKEN_TEXT_FILE) as fh: TEXT_FILE_SUBJECT_TOKEN = fh.read() with open(SUBJECT_TOKEN_JSON_FILE) as fh: JSON_FILE_CONTENT = json.load(fh) JSON_FILE_SUBJECT_TOKEN = JSON_FILE_CONTENT.get(SUBJECT_TOKEN_FIELD_NAME) TOKEN_URL = "https://sts.googleapis.com/v1/token" TOKEN_INFO_URL = "https://sts.googleapis.com/v1/introspect" SUBJECT_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:jwt" AUDIENCE = "//iam.googleapis.com/projects/123456/locations/global/workloadIdentityPools/POOL_ID/providers/PROVIDER_ID" WORKFORCE_AUDIENCE = ( "//iam.googleapis.com/locations/global/workforcePools/POOL_ID/providers/PROVIDER_ID" ) WORKFORCE_SUBJECT_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:id_token" WORKFORCE_POOL_USER_PROJECT = "WORKFORCE_POOL_USER_PROJECT_NUMBER" DEFAULT_UNIVERSE_DOMAIN = "googleapis.com" VALID_TOKEN_URLS = [ "https://sts.googleapis.com", "https://us-east-1.sts.googleapis.com", "https://US-EAST-1.sts.googleapis.com", "https://sts.us-east-1.googleapis.com", "https://sts.US-WEST-1.googleapis.com", "https://us-east-1-sts.googleapis.com", "https://US-WEST-1-sts.googleapis.com", "https://us-west-1-sts.googleapis.com/path?query", "https://sts-us-east-1.p.googleapis.com", ] INVALID_TOKEN_URLS = [ "https://iamcredentials.googleapis.com", "sts.googleapis.com", "https://", "http://sts.googleapis.com", "https://st.s.googleapis.com", "https://us-eas\t-1.sts.googleapis.com", "https:/us-east-1.sts.googleapis.com", "https://US-WE/ST-1-sts.googleapis.com", "https://sts-us-east-1.googleapis.com", "https://sts-US-WEST-1.googleapis.com", "testhttps://us-east-1.sts.googleapis.com", "https://us-east-1.sts.googleapis.comevil.com", "https://us-east-1.us-east-1.sts.googleapis.com", "https://us-ea.s.t.sts.googleapis.com", "https://sts.googleapis.comevil.com", "hhttps://us-east-1.sts.googleapis.com", "https://us- -1.sts.googleapis.com", "https://-sts.googleapis.com", "https://us-east-1.sts.googleapis.com.evil.com", "https://sts.pgoogleapis.com", "https://p.googleapis.com", "https://sts.p.com", "http://sts.p.googleapis.com", "https://xyz-sts.p.googleapis.com", "https://sts-xyz.123.p.googleapis.com", "https://sts-xyz.p1.googleapis.com", "https://sts-xyz.p.foo.com", "https://sts-xyz.p.foo.googleapis.com", ] VALID_SERVICE_ACCOUNT_IMPERSONATION_URLS = [ "https://iamcredentials.googleapis.com", "https://us-east-1.iamcredentials.googleapis.com", "https://US-EAST-1.iamcredentials.googleapis.com", "https://iamcredentials.us-east-1.googleapis.com", "https://iamcredentials.US-WEST-1.googleapis.com", "https://us-east-1-iamcredentials.googleapis.com", "https://US-WEST-1-iamcredentials.googleapis.com", "https://us-west-1-iamcredentials.googleapis.com/path?query", "https://iamcredentials-us-east-1.p.googleapis.com", ] INVALID_SERVICE_ACCOUNT_IMPERSONATION_URLS = [ "https://sts.googleapis.com", "iamcredentials.googleapis.com", "https://", "http://iamcredentials.googleapis.com", "https://iamcre.dentials.googleapis.com", "https://us-eas\t-1.iamcredentials.googleapis.com", "https:/us-east-1.iamcredentials.googleapis.com", "https://US-WE/ST-1-iamcredentials.googleapis.com", "https://iamcredentials-us-east-1.googleapis.com", "https://iamcredentials-US-WEST-1.googleapis.com", "testhttps://us-east-1.iamcredentials.googleapis.com", "https://us-east-1.iamcredentials.googleapis.comevil.com", "https://us-east-1.us-east-1.iamcredentials.googleapis.com", "https://us-ea.s.t.iamcredentials.googleapis.com", "https://iamcredentials.googleapis.comevil.com", "hhttps://us-east-1.iamcredentials.googleapis.com", "https://us- -1.iamcredentials.googleapis.com", "https://-iamcredentials.googleapis.com", "https://us-east-1.iamcredentials.googleapis.com.evil.com", "https://iamcredentials.pgoogleapis.com", "https://p.googleapis.com", "https://iamcredentials.p.com", "http://iamcredentials.p.googleapis.com", "https://xyz-iamcredentials.p.googleapis.com", "https://iamcredentials-xyz.123.p.googleapis.com", "https://iamcredentials-xyz.p1.googleapis.com", "https://iamcredentials-xyz.p.foo.com", "https://iamcredentials-xyz.p.foo.googleapis.com", ] class TestCredentials(object): CREDENTIAL_SOURCE_TEXT = {"file": SUBJECT_TOKEN_TEXT_FILE} CREDENTIAL_SOURCE_JSON = { "file": SUBJECT_TOKEN_JSON_FILE, "format": {"type": "json", "subject_token_field_name": "access_token"}, } CREDENTIAL_URL = "http://fakeurl.com" CREDENTIAL_SOURCE_TEXT_URL = {"url": CREDENTIAL_URL} CREDENTIAL_SOURCE_JSON_URL = { "url": CREDENTIAL_URL, "format": {"type": "json", "subject_token_field_name": "access_token"}, } SUCCESS_RESPONSE = { "access_token": "ACCESS_TOKEN", "issued_token_type": "urn:ietf:params:oauth:token-type:access_token", "token_type": "Bearer", "expires_in": 3600, "scope": " ".join(SCOPES), } @classmethod def make_mock_response(cls, status, data): response = mock.create_autospec(transport.Response, instance=True) response.status = status if isinstance(data, dict): response.data = json.dumps(data).encode("utf-8") else: response.data = data return response @classmethod def make_mock_request( cls, token_status=http_client.OK, token_data=None, *extra_requests ): responses = [] responses.append(cls.make_mock_response(token_status, token_data)) while len(extra_requests) > 0: # If service account impersonation is requested, mock the expected response. status, data, extra_requests = ( extra_requests[0], extra_requests[1], extra_requests[2:], ) responses.append(cls.make_mock_response(status, data)) request = mock.create_autospec(transport.Request) request.side_effect = responses return request @classmethod def assert_credential_request_kwargs( cls, request_kwargs, headers, url=CREDENTIAL_URL ): assert request_kwargs["url"] == url assert request_kwargs["method"] == "GET" assert request_kwargs["headers"] == headers assert request_kwargs.get("body", None) is None @classmethod def assert_token_request_kwargs( cls, request_kwargs, headers, request_data, token_url=TOKEN_URL ): assert request_kwargs["url"] == token_url assert request_kwargs["method"] == "POST" assert request_kwargs["headers"] == headers assert request_kwargs["body"] is not None body_tuples = urllib.parse.parse_qsl(request_kwargs["body"]) assert len(body_tuples) == len(request_data.keys()) for (k, v) in body_tuples: assert v.decode("utf-8") == request_data[k.decode("utf-8")] @classmethod def assert_impersonation_request_kwargs( cls, request_kwargs, headers, request_data, service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL, ): assert request_kwargs["url"] == service_account_impersonation_url assert request_kwargs["method"] == "POST" assert request_kwargs["headers"] == headers assert request_kwargs["body"] is not None body_json = json.loads(request_kwargs["body"].decode("utf-8")) assert body_json == request_data @classmethod def assert_underlying_credentials_refresh( cls, credentials, audience, subject_token, subject_token_type, token_url, service_account_impersonation_url=None, basic_auth_encoding=None, quota_project_id=None, used_scopes=None, credential_data=None, scopes=None, default_scopes=None, workforce_pool_user_project=None, ): """Utility to assert that a credentials are initialized with the expected attributes by calling refresh functionality and confirming response matches expected one and that the underlying requests were populated with the expected parameters. """ # STS token exchange request/response. token_response = cls.SUCCESS_RESPONSE.copy() token_headers = {"Content-Type": "application/x-www-form-urlencoded"} if basic_auth_encoding: token_headers["Authorization"] = "Basic " + basic_auth_encoding metrics_options = {} if credentials._service_account_impersonation_url: metrics_options["sa-impersonation"] = "true" else: metrics_options["sa-impersonation"] = "false" metrics_options["config-lifetime"] = "false" if credentials._credential_source_file: metrics_options["source"] = "file" else: metrics_options["source"] = "url" token_headers["x-goog-api-client"] = metrics.byoid_metrics_header( metrics_options ) if service_account_impersonation_url: token_scopes = "https://www.googleapis.com/auth/iam" else: token_scopes = " ".join(used_scopes or []) token_request_data = { "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange", "audience": audience, "requested_token_type": "urn:ietf:params:oauth:token-type:access_token", "scope": token_scopes, "subject_token": subject_token, "subject_token_type": subject_token_type, } if workforce_pool_user_project: token_request_data["options"] = urllib.parse.quote( json.dumps({"userProject": workforce_pool_user_project}) ) metrics_header_value = ( "gl-python/3.7 auth/1.1 auth-request-type/at cred-type/imp" ) if service_account_impersonation_url: # Service account impersonation request/response. expire_time = ( _helpers.utcnow().replace(microsecond=0) + datetime.timedelta(seconds=3600) ).isoformat("T") + "Z" impersonation_response = { "accessToken": "SA_ACCESS_TOKEN", "expireTime": expire_time, } impersonation_headers = { "Content-Type": "application/json", "authorization": "Bearer {}".format(token_response["access_token"]), "x-goog-api-client": metrics_header_value, "x-allowed-locations": "0x0", } impersonation_request_data = { "delegates": None, "scope": used_scopes, "lifetime": "3600s", } # Initialize mock request to handle token retrieval, token exchange and # service account impersonation request. requests = [] if credential_data: requests.append((http_client.OK, credential_data)) token_request_index = len(requests) requests.append((http_client.OK, token_response)) if service_account_impersonation_url: impersonation_request_index = len(requests) requests.append((http_client.OK, impersonation_response)) request = cls.make_mock_request(*[el for req in requests for el in req]) with mock.patch( "google.auth.metrics.token_request_access_token_impersonate", return_value=metrics_header_value, ): credentials.refresh(request) assert len(request.call_args_list) == len(requests) if credential_data: cls.assert_credential_request_kwargs(request.call_args_list[0][1], None) # Verify token exchange request parameters. cls.assert_token_request_kwargs( request.call_args_list[token_request_index][1], token_headers, token_request_data, token_url, ) # Verify service account impersonation request parameters if the request # is processed. if service_account_impersonation_url: cls.assert_impersonation_request_kwargs( request.call_args_list[impersonation_request_index][1], impersonation_headers, impersonation_request_data, service_account_impersonation_url, ) assert credentials.token == impersonation_response["accessToken"] else: assert credentials.token == token_response["access_token"] assert credentials.quota_project_id == quota_project_id assert credentials.scopes == scopes assert credentials.default_scopes == default_scopes @classmethod def make_credentials( cls, audience=AUDIENCE, subject_token_type=SUBJECT_TOKEN_TYPE, token_url=TOKEN_URL, token_info_url=TOKEN_INFO_URL, client_id=None, client_secret=None, quota_project_id=None, scopes=None, default_scopes=None, service_account_impersonation_url=None, credential_source=None, workforce_pool_user_project=None, ): return identity_pool.Credentials( audience=audience, subject_token_type=subject_token_type, token_url=token_url, token_info_url=token_info_url, service_account_impersonation_url=service_account_impersonation_url, credential_source=credential_source, client_id=client_id, client_secret=client_secret, quota_project_id=quota_project_id, scopes=scopes, default_scopes=default_scopes, workforce_pool_user_project=workforce_pool_user_project, ) @mock.patch.object(identity_pool.Credentials, "__init__", return_value=None) def test_from_info_full_options(self, mock_init): credentials = identity_pool.Credentials.from_info( { "audience": AUDIENCE, "subject_token_type": SUBJECT_TOKEN_TYPE, "token_url": TOKEN_URL, "token_info_url": TOKEN_INFO_URL, "service_account_impersonation_url": SERVICE_ACCOUNT_IMPERSONATION_URL, "service_account_impersonation": {"token_lifetime_seconds": 2800}, "client_id": CLIENT_ID, "client_secret": CLIENT_SECRET, "quota_project_id": QUOTA_PROJECT_ID, "credential_source": self.CREDENTIAL_SOURCE_TEXT, } ) # Confirm identity_pool.Credentials instantiated with expected attributes. assert isinstance(credentials, identity_pool.Credentials) mock_init.assert_called_once_with( audience=AUDIENCE, subject_token_type=SUBJECT_TOKEN_TYPE, token_url=TOKEN_URL, token_info_url=TOKEN_INFO_URL, service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL, service_account_impersonation_options={"token_lifetime_seconds": 2800}, client_id=CLIENT_ID, client_secret=CLIENT_SECRET, credential_source=self.CREDENTIAL_SOURCE_TEXT, quota_project_id=QUOTA_PROJECT_ID, workforce_pool_user_project=None, universe_domain=DEFAULT_UNIVERSE_DOMAIN, ) @mock.patch.object(identity_pool.Credentials, "__init__", return_value=None) def test_from_info_required_options_only(self, mock_init): credentials = identity_pool.Credentials.from_info( { "audience": AUDIENCE, "subject_token_type": SUBJECT_TOKEN_TYPE, "token_url": TOKEN_URL, "credential_source": self.CREDENTIAL_SOURCE_TEXT, } ) # Confirm identity_pool.Credentials instantiated with expected attributes. assert isinstance(credentials, identity_pool.Credentials) mock_init.assert_called_once_with( audience=AUDIENCE, subject_token_type=SUBJECT_TOKEN_TYPE, token_url=TOKEN_URL, token_info_url=None, service_account_impersonation_url=None, service_account_impersonation_options={}, client_id=None, client_secret=None, credential_source=self.CREDENTIAL_SOURCE_TEXT, quota_project_id=None, workforce_pool_user_project=None, universe_domain=DEFAULT_UNIVERSE_DOMAIN, ) @mock.patch.object(identity_pool.Credentials, "__init__", return_value=None) def test_from_info_workforce_pool(self, mock_init): credentials = identity_pool.Credentials.from_info( { "audience": WORKFORCE_AUDIENCE, "subject_token_type": WORKFORCE_SUBJECT_TOKEN_TYPE, "token_url": TOKEN_URL, "credential_source": self.CREDENTIAL_SOURCE_TEXT, "workforce_pool_user_project": WORKFORCE_POOL_USER_PROJECT, } ) # Confirm identity_pool.Credentials instantiated with expected attributes. assert isinstance(credentials, identity_pool.Credentials) mock_init.assert_called_once_with( audience=WORKFORCE_AUDIENCE, subject_token_type=WORKFORCE_SUBJECT_TOKEN_TYPE, token_url=TOKEN_URL, token_info_url=None, service_account_impersonation_url=None, service_account_impersonation_options={}, client_id=None, client_secret=None, credential_source=self.CREDENTIAL_SOURCE_TEXT, quota_project_id=None, workforce_pool_user_project=WORKFORCE_POOL_USER_PROJECT, universe_domain=DEFAULT_UNIVERSE_DOMAIN, ) @mock.patch.object(identity_pool.Credentials, "__init__", return_value=None) def test_from_file_full_options(self, mock_init, tmpdir): info = { "audience": AUDIENCE, "subject_token_type": SUBJECT_TOKEN_TYPE, "token_url": TOKEN_URL, "token_info_url": TOKEN_INFO_URL, "service_account_impersonation_url": SERVICE_ACCOUNT_IMPERSONATION_URL, "service_account_impersonation": {"token_lifetime_seconds": 2800}, "client_id": CLIENT_ID, "client_secret": CLIENT_SECRET, "quota_project_id": QUOTA_PROJECT_ID, "credential_source": self.CREDENTIAL_SOURCE_TEXT, } config_file = tmpdir.join("config.json") config_file.write(json.dumps(info)) credentials = identity_pool.Credentials.from_file(str(config_file)) # Confirm identity_pool.Credentials instantiated with expected attributes. assert isinstance(credentials, identity_pool.Credentials) mock_init.assert_called_once_with( audience=AUDIENCE, subject_token_type=SUBJECT_TOKEN_TYPE, token_url=TOKEN_URL, token_info_url=TOKEN_INFO_URL, service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL, service_account_impersonation_options={"token_lifetime_seconds": 2800}, client_id=CLIENT_ID, client_secret=CLIENT_SECRET, credential_source=self.CREDENTIAL_SOURCE_TEXT, quota_project_id=QUOTA_PROJECT_ID, workforce_pool_user_project=None, universe_domain=DEFAULT_UNIVERSE_DOMAIN, ) @mock.patch.object(identity_pool.Credentials, "__init__", return_value=None) def test_from_file_required_options_only(self, mock_init, tmpdir): info = { "audience": AUDIENCE, "subject_token_type": SUBJECT_TOKEN_TYPE, "token_url": TOKEN_URL, "credential_source": self.CREDENTIAL_SOURCE_TEXT, } config_file = tmpdir.join("config.json") config_file.write(json.dumps(info)) credentials = identity_pool.Credentials.from_file(str(config_file)) # Confirm identity_pool.Credentials instantiated with expected attributes. assert isinstance(credentials, identity_pool.Credentials) mock_init.assert_called_once_with( audience=AUDIENCE, subject_token_type=SUBJECT_TOKEN_TYPE, token_url=TOKEN_URL, token_info_url=None, service_account_impersonation_url=None, service_account_impersonation_options={}, client_id=None, client_secret=None, credential_source=self.CREDENTIAL_SOURCE_TEXT, quota_project_id=None, workforce_pool_user_project=None, universe_domain=DEFAULT_UNIVERSE_DOMAIN, ) @mock.patch.object(identity_pool.Credentials, "__init__", return_value=None) def test_from_file_workforce_pool(self, mock_init, tmpdir): info = { "audience": WORKFORCE_AUDIENCE, "subject_token_type": WORKFORCE_SUBJECT_TOKEN_TYPE, "token_url": TOKEN_URL, "credential_source": self.CREDENTIAL_SOURCE_TEXT, "workforce_pool_user_project": WORKFORCE_POOL_USER_PROJECT, } config_file = tmpdir.join("config.json") config_file.write(json.dumps(info)) credentials = identity_pool.Credentials.from_file(str(config_file)) # Confirm identity_pool.Credentials instantiated with expected attributes. assert isinstance(credentials, identity_pool.Credentials) mock_init.assert_called_once_with( audience=WORKFORCE_AUDIENCE, subject_token_type=WORKFORCE_SUBJECT_TOKEN_TYPE, token_url=TOKEN_URL, token_info_url=None, service_account_impersonation_url=None, service_account_impersonation_options={}, client_id=None, client_secret=None, credential_source=self.CREDENTIAL_SOURCE_TEXT, quota_project_id=None, workforce_pool_user_project=WORKFORCE_POOL_USER_PROJECT, universe_domain=DEFAULT_UNIVERSE_DOMAIN, ) def test_constructor_nonworkforce_with_workforce_pool_user_project(self): with pytest.raises(ValueError) as excinfo: self.make_credentials( audience=AUDIENCE, workforce_pool_user_project=WORKFORCE_POOL_USER_PROJECT, ) assert excinfo.match( "workforce_pool_user_project should not be set for non-workforce " "pool credentials" ) def test_constructor_invalid_options(self): credential_source = {"unsupported": "value"} with pytest.raises(ValueError) as excinfo: self.make_credentials(credential_source=credential_source) assert excinfo.match(r"Missing credential_source") def test_constructor_invalid_options_url_and_file(self): credential_source = { "url": self.CREDENTIAL_URL, "file": SUBJECT_TOKEN_TEXT_FILE, } with pytest.raises(ValueError) as excinfo: self.make_credentials(credential_source=credential_source) assert excinfo.match(r"Ambiguous credential_source") def test_constructor_invalid_options_environment_id(self): credential_source = {"url": self.CREDENTIAL_URL, "environment_id": "aws1"} with pytest.raises(ValueError) as excinfo: self.make_credentials(credential_source=credential_source) assert excinfo.match( r"Invalid Identity Pool credential_source field 'environment_id'" ) def test_constructor_invalid_credential_source(self): with pytest.raises(ValueError) as excinfo: self.make_credentials(credential_source="non-dict") assert excinfo.match(r"Missing credential_source") def test_constructor_invalid_credential_source_format_type(self): credential_source = {"format": {"type": "xml"}} with pytest.raises(ValueError) as excinfo: self.make_credentials(credential_source=credential_source) assert excinfo.match(r"Invalid credential_source format 'xml'") def test_constructor_missing_subject_token_field_name(self): credential_source = {"format": {"type": "json"}} with pytest.raises(ValueError) as excinfo: self.make_credentials(credential_source=credential_source) assert excinfo.match( r"Missing subject_token_field_name for JSON credential_source format" ) def test_info_with_workforce_pool_user_project(self): credentials = self.make_credentials( audience=WORKFORCE_AUDIENCE, subject_token_type=WORKFORCE_SUBJECT_TOKEN_TYPE, credential_source=self.CREDENTIAL_SOURCE_TEXT_URL.copy(), workforce_pool_user_project=WORKFORCE_POOL_USER_PROJECT, ) assert credentials.info == { "type": "external_account", "audience": WORKFORCE_AUDIENCE, "subject_token_type": WORKFORCE_SUBJECT_TOKEN_TYPE, "token_url": TOKEN_URL, "token_info_url": TOKEN_INFO_URL, "credential_source": self.CREDENTIAL_SOURCE_TEXT_URL, "workforce_pool_user_project": WORKFORCE_POOL_USER_PROJECT, "universe_domain": DEFAULT_UNIVERSE_DOMAIN, } def test_info_with_file_credential_source(self): credentials = self.make_credentials( credential_source=self.CREDENTIAL_SOURCE_TEXT_URL.copy() ) assert credentials.info == { "type": "external_account", "audience": AUDIENCE, "subject_token_type": SUBJECT_TOKEN_TYPE, "token_url": TOKEN_URL, "token_info_url": TOKEN_INFO_URL, "credential_source": self.CREDENTIAL_SOURCE_TEXT_URL, "universe_domain": DEFAULT_UNIVERSE_DOMAIN, } def test_info_with_url_credential_source(self): credentials = self.make_credentials( credential_source=self.CREDENTIAL_SOURCE_JSON_URL.copy() ) assert credentials.info == { "type": "external_account", "audience": AUDIENCE, "subject_token_type": SUBJECT_TOKEN_TYPE, "token_url": TOKEN_URL, "token_info_url": TOKEN_INFO_URL, "credential_source": self.CREDENTIAL_SOURCE_JSON_URL, "universe_domain": DEFAULT_UNIVERSE_DOMAIN, } def test_retrieve_subject_token_missing_subject_token(self, tmpdir): # Provide empty text file. empty_file = tmpdir.join("empty.txt") empty_file.write("") credential_source = {"file": str(empty_file)} credentials = self.make_credentials(credential_source=credential_source) with pytest.raises(exceptions.RefreshError) as excinfo: credentials.retrieve_subject_token(None) assert excinfo.match(r"Missing subject_token in the credential_source file") def test_retrieve_subject_token_text_file(self): credentials = self.make_credentials( credential_source=self.CREDENTIAL_SOURCE_TEXT ) subject_token = credentials.retrieve_subject_token(None) assert subject_token == TEXT_FILE_SUBJECT_TOKEN def test_retrieve_subject_token_json_file(self): credentials = self.make_credentials( credential_source=self.CREDENTIAL_SOURCE_JSON ) subject_token = credentials.retrieve_subject_token(None) assert subject_token == JSON_FILE_SUBJECT_TOKEN def test_retrieve_subject_token_json_file_invalid_field_name(self): credential_source = { "file": SUBJECT_TOKEN_JSON_FILE, "format": {"type": "json", "subject_token_field_name": "not_found"}, } credentials = self.make_credentials(credential_source=credential_source) with pytest.raises(exceptions.RefreshError) as excinfo: credentials.retrieve_subject_token(None) assert excinfo.match( "Unable to parse subject_token from JSON file '{}' using key '{}'".format( SUBJECT_TOKEN_JSON_FILE, "not_found" ) ) def test_retrieve_subject_token_invalid_json(self, tmpdir): # Provide JSON file. This should result in JSON parsing error. invalid_json_file = tmpdir.join("invalid.json") invalid_json_file.write("{") credential_source = { "file": str(invalid_json_file), "format": {"type": "json", "subject_token_field_name": "access_token"}, } credentials = self.make_credentials(credential_source=credential_source) with pytest.raises(exceptions.RefreshError) as excinfo: credentials.retrieve_subject_token(None) assert excinfo.match( "Unable to parse subject_token from JSON file '{}' using key '{}'".format( str(invalid_json_file), "access_token" ) ) def test_retrieve_subject_token_file_not_found(self): credential_source = {"file": "./not_found.txt"} credentials = self.make_credentials(credential_source=credential_source) with pytest.raises(exceptions.RefreshError) as excinfo: credentials.retrieve_subject_token(None) assert excinfo.match(r"File './not_found.txt' was not found") def test_token_info_url(self): credentials = self.make_credentials( credential_source=self.CREDENTIAL_SOURCE_JSON ) assert credentials.token_info_url == TOKEN_INFO_URL def test_token_info_url_custom(self): for url in VALID_TOKEN_URLS: credentials = self.make_credentials( credential_source=self.CREDENTIAL_SOURCE_JSON.copy(), token_info_url=(url + "/introspect"), ) assert credentials.token_info_url == url + "/introspect" def test_token_info_url_negative(self): credentials = self.make_credentials( credential_source=self.CREDENTIAL_SOURCE_JSON.copy(), token_info_url=None ) assert not credentials.token_info_url def test_token_url_custom(self): for url in VALID_TOKEN_URLS: credentials = self.make_credentials( credential_source=self.CREDENTIAL_SOURCE_JSON.copy(), token_url=(url + "/token"), ) assert credentials._token_url == (url + "/token") def test_service_account_impersonation_url_custom(self): for url in VALID_SERVICE_ACCOUNT_IMPERSONATION_URLS: credentials = self.make_credentials( credential_source=self.CREDENTIAL_SOURCE_JSON.copy(), service_account_impersonation_url=( url + SERVICE_ACCOUNT_IMPERSONATION_URL_ROUTE ), ) assert credentials._service_account_impersonation_url == ( url + SERVICE_ACCOUNT_IMPERSONATION_URL_ROUTE ) def test_refresh_text_file_success_without_impersonation_ignore_default_scopes( self, ): credentials = self.make_credentials( client_id=CLIENT_ID, client_secret=CLIENT_SECRET, # Test with text format type. credential_source=self.CREDENTIAL_SOURCE_TEXT, scopes=SCOPES, # Default scopes should be ignored. default_scopes=["ignored"], ) self.assert_underlying_credentials_refresh( credentials=credentials, audience=AUDIENCE, subject_token=TEXT_FILE_SUBJECT_TOKEN, subject_token_type=SUBJECT_TOKEN_TYPE, token_url=TOKEN_URL, service_account_impersonation_url=None, basic_auth_encoding=BASIC_AUTH_ENCODING, quota_project_id=None, used_scopes=SCOPES, scopes=SCOPES, default_scopes=["ignored"], ) def test_refresh_workforce_success_with_client_auth_without_impersonation(self): credentials = self.make_credentials( audience=WORKFORCE_AUDIENCE, subject_token_type=WORKFORCE_SUBJECT_TOKEN_TYPE, client_id=CLIENT_ID, client_secret=CLIENT_SECRET, # Test with text format type. credential_source=self.CREDENTIAL_SOURCE_TEXT, scopes=SCOPES, # This will be ignored in favor of client auth. workforce_pool_user_project=WORKFORCE_POOL_USER_PROJECT, ) self.assert_underlying_credentials_refresh( credentials=credentials, audience=WORKFORCE_AUDIENCE, subject_token=TEXT_FILE_SUBJECT_TOKEN, subject_token_type=WORKFORCE_SUBJECT_TOKEN_TYPE, token_url=TOKEN_URL, service_account_impersonation_url=None, basic_auth_encoding=BASIC_AUTH_ENCODING, quota_project_id=None, used_scopes=SCOPES, scopes=SCOPES, workforce_pool_user_project=None, ) def test_refresh_workforce_success_with_client_auth_and_no_workforce_project(self): credentials = self.make_credentials( audience=WORKFORCE_AUDIENCE, subject_token_type=WORKFORCE_SUBJECT_TOKEN_TYPE, client_id=CLIENT_ID, client_secret=CLIENT_SECRET, # Test with text format type. credential_source=self.CREDENTIAL_SOURCE_TEXT, scopes=SCOPES, # This is not needed when client Auth is used. workforce_pool_user_project=None, ) self.assert_underlying_credentials_refresh( credentials=credentials, audience=WORKFORCE_AUDIENCE, subject_token=TEXT_FILE_SUBJECT_TOKEN, subject_token_type=WORKFORCE_SUBJECT_TOKEN_TYPE, token_url=TOKEN_URL, service_account_impersonation_url=None, basic_auth_encoding=BASIC_AUTH_ENCODING, quota_project_id=None, used_scopes=SCOPES, scopes=SCOPES, workforce_pool_user_project=None, ) def test_refresh_workforce_success_without_client_auth_without_impersonation(self): credentials = self.make_credentials( audience=WORKFORCE_AUDIENCE, subject_token_type=WORKFORCE_SUBJECT_TOKEN_TYPE, client_id=None, client_secret=None, # Test with text format type. credential_source=self.CREDENTIAL_SOURCE_TEXT, scopes=SCOPES, # This will not be ignored as client auth is not used. workforce_pool_user_project=WORKFORCE_POOL_USER_PROJECT, ) self.assert_underlying_credentials_refresh( credentials=credentials, audience=WORKFORCE_AUDIENCE, subject_token=TEXT_FILE_SUBJECT_TOKEN, subject_token_type=WORKFORCE_SUBJECT_TOKEN_TYPE, token_url=TOKEN_URL, service_account_impersonation_url=None, basic_auth_encoding=None, quota_project_id=None, used_scopes=SCOPES, scopes=SCOPES, workforce_pool_user_project=WORKFORCE_POOL_USER_PROJECT, ) def test_refresh_workforce_success_without_client_auth_with_impersonation(self): credentials = self.make_credentials( audience=WORKFORCE_AUDIENCE, subject_token_type=WORKFORCE_SUBJECT_TOKEN_TYPE, client_id=None, client_secret=None, service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL, # Test with text format type. credential_source=self.CREDENTIAL_SOURCE_TEXT, scopes=SCOPES, # This will not be ignored as client auth is not used. workforce_pool_user_project=WORKFORCE_POOL_USER_PROJECT, ) self.assert_underlying_credentials_refresh( credentials=credentials, audience=WORKFORCE_AUDIENCE, subject_token=TEXT_FILE_SUBJECT_TOKEN, subject_token_type=WORKFORCE_SUBJECT_TOKEN_TYPE, token_url=TOKEN_URL, service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL, basic_auth_encoding=None, quota_project_id=None, used_scopes=SCOPES, scopes=SCOPES, workforce_pool_user_project=WORKFORCE_POOL_USER_PROJECT, ) def test_refresh_text_file_success_without_impersonation_use_default_scopes(self): credentials = self.make_credentials( client_id=CLIENT_ID, client_secret=CLIENT_SECRET, # Test with text format type. credential_source=self.CREDENTIAL_SOURCE_TEXT, scopes=None, # Default scopes should be used since user specified scopes are none. default_scopes=SCOPES, ) self.assert_underlying_credentials_refresh( credentials=credentials, audience=AUDIENCE, subject_token=TEXT_FILE_SUBJECT_TOKEN, subject_token_type=SUBJECT_TOKEN_TYPE, token_url=TOKEN_URL, service_account_impersonation_url=None, basic_auth_encoding=BASIC_AUTH_ENCODING, quota_project_id=None, used_scopes=SCOPES, scopes=None, default_scopes=SCOPES, ) def test_refresh_text_file_success_with_impersonation_ignore_default_scopes(self): # Initialize credentials with service account impersonation and basic auth. credentials = self.make_credentials( # Test with text format type. credential_source=self.CREDENTIAL_SOURCE_TEXT, service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL, scopes=SCOPES, # Default scopes should be ignored. default_scopes=["ignored"], ) self.assert_underlying_credentials_refresh( credentials=credentials, audience=AUDIENCE, subject_token=TEXT_FILE_SUBJECT_TOKEN, subject_token_type=SUBJECT_TOKEN_TYPE, token_url=TOKEN_URL, service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL, basic_auth_encoding=None, quota_project_id=None, used_scopes=SCOPES, scopes=SCOPES, default_scopes=["ignored"], ) def test_refresh_text_file_success_with_impersonation_use_default_scopes(self): # Initialize credentials with service account impersonation, basic auth # and default scopes (no user scopes). credentials = self.make_credentials( # Test with text format type. credential_source=self.CREDENTIAL_SOURCE_TEXT, service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL, scopes=None, # Default scopes should be used since user specified scopes are none. default_scopes=SCOPES, ) self.assert_underlying_credentials_refresh( credentials=credentials, audience=AUDIENCE, subject_token=TEXT_FILE_SUBJECT_TOKEN, subject_token_type=SUBJECT_TOKEN_TYPE, token_url=TOKEN_URL, service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL, basic_auth_encoding=None, quota_project_id=None, used_scopes=SCOPES, scopes=None, default_scopes=SCOPES, ) def test_refresh_json_file_success_without_impersonation(self): credentials = self.make_credentials( client_id=CLIENT_ID, client_secret=CLIENT_SECRET, # Test with JSON format type. credential_source=self.CREDENTIAL_SOURCE_JSON, scopes=SCOPES, ) self.assert_underlying_credentials_refresh( credentials=credentials, audience=AUDIENCE, subject_token=JSON_FILE_SUBJECT_TOKEN, subject_token_type=SUBJECT_TOKEN_TYPE, token_url=TOKEN_URL, service_account_impersonation_url=None, basic_auth_encoding=BASIC_AUTH_ENCODING, quota_project_id=None, used_scopes=SCOPES, scopes=SCOPES, default_scopes=None, ) def test_refresh_json_file_success_with_impersonation(self): # Initialize credentials with service account impersonation and basic auth. credentials = self.make_credentials( # Test with JSON format type. credential_source=self.CREDENTIAL_SOURCE_JSON, service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL, scopes=SCOPES, ) self.assert_underlying_credentials_refresh( credentials=credentials, audience=AUDIENCE, subject_token=JSON_FILE_SUBJECT_TOKEN, subject_token_type=SUBJECT_TOKEN_TYPE, token_url=TOKEN_URL, service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL, basic_auth_encoding=None, quota_project_id=None, used_scopes=SCOPES, scopes=SCOPES, default_scopes=None, ) def test_refresh_with_retrieve_subject_token_error(self): credential_source = { "file": SUBJECT_TOKEN_JSON_FILE, "format": {"type": "json", "subject_token_field_name": "not_found"}, } credentials = self.make_credentials(credential_source=credential_source) with pytest.raises(exceptions.RefreshError) as excinfo: credentials.refresh(None) assert excinfo.match( "Unable to parse subject_token from JSON file '{}' using key '{}'".format( SUBJECT_TOKEN_JSON_FILE, "not_found" ) ) def test_retrieve_subject_token_from_url(self): credentials = self.make_credentials( credential_source=self.CREDENTIAL_SOURCE_TEXT_URL ) request = self.make_mock_request(token_data=TEXT_FILE_SUBJECT_TOKEN) subject_token = credentials.retrieve_subject_token(request) assert subject_token == TEXT_FILE_SUBJECT_TOKEN self.assert_credential_request_kwargs(request.call_args_list[0][1], None) def test_retrieve_subject_token_from_url_with_headers(self): credentials = self.make_credentials( credential_source={"url": self.CREDENTIAL_URL, "headers": {"foo": "bar"}} ) request = self.make_mock_request(token_data=TEXT_FILE_SUBJECT_TOKEN) subject_token = credentials.retrieve_subject_token(request) assert subject_token == TEXT_FILE_SUBJECT_TOKEN self.assert_credential_request_kwargs( request.call_args_list[0][1], {"foo": "bar"} ) def test_retrieve_subject_token_from_url_json(self): credentials = self.make_credentials( credential_source=self.CREDENTIAL_SOURCE_JSON_URL ) request = self.make_mock_request(token_data=JSON_FILE_CONTENT) subject_token = credentials.retrieve_subject_token(request) assert subject_token == JSON_FILE_SUBJECT_TOKEN self.assert_credential_request_kwargs(request.call_args_list[0][1], None) def test_retrieve_subject_token_from_url_json_with_headers(self): credentials = self.make_credentials( credential_source={ "url": self.CREDENTIAL_URL, "format": {"type": "json", "subject_token_field_name": "access_token"}, "headers": {"foo": "bar"}, } ) request = self.make_mock_request(token_data=JSON_FILE_CONTENT) subject_token = credentials.retrieve_subject_token(request) assert subject_token == JSON_FILE_SUBJECT_TOKEN self.assert_credential_request_kwargs( request.call_args_list[0][1], {"foo": "bar"} ) def test_retrieve_subject_token_from_url_not_found(self): credentials = self.make_credentials( credential_source=self.CREDENTIAL_SOURCE_TEXT_URL ) with pytest.raises(exceptions.RefreshError) as excinfo: credentials.retrieve_subject_token( self.make_mock_request(token_status=404, token_data=JSON_FILE_CONTENT) ) assert excinfo.match("Unable to retrieve Identity Pool subject token") def test_retrieve_subject_token_from_url_json_invalid_field(self): credential_source = { "url": self.CREDENTIAL_URL, "format": {"type": "json", "subject_token_field_name": "not_found"}, } credentials = self.make_credentials(credential_source=credential_source) with pytest.raises(exceptions.RefreshError) as excinfo: credentials.retrieve_subject_token( self.make_mock_request(token_data=JSON_FILE_CONTENT) ) assert excinfo.match( "Unable to parse subject_token from JSON file '{}' using key '{}'".format( self.CREDENTIAL_URL, "not_found" ) ) def test_retrieve_subject_token_from_url_json_invalid_format(self): credentials = self.make_credentials( credential_source=self.CREDENTIAL_SOURCE_JSON_URL ) with pytest.raises(exceptions.RefreshError) as excinfo: credentials.retrieve_subject_token(self.make_mock_request(token_data="{")) assert excinfo.match( "Unable to parse subject_token from JSON file '{}' using key '{}'".format( self.CREDENTIAL_URL, "access_token" ) ) def test_refresh_text_file_success_without_impersonation_url(self): credentials = self.make_credentials( client_id=CLIENT_ID, client_secret=CLIENT_SECRET, # Test with text format type. credential_source=self.CREDENTIAL_SOURCE_TEXT_URL, scopes=SCOPES, ) self.assert_underlying_credentials_refresh( credentials=credentials, audience=AUDIENCE, subject_token=TEXT_FILE_SUBJECT_TOKEN, subject_token_type=SUBJECT_TOKEN_TYPE, token_url=TOKEN_URL, service_account_impersonation_url=None, basic_auth_encoding=BASIC_AUTH_ENCODING, quota_project_id=None, used_scopes=SCOPES, scopes=SCOPES, default_scopes=None, credential_data=TEXT_FILE_SUBJECT_TOKEN, ) def test_refresh_text_file_success_with_impersonation_url(self): # Initialize credentials with service account impersonation and basic auth. credentials = self.make_credentials( # Test with text format type. credential_source=self.CREDENTIAL_SOURCE_TEXT_URL, service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL, scopes=SCOPES, ) self.assert_underlying_credentials_refresh( credentials=credentials, audience=AUDIENCE, subject_token=TEXT_FILE_SUBJECT_TOKEN, subject_token_type=SUBJECT_TOKEN_TYPE, token_url=TOKEN_URL, service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL, basic_auth_encoding=None, quota_project_id=None, used_scopes=SCOPES, scopes=SCOPES, default_scopes=None, credential_data=TEXT_FILE_SUBJECT_TOKEN, ) def test_refresh_json_file_success_without_impersonation_url(self): credentials = self.make_credentials( client_id=CLIENT_ID, client_secret=CLIENT_SECRET, # Test with JSON format type. credential_source=self.CREDENTIAL_SOURCE_JSON_URL, scopes=SCOPES, ) self.assert_underlying_credentials_refresh( credentials=credentials, audience=AUDIENCE, subject_token=JSON_FILE_SUBJECT_TOKEN, subject_token_type=SUBJECT_TOKEN_TYPE, token_url=TOKEN_URL, service_account_impersonation_url=None, basic_auth_encoding=BASIC_AUTH_ENCODING, quota_project_id=None, used_scopes=SCOPES, scopes=SCOPES, default_scopes=None, credential_data=JSON_FILE_CONTENT, ) def test_refresh_json_file_success_with_impersonation_url(self): # Initialize credentials with service account impersonation and basic auth. credentials = self.make_credentials( # Test with JSON format type. credential_source=self.CREDENTIAL_SOURCE_JSON_URL, service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL, scopes=SCOPES, ) self.assert_underlying_credentials_refresh( credentials=credentials, audience=AUDIENCE, subject_token=JSON_FILE_SUBJECT_TOKEN, subject_token_type=SUBJECT_TOKEN_TYPE, token_url=TOKEN_URL, service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL, basic_auth_encoding=None, quota_project_id=None, used_scopes=SCOPES, scopes=SCOPES, default_scopes=None, credential_data=JSON_FILE_CONTENT, ) def test_refresh_with_retrieve_subject_token_error_url(self): credential_source = { "url": self.CREDENTIAL_URL, "format": {"type": "json", "subject_token_field_name": "not_found"}, } credentials = self.make_credentials(credential_source=credential_source) with pytest.raises(exceptions.RefreshError) as excinfo: credentials.refresh(self.make_mock_request(token_data=JSON_FILE_CONTENT)) assert excinfo.match( "Unable to parse subject_token from JSON file '{}' using key '{}'".format( self.CREDENTIAL_URL, "not_found" ) ) test_jwt.py 0000644 00000056305 15025175543 0007001 0 ustar 00 # Copyright 2014 Google Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import base64 import datetime import json import os import mock import pytest # type: ignore from google.auth import _helpers from google.auth import crypt from google.auth import exceptions from google.auth import jwt DATA_DIR = os.path.join(os.path.dirname(__file__), "data") with open(os.path.join(DATA_DIR, "privatekey.pem"), "rb") as fh: PRIVATE_KEY_BYTES = fh.read() with open(os.path.join(DATA_DIR, "public_cert.pem"), "rb") as fh: PUBLIC_CERT_BYTES = fh.read() with open(os.path.join(DATA_DIR, "other_cert.pem"), "rb") as fh: OTHER_CERT_BYTES = fh.read() with open(os.path.join(DATA_DIR, "es256_privatekey.pem"), "rb") as fh: EC_PRIVATE_KEY_BYTES = fh.read() with open(os.path.join(DATA_DIR, "es256_public_cert.pem"), "rb") as fh: EC_PUBLIC_CERT_BYTES = fh.read() SERVICE_ACCOUNT_JSON_FILE = os.path.join(DATA_DIR, "service_account.json") with open(SERVICE_ACCOUNT_JSON_FILE, "rb") as fh: SERVICE_ACCOUNT_INFO = json.load(fh) @pytest.fixture def signer(): return crypt.RSASigner.from_string(PRIVATE_KEY_BYTES, "1") def test_encode_basic(signer): test_payload = {"test": "value"} encoded = jwt.encode(signer, test_payload) header, payload, _, _ = jwt._unverified_decode(encoded) assert payload == test_payload assert header == {"typ": "JWT", "alg": "RS256", "kid": signer.key_id} def test_encode_extra_headers(signer): encoded = jwt.encode(signer, {}, header={"extra": "value"}) header = jwt.decode_header(encoded) assert header == { "typ": "JWT", "alg": "RS256", "kid": signer.key_id, "extra": "value", } def test_encode_custom_alg_in_headers(signer): encoded = jwt.encode(signer, {}, header={"alg": "foo"}) header = jwt.decode_header(encoded) assert header == {"typ": "JWT", "alg": "foo", "kid": signer.key_id} @pytest.fixture def es256_signer(): return crypt.ES256Signer.from_string(EC_PRIVATE_KEY_BYTES, "1") def test_encode_basic_es256(es256_signer): test_payload = {"test": "value"} encoded = jwt.encode(es256_signer, test_payload) header, payload, _, _ = jwt._unverified_decode(encoded) assert payload == test_payload assert header == {"typ": "JWT", "alg": "ES256", "kid": es256_signer.key_id} @pytest.fixture def token_factory(signer, es256_signer): def factory(claims=None, key_id=None, use_es256_signer=False): now = _helpers.datetime_to_secs(_helpers.utcnow()) payload = { "aud": "audience@example.com", "iat": now, "exp": now + 300, "user": "billy bob", "metadata": {"meta": "data"}, } payload.update(claims or {}) # False is specified to remove the signer's key id for testing # headers without key ids. if key_id is False: signer._key_id = None key_id = None if use_es256_signer: return jwt.encode(es256_signer, payload, key_id=key_id) else: return jwt.encode(signer, payload, key_id=key_id) return factory def test_decode_valid(token_factory): payload = jwt.decode(token_factory(), certs=PUBLIC_CERT_BYTES) assert payload["aud"] == "audience@example.com" assert payload["user"] == "billy bob" assert payload["metadata"]["meta"] == "data" def test_decode_header_object(token_factory): payload = token_factory() # Create a malformed JWT token with a number as a header instead of a # dictionary (3 == base64d(M7==)) payload = b"M7." + b".".join(payload.split(b".")[1:]) with pytest.raises(ValueError) as excinfo: jwt.decode(payload, certs=PUBLIC_CERT_BYTES) assert excinfo.match(r"Header segment should be a JSON object: " + str(b"M7")) def test_decode_payload_object(signer): # Create a malformed JWT token with a payload containing both "iat" and # "exp" strings, although not as fields of a dictionary payload = jwt.encode(signer, "iatexp") with pytest.raises(ValueError) as excinfo: jwt.decode(payload, certs=PUBLIC_CERT_BYTES) assert excinfo.match( r"Payload segment should be a JSON object: " + str(b"ImlhdGV4cCI") ) def test_decode_valid_es256(token_factory): payload = jwt.decode( token_factory(use_es256_signer=True), certs=EC_PUBLIC_CERT_BYTES ) assert payload["aud"] == "audience@example.com" assert payload["user"] == "billy bob" assert payload["metadata"]["meta"] == "data" def test_decode_valid_with_audience(token_factory): payload = jwt.decode( token_factory(), certs=PUBLIC_CERT_BYTES, audience="audience@example.com" ) assert payload["aud"] == "audience@example.com" assert payload["user"] == "billy bob" assert payload["metadata"]["meta"] == "data" def test_decode_valid_with_audience_list(token_factory): payload = jwt.decode( token_factory(), certs=PUBLIC_CERT_BYTES, audience=["audience@example.com", "another_audience@example.com"], ) assert payload["aud"] == "audience@example.com" assert payload["user"] == "billy bob" assert payload["metadata"]["meta"] == "data" def test_decode_valid_unverified(token_factory): payload = jwt.decode(token_factory(), certs=OTHER_CERT_BYTES, verify=False) assert payload["aud"] == "audience@example.com" assert payload["user"] == "billy bob" assert payload["metadata"]["meta"] == "data" def test_decode_bad_token_wrong_number_of_segments(): with pytest.raises(ValueError) as excinfo: jwt.decode("1.2", PUBLIC_CERT_BYTES) assert excinfo.match(r"Wrong number of segments") def test_decode_bad_token_not_base64(): with pytest.raises((ValueError, TypeError)) as excinfo: jwt.decode("1.2.3", PUBLIC_CERT_BYTES) assert excinfo.match(r"Incorrect padding|more than a multiple of 4") def test_decode_bad_token_not_json(): token = b".".join([base64.urlsafe_b64encode(b"123!")] * 3) with pytest.raises(ValueError) as excinfo: jwt.decode(token, PUBLIC_CERT_BYTES) assert excinfo.match(r"Can\'t parse segment") def test_decode_bad_token_no_iat_or_exp(signer): token = jwt.encode(signer, {"test": "value"}) with pytest.raises(ValueError) as excinfo: jwt.decode(token, PUBLIC_CERT_BYTES) assert excinfo.match(r"Token does not contain required claim") def test_decode_bad_token_too_early(token_factory): token = token_factory( claims={ "iat": _helpers.datetime_to_secs( _helpers.utcnow() + datetime.timedelta(hours=1) ) } ) with pytest.raises(ValueError) as excinfo: jwt.decode(token, PUBLIC_CERT_BYTES, clock_skew_in_seconds=59) assert excinfo.match(r"Token used too early") def test_decode_bad_token_expired(token_factory): token = token_factory( claims={ "exp": _helpers.datetime_to_secs( _helpers.utcnow() - datetime.timedelta(hours=1) ) } ) with pytest.raises(ValueError) as excinfo: jwt.decode(token, PUBLIC_CERT_BYTES, clock_skew_in_seconds=59) assert excinfo.match(r"Token expired") def test_decode_success_with_no_clock_skew(token_factory): token = token_factory( claims={ "exp": _helpers.datetime_to_secs( _helpers.utcnow() + datetime.timedelta(seconds=1) ), "iat": _helpers.datetime_to_secs( _helpers.utcnow() - datetime.timedelta(seconds=1) ), } ) jwt.decode(token, PUBLIC_CERT_BYTES) def test_decode_success_with_custom_clock_skew(token_factory): token = token_factory( claims={ "exp": _helpers.datetime_to_secs( _helpers.utcnow() + datetime.timedelta(seconds=2) ), "iat": _helpers.datetime_to_secs( _helpers.utcnow() - datetime.timedelta(seconds=2) ), } ) jwt.decode(token, PUBLIC_CERT_BYTES, clock_skew_in_seconds=1) def test_decode_bad_token_wrong_audience(token_factory): token = token_factory() audience = "audience2@example.com" with pytest.raises(ValueError) as excinfo: jwt.decode(token, PUBLIC_CERT_BYTES, audience=audience) assert excinfo.match(r"Token has wrong audience") def test_decode_bad_token_wrong_audience_list(token_factory): token = token_factory() audience = ["audience2@example.com", "audience3@example.com"] with pytest.raises(ValueError) as excinfo: jwt.decode(token, PUBLIC_CERT_BYTES, audience=audience) assert excinfo.match(r"Token has wrong audience") def test_decode_wrong_cert(token_factory): with pytest.raises(ValueError) as excinfo: jwt.decode(token_factory(), OTHER_CERT_BYTES) assert excinfo.match(r"Could not verify token signature") def test_decode_multicert_bad_cert(token_factory): certs = {"1": OTHER_CERT_BYTES, "2": PUBLIC_CERT_BYTES} with pytest.raises(ValueError) as excinfo: jwt.decode(token_factory(), certs) assert excinfo.match(r"Could not verify token signature") def test_decode_no_cert(token_factory): certs = {"2": PUBLIC_CERT_BYTES} with pytest.raises(ValueError) as excinfo: jwt.decode(token_factory(), certs) assert excinfo.match(r"Certificate for key id 1 not found") def test_decode_no_key_id(token_factory): token = token_factory(key_id=False) certs = {"2": PUBLIC_CERT_BYTES} payload = jwt.decode(token, certs) assert payload["user"] == "billy bob" def test_decode_unknown_alg(): headers = json.dumps({u"kid": u"1", u"alg": u"fakealg"}) token = b".".join( map(lambda seg: base64.b64encode(seg.encode("utf-8")), [headers, u"{}", u"sig"]) ) with pytest.raises(ValueError) as excinfo: jwt.decode(token) assert excinfo.match(r"fakealg") def test_decode_missing_crytography_alg(monkeypatch): monkeypatch.delitem(jwt._ALGORITHM_TO_VERIFIER_CLASS, "ES256") headers = json.dumps({u"kid": u"1", u"alg": u"ES256"}) token = b".".join( map(lambda seg: base64.b64encode(seg.encode("utf-8")), [headers, u"{}", u"sig"]) ) with pytest.raises(ValueError) as excinfo: jwt.decode(token) assert excinfo.match(r"cryptography") def test_roundtrip_explicit_key_id(token_factory): token = token_factory(key_id="3") certs = {"2": OTHER_CERT_BYTES, "3": PUBLIC_CERT_BYTES} payload = jwt.decode(token, certs) assert payload["user"] == "billy bob" class TestCredentials(object): SERVICE_ACCOUNT_EMAIL = "service-account@example.com" SUBJECT = "subject" AUDIENCE = "audience" ADDITIONAL_CLAIMS = {"meta": "data"} credentials = None @pytest.fixture(autouse=True) def credentials_fixture(self, signer): self.credentials = jwt.Credentials( signer, self.SERVICE_ACCOUNT_EMAIL, self.SERVICE_ACCOUNT_EMAIL, self.AUDIENCE, ) def test_from_service_account_info(self): with open(SERVICE_ACCOUNT_JSON_FILE, "r") as fh: info = json.load(fh) credentials = jwt.Credentials.from_service_account_info( info, audience=self.AUDIENCE ) assert credentials._signer.key_id == info["private_key_id"] assert credentials._issuer == info["client_email"] assert credentials._subject == info["client_email"] assert credentials._audience == self.AUDIENCE def test_from_service_account_info_args(self): info = SERVICE_ACCOUNT_INFO.copy() credentials = jwt.Credentials.from_service_account_info( info, subject=self.SUBJECT, audience=self.AUDIENCE, additional_claims=self.ADDITIONAL_CLAIMS, ) assert credentials._signer.key_id == info["private_key_id"] assert credentials._issuer == info["client_email"] assert credentials._subject == self.SUBJECT assert credentials._audience == self.AUDIENCE assert credentials._additional_claims == self.ADDITIONAL_CLAIMS def test_from_service_account_file(self): info = SERVICE_ACCOUNT_INFO.copy() credentials = jwt.Credentials.from_service_account_file( SERVICE_ACCOUNT_JSON_FILE, audience=self.AUDIENCE ) assert credentials._signer.key_id == info["private_key_id"] assert credentials._issuer == info["client_email"] assert credentials._subject == info["client_email"] assert credentials._audience == self.AUDIENCE def test_from_service_account_file_args(self): info = SERVICE_ACCOUNT_INFO.copy() credentials = jwt.Credentials.from_service_account_file( SERVICE_ACCOUNT_JSON_FILE, subject=self.SUBJECT, audience=self.AUDIENCE, additional_claims=self.ADDITIONAL_CLAIMS, ) assert credentials._signer.key_id == info["private_key_id"] assert credentials._issuer == info["client_email"] assert credentials._subject == self.SUBJECT assert credentials._audience == self.AUDIENCE assert credentials._additional_claims == self.ADDITIONAL_CLAIMS def test_from_signing_credentials(self): jwt_from_signing = self.credentials.from_signing_credentials( self.credentials, audience=mock.sentinel.new_audience ) jwt_from_info = jwt.Credentials.from_service_account_info( SERVICE_ACCOUNT_INFO, audience=mock.sentinel.new_audience ) assert isinstance(jwt_from_signing, jwt.Credentials) assert jwt_from_signing._signer.key_id == jwt_from_info._signer.key_id assert jwt_from_signing._issuer == jwt_from_info._issuer assert jwt_from_signing._subject == jwt_from_info._subject assert jwt_from_signing._audience == jwt_from_info._audience def test_default_state(self): assert not self.credentials.valid # Expiration hasn't been set yet assert not self.credentials.expired def test_with_claims(self): new_audience = "new_audience" new_credentials = self.credentials.with_claims(audience=new_audience) 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 == new_audience assert new_credentials._additional_claims == self.credentials._additional_claims assert new_credentials._quota_project_id == self.credentials._quota_project_id def test__make_jwt_without_audience(self): cred = jwt.Credentials.from_service_account_info( SERVICE_ACCOUNT_INFO.copy(), subject=self.SUBJECT, audience=None, additional_claims={"scope": "foo bar"}, ) token, _ = cred._make_jwt() payload = jwt.decode(token, PUBLIC_CERT_BYTES) assert payload["scope"] == "foo bar" assert "aud" not in payload 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.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) assert crypt.verify_signature(to_sign, signature, PUBLIC_CERT_BYTES) def test_signer(self): assert isinstance(self.credentials.signer, crypt.RSASigner) def test_signer_email(self): assert self.credentials.signer_email == SERVICE_ACCOUNT_INFO["client_email"] def _verify_token(self, token): payload = jwt.decode(token, PUBLIC_CERT_BYTES) assert payload["iss"] == self.SERVICE_ACCOUNT_EMAIL return payload def test_refresh(self): self.credentials.refresh(None) assert self.credentials.valid assert not self.credentials.expired def test_expired(self): assert not self.credentials.expired self.credentials.refresh(None) assert not self.credentials.expired with mock.patch("google.auth._helpers.utcnow") as now: one_day = datetime.timedelta(days=1) now.return_value = self.credentials.expiry + one_day assert self.credentials.expired def test_before_request(self): headers = {} self.credentials.refresh(None) self.credentials.before_request( None, "GET", "http://example.com?a=1#3", headers ) header_value = headers["authorization"] _, token = header_value.split(" ") # Since the audience is set, it should use the existing token. assert token.encode("utf-8") == self.credentials.token payload = self._verify_token(token) assert payload["aud"] == self.AUDIENCE def test_before_request_refreshes(self): assert not self.credentials.valid self.credentials.before_request(None, "GET", "http://example.com?a=1#3", {}) assert self.credentials.valid class TestOnDemandCredentials(object): SERVICE_ACCOUNT_EMAIL = "service-account@example.com" SUBJECT = "subject" ADDITIONAL_CLAIMS = {"meta": "data"} credentials = None @pytest.fixture(autouse=True) def credentials_fixture(self, signer): self.credentials = jwt.OnDemandCredentials( signer, self.SERVICE_ACCOUNT_EMAIL, self.SERVICE_ACCOUNT_EMAIL, max_cache_size=2, ) def test_from_service_account_info(self): with open(SERVICE_ACCOUNT_JSON_FILE, "r") as fh: info = json.load(fh) credentials = jwt.OnDemandCredentials.from_service_account_info(info) assert credentials._signer.key_id == info["private_key_id"] assert credentials._issuer == info["client_email"] assert credentials._subject == info["client_email"] def test_from_service_account_info_args(self): info = SERVICE_ACCOUNT_INFO.copy() credentials = jwt.OnDemandCredentials.from_service_account_info( info, subject=self.SUBJECT, additional_claims=self.ADDITIONAL_CLAIMS ) assert credentials._signer.key_id == info["private_key_id"] assert credentials._issuer == info["client_email"] assert credentials._subject == self.SUBJECT assert credentials._additional_claims == self.ADDITIONAL_CLAIMS def test_from_service_account_file(self): info = SERVICE_ACCOUNT_INFO.copy() credentials = jwt.OnDemandCredentials.from_service_account_file( SERVICE_ACCOUNT_JSON_FILE ) assert credentials._signer.key_id == info["private_key_id"] assert credentials._issuer == info["client_email"] assert credentials._subject == info["client_email"] def test_from_service_account_file_args(self): info = SERVICE_ACCOUNT_INFO.copy() credentials = jwt.OnDemandCredentials.from_service_account_file( SERVICE_ACCOUNT_JSON_FILE, subject=self.SUBJECT, additional_claims=self.ADDITIONAL_CLAIMS, ) assert credentials._signer.key_id == info["private_key_id"] assert credentials._issuer == info["client_email"] assert credentials._subject == self.SUBJECT assert credentials._additional_claims == self.ADDITIONAL_CLAIMS def test_from_signing_credentials(self): jwt_from_signing = self.credentials.from_signing_credentials(self.credentials) jwt_from_info = jwt.OnDemandCredentials.from_service_account_info( SERVICE_ACCOUNT_INFO ) assert isinstance(jwt_from_signing, jwt.OnDemandCredentials) assert jwt_from_signing._signer.key_id == jwt_from_info._signer.key_id assert jwt_from_signing._issuer == jwt_from_info._issuer assert jwt_from_signing._subject == jwt_from_info._subject def test_default_state(self): # Credentials are *always* valid. assert self.credentials.valid # Credentials *never* expire. assert not self.credentials.expired def test_with_claims(self): new_claims = {"meep": "moop"} new_credentials = self.credentials.with_claims(additional_claims=new_claims) 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 == 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) assert crypt.verify_signature(to_sign, signature, PUBLIC_CERT_BYTES) def test_signer(self): assert isinstance(self.credentials.signer, crypt.RSASigner) def test_signer_email(self): assert self.credentials.signer_email == SERVICE_ACCOUNT_INFO["client_email"] def _verify_token(self, token): payload = jwt.decode(token, PUBLIC_CERT_BYTES) assert payload["iss"] == self.SERVICE_ACCOUNT_EMAIL return payload def test_refresh(self): with pytest.raises(exceptions.RefreshError): self.credentials.refresh(None) def test_before_request(self): headers = {} self.credentials.before_request( None, "GET", "http://example.com?a=1#3", headers ) _, token = headers["authorization"].split(" ") payload = self._verify_token(token) assert payload["aud"] == "http://example.com" # Making another request should re-use the same token. self.credentials.before_request(None, "GET", "http://example.com?b=2", headers) _, new_token = headers["authorization"].split(" ") assert new_token == token def test_expired_token(self): self.credentials._cache["audience"] = ( mock.sentinel.token, datetime.datetime.min, ) token = self.credentials._get_jwt_for_audience("audience") assert token != mock.sentinel.token test_external_account.py 0000644 00000231020 15025175543 0011520 0 ustar 00 # Copyright 2020 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import datetime import http.client as http_client import json import urllib import mock import pytest # type: ignore from google.auth import _helpers from google.auth import exceptions from google.auth import external_account from google.auth import transport IMPERSONATE_ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE = ( "gl-python/3.7 auth/1.1 auth-request-type/at cred-type/imp" ) LANG_LIBRARY_METRICS_HEADER_VALUE = "gl-python/3.7 auth/1.1" CLIENT_ID = "username" CLIENT_SECRET = "password" # Base64 encoding of "username:password" BASIC_AUTH_ENCODING = "dXNlcm5hbWU6cGFzc3dvcmQ=" SERVICE_ACCOUNT_EMAIL = "service-1234@service-name.iam.gserviceaccount.com" # List of valid workforce pool audiences. TEST_USER_AUDIENCES = [ "//iam.googleapis.com/locations/global/workforcePools/pool-id/providers/provider-id", "//iam.googleapis.com/locations/eu/workforcePools/pool-id/providers/provider-id", "//iam.googleapis.com/locations/eu/workforcePools/workloadIdentityPools/providers/provider-id", ] # Workload identity pool audiences or invalid workforce pool audiences. TEST_NON_USER_AUDIENCES = [ # Legacy K8s audience format. "identitynamespace:1f12345:my_provider", ( "//iam.googleapis.com/projects/123456/locations/" "global/workloadIdentityPools/pool-id/providers/" "provider-id" ), ( "//iam.googleapis.com/projects/123456/locations/" "eu/workloadIdentityPools/pool-id/providers/" "provider-id" ), # Pool ID with workforcePools string. ( "//iam.googleapis.com/projects/123456/locations/" "global/workloadIdentityPools/workforcePools/providers/" "provider-id" ), # Unrealistic / incorrect workforce pool audiences. "//iamgoogleapis.com/locations/eu/workforcePools/pool-id/providers/provider-id", "//iam.googleapiscom/locations/eu/workforcePools/pool-id/providers/provider-id", "//iam.googleapis.com/locations/workforcePools/pool-id/providers/provider-id", "//iam.googleapis.com/locations/eu/workforcePool/pool-id/providers/provider-id", "//iam.googleapis.com/locations//workforcePool/pool-id/providers/provider-id", ] class CredentialsImpl(external_account.Credentials): def __init__(self, **kwargs): super(CredentialsImpl, self).__init__(**kwargs) self._counter = 0 def retrieve_subject_token(self, request): counter = self._counter self._counter += 1 return "subject_token_{}".format(counter) class TestCredentials(object): TOKEN_URL = "https://sts.googleapis.com/v1/token" TOKEN_INFO_URL = "https://sts.googleapis.com/v1/introspect" PROJECT_NUMBER = "123456" POOL_ID = "POOL_ID" PROVIDER_ID = "PROVIDER_ID" AUDIENCE = ( "//iam.googleapis.com/projects/{}" "/locations/global/workloadIdentityPools/{}" "/providers/{}" ).format(PROJECT_NUMBER, POOL_ID, PROVIDER_ID) WORKFORCE_AUDIENCE = ( "//iam.googleapis.com/locations/global/workforcePools/{}/providers/{}" ).format(POOL_ID, PROVIDER_ID) WORKFORCE_POOL_USER_PROJECT = "WORKFORCE_POOL_USER_PROJECT_NUMBER" SUBJECT_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:jwt" WORKFORCE_SUBJECT_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:id_token" CREDENTIAL_SOURCE = {"file": "/var/run/secrets/goog.id/token"} SUCCESS_RESPONSE = { "access_token": "ACCESS_TOKEN", "issued_token_type": "urn:ietf:params:oauth:token-type:access_token", "token_type": "Bearer", "expires_in": 3600, "scope": "scope1 scope2", } ERROR_RESPONSE = { "error": "invalid_request", "error_description": "Invalid subject token", "error_uri": "https://tools.ietf.org/html/rfc6749", } QUOTA_PROJECT_ID = "QUOTA_PROJECT_ID" SERVICE_ACCOUNT_IMPERSONATION_URL = ( "https://us-east1-iamcredentials.googleapis.com/v1/projects/-" + "/serviceAccounts/{}:generateAccessToken".format(SERVICE_ACCOUNT_EMAIL) ) SCOPES = ["scope1", "scope2"] IMPERSONATION_ERROR_RESPONSE = { "error": { "code": 400, "message": "Request contains an invalid argument", "status": "INVALID_ARGUMENT", } } PROJECT_ID = "my-proj-id" CLOUD_RESOURCE_MANAGER_URL = ( "https://cloudresourcemanager.googleapis.com/v1/projects/" ) CLOUD_RESOURCE_MANAGER_SUCCESS_RESPONSE = { "projectNumber": PROJECT_NUMBER, "projectId": PROJECT_ID, "lifecycleState": "ACTIVE", "name": "project-name", "createTime": "2018-11-06T04:42:54.109Z", "parent": {"type": "folder", "id": "12345678901"}, } @classmethod def make_credentials( cls, client_id=None, client_secret=None, quota_project_id=None, token_info_url=None, scopes=None, default_scopes=None, service_account_impersonation_url=None, service_account_impersonation_options={}, universe_domain=external_account._DEFAULT_UNIVERSE_DOMAIN, ): return CredentialsImpl( audience=cls.AUDIENCE, subject_token_type=cls.SUBJECT_TOKEN_TYPE, token_url=cls.TOKEN_URL, token_info_url=token_info_url, service_account_impersonation_url=service_account_impersonation_url, service_account_impersonation_options=service_account_impersonation_options, credential_source=cls.CREDENTIAL_SOURCE, client_id=client_id, client_secret=client_secret, quota_project_id=quota_project_id, scopes=scopes, default_scopes=default_scopes, universe_domain=universe_domain, ) @classmethod def make_workforce_pool_credentials( cls, client_id=None, client_secret=None, quota_project_id=None, scopes=None, default_scopes=None, service_account_impersonation_url=None, workforce_pool_user_project=None, ): return CredentialsImpl( audience=cls.WORKFORCE_AUDIENCE, subject_token_type=cls.WORKFORCE_SUBJECT_TOKEN_TYPE, token_url=cls.TOKEN_URL, service_account_impersonation_url=service_account_impersonation_url, credential_source=cls.CREDENTIAL_SOURCE, client_id=client_id, client_secret=client_secret, quota_project_id=quota_project_id, scopes=scopes, default_scopes=default_scopes, workforce_pool_user_project=workforce_pool_user_project, ) @classmethod def make_mock_request( cls, status=http_client.OK, data=None, impersonation_status=None, impersonation_data=None, cloud_resource_manager_status=None, cloud_resource_manager_data=None, ): # STS token exchange request. token_response = mock.create_autospec(transport.Response, instance=True) token_response.status = status token_response.data = json.dumps(data).encode("utf-8") responses = [token_response] # If service account impersonation is requested, mock the expected response. if impersonation_status: impersonation_response = mock.create_autospec( transport.Response, instance=True ) impersonation_response.status = impersonation_status impersonation_response.data = json.dumps(impersonation_data).encode("utf-8") responses.append(impersonation_response) # If cloud resource manager is requested, mock the expected response. if cloud_resource_manager_status: cloud_resource_manager_response = mock.create_autospec( transport.Response, instance=True ) cloud_resource_manager_response.status = cloud_resource_manager_status cloud_resource_manager_response.data = json.dumps( cloud_resource_manager_data ).encode("utf-8") responses.append(cloud_resource_manager_response) request = mock.create_autospec(transport.Request) request.side_effect = responses return request @classmethod def assert_token_request_kwargs(cls, request_kwargs, headers, request_data): assert request_kwargs["url"] == cls.TOKEN_URL assert request_kwargs["method"] == "POST" assert request_kwargs["headers"] == headers assert request_kwargs["body"] is not None body_tuples = urllib.parse.parse_qsl(request_kwargs["body"]) for (k, v) in body_tuples: assert v.decode("utf-8") == request_data[k.decode("utf-8")] assert len(body_tuples) == len(request_data.keys()) @classmethod def assert_impersonation_request_kwargs(cls, request_kwargs, headers, request_data): assert request_kwargs["url"] == cls.SERVICE_ACCOUNT_IMPERSONATION_URL assert request_kwargs["method"] == "POST" assert request_kwargs["headers"] == headers assert request_kwargs["body"] is not None body_json = json.loads(request_kwargs["body"].decode("utf-8")) assert body_json == request_data @classmethod def assert_resource_manager_request_kwargs( cls, request_kwargs, project_number, headers ): assert request_kwargs["url"] == cls.CLOUD_RESOURCE_MANAGER_URL + project_number assert request_kwargs["method"] == "GET" assert request_kwargs["headers"] == headers assert "body" not in request_kwargs def test_default_state(self): credentials = self.make_credentials( service_account_impersonation_url=self.SERVICE_ACCOUNT_IMPERSONATION_URL ) # Token url and service account impersonation url should be set assert credentials._token_url assert credentials._service_account_impersonation_url # Not token acquired yet assert not credentials.token assert not credentials.valid # Expiration hasn't been set yet assert not credentials.expiry assert not credentials.expired # Scopes are required assert not credentials.scopes assert credentials.requires_scopes assert not credentials.quota_project_id # Token info url not set yet assert not credentials.token_info_url def test_nonworkforce_with_workforce_pool_user_project(self): with pytest.raises(ValueError) as excinfo: CredentialsImpl( audience=self.AUDIENCE, subject_token_type=self.SUBJECT_TOKEN_TYPE, token_url=self.TOKEN_URL, credential_source=self.CREDENTIAL_SOURCE, workforce_pool_user_project=self.WORKFORCE_POOL_USER_PROJECT, ) assert excinfo.match( "workforce_pool_user_project should not be set for non-workforce " "pool credentials" ) def test_with_scopes(self): credentials = self.make_credentials() assert not credentials.scopes assert credentials.requires_scopes scoped_credentials = credentials.with_scopes(["email"]) assert scoped_credentials.has_scopes(["email"]) assert not scoped_credentials.requires_scopes def test_with_scopes_workforce_pool(self): credentials = self.make_workforce_pool_credentials( workforce_pool_user_project=self.WORKFORCE_POOL_USER_PROJECT ) assert not credentials.scopes assert credentials.requires_scopes scoped_credentials = credentials.with_scopes(["email"]) assert scoped_credentials.has_scopes(["email"]) assert not scoped_credentials.requires_scopes assert ( scoped_credentials.info.get("workforce_pool_user_project") == self.WORKFORCE_POOL_USER_PROJECT ) def test_with_scopes_using_user_and_default_scopes(self): credentials = self.make_credentials() assert not credentials.scopes assert credentials.requires_scopes scoped_credentials = credentials.with_scopes( ["email"], default_scopes=["profile"] ) assert scoped_credentials.has_scopes(["email"]) assert not scoped_credentials.has_scopes(["profile"]) assert not scoped_credentials.requires_scopes assert scoped_credentials.scopes == ["email"] assert scoped_credentials.default_scopes == ["profile"] def test_with_scopes_using_default_scopes_only(self): credentials = self.make_credentials() assert not credentials.scopes assert credentials.requires_scopes scoped_credentials = credentials.with_scopes(None, default_scopes=["profile"]) assert scoped_credentials.has_scopes(["profile"]) assert not scoped_credentials.requires_scopes def test_with_scopes_full_options_propagated(self): credentials = self.make_credentials( client_id=CLIENT_ID, client_secret=CLIENT_SECRET, quota_project_id=self.QUOTA_PROJECT_ID, scopes=self.SCOPES, token_info_url=self.TOKEN_INFO_URL, default_scopes=["default1"], service_account_impersonation_url=self.SERVICE_ACCOUNT_IMPERSONATION_URL, service_account_impersonation_options={"token_lifetime_seconds": 2800}, ) with mock.patch.object( external_account.Credentials, "__init__", return_value=None ) as mock_init: credentials.with_scopes(["email"], ["default2"]) # Confirm with_scopes initialized the credential with the expected # parameters and scopes. mock_init.assert_called_once_with( audience=self.AUDIENCE, subject_token_type=self.SUBJECT_TOKEN_TYPE, token_url=self.TOKEN_URL, token_info_url=self.TOKEN_INFO_URL, credential_source=self.CREDENTIAL_SOURCE, service_account_impersonation_url=self.SERVICE_ACCOUNT_IMPERSONATION_URL, service_account_impersonation_options={"token_lifetime_seconds": 2800}, client_id=CLIENT_ID, client_secret=CLIENT_SECRET, quota_project_id=self.QUOTA_PROJECT_ID, scopes=["email"], default_scopes=["default2"], universe_domain=external_account._DEFAULT_UNIVERSE_DOMAIN, ) 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() 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_with_quota_project_workforce_pool(self): credentials = self.make_workforce_pool_credentials( workforce_pool_user_project=self.WORKFORCE_POOL_USER_PROJECT ) 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" assert ( quota_project_creds.info.get("workforce_pool_user_project") == self.WORKFORCE_POOL_USER_PROJECT ) def test_with_quota_project_full_options_propagated(self): credentials = self.make_credentials( client_id=CLIENT_ID, client_secret=CLIENT_SECRET, token_info_url=self.TOKEN_INFO_URL, quota_project_id=self.QUOTA_PROJECT_ID, scopes=self.SCOPES, default_scopes=["default1"], service_account_impersonation_url=self.SERVICE_ACCOUNT_IMPERSONATION_URL, service_account_impersonation_options={"token_lifetime_seconds": 2800}, ) with mock.patch.object( external_account.Credentials, "__init__", return_value=None ) as mock_init: credentials.with_quota_project("project-foo") # Confirm with_quota_project initialized the credential with the # expected parameters and quota project ID. mock_init.assert_called_once_with( audience=self.AUDIENCE, subject_token_type=self.SUBJECT_TOKEN_TYPE, token_url=self.TOKEN_URL, token_info_url=self.TOKEN_INFO_URL, credential_source=self.CREDENTIAL_SOURCE, service_account_impersonation_url=self.SERVICE_ACCOUNT_IMPERSONATION_URL, service_account_impersonation_options={"token_lifetime_seconds": 2800}, client_id=CLIENT_ID, client_secret=CLIENT_SECRET, quota_project_id="project-foo", scopes=self.SCOPES, default_scopes=["default1"], universe_domain=external_account._DEFAULT_UNIVERSE_DOMAIN, ) def test_with_invalid_impersonation_target_principal(self): invalid_url = "https://iamcredentials.googleapis.com/v1/invalid" with pytest.raises(exceptions.RefreshError) as excinfo: self.make_credentials(service_account_impersonation_url=invalid_url) assert excinfo.match( r"Unable to determine target principal from service account impersonation URL." ) def test_info(self): credentials = self.make_credentials(universe_domain="dummy_universe.com") assert credentials.info == { "type": "external_account", "audience": self.AUDIENCE, "subject_token_type": self.SUBJECT_TOKEN_TYPE, "token_url": self.TOKEN_URL, "credential_source": self.CREDENTIAL_SOURCE.copy(), "universe_domain": "dummy_universe.com", } def test_universe_domain(self): credentials = self.make_credentials(universe_domain="dummy_universe.com") assert credentials.universe_domain == "dummy_universe.com" credentials = self.make_credentials() assert credentials.universe_domain == external_account._DEFAULT_UNIVERSE_DOMAIN def test_info_workforce_pool(self): credentials = self.make_workforce_pool_credentials( workforce_pool_user_project=self.WORKFORCE_POOL_USER_PROJECT ) assert credentials.info == { "type": "external_account", "audience": self.WORKFORCE_AUDIENCE, "subject_token_type": self.WORKFORCE_SUBJECT_TOKEN_TYPE, "token_url": self.TOKEN_URL, "credential_source": self.CREDENTIAL_SOURCE.copy(), "workforce_pool_user_project": self.WORKFORCE_POOL_USER_PROJECT, "universe_domain": external_account._DEFAULT_UNIVERSE_DOMAIN, } def test_info_with_full_options(self): credentials = self.make_credentials( client_id=CLIENT_ID, client_secret=CLIENT_SECRET, quota_project_id=self.QUOTA_PROJECT_ID, token_info_url=self.TOKEN_INFO_URL, service_account_impersonation_url=self.SERVICE_ACCOUNT_IMPERSONATION_URL, service_account_impersonation_options={"token_lifetime_seconds": 2800}, ) assert credentials.info == { "type": "external_account", "audience": self.AUDIENCE, "subject_token_type": self.SUBJECT_TOKEN_TYPE, "token_url": self.TOKEN_URL, "token_info_url": self.TOKEN_INFO_URL, "service_account_impersonation_url": self.SERVICE_ACCOUNT_IMPERSONATION_URL, "service_account_impersonation": {"token_lifetime_seconds": 2800}, "credential_source": self.CREDENTIAL_SOURCE.copy(), "quota_project_id": self.QUOTA_PROJECT_ID, "client_id": CLIENT_ID, "client_secret": CLIENT_SECRET, "universe_domain": external_account._DEFAULT_UNIVERSE_DOMAIN, } def test_service_account_email_without_impersonation(self): credentials = self.make_credentials() assert credentials.service_account_email is None def test_service_account_email_with_impersonation(self): credentials = self.make_credentials( service_account_impersonation_url=self.SERVICE_ACCOUNT_IMPERSONATION_URL ) assert credentials.service_account_email == SERVICE_ACCOUNT_EMAIL @pytest.mark.parametrize("audience", TEST_NON_USER_AUDIENCES) def test_is_user_with_non_users(self, audience): credentials = CredentialsImpl( audience=audience, subject_token_type=self.SUBJECT_TOKEN_TYPE, token_url=self.TOKEN_URL, credential_source=self.CREDENTIAL_SOURCE, ) assert credentials.is_user is False @pytest.mark.parametrize("audience", TEST_USER_AUDIENCES) def test_is_user_with_users(self, audience): credentials = CredentialsImpl( audience=audience, subject_token_type=self.SUBJECT_TOKEN_TYPE, token_url=self.TOKEN_URL, credential_source=self.CREDENTIAL_SOURCE, ) assert credentials.is_user is True @pytest.mark.parametrize("audience", TEST_USER_AUDIENCES) def test_is_user_with_users_and_impersonation(self, audience): # Initialize the credentials with service account impersonation. credentials = CredentialsImpl( audience=audience, subject_token_type=self.SUBJECT_TOKEN_TYPE, token_url=self.TOKEN_URL, credential_source=self.CREDENTIAL_SOURCE, service_account_impersonation_url=self.SERVICE_ACCOUNT_IMPERSONATION_URL, ) # Even though the audience is for a workforce pool, since service account # impersonation is used, the credentials will represent a service account and # not a user. assert credentials.is_user is False @pytest.mark.parametrize("audience", TEST_NON_USER_AUDIENCES) def test_is_workforce_pool_with_non_users(self, audience): credentials = CredentialsImpl( audience=audience, subject_token_type=self.SUBJECT_TOKEN_TYPE, token_url=self.TOKEN_URL, credential_source=self.CREDENTIAL_SOURCE, ) assert credentials.is_workforce_pool is False @pytest.mark.parametrize("audience", TEST_USER_AUDIENCES) def test_is_workforce_pool_with_users(self, audience): credentials = CredentialsImpl( audience=audience, subject_token_type=self.SUBJECT_TOKEN_TYPE, token_url=self.TOKEN_URL, credential_source=self.CREDENTIAL_SOURCE, ) assert credentials.is_workforce_pool is True @pytest.mark.parametrize("audience", TEST_USER_AUDIENCES) def test_is_workforce_pool_with_users_and_impersonation(self, audience): # Initialize the credentials with workforce audience and service account # impersonation. credentials = CredentialsImpl( audience=audience, subject_token_type=self.SUBJECT_TOKEN_TYPE, token_url=self.TOKEN_URL, credential_source=self.CREDENTIAL_SOURCE, service_account_impersonation_url=self.SERVICE_ACCOUNT_IMPERSONATION_URL, ) # Even though impersonation is used, is_workforce_pool should still return True. assert credentials.is_workforce_pool is True @pytest.mark.parametrize("mock_expires_in", [2800, "2800"]) @mock.patch( "google.auth.metrics.python_and_auth_lib_version", return_value=LANG_LIBRARY_METRICS_HEADER_VALUE, ) @mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min) def test_refresh_without_client_auth_success( self, unused_utcnow, mock_auth_lib_value, mock_expires_in ): response = self.SUCCESS_RESPONSE.copy() # Test custom expiration to confirm expiry is set correctly. response["expires_in"] = mock_expires_in expected_expiry = datetime.datetime.min + datetime.timedelta( seconds=int(mock_expires_in) ) headers = { "Content-Type": "application/x-www-form-urlencoded", "x-goog-api-client": "gl-python/3.7 auth/1.1 google-byoid-sdk sa-impersonation/false config-lifetime/false", } request_data = { "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange", "audience": self.AUDIENCE, "requested_token_type": "urn:ietf:params:oauth:token-type:access_token", "subject_token": "subject_token_0", "subject_token_type": self.SUBJECT_TOKEN_TYPE, } request = self.make_mock_request(status=http_client.OK, data=response) credentials = self.make_credentials() credentials.refresh(request) self.assert_token_request_kwargs(request.call_args[1], headers, request_data) assert credentials.valid assert credentials.expiry == expected_expiry assert not credentials.expired assert credentials.token == response["access_token"] @mock.patch( "google.auth.metrics.python_and_auth_lib_version", return_value=LANG_LIBRARY_METRICS_HEADER_VALUE, ) @mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min) def test_refresh_workforce_without_client_auth_success( self, unused_utcnow, test_auth_lib_value ): response = self.SUCCESS_RESPONSE.copy() # Test custom expiration to confirm expiry is set correctly. response["expires_in"] = 2800 expected_expiry = datetime.datetime.min + datetime.timedelta( seconds=response["expires_in"] ) headers = { "Content-Type": "application/x-www-form-urlencoded", "x-goog-api-client": "gl-python/3.7 auth/1.1 google-byoid-sdk sa-impersonation/false config-lifetime/false", } request_data = { "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange", "audience": self.WORKFORCE_AUDIENCE, "requested_token_type": "urn:ietf:params:oauth:token-type:access_token", "subject_token": "subject_token_0", "subject_token_type": self.WORKFORCE_SUBJECT_TOKEN_TYPE, "options": urllib.parse.quote( json.dumps({"userProject": self.WORKFORCE_POOL_USER_PROJECT}) ), } request = self.make_mock_request(status=http_client.OK, data=response) credentials = self.make_workforce_pool_credentials( workforce_pool_user_project=self.WORKFORCE_POOL_USER_PROJECT ) credentials.refresh(request) self.assert_token_request_kwargs(request.call_args[1], headers, request_data) assert credentials.valid assert credentials.expiry == expected_expiry assert not credentials.expired assert credentials.token == response["access_token"] @mock.patch( "google.auth.metrics.python_and_auth_lib_version", return_value=LANG_LIBRARY_METRICS_HEADER_VALUE, ) @mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min) def test_refresh_workforce_with_client_auth_success( self, unused_utcnow, mock_auth_lib_value ): response = self.SUCCESS_RESPONSE.copy() # Test custom expiration to confirm expiry is set correctly. response["expires_in"] = 2800 expected_expiry = datetime.datetime.min + datetime.timedelta( seconds=response["expires_in"] ) headers = { "Content-Type": "application/x-www-form-urlencoded", "Authorization": "Basic {}".format(BASIC_AUTH_ENCODING), "x-goog-api-client": "gl-python/3.7 auth/1.1 google-byoid-sdk sa-impersonation/false config-lifetime/false", } request_data = { "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange", "audience": self.WORKFORCE_AUDIENCE, "requested_token_type": "urn:ietf:params:oauth:token-type:access_token", "subject_token": "subject_token_0", "subject_token_type": self.WORKFORCE_SUBJECT_TOKEN_TYPE, } request = self.make_mock_request(status=http_client.OK, data=response) # Client Auth will have higher priority over workforce_pool_user_project. credentials = self.make_workforce_pool_credentials( client_id=CLIENT_ID, client_secret=CLIENT_SECRET, workforce_pool_user_project=self.WORKFORCE_POOL_USER_PROJECT, ) credentials.refresh(request) self.assert_token_request_kwargs(request.call_args[1], headers, request_data) assert credentials.valid assert credentials.expiry == expected_expiry assert not credentials.expired assert credentials.token == response["access_token"] @mock.patch( "google.auth.metrics.python_and_auth_lib_version", return_value=LANG_LIBRARY_METRICS_HEADER_VALUE, ) @mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min) def test_refresh_workforce_with_client_auth_and_no_workforce_project_success( self, unused_utcnow, mock_lib_version_value ): response = self.SUCCESS_RESPONSE.copy() # Test custom expiration to confirm expiry is set correctly. response["expires_in"] = 2800 expected_expiry = datetime.datetime.min + datetime.timedelta( seconds=response["expires_in"] ) headers = { "Content-Type": "application/x-www-form-urlencoded", "Authorization": "Basic {}".format(BASIC_AUTH_ENCODING), "x-goog-api-client": "gl-python/3.7 auth/1.1 google-byoid-sdk sa-impersonation/false config-lifetime/false", } request_data = { "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange", "audience": self.WORKFORCE_AUDIENCE, "requested_token_type": "urn:ietf:params:oauth:token-type:access_token", "subject_token": "subject_token_0", "subject_token_type": self.WORKFORCE_SUBJECT_TOKEN_TYPE, } request = self.make_mock_request(status=http_client.OK, data=response) # Client Auth will be sufficient for user project determination. credentials = self.make_workforce_pool_credentials( client_id=CLIENT_ID, client_secret=CLIENT_SECRET, workforce_pool_user_project=None, ) credentials.refresh(request) self.assert_token_request_kwargs(request.call_args[1], headers, request_data) assert credentials.valid assert credentials.expiry == expected_expiry assert not credentials.expired assert credentials.token == response["access_token"] @mock.patch( "google.auth.metrics.token_request_access_token_impersonate", return_value=IMPERSONATE_ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE, ) @mock.patch( "google.auth.metrics.python_and_auth_lib_version", return_value=LANG_LIBRARY_METRICS_HEADER_VALUE, ) def test_refresh_impersonation_without_client_auth_success( self, mock_metrics_header_value, mock_auth_lib_value ): # Simulate service account access token expires in 2800 seconds. expire_time = ( _helpers.utcnow().replace(microsecond=0) + datetime.timedelta(seconds=2800) ).isoformat("T") + "Z" expected_expiry = datetime.datetime.strptime(expire_time, "%Y-%m-%dT%H:%M:%SZ") # STS token exchange request/response. token_response = self.SUCCESS_RESPONSE.copy() token_headers = { "Content-Type": "application/x-www-form-urlencoded", "x-goog-api-client": "gl-python/3.7 auth/1.1 google-byoid-sdk sa-impersonation/true config-lifetime/false", } token_request_data = { "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange", "audience": self.AUDIENCE, "requested_token_type": "urn:ietf:params:oauth:token-type:access_token", "subject_token": "subject_token_0", "subject_token_type": self.SUBJECT_TOKEN_TYPE, "scope": "https://www.googleapis.com/auth/iam", } # Service account impersonation request/response. impersonation_response = { "accessToken": "SA_ACCESS_TOKEN", "expireTime": expire_time, } impersonation_headers = { "Content-Type": "application/json", "authorization": "Bearer {}".format(token_response["access_token"]), "x-goog-api-client": IMPERSONATE_ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE, "x-allowed-locations": "0x0", } impersonation_request_data = { "delegates": None, "scope": self.SCOPES, "lifetime": "3600s", } # Initialize mock request to handle token exchange and service account # impersonation request. request = self.make_mock_request( status=http_client.OK, data=token_response, impersonation_status=http_client.OK, impersonation_data=impersonation_response, ) # Initialize credentials with service account impersonation. credentials = self.make_credentials( service_account_impersonation_url=self.SERVICE_ACCOUNT_IMPERSONATION_URL, scopes=self.SCOPES, ) credentials.refresh(request) # Only 2 requests should be processed. assert len(request.call_args_list) == 2 # Verify token exchange request parameters. self.assert_token_request_kwargs( request.call_args_list[0][1], token_headers, token_request_data ) # Verify service account impersonation request parameters. self.assert_impersonation_request_kwargs( request.call_args_list[1][1], impersonation_headers, impersonation_request_data, ) assert credentials.valid assert credentials.expiry == expected_expiry assert not credentials.expired assert credentials.token == impersonation_response["accessToken"] @mock.patch( "google.auth.metrics.token_request_access_token_impersonate", return_value=IMPERSONATE_ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE, ) @mock.patch( "google.auth.metrics.python_and_auth_lib_version", return_value=LANG_LIBRARY_METRICS_HEADER_VALUE, ) def test_refresh_workforce_impersonation_without_client_auth_success( self, mock_metrics_header_value, mock_auth_lib_value ): # Simulate service account access token expires in 2800 seconds. expire_time = ( _helpers.utcnow().replace(microsecond=0) + datetime.timedelta(seconds=2800) ).isoformat("T") + "Z" expected_expiry = datetime.datetime.strptime(expire_time, "%Y-%m-%dT%H:%M:%SZ") # STS token exchange request/response. token_response = self.SUCCESS_RESPONSE.copy() token_headers = { "Content-Type": "application/x-www-form-urlencoded", "x-goog-api-client": "gl-python/3.7 auth/1.1 google-byoid-sdk sa-impersonation/true config-lifetime/false", } token_request_data = { "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange", "audience": self.WORKFORCE_AUDIENCE, "requested_token_type": "urn:ietf:params:oauth:token-type:access_token", "subject_token": "subject_token_0", "subject_token_type": self.WORKFORCE_SUBJECT_TOKEN_TYPE, "scope": "https://www.googleapis.com/auth/iam", "options": urllib.parse.quote( json.dumps({"userProject": self.WORKFORCE_POOL_USER_PROJECT}) ), } # Service account impersonation request/response. impersonation_response = { "accessToken": "SA_ACCESS_TOKEN", "expireTime": expire_time, } impersonation_headers = { "Content-Type": "application/json", "authorization": "Bearer {}".format(token_response["access_token"]), "x-goog-api-client": IMPERSONATE_ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE, "x-allowed-locations": "0x0", } impersonation_request_data = { "delegates": None, "scope": self.SCOPES, "lifetime": "3600s", } # Initialize mock request to handle token exchange and service account # impersonation request. request = self.make_mock_request( status=http_client.OK, data=token_response, impersonation_status=http_client.OK, impersonation_data=impersonation_response, ) # Initialize credentials with service account impersonation. credentials = self.make_workforce_pool_credentials( service_account_impersonation_url=self.SERVICE_ACCOUNT_IMPERSONATION_URL, scopes=self.SCOPES, workforce_pool_user_project=self.WORKFORCE_POOL_USER_PROJECT, ) credentials.refresh(request) # Only 2 requests should be processed. assert len(request.call_args_list) == 2 # Verify token exchange request parameters. self.assert_token_request_kwargs( request.call_args_list[0][1], token_headers, token_request_data ) # Verify service account impersonation request parameters. self.assert_impersonation_request_kwargs( request.call_args_list[1][1], impersonation_headers, impersonation_request_data, ) assert credentials.valid assert credentials.expiry == expected_expiry assert not credentials.expired assert credentials.token == impersonation_response["accessToken"] @mock.patch( "google.auth.metrics.python_and_auth_lib_version", return_value=LANG_LIBRARY_METRICS_HEADER_VALUE, ) def test_refresh_without_client_auth_success_explicit_user_scopes_ignore_default_scopes( self, mock_auth_lib_value ): headers = { "Content-Type": "application/x-www-form-urlencoded", "x-goog-api-client": "gl-python/3.7 auth/1.1 google-byoid-sdk sa-impersonation/false config-lifetime/false", } request_data = { "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange", "audience": self.AUDIENCE, "requested_token_type": "urn:ietf:params:oauth:token-type:access_token", "scope": "scope1 scope2", "subject_token": "subject_token_0", "subject_token_type": self.SUBJECT_TOKEN_TYPE, } request = self.make_mock_request( status=http_client.OK, data=self.SUCCESS_RESPONSE ) credentials = self.make_credentials( scopes=["scope1", "scope2"], # Default scopes will be ignored in favor of user scopes. default_scopes=["ignored"], ) credentials.refresh(request) self.assert_token_request_kwargs(request.call_args[1], headers, request_data) assert credentials.valid assert not credentials.expired assert credentials.token == self.SUCCESS_RESPONSE["access_token"] assert credentials.has_scopes(["scope1", "scope2"]) assert not credentials.has_scopes(["ignored"]) @mock.patch( "google.auth.metrics.python_and_auth_lib_version", return_value=LANG_LIBRARY_METRICS_HEADER_VALUE, ) def test_refresh_without_client_auth_success_explicit_default_scopes_only( self, mock_auth_lib_value ): headers = { "Content-Type": "application/x-www-form-urlencoded", "x-goog-api-client": "gl-python/3.7 auth/1.1 google-byoid-sdk sa-impersonation/false config-lifetime/false", } request_data = { "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange", "audience": self.AUDIENCE, "requested_token_type": "urn:ietf:params:oauth:token-type:access_token", "scope": "scope1 scope2", "subject_token": "subject_token_0", "subject_token_type": self.SUBJECT_TOKEN_TYPE, } request = self.make_mock_request( status=http_client.OK, data=self.SUCCESS_RESPONSE ) credentials = self.make_credentials( scopes=None, # Default scopes will be used since user scopes are none. default_scopes=["scope1", "scope2"], ) credentials.refresh(request) self.assert_token_request_kwargs(request.call_args[1], headers, request_data) assert credentials.valid assert not credentials.expired assert credentials.token == self.SUCCESS_RESPONSE["access_token"] assert credentials.has_scopes(["scope1", "scope2"]) def test_refresh_without_client_auth_error(self): request = self.make_mock_request( status=http_client.BAD_REQUEST, data=self.ERROR_RESPONSE ) credentials = self.make_credentials() with pytest.raises(exceptions.OAuthError) as excinfo: credentials.refresh(request) assert excinfo.match( r"Error code invalid_request: Invalid subject token - https://tools.ietf.org/html/rfc6749" ) assert not credentials.expired assert credentials.token is None def test_refresh_impersonation_without_client_auth_error(self): request = self.make_mock_request( status=http_client.OK, data=self.SUCCESS_RESPONSE, impersonation_status=http_client.BAD_REQUEST, impersonation_data=self.IMPERSONATION_ERROR_RESPONSE, ) credentials = self.make_credentials( service_account_impersonation_url=self.SERVICE_ACCOUNT_IMPERSONATION_URL, scopes=self.SCOPES, ) with pytest.raises(exceptions.RefreshError) as excinfo: credentials.refresh(request) assert excinfo.match(r"Unable to acquire impersonated credentials") assert not credentials.expired assert credentials.token is None @mock.patch( "google.auth.metrics.python_and_auth_lib_version", return_value=LANG_LIBRARY_METRICS_HEADER_VALUE, ) def test_refresh_with_client_auth_success(self, mock_auth_lib_value): headers = { "Content-Type": "application/x-www-form-urlencoded", "Authorization": "Basic {}".format(BASIC_AUTH_ENCODING), "x-goog-api-client": "gl-python/3.7 auth/1.1 google-byoid-sdk sa-impersonation/false config-lifetime/false", } request_data = { "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange", "audience": self.AUDIENCE, "requested_token_type": "urn:ietf:params:oauth:token-type:access_token", "subject_token": "subject_token_0", "subject_token_type": self.SUBJECT_TOKEN_TYPE, } request = self.make_mock_request( status=http_client.OK, data=self.SUCCESS_RESPONSE ) credentials = self.make_credentials( client_id=CLIENT_ID, client_secret=CLIENT_SECRET ) credentials.refresh(request) self.assert_token_request_kwargs(request.call_args[1], headers, request_data) assert credentials.valid assert not credentials.expired assert credentials.token == self.SUCCESS_RESPONSE["access_token"] @mock.patch( "google.auth.metrics.token_request_access_token_impersonate", return_value=IMPERSONATE_ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE, ) @mock.patch( "google.auth.metrics.python_and_auth_lib_version", return_value=LANG_LIBRARY_METRICS_HEADER_VALUE, ) def test_refresh_impersonation_with_client_auth_success_ignore_default_scopes( self, mock_metrics_header_value, mock_auth_lib_value ): # Simulate service account access token expires in 2800 seconds. expire_time = ( _helpers.utcnow().replace(microsecond=0) + datetime.timedelta(seconds=2800) ).isoformat("T") + "Z" expected_expiry = datetime.datetime.strptime(expire_time, "%Y-%m-%dT%H:%M:%SZ") # STS token exchange request/response. token_response = self.SUCCESS_RESPONSE.copy() token_headers = { "Content-Type": "application/x-www-form-urlencoded", "Authorization": "Basic {}".format(BASIC_AUTH_ENCODING), "x-goog-api-client": "gl-python/3.7 auth/1.1 google-byoid-sdk sa-impersonation/true config-lifetime/false", } token_request_data = { "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange", "audience": self.AUDIENCE, "requested_token_type": "urn:ietf:params:oauth:token-type:access_token", "subject_token": "subject_token_0", "subject_token_type": self.SUBJECT_TOKEN_TYPE, "scope": "https://www.googleapis.com/auth/iam", } # Service account impersonation request/response. impersonation_response = { "accessToken": "SA_ACCESS_TOKEN", "expireTime": expire_time, } impersonation_headers = { "Content-Type": "application/json", "authorization": "Bearer {}".format(token_response["access_token"]), "x-goog-api-client": IMPERSONATE_ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE, "x-allowed-locations": "0x0", } impersonation_request_data = { "delegates": None, "scope": self.SCOPES, "lifetime": "3600s", } # Initialize mock request to handle token exchange and service account # impersonation request. request = self.make_mock_request( status=http_client.OK, data=token_response, impersonation_status=http_client.OK, impersonation_data=impersonation_response, ) # Initialize credentials with service account impersonation and basic auth. credentials = self.make_credentials( client_id=CLIENT_ID, client_secret=CLIENT_SECRET, service_account_impersonation_url=self.SERVICE_ACCOUNT_IMPERSONATION_URL, scopes=self.SCOPES, # Default scopes will be ignored since user scopes are specified. default_scopes=["ignored"], ) credentials.refresh(request) # Only 2 requests should be processed. assert len(request.call_args_list) == 2 # Verify token exchange request parameters. self.assert_token_request_kwargs( request.call_args_list[0][1], token_headers, token_request_data ) # Verify service account impersonation request parameters. self.assert_impersonation_request_kwargs( request.call_args_list[1][1], impersonation_headers, impersonation_request_data, ) assert credentials.valid assert credentials.expiry == expected_expiry assert not credentials.expired assert credentials.token == impersonation_response["accessToken"] @mock.patch( "google.auth.metrics.token_request_access_token_impersonate", return_value=IMPERSONATE_ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE, ) @mock.patch( "google.auth.metrics.python_and_auth_lib_version", return_value=LANG_LIBRARY_METRICS_HEADER_VALUE, ) def test_refresh_impersonation_with_client_auth_success_use_default_scopes( self, mock_metrics_header_value, mock_auth_lib_value ): # Simulate service account access token expires in 2800 seconds. expire_time = ( _helpers.utcnow().replace(microsecond=0) + datetime.timedelta(seconds=2800) ).isoformat("T") + "Z" expected_expiry = datetime.datetime.strptime(expire_time, "%Y-%m-%dT%H:%M:%SZ") # STS token exchange request/response. token_response = self.SUCCESS_RESPONSE.copy() token_headers = { "Content-Type": "application/x-www-form-urlencoded", "Authorization": "Basic {}".format(BASIC_AUTH_ENCODING), "x-goog-api-client": "gl-python/3.7 auth/1.1 google-byoid-sdk sa-impersonation/true config-lifetime/false", } token_request_data = { "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange", "audience": self.AUDIENCE, "requested_token_type": "urn:ietf:params:oauth:token-type:access_token", "subject_token": "subject_token_0", "subject_token_type": self.SUBJECT_TOKEN_TYPE, "scope": "https://www.googleapis.com/auth/iam", } # Service account impersonation request/response. impersonation_response = { "accessToken": "SA_ACCESS_TOKEN", "expireTime": expire_time, } impersonation_headers = { "Content-Type": "application/json", "authorization": "Bearer {}".format(token_response["access_token"]), "x-goog-api-client": IMPERSONATE_ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE, "x-allowed-locations": "0x0", } impersonation_request_data = { "delegates": None, "scope": self.SCOPES, "lifetime": "3600s", } # Initialize mock request to handle token exchange and service account # impersonation request. request = self.make_mock_request( status=http_client.OK, data=token_response, impersonation_status=http_client.OK, impersonation_data=impersonation_response, ) # Initialize credentials with service account impersonation and basic auth. credentials = self.make_credentials( client_id=CLIENT_ID, client_secret=CLIENT_SECRET, service_account_impersonation_url=self.SERVICE_ACCOUNT_IMPERSONATION_URL, scopes=None, # Default scopes will be used since user specified scopes are none. default_scopes=self.SCOPES, ) credentials.refresh(request) # Only 2 requests should be processed. assert len(request.call_args_list) == 2 # Verify token exchange request parameters. self.assert_token_request_kwargs( request.call_args_list[0][1], token_headers, token_request_data ) # Verify service account impersonation request parameters. self.assert_impersonation_request_kwargs( request.call_args_list[1][1], impersonation_headers, impersonation_request_data, ) assert credentials.valid assert credentials.expiry == expected_expiry assert not credentials.expired assert credentials.token == impersonation_response["accessToken"] def test_apply_without_quota_project_id(self): headers = {} request = self.make_mock_request( status=http_client.OK, data=self.SUCCESS_RESPONSE ) credentials = self.make_credentials() credentials.refresh(request) credentials.apply(headers) assert headers == { "authorization": "Bearer {}".format(self.SUCCESS_RESPONSE["access_token"]), "x-allowed-locations": "0x0", } def test_apply_workforce_without_quota_project_id(self): headers = {} request = self.make_mock_request( status=http_client.OK, data=self.SUCCESS_RESPONSE ) credentials = self.make_workforce_pool_credentials( workforce_pool_user_project=self.WORKFORCE_POOL_USER_PROJECT ) credentials.refresh(request) credentials.apply(headers) assert headers == { "authorization": "Bearer {}".format(self.SUCCESS_RESPONSE["access_token"]), "x-allowed-locations": "0x0", } def test_apply_impersonation_without_quota_project_id(self): expire_time = ( _helpers.utcnow().replace(microsecond=0) + datetime.timedelta(seconds=3600) ).isoformat("T") + "Z" # Service account impersonation response. impersonation_response = { "accessToken": "SA_ACCESS_TOKEN", "expireTime": expire_time, } # Initialize mock request to handle token exchange and service account # impersonation request. request = self.make_mock_request( status=http_client.OK, data=self.SUCCESS_RESPONSE.copy(), impersonation_status=http_client.OK, impersonation_data=impersonation_response, ) # Initialize credentials with service account impersonation. credentials = self.make_credentials( service_account_impersonation_url=self.SERVICE_ACCOUNT_IMPERSONATION_URL, scopes=self.SCOPES, ) headers = {} credentials.refresh(request) credentials.apply(headers) assert headers == { "authorization": "Bearer {}".format(impersonation_response["accessToken"]), "x-allowed-locations": "0x0", } def test_apply_with_quota_project_id(self): headers = {"other": "header-value"} request = self.make_mock_request( status=http_client.OK, data=self.SUCCESS_RESPONSE ) credentials = self.make_credentials(quota_project_id=self.QUOTA_PROJECT_ID) credentials.refresh(request) credentials.apply(headers) assert headers == { "other": "header-value", "authorization": "Bearer {}".format(self.SUCCESS_RESPONSE["access_token"]), "x-goog-user-project": self.QUOTA_PROJECT_ID, "x-allowed-locations": "0x0", } def test_apply_impersonation_with_quota_project_id(self): expire_time = ( _helpers.utcnow().replace(microsecond=0) + datetime.timedelta(seconds=3600) ).isoformat("T") + "Z" # Service account impersonation response. impersonation_response = { "accessToken": "SA_ACCESS_TOKEN", "expireTime": expire_time, } # Initialize mock request to handle token exchange and service account # impersonation request. request = self.make_mock_request( status=http_client.OK, data=self.SUCCESS_RESPONSE.copy(), impersonation_status=http_client.OK, impersonation_data=impersonation_response, ) # Initialize credentials with service account impersonation. credentials = self.make_credentials( service_account_impersonation_url=self.SERVICE_ACCOUNT_IMPERSONATION_URL, scopes=self.SCOPES, quota_project_id=self.QUOTA_PROJECT_ID, ) headers = {"other": "header-value"} credentials.refresh(request) credentials.apply(headers) assert headers == { "other": "header-value", "authorization": "Bearer {}".format(impersonation_response["accessToken"]), "x-goog-user-project": self.QUOTA_PROJECT_ID, "x-allowed-locations": "0x0", } def test_before_request(self): headers = {"other": "header-value"} request = self.make_mock_request( status=http_client.OK, data=self.SUCCESS_RESPONSE ) credentials = self.make_credentials() # First call should call refresh, setting the token. credentials.before_request(request, "POST", "https://example.com/api", headers) assert headers == { "other": "header-value", "authorization": "Bearer {}".format(self.SUCCESS_RESPONSE["access_token"]), "x-allowed-locations": "0x0", } # Second call shouldn't call refresh. credentials.before_request(request, "POST", "https://example.com/api", headers) assert headers == { "other": "header-value", "authorization": "Bearer {}".format(self.SUCCESS_RESPONSE["access_token"]), "x-allowed-locations": "0x0", } def test_before_request_workforce(self): headers = {"other": "header-value"} request = self.make_mock_request( status=http_client.OK, data=self.SUCCESS_RESPONSE ) credentials = self.make_workforce_pool_credentials( workforce_pool_user_project=self.WORKFORCE_POOL_USER_PROJECT ) # First call should call refresh, setting the token. credentials.before_request(request, "POST", "https://example.com/api", headers) assert headers == { "other": "header-value", "authorization": "Bearer {}".format(self.SUCCESS_RESPONSE["access_token"]), "x-allowed-locations": "0x0", } # Second call shouldn't call refresh. credentials.before_request(request, "POST", "https://example.com/api", headers) assert headers == { "other": "header-value", "authorization": "Bearer {}".format(self.SUCCESS_RESPONSE["access_token"]), "x-allowed-locations": "0x0", } def test_before_request_impersonation(self): expire_time = ( _helpers.utcnow().replace(microsecond=0) + datetime.timedelta(seconds=3600) ).isoformat("T") + "Z" # Service account impersonation response. impersonation_response = { "accessToken": "SA_ACCESS_TOKEN", "expireTime": expire_time, } # Initialize mock request to handle token exchange and service account # impersonation request. request = self.make_mock_request( status=http_client.OK, data=self.SUCCESS_RESPONSE.copy(), impersonation_status=http_client.OK, impersonation_data=impersonation_response, ) headers = {"other": "header-value"} credentials = self.make_credentials( service_account_impersonation_url=self.SERVICE_ACCOUNT_IMPERSONATION_URL ) # First call should call refresh, setting the token. credentials.before_request(request, "POST", "https://example.com/api", headers) assert headers == { "other": "header-value", "authorization": "Bearer {}".format(impersonation_response["accessToken"]), "x-allowed-locations": "0x0", } # Second call shouldn't call refresh. credentials.before_request(request, "POST", "https://example.com/api", headers) assert headers == { "other": "header-value", "authorization": "Bearer {}".format(impersonation_response["accessToken"]), "x-allowed-locations": "0x0", } @mock.patch("google.auth._helpers.utcnow") def test_before_request_expired(self, utcnow): headers = {} request = self.make_mock_request( status=http_client.OK, data=self.SUCCESS_RESPONSE ) credentials = self.make_credentials() credentials.token = "token" utcnow.return_value = datetime.datetime.min # Set the expiration to one second more than now plus the clock skew # accomodation. These credentials should be valid. credentials.expiry = ( datetime.datetime.min + _helpers.REFRESH_THRESHOLD + datetime.timedelta(seconds=1) ) assert credentials.valid assert not credentials.expired credentials.before_request(request, "POST", "https://example.com/api", headers) # Cached token should be used. assert headers == { "authorization": "Bearer token", "x-allowed-locations": "0x0", } # Next call should simulate 1 second passed. utcnow.return_value = datetime.datetime.min + datetime.timedelta(seconds=1) assert not credentials.valid assert credentials.expired credentials.before_request(request, "POST", "https://example.com/api", headers) # New token should be retrieved. assert headers == { "authorization": "Bearer {}".format(self.SUCCESS_RESPONSE["access_token"]), "x-allowed-locations": "0x0", } @mock.patch("google.auth._helpers.utcnow") def test_before_request_impersonation_expired(self, utcnow): headers = {} expire_time = ( datetime.datetime.min + datetime.timedelta(seconds=3601) ).isoformat("T") + "Z" # Service account impersonation response. impersonation_response = { "accessToken": "SA_ACCESS_TOKEN", "expireTime": expire_time, } # Initialize mock request to handle token exchange and service account # impersonation request. request = self.make_mock_request( status=http_client.OK, data=self.SUCCESS_RESPONSE.copy(), impersonation_status=http_client.OK, impersonation_data=impersonation_response, ) credentials = self.make_credentials( service_account_impersonation_url=self.SERVICE_ACCOUNT_IMPERSONATION_URL ) credentials.token = "token" utcnow.return_value = datetime.datetime.min # Set the expiration to one second more than now plus the clock skew # accomodation. These credentials should be valid. credentials.expiry = ( datetime.datetime.min + _helpers.REFRESH_THRESHOLD + datetime.timedelta(seconds=1) ) assert credentials.valid assert not credentials.expired credentials.before_request(request, "POST", "https://example.com/api", headers) # Cached token should be used. assert headers == { "authorization": "Bearer token", "x-allowed-locations": "0x0", } # Next call should simulate 1 second passed. This will trigger the expiration # threshold. utcnow.return_value = datetime.datetime.min + datetime.timedelta(seconds=1) assert not credentials.valid assert credentials.expired credentials.before_request(request, "POST", "https://example.com/api", headers) # New token should be retrieved. assert headers == { "authorization": "Bearer {}".format(impersonation_response["accessToken"]), "x-allowed-locations": "0x0", } @pytest.mark.parametrize( "audience", [ # Legacy K8s audience format. "identitynamespace:1f12345:my_provider", # Unrealistic audiences. "//iam.googleapis.com/projects", "//iam.googleapis.com/projects/", "//iam.googleapis.com/project/123456", "//iam.googleapis.com/projects//123456", "//iam.googleapis.com/prefix_projects/123456", "//iam.googleapis.com/projects_suffix/123456", ], ) def test_project_number_indeterminable(self, audience): credentials = CredentialsImpl( audience=audience, subject_token_type=self.SUBJECT_TOKEN_TYPE, token_url=self.TOKEN_URL, credential_source=self.CREDENTIAL_SOURCE, ) assert credentials.project_number is None assert credentials.get_project_id(None) is None def test_project_number_determinable(self): credentials = CredentialsImpl( audience=self.AUDIENCE, subject_token_type=self.SUBJECT_TOKEN_TYPE, token_url=self.TOKEN_URL, credential_source=self.CREDENTIAL_SOURCE, ) assert credentials.project_number == self.PROJECT_NUMBER def test_project_number_workforce(self): credentials = CredentialsImpl( audience=self.WORKFORCE_AUDIENCE, subject_token_type=self.WORKFORCE_SUBJECT_TOKEN_TYPE, token_url=self.TOKEN_URL, credential_source=self.CREDENTIAL_SOURCE, workforce_pool_user_project=self.WORKFORCE_POOL_USER_PROJECT, ) assert credentials.project_number is None def test_project_id_without_scopes(self): # Initialize credentials with no scopes. credentials = CredentialsImpl( audience=self.AUDIENCE, subject_token_type=self.SUBJECT_TOKEN_TYPE, token_url=self.TOKEN_URL, credential_source=self.CREDENTIAL_SOURCE, ) assert credentials.get_project_id(None) is None @mock.patch( "google.auth.metrics.token_request_access_token_impersonate", return_value=IMPERSONATE_ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE, ) @mock.patch( "google.auth.metrics.python_and_auth_lib_version", return_value=LANG_LIBRARY_METRICS_HEADER_VALUE, ) def test_get_project_id_cloud_resource_manager_success( self, mock_metrics_header_value, mock_auth_lib_value ): # STS token exchange request/response. token_response = self.SUCCESS_RESPONSE.copy() token_headers = { "Content-Type": "application/x-www-form-urlencoded", "x-goog-api-client": "gl-python/3.7 auth/1.1 google-byoid-sdk sa-impersonation/true config-lifetime/false", } token_request_data = { "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange", "audience": self.AUDIENCE, "requested_token_type": "urn:ietf:params:oauth:token-type:access_token", "subject_token": "subject_token_0", "subject_token_type": self.SUBJECT_TOKEN_TYPE, "scope": "https://www.googleapis.com/auth/iam", } # Service account impersonation request/response. expire_time = ( _helpers.utcnow().replace(microsecond=0) + datetime.timedelta(seconds=3600) ).isoformat("T") + "Z" expected_expiry = datetime.datetime.strptime(expire_time, "%Y-%m-%dT%H:%M:%SZ") impersonation_response = { "accessToken": "SA_ACCESS_TOKEN", "expireTime": expire_time, } impersonation_headers = { "Content-Type": "application/json", "x-goog-user-project": self.QUOTA_PROJECT_ID, "authorization": "Bearer {}".format(token_response["access_token"]), "x-goog-api-client": IMPERSONATE_ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE, "x-allowed-locations": "0x0", } impersonation_request_data = { "delegates": None, "scope": self.SCOPES, "lifetime": "3600s", } # Initialize mock request to handle token exchange, service account # impersonation and cloud resource manager request. request = self.make_mock_request( status=http_client.OK, data=self.SUCCESS_RESPONSE.copy(), impersonation_status=http_client.OK, impersonation_data=impersonation_response, cloud_resource_manager_status=http_client.OK, cloud_resource_manager_data=self.CLOUD_RESOURCE_MANAGER_SUCCESS_RESPONSE, ) credentials = self.make_credentials( service_account_impersonation_url=self.SERVICE_ACCOUNT_IMPERSONATION_URL, scopes=self.SCOPES, quota_project_id=self.QUOTA_PROJECT_ID, ) # Expected project ID from cloud resource manager response should be returned. project_id = credentials.get_project_id(request) assert project_id == self.PROJECT_ID # 3 requests should be processed. assert len(request.call_args_list) == 3 # Verify token exchange request parameters. self.assert_token_request_kwargs( request.call_args_list[0][1], token_headers, token_request_data ) # Verify service account impersonation request parameters. self.assert_impersonation_request_kwargs( request.call_args_list[1][1], impersonation_headers, impersonation_request_data, ) # In the process of getting project ID, an access token should be # retrieved. assert credentials.valid assert credentials.expiry == expected_expiry assert not credentials.expired assert credentials.token == impersonation_response["accessToken"] # Verify cloud resource manager request parameters. self.assert_resource_manager_request_kwargs( request.call_args_list[2][1], self.PROJECT_NUMBER, { "x-goog-user-project": self.QUOTA_PROJECT_ID, "authorization": "Bearer {}".format( impersonation_response["accessToken"] ), "x-allowed-locations": "0x0", }, ) # Calling get_project_id again should return the cached project_id. project_id = credentials.get_project_id(request) assert project_id == self.PROJECT_ID # No additional requests. assert len(request.call_args_list) == 3 @mock.patch( "google.auth.metrics.python_and_auth_lib_version", return_value=LANG_LIBRARY_METRICS_HEADER_VALUE, ) def test_workforce_pool_get_project_id_cloud_resource_manager_success( self, mock_auth_lib_value ): # STS token exchange request/response. token_headers = { "Content-Type": "application/x-www-form-urlencoded", "x-goog-api-client": "gl-python/3.7 auth/1.1 google-byoid-sdk sa-impersonation/false config-lifetime/false", } token_request_data = { "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange", "audience": self.WORKFORCE_AUDIENCE, "requested_token_type": "urn:ietf:params:oauth:token-type:access_token", "subject_token": "subject_token_0", "subject_token_type": self.WORKFORCE_SUBJECT_TOKEN_TYPE, "scope": "scope1 scope2", "options": urllib.parse.quote( json.dumps({"userProject": self.WORKFORCE_POOL_USER_PROJECT}) ), } # Initialize mock request to handle token exchange and cloud resource # manager request. request = self.make_mock_request( status=http_client.OK, data=self.SUCCESS_RESPONSE.copy(), cloud_resource_manager_status=http_client.OK, cloud_resource_manager_data=self.CLOUD_RESOURCE_MANAGER_SUCCESS_RESPONSE, ) credentials = self.make_workforce_pool_credentials( scopes=self.SCOPES, quota_project_id=self.QUOTA_PROJECT_ID, workforce_pool_user_project=self.WORKFORCE_POOL_USER_PROJECT, ) # Expected project ID from cloud resource manager response should be returned. project_id = credentials.get_project_id(request) assert project_id == self.PROJECT_ID # 2 requests should be processed. assert len(request.call_args_list) == 2 # Verify token exchange request parameters. self.assert_token_request_kwargs( request.call_args_list[0][1], token_headers, token_request_data ) # In the process of getting project ID, an access token should be # retrieved. assert credentials.valid assert not credentials.expired assert credentials.token == self.SUCCESS_RESPONSE["access_token"] # Verify cloud resource manager request parameters. self.assert_resource_manager_request_kwargs( request.call_args_list[1][1], self.WORKFORCE_POOL_USER_PROJECT, { "x-goog-user-project": self.QUOTA_PROJECT_ID, "authorization": "Bearer {}".format( self.SUCCESS_RESPONSE["access_token"] ), "x-allowed-locations": "0x0", }, ) # Calling get_project_id again should return the cached project_id. project_id = credentials.get_project_id(request) assert project_id == self.PROJECT_ID # No additional requests. assert len(request.call_args_list) == 2 @mock.patch( "google.auth.metrics.token_request_access_token_impersonate", return_value=IMPERSONATE_ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE, ) @mock.patch( "google.auth.metrics.python_and_auth_lib_version", return_value=LANG_LIBRARY_METRICS_HEADER_VALUE, ) def test_refresh_impersonation_with_lifetime( self, mock_metrics_header_value, mock_auth_lib_value ): # Simulate service account access token expires in 2800 seconds. expire_time = ( _helpers.utcnow().replace(microsecond=0) + datetime.timedelta(seconds=2800) ).isoformat("T") + "Z" expected_expiry = datetime.datetime.strptime(expire_time, "%Y-%m-%dT%H:%M:%SZ") # STS token exchange request/response. token_response = self.SUCCESS_RESPONSE.copy() token_headers = { "Content-Type": "application/x-www-form-urlencoded", "x-goog-api-client": "gl-python/3.7 auth/1.1 google-byoid-sdk sa-impersonation/true config-lifetime/true", } token_request_data = { "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange", "audience": self.AUDIENCE, "requested_token_type": "urn:ietf:params:oauth:token-type:access_token", "subject_token": "subject_token_0", "subject_token_type": self.SUBJECT_TOKEN_TYPE, "scope": "https://www.googleapis.com/auth/iam", } # Service account impersonation request/response. impersonation_response = { "accessToken": "SA_ACCESS_TOKEN", "expireTime": expire_time, } impersonation_headers = { "Content-Type": "application/json", "authorization": "Bearer {}".format(token_response["access_token"]), "x-goog-api-client": IMPERSONATE_ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE, "x-allowed-locations": "0x0", } impersonation_request_data = { "delegates": None, "scope": self.SCOPES, "lifetime": "2800s", } # Initialize mock request to handle token exchange and service account # impersonation request. request = self.make_mock_request( status=http_client.OK, data=token_response, impersonation_status=http_client.OK, impersonation_data=impersonation_response, ) # Initialize credentials with service account impersonation. credentials = self.make_credentials( service_account_impersonation_url=self.SERVICE_ACCOUNT_IMPERSONATION_URL, service_account_impersonation_options={"token_lifetime_seconds": 2800}, scopes=self.SCOPES, ) credentials.refresh(request) # Only 2 requests should be processed. assert len(request.call_args_list) == 2 # Verify token exchange request parameters. self.assert_token_request_kwargs( request.call_args_list[0][1], token_headers, token_request_data ) # Verify service account impersonation request parameters. self.assert_impersonation_request_kwargs( request.call_args_list[1][1], impersonation_headers, impersonation_request_data, ) assert credentials.valid assert credentials.expiry == expected_expiry assert not credentials.expired assert credentials.token == impersonation_response["accessToken"] def test_get_project_id_cloud_resource_manager_error(self): # Simulate resource doesn't have sufficient permissions to access # cloud resource manager. request = self.make_mock_request( status=http_client.OK, data=self.SUCCESS_RESPONSE.copy(), cloud_resource_manager_status=http_client.UNAUTHORIZED, ) credentials = self.make_credentials(scopes=self.SCOPES) project_id = credentials.get_project_id(request) assert project_id is None # Only 2 requests to STS and cloud resource manager should be sent. assert len(request.call_args_list) == 2 compute_engine/test_credentials.py 0000644 00000103240 15025175543 0013462 0 ustar 00 # Copyright 2016 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import base64 import datetime import mock import pytest # type: ignore import responses # type: ignore from google.auth import _helpers from google.auth import exceptions from google.auth import jwt from google.auth import transport from google.auth.compute_engine import credentials from google.auth.transport import requests SAMPLE_ID_TOKEN_EXP = 1584393400 # header: {"alg": "RS256", "typ": "JWT", "kid": "1"} # payload: {"iss": "issuer", "iat": 1584393348, "sub": "subject", # "exp": 1584393400,"aud": "audience"} SAMPLE_ID_TOKEN = ( b"eyJhbGciOiAiUlMyNTYiLCAidHlwIjogIkpXVCIsICJraWQiOiAiMSJ9." b"eyJpc3MiOiAiaXNzdWVyIiwgImlhdCI6IDE1ODQzOTMzNDgsICJzdWIiO" b"iAic3ViamVjdCIsICJleHAiOiAxNTg0MzkzNDAwLCAiYXVkIjogImF1ZG" b"llbmNlIn0." b"OquNjHKhTmlgCk361omRo18F_uY-7y0f_AmLbzW062Q1Zr61HAwHYP5FM" b"316CK4_0cH8MUNGASsvZc3VqXAqub6PUTfhemH8pFEwBdAdG0LhrNkU0H" b"WN1YpT55IiQ31esLdL5q-qDsOPpNZJUti1y1lAreM5nIn2srdWzGXGs4i" b"TRQsn0XkNUCL4RErpciXmjfhMrPkcAjKA-mXQm2fa4jmTlEZFqFmUlym1" b"ozJ0yf5grjN6AslN4OGvAv1pS-_Ko_pGBS6IQtSBC6vVKCUuBfaqNjykg" b"bsxbLa6Fp0SYeYwO8ifEnkRvasVpc1WTQqfRB2JCj5pTBDzJpIpFCMmnQ" ) ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE = ( "gl-python/3.7 auth/1.1 auth-request-type/at cred-type/mds" ) ID_TOKEN_REQUEST_METRICS_HEADER_VALUE = ( "gl-python/3.7 auth/1.1 auth-request-type/it cred-type/mds" ) class TestCredentials(object): credentials = None @pytest.fixture(autouse=True) def credentials_fixture(self): self.credentials = credentials.Credentials() def test_default_state(self): assert not self.credentials.valid # Expiration hasn't been set yet assert not self.credentials.expired # 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 assert not self.credentials._quota_project_id @mock.patch( "google.auth._helpers.utcnow", return_value=datetime.datetime.min + _helpers.REFRESH_THRESHOLD, ) @mock.patch("google.auth.compute_engine._metadata.get", autospec=True) def test_refresh_success(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 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 == ["one", "two"] # Check that the credentials are valid (have a token and are not # expired) assert self.credentials.valid @mock.patch( "google.auth.metrics.token_request_access_token_mds", return_value=ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE, ) @mock.patch( "google.auth._helpers.utcnow", return_value=datetime.datetime.min + _helpers.REFRESH_THRESHOLD, ) @mock.patch("google.auth.compute_engine._metadata.get", autospec=True) def test_refresh_success_with_scopes(self, get, utcnow, mock_metrics_header_value): 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"} assert kwargs["headers"] == { "x-goog-api-client": ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE } @mock.patch("google.auth.compute_engine._metadata.get", autospec=True) def test_refresh_error(self, get): get.side_effect = exceptions.TransportError("http error") with pytest.raises(exceptions.RefreshError) as excinfo: self.credentials.refresh(None) assert excinfo.match(r"http error") @mock.patch("google.auth.compute_engine._metadata.get", autospec=True) def test_before_request_refreshes(self, get): 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, }, ] # Credentials should start as invalid assert not self.credentials.valid # before_request should cause a refresh request = mock.create_autospec(transport.Request, instance=True) self.credentials.before_request(request, "GET", "http://example.com?a=1#3", {}) # The refresh endpoint should've been called. assert get.called # 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" 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 def test_token_usage_metrics(self): self.credentials.token = "token" self.credentials.expiry = None headers = {} self.credentials.before_request(mock.Mock(), None, None, headers) assert headers["authorization"] == "Bearer token" assert headers["x-goog-api-client"] == "cred-type/mds" @mock.patch( "google.auth.compute_engine._metadata.get_universe_domain", return_value="fake_universe_domain", ) def test_universe_domain(self, get_universe_domain): self.credentials._universe_domain_cached = False self.credentials._universe_domain = "googleapis.com" # calling the universe_domain property should trigger a call to # get_universe_domain to fetch the value. The value should be cached. assert self.credentials.universe_domain == "fake_universe_domain" assert self.credentials._universe_domain == "fake_universe_domain" assert self.credentials._universe_domain_cached get_universe_domain.assert_called_once() # calling the universe_domain property the second time should use the # cached value instead of calling get_universe_domain assert self.credentials.universe_domain == "fake_universe_domain" get_universe_domain.assert_called_once() class TestIDTokenCredentials(object): credentials = None @mock.patch("google.auth.compute_engine._metadata.get", autospec=True) def test_default_state(self, get): get.side_effect = [ {"email": "service-account@example.com", "scope": ["one", "two"]} ] request = mock.create_autospec(transport.Request, instance=True) self.credentials = credentials.IDTokenCredentials( request=request, target_audience="https://example.com" ) assert not self.credentials.valid # Expiration hasn't been set yet assert not self.credentials.expired # Service account email hasn't been populated assert self.credentials.service_account_email == "service-account@example.com" # Signer is initialized assert self.credentials.signer assert self.credentials.signer_email == "service-account@example.com" # No quota project assert not self.credentials._quota_project_id @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_make_authorization_grant_assertion(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" ) # 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": "service-account@example.com", "target_audience": "https://audience.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_service_account(self, sign, get, utcnow): sign.side_effect = [b"signature"] request = mock.create_autospec(transport.Request, instance=True) self.credentials = credentials.IDTokenCredentials( request=request, target_audience="https://audience.com", service_account_email="service-account@other.com", ) # 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": "service-account@other.com", "target_audience": "https://audience.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_additional_claims(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", additional_claims={"foo": "bar"}, ) # 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": "service-account@example.com", "target_audience": "https://audience.com", "foo": "bar", } def test_token_uri(self): request = mock.create_autospec(transport.Request, instance=True) self.credentials = credentials.IDTokenCredentials( request=request, signer=mock.Mock(), service_account_email="foo@example.com", target_audience="https://audience.com", ) assert self.credentials._token_uri == credentials._DEFAULT_TOKEN_URI self.credentials = credentials.IDTokenCredentials( request=request, signer=mock.Mock(), service_account_email="foo@example.com", target_audience="https://audience.com", token_uri="https://example.com/token", ) assert self.credentials._token_uri == "https://example.com/token" @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_target_audience(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" ) self.credentials = self.credentials.with_target_audience("https://actually.not") # 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": "service-account@example.com", "target_audience": "https://actually.not", } # Check that the signer have been initialized with a Request object assert isinstance(self.credentials._signer._request, transport.Request) @responses.activate def test_with_target_audience_integration(self): """ Test that it is possible to refresh credentials generated from `with_target_audience`. 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": "service-account@example.com", "aliases": ["default"], }, ) # mock token for credentials responses.add( responses.GET, "http://metadata.google.internal/computeMetadata/v1/instance/" "service-accounts/service-account@example.com/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/service-account@example.com: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="service-account@example.com", target_audience="https://audience.com", ) self.credentials = self.credentials.with_target_audience("https://actually.not") self.credentials.refresh(requests.Request()) assert self.credentials.token is not None @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_quota_project(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" ) 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": "service-account@example.com", "target_audience": "https://audience.com", } # 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://example.com") assert creds_with_token_uri._token_uri == "http://example.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://example.com") @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": "service-account@example.com", "aliases": ["default"], }, ) # mock token for credentials responses.add( responses.GET, "http://metadata.google.internal/computeMetadata/v1/instance/" "service-accounts/service-account@example.com/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/service-account@example.com: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="service-account@example.com", 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 = [ {"email": "service-account@example.com", "scopes": ["one", "two"]} ] sign.side_effect = [b"signature"] id_token_jwt_grant.side_effect = [ ("idtoken", datetime.datetime.utcfromtimestamp(3600), {}) ] request = mock.create_autospec(transport.Request, instance=True) self.credentials = credentials.IDTokenCredentials( request=request, target_audience="https://audience.com" ) # Refresh credentials self.credentials.refresh(None) # Check that the credentials have the token and proper expiration assert self.credentials.token == "idtoken" assert self.credentials.expiry == (datetime.datetime.utcfromtimestamp(3600)) # Check the credential info assert self.credentials.service_account_email == "service-account@example.com" # Check that the credentials are valid (have a token and are not # expired) assert self.credentials.valid @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_refresh_error(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) response = mock.Mock() response.data = b'{"error": "http error"}' response.status = 404 # Throw a 404 so the request is not retried. request.side_effect = [response] self.credentials = credentials.IDTokenCredentials( request=request, target_audience="https://audience.com" ) with pytest.raises(exceptions.RefreshError) as excinfo: self.credentials.refresh(request) assert excinfo.match(r"http error") @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_before_request_refreshes(self, id_token_jwt_grant, sign, get, utcnow): get.side_effect = [ {"email": "service-account@example.com", "scopes": "one two"} ] sign.side_effect = [b"signature"] id_token_jwt_grant.side_effect = [ ("idtoken", datetime.datetime.utcfromtimestamp(3600), {}) ] request = mock.create_autospec(transport.Request, instance=True) self.credentials = credentials.IDTokenCredentials( request=request, target_audience="https://audience.com" ) # Credentials should start as invalid assert not self.credentials.valid # before_request should cause a refresh request = mock.create_autospec(transport.Request, instance=True) self.credentials.before_request(request, "GET", "http://example.com?a=1#3", {}) # The refresh endpoint should've been called. assert get.called # Credentials should now be valid. assert self.credentials.valid @mock.patch("google.auth.compute_engine._metadata.get", autospec=True) @mock.patch("google.auth.iam.Signer.sign", autospec=True) def test_sign_bytes(self, sign, get): get.side_effect = [ {"email": "service-account@example.com", "scopes": ["one", "two"]} ] sign.side_effect = [b"signature"] request = mock.create_autospec(transport.Request, instance=True) response = mock.Mock() response.data = b'{"signature": "c2lnbmF0dXJl"}' response.status = 200 request.side_effect = [response] self.credentials = credentials.IDTokenCredentials( request=request, target_audience="https://audience.com" ) # Generate authorization grant: signature = self.credentials.sign_bytes(b"some bytes") # The JWT token signature is 'signature' encoded in base 64: assert signature == b"signature" @mock.patch( "google.auth.metrics.token_request_id_token_mds", return_value=ID_TOKEN_REQUEST_METRICS_HEADER_VALUE, ) @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_get_id_token_from_metadata( self, get, get_service_account_info, mock_metrics_header_value ): get.return_value = SAMPLE_ID_TOKEN get_service_account_info.return_value = {"email": "foo@example.com"} cred = credentials.IDTokenCredentials( mock.Mock(), "audience", use_metadata_identity_endpoint=True ) cred.refresh(request=mock.Mock()) assert get.call_args.kwargs["headers"] == { "x-goog-api-client": ID_TOKEN_REQUEST_METRICS_HEADER_VALUE } assert cred.token == SAMPLE_ID_TOKEN assert cred.expiry == datetime.datetime.utcfromtimestamp(SAMPLE_ID_TOKEN_EXP) assert cred._use_metadata_identity_endpoint assert cred._signer is None assert cred._token_uri is None assert cred._service_account_email == "foo@example.com" assert cred._target_audience == "audience" with pytest.raises(ValueError): cred.sign_bytes(b"bytes") @mock.patch( "google.auth.compute_engine._metadata.get_service_account_info", autospec=True ) def test_with_target_audience_for_metadata(self, get_service_account_info): get_service_account_info.return_value = {"email": "foo@example.com"} cred = credentials.IDTokenCredentials( mock.Mock(), "audience", use_metadata_identity_endpoint=True ) cred = cred.with_target_audience("new_audience") assert cred._target_audience == "new_audience" assert cred._use_metadata_identity_endpoint assert cred._signer is None assert cred._token_uri is None assert cred._service_account_email == "foo@example.com" @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": "foo@example.com"} 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 == "foo@example.com" @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" get_service_account_info.return_value = {"email": "foo@example.com"} cred = credentials.IDTokenCredentials( mock.Mock(), "audience", use_metadata_identity_endpoint=True ) with pytest.raises(ValueError): cred.refresh(request=mock.Mock()) @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_transport_error_from_metadata(self, get, get_service_account_info): get.side_effect = exceptions.TransportError("transport error") get_service_account_info.return_value = {"email": "foo@example.com"} cred = credentials.IDTokenCredentials( mock.Mock(), "audience", use_metadata_identity_endpoint=True ) with pytest.raises(exceptions.RefreshError) as excinfo: cred.refresh(request=mock.Mock()) assert excinfo.match(r"transport error") def test_get_id_token_from_metadata_constructor(self): with pytest.raises(ValueError): credentials.IDTokenCredentials( mock.Mock(), "audience", use_metadata_identity_endpoint=True, token_uri="token_uri", ) with pytest.raises(ValueError): credentials.IDTokenCredentials( mock.Mock(), "audience", use_metadata_identity_endpoint=True, signer=mock.Mock(), ) with pytest.raises(ValueError): credentials.IDTokenCredentials( mock.Mock(), "audience", use_metadata_identity_endpoint=True, additional_claims={"key", "value"}, ) with pytest.raises(ValueError): credentials.IDTokenCredentials( mock.Mock(), "audience", use_metadata_identity_endpoint=True, service_account_email="foo@example.com", ) compute_engine/test__metadata.py 0000644 00000037341 15025175543 0013114 0 ustar 00 # Copyright 2016 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import datetime import http.client as http_client import importlib import json import os import mock import pytest # type: ignore from google.auth import _helpers from google.auth import environment_vars from google.auth import exceptions from google.auth import transport from google.auth.compute_engine import _metadata PATH = "instance/service-accounts/default" DATA_DIR = os.path.join(os.path.dirname(__file__), "data") SMBIOS_PRODUCT_NAME_FILE = os.path.join(DATA_DIR, "smbios_product_name") SMBIOS_PRODUCT_NAME_NONEXISTENT_FILE = os.path.join( DATA_DIR, "smbios_product_name_nonexistent" ) SMBIOS_PRODUCT_NAME_NON_GOOGLE = os.path.join( DATA_DIR, "smbios_product_name_non_google" ) ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE = ( "gl-python/3.7 auth/1.1 auth-request-type/at cred-type/mds" ) MDS_PING_METRICS_HEADER_VALUE = "gl-python/3.7 auth/1.1 auth-request-type/mds" MDS_PING_REQUEST_HEADER = { "metadata-flavor": "Google", "x-goog-api-client": MDS_PING_METRICS_HEADER_VALUE, } def make_request(data, status=http_client.OK, headers=None, retry=False): response = mock.create_autospec(transport.Response, instance=True) response.status = status response.data = _helpers.to_bytes(data) response.headers = headers or {} request = mock.create_autospec(transport.Request) if retry: request.side_effect = [exceptions.TransportError(), response] else: request.return_value = response return request def test_detect_gce_residency_linux_success(): _metadata._GCE_PRODUCT_NAME_FILE = SMBIOS_PRODUCT_NAME_FILE assert _metadata.detect_gce_residency_linux() def test_detect_gce_residency_linux_non_google(): _metadata._GCE_PRODUCT_NAME_FILE = SMBIOS_PRODUCT_NAME_NON_GOOGLE assert not _metadata.detect_gce_residency_linux() def test_detect_gce_residency_linux_nonexistent(): _metadata._GCE_PRODUCT_NAME_FILE = SMBIOS_PRODUCT_NAME_NONEXISTENT_FILE assert not _metadata.detect_gce_residency_linux() def test_is_on_gce_ping_success(): request = make_request("", headers=_metadata._METADATA_HEADERS) assert _metadata.is_on_gce(request) @mock.patch("os.name", new="nt") def test_is_on_gce_windows_success(): request = make_request("", headers={_metadata._METADATA_FLAVOR_HEADER: "meep"}) assert not _metadata.is_on_gce(request) @mock.patch("os.name", new="posix") def test_is_on_gce_linux_success(): request = make_request("", headers={_metadata._METADATA_FLAVOR_HEADER: "meep"}) _metadata._GCE_PRODUCT_NAME_FILE = SMBIOS_PRODUCT_NAME_FILE assert _metadata.is_on_gce(request) @mock.patch("google.auth.metrics.mds_ping", return_value=MDS_PING_METRICS_HEADER_VALUE) def test_ping_success(mock_metrics_header_value): request = make_request("", headers=_metadata._METADATA_HEADERS) assert _metadata.ping(request) request.assert_called_once_with( method="GET", url=_metadata._METADATA_IP_ROOT, headers=MDS_PING_REQUEST_HEADER, timeout=_metadata._METADATA_DEFAULT_TIMEOUT, ) @mock.patch("google.auth.metrics.mds_ping", return_value=MDS_PING_METRICS_HEADER_VALUE) def test_ping_success_retry(mock_metrics_header_value): request = make_request("", headers=_metadata._METADATA_HEADERS, retry=True) assert _metadata.ping(request) request.assert_called_with( method="GET", url=_metadata._METADATA_IP_ROOT, headers=MDS_PING_REQUEST_HEADER, timeout=_metadata._METADATA_DEFAULT_TIMEOUT, ) assert request.call_count == 2 def test_ping_failure_bad_flavor(): request = make_request("", headers={_metadata._METADATA_FLAVOR_HEADER: "meep"}) assert not _metadata.ping(request) def test_ping_failure_connection_failed(): request = make_request("") request.side_effect = exceptions.TransportError() assert not _metadata.ping(request) @mock.patch("google.auth.metrics.mds_ping", return_value=MDS_PING_METRICS_HEADER_VALUE) def test_ping_success_custom_root(mock_metrics_header_value): request = make_request("", headers=_metadata._METADATA_HEADERS) fake_ip = "1.2.3.4" os.environ[environment_vars.GCE_METADATA_IP] = fake_ip importlib.reload(_metadata) try: assert _metadata.ping(request) finally: del os.environ[environment_vars.GCE_METADATA_IP] importlib.reload(_metadata) request.assert_called_once_with( method="GET", url="http://" + fake_ip, headers=MDS_PING_REQUEST_HEADER, timeout=_metadata._METADATA_DEFAULT_TIMEOUT, ) def test_get_success_json(): key, value = "foo", "bar" data = json.dumps({key: value}) request = make_request(data, headers={"content-type": "application/json"}) result = _metadata.get(request, PATH) request.assert_called_once_with( method="GET", url=_metadata._METADATA_ROOT + PATH, headers=_metadata._METADATA_HEADERS, ) assert result[key] == value def test_get_success_json_content_type_charset(): key, value = "foo", "bar" data = json.dumps({key: value}) request = make_request( data, headers={"content-type": "application/json; charset=UTF-8"} ) result = _metadata.get(request, PATH) request.assert_called_once_with( method="GET", url=_metadata._METADATA_ROOT + PATH, headers=_metadata._METADATA_HEADERS, ) assert result[key] == value def test_get_success_retry(): key, value = "foo", "bar" data = json.dumps({key: value}) request = make_request( data, headers={"content-type": "application/json"}, retry=True ) result = _metadata.get(request, PATH) request.assert_called_with( method="GET", url=_metadata._METADATA_ROOT + PATH, headers=_metadata._METADATA_HEADERS, ) assert request.call_count == 2 assert result[key] == value def test_get_success_text(): data = "foobar" request = make_request(data, headers={"content-type": "text/plain"}) result = _metadata.get(request, PATH) request.assert_called_once_with( method="GET", url=_metadata._METADATA_ROOT + PATH, headers=_metadata._METADATA_HEADERS, ) assert result == data def test_get_success_params(): data = "foobar" request = make_request(data, headers={"content-type": "text/plain"}) params = {"recursive": "true"} result = _metadata.get(request, PATH, params=params) request.assert_called_once_with( method="GET", url=_metadata._METADATA_ROOT + PATH + "?recursive=true", headers=_metadata._METADATA_HEADERS, ) assert result == data def test_get_success_recursive_and_params(): data = "foobar" request = make_request(data, headers={"content-type": "text/plain"}) params = {"recursive": "false"} result = _metadata.get(request, PATH, recursive=True, params=params) request.assert_called_once_with( method="GET", url=_metadata._METADATA_ROOT + PATH + "?recursive=true", headers=_metadata._METADATA_HEADERS, ) assert result == data def test_get_success_recursive(): data = "foobar" request = make_request(data, headers={"content-type": "text/plain"}) result = _metadata.get(request, PATH, recursive=True) request.assert_called_once_with( method="GET", url=_metadata._METADATA_ROOT + PATH + "?recursive=true", headers=_metadata._METADATA_HEADERS, ) assert result == data def test_get_success_custom_root_new_variable(): request = make_request("{}", headers={"content-type": "application/json"}) fake_root = "another.metadata.service" os.environ[environment_vars.GCE_METADATA_HOST] = fake_root importlib.reload(_metadata) try: _metadata.get(request, PATH) finally: del os.environ[environment_vars.GCE_METADATA_HOST] importlib.reload(_metadata) request.assert_called_once_with( method="GET", url="http://{}/computeMetadata/v1/{}".format(fake_root, PATH), headers=_metadata._METADATA_HEADERS, ) def test_get_success_custom_root_old_variable(): request = make_request("{}", headers={"content-type": "application/json"}) fake_root = "another.metadata.service" os.environ[environment_vars.GCE_METADATA_ROOT] = fake_root importlib.reload(_metadata) try: _metadata.get(request, PATH) finally: del os.environ[environment_vars.GCE_METADATA_ROOT] importlib.reload(_metadata) request.assert_called_once_with( method="GET", url="http://{}/computeMetadata/v1/{}".format(fake_root, PATH), headers=_metadata._METADATA_HEADERS, ) def test_get_failure(): request = make_request("Metadata error", status=http_client.NOT_FOUND) with pytest.raises(exceptions.TransportError) as excinfo: _metadata.get(request, PATH) assert excinfo.match(r"Metadata error") request.assert_called_once_with( method="GET", url=_metadata._METADATA_ROOT + PATH, headers=_metadata._METADATA_HEADERS, ) def test_get_return_none_for_not_found_error(): request = make_request("Metadata error", status=http_client.NOT_FOUND) assert _metadata.get(request, PATH, return_none_for_not_found_error=True) is None request.assert_called_once_with( method="GET", url=_metadata._METADATA_ROOT + PATH, headers=_metadata._METADATA_HEADERS, ) def test_get_failure_connection_failed(): request = make_request("") request.side_effect = exceptions.TransportError() with pytest.raises(exceptions.TransportError) as excinfo: _metadata.get(request, PATH) assert excinfo.match(r"Compute Engine Metadata server unavailable") request.assert_called_with( method="GET", url=_metadata._METADATA_ROOT + PATH, headers=_metadata._METADATA_HEADERS, ) assert request.call_count == 5 def test_get_failure_bad_json(): request = make_request("{", headers={"content-type": "application/json"}) with pytest.raises(exceptions.TransportError) as excinfo: _metadata.get(request, PATH) assert excinfo.match(r"invalid JSON") request.assert_called_once_with( method="GET", url=_metadata._METADATA_ROOT + PATH, headers=_metadata._METADATA_HEADERS, ) def test_get_project_id(): project = "example-project" request = make_request(project, headers={"content-type": "text/plain"}) project_id = _metadata.get_project_id(request) request.assert_called_once_with( method="GET", url=_metadata._METADATA_ROOT + "project/project-id", headers=_metadata._METADATA_HEADERS, ) assert project_id == project def test_get_universe_domain_success(): request = make_request( "fake_universe_domain", headers={"content-type": "text/plain"} ) universe_domain = _metadata.get_universe_domain(request) request.assert_called_once_with( method="GET", url=_metadata._METADATA_ROOT + "universe/universe_domain", headers=_metadata._METADATA_HEADERS, ) assert universe_domain == "fake_universe_domain" def test_get_universe_domain_not_found(): # Test that if the universe domain endpoint returns 404 error, we should # use googleapis.com as the universe domain request = make_request("not found", status=http_client.NOT_FOUND) universe_domain = _metadata.get_universe_domain(request) request.assert_called_once_with( method="GET", url=_metadata._METADATA_ROOT + "universe/universe_domain", headers=_metadata._METADATA_HEADERS, ) assert universe_domain == "googleapis.com" def test_get_universe_domain_other_error(): # Test that if the universe domain endpoint returns an error other than 404 # we should throw the error request = make_request("unauthorized", status=http_client.UNAUTHORIZED) with pytest.raises(exceptions.TransportError) as excinfo: _metadata.get_universe_domain(request) assert excinfo.match(r"unauthorized") request.assert_called_once_with( method="GET", url=_metadata._METADATA_ROOT + "universe/universe_domain", headers=_metadata._METADATA_HEADERS, ) @mock.patch( "google.auth.metrics.token_request_access_token_mds", return_value=ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE, ) @mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min) def test_get_service_account_token(utcnow, mock_metrics_header_value): ttl = 500 request = make_request( json.dumps({"access_token": "token", "expires_in": ttl}), headers={"content-type": "application/json"}, ) token, expiry = _metadata.get_service_account_token(request) request.assert_called_once_with( method="GET", url=_metadata._METADATA_ROOT + PATH + "/token", headers={ "metadata-flavor": "Google", "x-goog-api-client": ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE, }, ) assert token == "token" assert expiry == utcnow() + datetime.timedelta(seconds=ttl) @mock.patch( "google.auth.metrics.token_request_access_token_mds", return_value=ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE, ) @mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min) def test_get_service_account_token_with_scopes_list(utcnow, mock_metrics_header_value): ttl = 500 request = make_request( json.dumps({"access_token": "token", "expires_in": ttl}), headers={"content-type": "application/json"}, ) token, expiry = _metadata.get_service_account_token(request, scopes=["foo", "bar"]) request.assert_called_once_with( method="GET", url=_metadata._METADATA_ROOT + PATH + "/token" + "?scopes=foo%2Cbar", headers={ "metadata-flavor": "Google", "x-goog-api-client": ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE, }, ) assert token == "token" assert expiry == utcnow() + datetime.timedelta(seconds=ttl) @mock.patch( "google.auth.metrics.token_request_access_token_mds", return_value=ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE, ) @mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min) def test_get_service_account_token_with_scopes_string( utcnow, mock_metrics_header_value ): ttl = 500 request = make_request( json.dumps({"access_token": "token", "expires_in": ttl}), headers={"content-type": "application/json"}, ) token, expiry = _metadata.get_service_account_token(request, scopes="foo,bar") request.assert_called_once_with( method="GET", url=_metadata._METADATA_ROOT + PATH + "/token" + "?scopes=foo%2Cbar", headers={ "metadata-flavor": "Google", "x-goog-api-client": ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE, }, ) assert token == "token" assert expiry == utcnow() + datetime.timedelta(seconds=ttl) def test_get_service_account_info(): key, value = "foo", "bar" request = make_request( json.dumps({key: value}), headers={"content-type": "application/json"} ) info = _metadata.get_service_account_info(request) request.assert_called_once_with( method="GET", url=_metadata._METADATA_ROOT + PATH + "/?recursive=true", headers=_metadata._METADATA_HEADERS, ) assert info[key] == value compute_engine/data/smbios_product_name_non_google 0000644 00000000023 15025175543 0016645 0 ustar 00 ABC Compute Engine compute_engine/data/smbios_product_name 0000644 00000000026 15025175543 0014442 0 ustar 00 Google Compute Engine compute_engine/__init__.py 0000644 00000000000 15025175543 0011653 0 ustar 00 test_credentials.py 0000644 00000015372 15025175543 0010471 0 ustar 00 # Copyright 2016 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import datetime import pytest # type: ignore from google.auth import _helpers from google.auth import credentials class CredentialsImpl(credentials.Credentials): def refresh(self, request): self.token = request def with_quota_project(self, quota_project_id): raise NotImplementedError() class CredentialsImplWithMetrics(credentials.Credentials): def refresh(self, request): self.token = request def _metric_header_for_usage(self): return "foo" def test_credentials_constructor(): credentials = CredentialsImpl() assert not credentials.token assert not credentials.expiry assert not credentials.expired assert not credentials.valid assert credentials.universe_domain == "googleapis.com" def test_expired_and_valid(): credentials = CredentialsImpl() credentials.token = "token" assert credentials.valid assert not credentials.expired # Set the expiration to one second more than now plus the clock skew # accomodation. These credentials should be valid. credentials.expiry = ( _helpers.utcnow() + _helpers.REFRESH_THRESHOLD + datetime.timedelta(seconds=1) ) assert credentials.valid assert not credentials.expired # Set the credentials expiration to now. Because of the clock skew # accomodation, these credentials should report as expired. credentials.expiry = _helpers.utcnow() assert not credentials.valid assert credentials.expired def test_before_request(): credentials = CredentialsImpl() request = "token" headers = {} # First call should call refresh, setting the token. credentials.before_request(request, "http://example.com", "GET", headers) assert credentials.valid assert credentials.token == "token" assert headers["authorization"] == "Bearer token" assert "x-allowed-locations" not in headers request = "token2" headers = {} # Second call shouldn't call refresh. credentials.before_request(request, "http://example.com", "GET", headers) assert credentials.valid assert credentials.token == "token" assert headers["authorization"] == "Bearer token" assert "x-allowed-locations" not in headers def test_before_request_with_trust_boundary(): DUMMY_BOUNDARY = "0xA30" credentials = CredentialsImpl() credentials._trust_boundary = {"locations": [], "encoded_locations": DUMMY_BOUNDARY} request = "token" headers = {} # First call should call refresh, setting the token. credentials.before_request(request, "http://example.com", "GET", headers) assert credentials.valid assert credentials.token == "token" assert headers["authorization"] == "Bearer token" assert headers["x-allowed-locations"] == DUMMY_BOUNDARY request = "token2" headers = {} # Second call shouldn't call refresh. credentials.before_request(request, "http://example.com", "GET", headers) assert credentials.valid assert credentials.token == "token" assert headers["authorization"] == "Bearer token" assert headers["x-allowed-locations"] == DUMMY_BOUNDARY def test_before_request_metrics(): credentials = CredentialsImplWithMetrics() request = "token" headers = {} credentials.before_request(request, "http://example.com", "GET", headers) assert headers["x-goog-api-client"] == "foo" def test_anonymous_credentials_ctor(): anon = credentials.AnonymousCredentials() assert anon.token is None assert anon.expiry is None assert not anon.expired assert anon.valid def test_anonymous_credentials_refresh(): anon = credentials.AnonymousCredentials() request = object() with pytest.raises(ValueError): anon.refresh(request) def test_anonymous_credentials_apply_default(): anon = credentials.AnonymousCredentials() headers = {} anon.apply(headers) assert headers == {} with pytest.raises(ValueError): anon.apply(headers, token="TOKEN") def test_anonymous_credentials_before_request(): anon = credentials.AnonymousCredentials() request = object() method = "GET" url = "https://example.com/api/endpoint" headers = {} anon.before_request(request, method, url, headers) assert headers == {} class ReadOnlyScopedCredentialsImpl(credentials.ReadOnlyScoped, CredentialsImpl): @property def requires_scopes(self): return super(ReadOnlyScopedCredentialsImpl, self).requires_scopes def test_readonly_scoped_credentials_constructor(): credentials = ReadOnlyScopedCredentialsImpl() assert credentials._scopes is None def test_readonly_scoped_credentials_scopes(): credentials = ReadOnlyScopedCredentialsImpl() credentials._scopes = ["one", "two"] assert credentials.scopes == ["one", "two"] assert credentials.has_scopes(["one"]) assert credentials.has_scopes(["two"]) assert credentials.has_scopes(["one", "two"]) assert not credentials.has_scopes(["three"]) def test_readonly_scoped_credentials_requires_scopes(): credentials = ReadOnlyScopedCredentialsImpl() assert not credentials.requires_scopes class RequiresScopedCredentialsImpl(credentials.Scoped, CredentialsImpl): def __init__(self, scopes=None, default_scopes=None): super(RequiresScopedCredentialsImpl, self).__init__() self._scopes = scopes self._default_scopes = default_scopes @property def requires_scopes(self): return not self.scopes def with_scopes(self, scopes, default_scopes=None): return RequiresScopedCredentialsImpl( scopes=scopes, default_scopes=default_scopes ) def test_create_scoped_if_required_scoped(): unscoped_credentials = RequiresScopedCredentialsImpl() scoped_credentials = credentials.with_scopes_if_required( unscoped_credentials, ["one", "two"] ) assert scoped_credentials is not unscoped_credentials assert not scoped_credentials.requires_scopes assert scoped_credentials.has_scopes(["one", "two"]) def test_create_scoped_if_required_not_scopes(): unscoped_credentials = CredentialsImpl() scoped_credentials = credentials.with_scopes_if_required( unscoped_credentials, ["one", "two"] ) assert scoped_credentials is unscoped_credentials test_exceptions.py 0000644 00000003434 15025175543 0010351 0 ustar 00 # Copyright 2022 Google Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import pytest # type: ignore from google.auth import exceptions # type:ignore @pytest.fixture( params=[ exceptions.GoogleAuthError, exceptions.TransportError, exceptions.RefreshError, exceptions.UserAccessTokenError, exceptions.DefaultCredentialsError, exceptions.MutualTLSChannelError, exceptions.OAuthError, exceptions.ReauthFailError, exceptions.ReauthSamlChallengeFailError, ] ) def retryable_exception(request): return request.param @pytest.fixture(params=[exceptions.ClientCertError]) def non_retryable_exception(request): return request.param def test_default_retryable_exceptions(retryable_exception): assert not retryable_exception().retryable @pytest.mark.parametrize("retryable", [True, False]) def test_retryable_exceptions(retryable_exception, retryable): retryable_exception = retryable_exception(retryable=retryable) assert retryable_exception.retryable == retryable @pytest.mark.parametrize("retryable", [True, False]) def test_non_retryable_exceptions(non_retryable_exception, retryable): non_retryable_exception = non_retryable_exception(retryable=retryable) assert not non_retryable_exception.retryable test__exponential_backoff.py 0000644 00000003164 15025175543 0012330 0 ustar 00 # Copyright 2022 Google Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import mock from google.auth import _exponential_backoff @mock.patch("time.sleep", return_value=None) def test_exponential_backoff(mock_time): eb = _exponential_backoff.ExponentialBackoff() curr_wait = eb._current_wait_in_seconds iteration_count = 0 for attempt in eb: backoff_interval = mock_time.call_args[0][0] jitter = curr_wait * eb._randomization_factor assert (curr_wait - jitter) <= backoff_interval <= (curr_wait + jitter) assert attempt == iteration_count + 1 assert eb.backoff_count == iteration_count + 1 assert eb._current_wait_in_seconds == eb._multiplier ** (iteration_count + 1) curr_wait = eb._current_wait_in_seconds iteration_count += 1 assert eb.total_attempts == _exponential_backoff._DEFAULT_RETRY_TOTAL_ATTEMPTS assert eb.backoff_count == _exponential_backoff._DEFAULT_RETRY_TOTAL_ATTEMPTS assert iteration_count == _exponential_backoff._DEFAULT_RETRY_TOTAL_ATTEMPTS assert mock_time.call_count == _exponential_backoff._DEFAULT_RETRY_TOTAL_ATTEMPTS transport/test__http_client.py 0000644 00000002146 15025175543 0012677 0 ustar 00 # Copyright 2016 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import pytest # type: ignore from google.auth import exceptions import google.auth.transport._http_client from tests.transport import compliance class TestRequestResponse(compliance.RequestResponseTests): def make_request(self): return google.auth.transport._http_client.Request() def test_non_http(self): request = self.make_request() with pytest.raises(exceptions.TransportError) as excinfo: request(url="https://{}".format(compliance.NXDOMAIN), method="GET") assert excinfo.match("https") transport/compliance.py 0000644 00000007236 15025175543 0011303 0 ustar 00 # Copyright 2016 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import http.client as http_client import time import flask # type: ignore import pytest # type: ignore from pytest_localserver.http import WSGIServer # type: ignore from google.auth import exceptions # .invalid will never resolve, see https://tools.ietf.org/html/rfc2606 NXDOMAIN = "test.invalid" class RequestResponseTests(object): @pytest.fixture(scope="module") def server(self): """Provides a test HTTP server. The test server is automatically created before a test and destroyed at the end. The server is serving a test application that can be used to verify requests. """ app = flask.Flask(__name__) app.debug = True # pylint: disable=unused-variable # (pylint thinks the flask routes are unusued.) @app.route("/basic") def index(): header_value = flask.request.headers.get("x-test-header", "value") headers = {"X-Test-Header": header_value} return "Basic Content", http_client.OK, headers @app.route("/server_error") def server_error(): return "Error", http_client.INTERNAL_SERVER_ERROR @app.route("/wait") def wait(): time.sleep(3) return "Waited" # pylint: enable=unused-variable server = WSGIServer(application=app.wsgi_app) server.start() yield server server.stop() def test_request_basic(self, server): request = self.make_request() response = request(url=server.url + "/basic", method="GET") assert response.status == http_client.OK assert response.headers["x-test-header"] == "value" assert response.data == b"Basic Content" def test_request_with_timeout_success(self, server): request = self.make_request() response = request(url=server.url + "/basic", method="GET", timeout=2) assert response.status == http_client.OK assert response.headers["x-test-header"] == "value" assert response.data == b"Basic Content" def test_request_with_timeout_failure(self, server): request = self.make_request() with pytest.raises(exceptions.TransportError): request(url=server.url + "/wait", method="GET", timeout=1) def test_request_headers(self, server): request = self.make_request() response = request( url=server.url + "/basic", method="GET", headers={"x-test-header": "hello world"}, ) assert response.status == http_client.OK assert response.headers["x-test-header"] == "hello world" assert response.data == b"Basic Content" def test_request_error(self, server): request = self.make_request() response = request(url=server.url + "/server_error", method="GET") assert response.status == http_client.INTERNAL_SERVER_ERROR assert response.data == b"Error" def test_connection_error(self): request = self.make_request() with pytest.raises(exceptions.TransportError): request(url="http://{}".format(NXDOMAIN), method="GET") transport/test_grpc.py 0000644 00000044055 15025175543 0011163 0 ustar 00 # Copyright 2016 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import datetime import os import time import mock import pytest # type: ignore from google.auth import _helpers from google.auth import credentials from google.auth import environment_vars from google.auth import exceptions from google.auth import transport from google.oauth2 import service_account try: # pylint: disable=ungrouped-imports import grpc # type: ignore import google.auth.transport.grpc HAS_GRPC = True except ImportError: # pragma: NO COVER HAS_GRPC = False DATA_DIR = os.path.join(os.path.dirname(__file__), "..", "data") METADATA_PATH = os.path.join(DATA_DIR, "context_aware_metadata.json") with open(os.path.join(DATA_DIR, "privatekey.pem"), "rb") as fh: PRIVATE_KEY_BYTES = fh.read() with open(os.path.join(DATA_DIR, "public_cert.pem"), "rb") as fh: PUBLIC_CERT_BYTES = fh.read() pytestmark = pytest.mark.skipif(not HAS_GRPC, reason="gRPC is unavailable.") class CredentialsStub(credentials.Credentials): def __init__(self, token="token"): super(CredentialsStub, self).__init__() self.token = token self.expiry = None 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): credentials = CredentialsStub() request = mock.create_autospec(transport.Request) plugin = google.auth.transport.grpc.AuthMetadataPlugin(credentials, request) context = mock.create_autospec(grpc.AuthMetadataContext, instance=True) context.method_name = mock.sentinel.method_name context.service_url = mock.sentinel.service_url callback = mock.create_autospec(grpc.AuthMetadataPluginCallback) plugin(context, callback) time.sleep(2) callback.assert_called_once_with( [("authorization", "Bearer {}".format(credentials.token))], None ) def test_call_refresh(self): credentials = CredentialsStub() credentials.expiry = datetime.datetime.min + _helpers.REFRESH_THRESHOLD request = mock.create_autospec(transport.Request) plugin = google.auth.transport.grpc.AuthMetadataPlugin(credentials, request) context = mock.create_autospec(grpc.AuthMetadataContext, instance=True) context.method_name = mock.sentinel.method_name context.service_url = mock.sentinel.service_url callback = mock.create_autospec(grpc.AuthMetadataPluginCallback) plugin(context, callback) time.sleep(2) assert credentials.token == "token1" callback.assert_called_once_with( [("authorization", "Bearer {}".format(credentials.token))], None ) def test__get_authorization_headers_with_service_account(self): credentials = mock.create_autospec(service_account.Credentials) request = mock.create_autospec(transport.Request) plugin = google.auth.transport.grpc.AuthMetadataPlugin(credentials, request) context = mock.create_autospec(grpc.AuthMetadataContext, instance=True) context.method_name = "methodName" context.service_url = "https://pubsub.googleapis.com/methodName" plugin._get_authorization_headers(context) credentials._create_self_signed_jwt.assert_called_once_with(None) def test__get_authorization_headers_with_service_account_and_default_host(self): credentials = mock.create_autospec(service_account.Credentials) request = mock.create_autospec(transport.Request) default_host = "pubsub.googleapis.com" plugin = google.auth.transport.grpc.AuthMetadataPlugin( credentials, request, default_host=default_host ) context = mock.create_autospec(grpc.AuthMetadataContext, instance=True) context.method_name = "methodName" context.service_url = "https://pubsub.googleapis.com/methodName" plugin._get_authorization_headers(context) credentials._create_self_signed_jwt.assert_called_once_with( "https://{}/".format(default_host) ) @mock.patch( "google.auth.transport._mtls_helper.get_client_ssl_credentials", autospec=True ) @mock.patch("grpc.composite_channel_credentials", autospec=True) @mock.patch("grpc.metadata_call_credentials", autospec=True) @mock.patch("grpc.ssl_channel_credentials", autospec=True) @mock.patch("grpc.secure_channel", autospec=True) class TestSecureAuthorizedChannel(object): @mock.patch( "google.auth.transport._mtls_helper._read_dca_metadata_file", autospec=True ) @mock.patch( "google.auth.transport._mtls_helper._check_dca_metadata_path", autospec=True ) def test_secure_authorized_channel_adc( self, check_dca_metadata_path, read_dca_metadata_file, secure_channel, ssl_channel_credentials, metadata_call_credentials, composite_channel_credentials, get_client_ssl_credentials, ): credentials = CredentialsStub() request = mock.create_autospec(transport.Request) target = "example.com:80" # Mock the context aware metadata and client cert/key so mTLS SSL channel # will be used. check_dca_metadata_path.return_value = METADATA_PATH read_dca_metadata_file.return_value = { "cert_provider_command": ["some command"] } get_client_ssl_credentials.return_value = ( True, PUBLIC_CERT_BYTES, PRIVATE_KEY_BYTES, None, ) channel = None with mock.patch.dict( os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"} ): channel = google.auth.transport.grpc.secure_authorized_channel( credentials, request, target, options=mock.sentinel.options ) # Check the auth plugin construction. auth_plugin = metadata_call_credentials.call_args[0][0] assert isinstance(auth_plugin, google.auth.transport.grpc.AuthMetadataPlugin) assert auth_plugin._credentials == credentials assert auth_plugin._request == request # Check the ssl channel call. ssl_channel_credentials.assert_called_once_with( certificate_chain=PUBLIC_CERT_BYTES, private_key=PRIVATE_KEY_BYTES ) # Check the composite credentials call. composite_channel_credentials.assert_called_once_with( ssl_channel_credentials.return_value, metadata_call_credentials.return_value ) # Check the channel call. secure_channel.assert_called_once_with( target, composite_channel_credentials.return_value, options=mock.sentinel.options, ) assert channel == secure_channel.return_value @mock.patch("google.auth.transport.grpc.SslCredentials", autospec=True) def test_secure_authorized_channel_adc_without_client_cert_env( self, ssl_credentials_adc_method, secure_channel, ssl_channel_credentials, metadata_call_credentials, composite_channel_credentials, get_client_ssl_credentials, ): # Test client cert won't be used if GOOGLE_API_USE_CLIENT_CERTIFICATE # environment variable is not set. credentials = CredentialsStub() request = mock.create_autospec(transport.Request) target = "example.com:80" channel = google.auth.transport.grpc.secure_authorized_channel( credentials, request, target, options=mock.sentinel.options ) # Check the auth plugin construction. auth_plugin = metadata_call_credentials.call_args[0][0] assert isinstance(auth_plugin, google.auth.transport.grpc.AuthMetadataPlugin) assert auth_plugin._credentials == credentials assert auth_plugin._request == request # Check the ssl channel call. ssl_channel_credentials.assert_called_once() ssl_credentials_adc_method.assert_not_called() # Check the composite credentials call. composite_channel_credentials.assert_called_once_with( ssl_channel_credentials.return_value, metadata_call_credentials.return_value ) # Check the channel call. secure_channel.assert_called_once_with( target, composite_channel_credentials.return_value, options=mock.sentinel.options, ) assert channel == secure_channel.return_value def test_secure_authorized_channel_explicit_ssl( self, secure_channel, ssl_channel_credentials, metadata_call_credentials, composite_channel_credentials, get_client_ssl_credentials, ): credentials = mock.Mock() request = mock.Mock() target = "example.com:80" ssl_credentials = mock.Mock() google.auth.transport.grpc.secure_authorized_channel( credentials, request, target, ssl_credentials=ssl_credentials ) # Since explicit SSL credentials are provided, get_client_ssl_credentials # shouldn't be called. assert not get_client_ssl_credentials.called # Check the ssl channel call. assert not ssl_channel_credentials.called # Check the composite credentials call. composite_channel_credentials.assert_called_once_with( ssl_credentials, metadata_call_credentials.return_value ) def test_secure_authorized_channel_mutual_exclusive( self, secure_channel, ssl_channel_credentials, metadata_call_credentials, composite_channel_credentials, get_client_ssl_credentials, ): credentials = mock.Mock() request = mock.Mock() target = "example.com:80" ssl_credentials = mock.Mock() client_cert_callback = mock.Mock() with pytest.raises(ValueError): google.auth.transport.grpc.secure_authorized_channel( credentials, request, target, ssl_credentials=ssl_credentials, client_cert_callback=client_cert_callback, ) def test_secure_authorized_channel_with_client_cert_callback_success( self, secure_channel, ssl_channel_credentials, metadata_call_credentials, composite_channel_credentials, get_client_ssl_credentials, ): credentials = mock.Mock() request = mock.Mock() target = "example.com:80" client_cert_callback = mock.Mock() client_cert_callback.return_value = (PUBLIC_CERT_BYTES, PRIVATE_KEY_BYTES) with mock.patch.dict( os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"} ): google.auth.transport.grpc.secure_authorized_channel( credentials, request, target, client_cert_callback=client_cert_callback ) client_cert_callback.assert_called_once() # Check we are using the cert and key provided by client_cert_callback. ssl_channel_credentials.assert_called_once_with( certificate_chain=PUBLIC_CERT_BYTES, private_key=PRIVATE_KEY_BYTES ) # Check the composite credentials call. composite_channel_credentials.assert_called_once_with( ssl_channel_credentials.return_value, metadata_call_credentials.return_value ) @mock.patch( "google.auth.transport._mtls_helper._read_dca_metadata_file", autospec=True ) @mock.patch( "google.auth.transport._mtls_helper._check_dca_metadata_path", autospec=True ) def test_secure_authorized_channel_with_client_cert_callback_failure( self, check_dca_metadata_path, read_dca_metadata_file, secure_channel, ssl_channel_credentials, metadata_call_credentials, composite_channel_credentials, get_client_ssl_credentials, ): credentials = mock.Mock() request = mock.Mock() target = "example.com:80" client_cert_callback = mock.Mock() client_cert_callback.side_effect = Exception("callback exception") with pytest.raises(Exception) as excinfo: with mock.patch.dict( os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"} ): google.auth.transport.grpc.secure_authorized_channel( credentials, request, target, client_cert_callback=client_cert_callback, ) assert str(excinfo.value) == "callback exception" def test_secure_authorized_channel_cert_callback_without_client_cert_env( self, secure_channel, ssl_channel_credentials, metadata_call_credentials, composite_channel_credentials, get_client_ssl_credentials, ): # Test client cert won't be used if GOOGLE_API_USE_CLIENT_CERTIFICATE # environment variable is not set. credentials = mock.Mock() request = mock.Mock() target = "example.com:80" client_cert_callback = mock.Mock() google.auth.transport.grpc.secure_authorized_channel( credentials, request, target, client_cert_callback=client_cert_callback ) # Check client_cert_callback is not called because GOOGLE_API_USE_CLIENT_CERTIFICATE # is not set. client_cert_callback.assert_not_called() ssl_channel_credentials.assert_called_once() # Check the composite credentials call. composite_channel_credentials.assert_called_once_with( ssl_channel_credentials.return_value, metadata_call_credentials.return_value ) @mock.patch("grpc.ssl_channel_credentials", autospec=True) @mock.patch( "google.auth.transport._mtls_helper.get_client_ssl_credentials", autospec=True ) @mock.patch("google.auth.transport._mtls_helper._read_dca_metadata_file", autospec=True) @mock.patch( "google.auth.transport._mtls_helper._check_dca_metadata_path", autospec=True ) class TestSslCredentials(object): def test_no_context_aware_metadata( self, mock_check_dca_metadata_path, mock_read_dca_metadata_file, mock_get_client_ssl_credentials, mock_ssl_channel_credentials, ): # Mock that the metadata file doesn't exist. mock_check_dca_metadata_path.return_value = None with mock.patch.dict( os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"} ): ssl_credentials = google.auth.transport.grpc.SslCredentials() # Since no context aware metadata is found, we wouldn't call # get_client_ssl_credentials, and the SSL channel credentials created is # non mTLS. assert ssl_credentials.ssl_credentials is not None assert not ssl_credentials.is_mtls mock_get_client_ssl_credentials.assert_not_called() mock_ssl_channel_credentials.assert_called_once_with() def test_get_client_ssl_credentials_failure( self, mock_check_dca_metadata_path, mock_read_dca_metadata_file, mock_get_client_ssl_credentials, mock_ssl_channel_credentials, ): mock_check_dca_metadata_path.return_value = METADATA_PATH mock_read_dca_metadata_file.return_value = { "cert_provider_command": ["some command"] } # Mock that client cert and key are not loaded and exception is raised. mock_get_client_ssl_credentials.side_effect = exceptions.ClientCertError() with pytest.raises(exceptions.MutualTLSChannelError): with mock.patch.dict( os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"} ): assert google.auth.transport.grpc.SslCredentials().ssl_credentials def test_get_client_ssl_credentials_success( self, mock_check_dca_metadata_path, mock_read_dca_metadata_file, mock_get_client_ssl_credentials, mock_ssl_channel_credentials, ): mock_check_dca_metadata_path.return_value = METADATA_PATH mock_read_dca_metadata_file.return_value = { "cert_provider_command": ["some command"] } mock_get_client_ssl_credentials.return_value = ( True, PUBLIC_CERT_BYTES, PRIVATE_KEY_BYTES, None, ) with mock.patch.dict( os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"} ): ssl_credentials = google.auth.transport.grpc.SslCredentials() assert ssl_credentials.ssl_credentials is not None assert ssl_credentials.is_mtls mock_get_client_ssl_credentials.assert_called_once() mock_ssl_channel_credentials.assert_called_once_with( certificate_chain=PUBLIC_CERT_BYTES, private_key=PRIVATE_KEY_BYTES ) def test_get_client_ssl_credentials_without_client_cert_env( self, mock_check_dca_metadata_path, mock_read_dca_metadata_file, mock_get_client_ssl_credentials, mock_ssl_channel_credentials, ): # Test client cert won't be used if GOOGLE_API_USE_CLIENT_CERTIFICATE is not set. ssl_credentials = google.auth.transport.grpc.SslCredentials() assert ssl_credentials.ssl_credentials is not None assert not ssl_credentials.is_mtls mock_check_dca_metadata_path.assert_not_called() mock_read_dca_metadata_file.assert_not_called() mock_get_client_ssl_credentials.assert_not_called() mock_ssl_channel_credentials.assert_called_once() transport/test_requests.py 0000644 00000052603 15025175543 0012101 0 ustar 00 # Copyright 2016 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import datetime import functools import http.client as http_client import os import sys import freezegun import mock import OpenSSL import pytest # type: ignore import requests import requests.adapters from google.auth import environment_vars from google.auth import exceptions import google.auth.credentials import google.auth.transport._custom_tls_signer import google.auth.transport._mtls_helper import google.auth.transport.requests from google.oauth2 import service_account from tests.transport import compliance @pytest.fixture def frozen_time(): with freezegun.freeze_time("1970-01-01 00:00:00", tick=False) as frozen: yield frozen class TestRequestResponse(compliance.RequestResponseTests): def make_request(self): return google.auth.transport.requests.Request() def test_timeout(self): http = mock.create_autospec(requests.Session, instance=True) request = google.auth.transport.requests.Request(http) request(url="http://example.com", method="GET", timeout=5) assert http.request.call_args[1]["timeout"] == 5 def test_session_closed_on_del(self): http = mock.create_autospec(requests.Session, instance=True) request = google.auth.transport.requests.Request(http) request.__del__() http.close.assert_called_with() http = mock.create_autospec(requests.Session, instance=True) http.close.side_effect = TypeError("test injected TypeError") request = google.auth.transport.requests.Request(http) request.__del__() http.close.assert_called_with() class TestTimeoutGuard(object): def make_guard(self, *args, **kwargs): return google.auth.transport.requests.TimeoutGuard(*args, **kwargs) def test_tracks_elapsed_time_w_numeric_timeout(self, frozen_time): with self.make_guard(timeout=10) as guard: frozen_time.tick(delta=datetime.timedelta(seconds=3.8)) assert guard.remaining_timeout == 6.2 def test_tracks_elapsed_time_w_tuple_timeout(self, frozen_time): with self.make_guard(timeout=(16, 19)) as guard: frozen_time.tick(delta=datetime.timedelta(seconds=3.8)) assert guard.remaining_timeout == (12.2, 15.2) def test_noop_if_no_timeout(self, frozen_time): with self.make_guard(timeout=None) as guard: frozen_time.tick(delta=datetime.timedelta(days=3650)) # NOTE: no timeout error raised, despite years have passed assert guard.remaining_timeout is None def test_timeout_error_w_numeric_timeout(self, frozen_time): with pytest.raises(requests.exceptions.Timeout): with self.make_guard(timeout=10) as guard: frozen_time.tick(delta=datetime.timedelta(seconds=10.001)) assert guard.remaining_timeout == pytest.approx(-0.001) def test_timeout_error_w_tuple_timeout(self, frozen_time): with pytest.raises(requests.exceptions.Timeout): with self.make_guard(timeout=(11, 10)) as guard: frozen_time.tick(delta=datetime.timedelta(seconds=10.001)) assert guard.remaining_timeout == pytest.approx((0.999, -0.001)) def test_custom_timeout_error_type(self, frozen_time): class FooError(Exception): pass with pytest.raises(FooError): with self.make_guard(timeout=1, timeout_error_type=FooError): frozen_time.tick(delta=datetime.timedelta(seconds=2)) def test_lets_suite_errors_bubble_up(self, frozen_time): with pytest.raises(IndexError): with self.make_guard(timeout=1): [1, 2, 3][3] class CredentialsStub(google.auth.credentials.Credentials): def __init__(self, token="token"): super(CredentialsStub, self).__init__() self.token = token def apply(self, headers, token=None): headers["authorization"] = self.token def before_request(self, request, method, url, headers): self.apply(headers) 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.""" def __init__(self, time_tick, token="token"): self._time_tick = time_tick super(TimeTickCredentialsStub, self).__init__(token=token) def refresh(self, request): self._time_tick() super(TimeTickCredentialsStub, self).refresh(requests) class AdapterStub(requests.adapters.BaseAdapter): def __init__(self, responses, headers=None): super(AdapterStub, self).__init__() self.responses = responses self.requests = [] self.headers = headers or {} def send(self, request, **kwargs): # pylint: disable=arguments-differ # request is the only required argument here and the only argument # we care about. self.requests.append(request) return self.responses.pop(0) def close(self): # pragma: NO COVER # pylint wants this to be here because it's abstract in the base # class, but requests never actually calls it. return class TimeTickAdapterStub(AdapterStub): """Adapter that spends some (mocked) time when making a request.""" def __init__(self, time_tick, responses, headers=None): self._time_tick = time_tick super(TimeTickAdapterStub, self).__init__(responses, headers=headers) def send(self, request, **kwargs): self._time_tick() return super(TimeTickAdapterStub, self).send(request, **kwargs) class TestMutualTlsAdapter(object): @mock.patch.object(requests.adapters.HTTPAdapter, "init_poolmanager") @mock.patch.object(requests.adapters.HTTPAdapter, "proxy_manager_for") def test_success(self, mock_proxy_manager_for, mock_init_poolmanager): adapter = google.auth.transport.requests._MutualTlsAdapter( pytest.public_cert_bytes, pytest.private_key_bytes ) adapter.init_poolmanager() mock_init_poolmanager.assert_called_with(ssl_context=adapter._ctx_poolmanager) adapter.proxy_manager_for() mock_proxy_manager_for.assert_called_with(ssl_context=adapter._ctx_proxymanager) def test_invalid_cert_or_key(self): with pytest.raises(OpenSSL.crypto.Error): google.auth.transport.requests._MutualTlsAdapter( b"invalid cert", b"invalid key" ) @mock.patch.dict("sys.modules", {"OpenSSL.crypto": None}) def test_import_error(self): with pytest.raises(ImportError): google.auth.transport.requests._MutualTlsAdapter( pytest.public_cert_bytes, pytest.private_key_bytes ) def make_response(status=http_client.OK, data=None): response = requests.Response() response.status_code = status response._content = data return response class TestAuthorizedSession(object): TEST_URL = "http://example.com/" def test_constructor(self): authed_session = google.auth.transport.requests.AuthorizedSession( mock.sentinel.credentials ) assert authed_session.credentials == mock.sentinel.credentials def test_constructor_with_auth_request(self): http = mock.create_autospec(requests.Session) auth_request = google.auth.transport.requests.Request(http) authed_session = google.auth.transport.requests.AuthorizedSession( mock.sentinel.credentials, auth_request=auth_request ) assert authed_session._auth_request is auth_request def test_request_default_timeout(self): credentials = mock.Mock(wraps=CredentialsStub()) response = make_response() adapter = AdapterStub([response]) authed_session = google.auth.transport.requests.AuthorizedSession(credentials) authed_session.mount(self.TEST_URL, adapter) patcher = mock.patch("google.auth.transport.requests.requests.Session.request") with patcher as patched_request: authed_session.request("GET", self.TEST_URL) expected_timeout = google.auth.transport.requests._DEFAULT_TIMEOUT assert patched_request.call_args[1]["timeout"] == expected_timeout def test_request_no_refresh(self): credentials = mock.Mock(wraps=CredentialsStub()) response = make_response() adapter = AdapterStub([response]) authed_session = google.auth.transport.requests.AuthorizedSession(credentials) authed_session.mount(self.TEST_URL, adapter) result = authed_session.request("GET", self.TEST_URL) assert response == result assert credentials.before_request.called assert not credentials.refresh.called assert len(adapter.requests) == 1 assert adapter.requests[0].url == self.TEST_URL assert adapter.requests[0].headers["authorization"] == "token" def test_request_refresh(self): credentials = mock.Mock(wraps=CredentialsStub()) final_response = make_response(status=http_client.OK) # First request will 401, second request will succeed. adapter = AdapterStub( [make_response(status=http_client.UNAUTHORIZED), final_response] ) authed_session = google.auth.transport.requests.AuthorizedSession( credentials, refresh_timeout=60 ) authed_session.mount(self.TEST_URL, adapter) result = authed_session.request("GET", self.TEST_URL) assert result == final_response assert credentials.before_request.call_count == 2 assert credentials.refresh.called assert len(adapter.requests) == 2 assert adapter.requests[0].url == self.TEST_URL assert adapter.requests[0].headers["authorization"] == "token" assert adapter.requests[1].url == self.TEST_URL assert adapter.requests[1].headers["authorization"] == "token1" def test_request_max_allowed_time_timeout_error(self, frozen_time): tick_one_second = functools.partial( frozen_time.tick, delta=datetime.timedelta(seconds=1.0) ) credentials = mock.Mock( wraps=TimeTickCredentialsStub(time_tick=tick_one_second) ) adapter = TimeTickAdapterStub( time_tick=tick_one_second, responses=[make_response(status=http_client.OK)] ) authed_session = google.auth.transport.requests.AuthorizedSession(credentials) authed_session.mount(self.TEST_URL, adapter) # Because a request takes a full mocked second, max_allowed_time shorter # than that will cause a timeout error. with pytest.raises(requests.exceptions.Timeout): authed_session.request("GET", self.TEST_URL, max_allowed_time=0.9) def test_request_max_allowed_time_w_transport_timeout_no_error(self, frozen_time): tick_one_second = functools.partial( frozen_time.tick, delta=datetime.timedelta(seconds=1.0) ) credentials = mock.Mock( wraps=TimeTickCredentialsStub(time_tick=tick_one_second) ) adapter = TimeTickAdapterStub( time_tick=tick_one_second, responses=[ make_response(status=http_client.UNAUTHORIZED), make_response(status=http_client.OK), ], ) authed_session = google.auth.transport.requests.AuthorizedSession(credentials) authed_session.mount(self.TEST_URL, adapter) # A short configured transport timeout does not affect max_allowed_time. # The latter is not adjusted to it and is only concerned with the actual # execution time. The call below should thus not raise a timeout error. authed_session.request("GET", self.TEST_URL, timeout=0.5, max_allowed_time=3.1) def test_request_max_allowed_time_w_refresh_timeout_no_error(self, frozen_time): tick_one_second = functools.partial( frozen_time.tick, delta=datetime.timedelta(seconds=1.0) ) credentials = mock.Mock( wraps=TimeTickCredentialsStub(time_tick=tick_one_second) ) adapter = TimeTickAdapterStub( time_tick=tick_one_second, responses=[ make_response(status=http_client.UNAUTHORIZED), make_response(status=http_client.OK), ], ) authed_session = google.auth.transport.requests.AuthorizedSession( credentials, refresh_timeout=1.1 ) authed_session.mount(self.TEST_URL, adapter) # A short configured refresh timeout does not affect max_allowed_time. # The latter is not adjusted to it and is only concerned with the actual # execution time. The call below should thus not raise a timeout error # (and `timeout` does not come into play either, as it's very long). authed_session.request("GET", self.TEST_URL, timeout=60, max_allowed_time=3.1) def test_request_timeout_w_refresh_timeout_timeout_error(self, frozen_time): tick_one_second = functools.partial( frozen_time.tick, delta=datetime.timedelta(seconds=1.0) ) credentials = mock.Mock( wraps=TimeTickCredentialsStub(time_tick=tick_one_second) ) adapter = TimeTickAdapterStub( time_tick=tick_one_second, responses=[ make_response(status=http_client.UNAUTHORIZED), make_response(status=http_client.OK), ], ) authed_session = google.auth.transport.requests.AuthorizedSession( credentials, refresh_timeout=100 ) authed_session.mount(self.TEST_URL, adapter) # An UNAUTHORIZED response triggers a refresh (an extra request), thus # the final request that otherwise succeeds results in a timeout error # (all three requests together last 3 mocked seconds). with pytest.raises(requests.exceptions.Timeout): authed_session.request( "GET", self.TEST_URL, timeout=60, max_allowed_time=2.9 ) def test_authorized_session_without_default_host(self): credentials = mock.create_autospec(service_account.Credentials) authed_session = google.auth.transport.requests.AuthorizedSession(credentials) authed_session.credentials._create_self_signed_jwt.assert_called_once_with(None) def test_authorized_session_with_default_host(self): default_host = "pubsub.googleapis.com" credentials = mock.create_autospec(service_account.Credentials) authed_session = google.auth.transport.requests.AuthorizedSession( credentials, default_host=default_host ) authed_session.credentials._create_self_signed_jwt.assert_called_once_with( "https://{}/".format(default_host) ) def test_configure_mtls_channel_with_callback(self): mock_callback = mock.Mock() mock_callback.return_value = ( pytest.public_cert_bytes, pytest.private_key_bytes, ) auth_session = google.auth.transport.requests.AuthorizedSession( credentials=mock.Mock() ) with mock.patch.dict( os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"} ): auth_session.configure_mtls_channel(mock_callback) assert auth_session.is_mtls assert isinstance( auth_session.adapters["https://"], google.auth.transport.requests._MutualTlsAdapter, ) @mock.patch( "google.auth.transport._mtls_helper.get_client_cert_and_key", autospec=True ) def test_configure_mtls_channel_with_metadata(self, mock_get_client_cert_and_key): mock_get_client_cert_and_key.return_value = ( True, pytest.public_cert_bytes, pytest.private_key_bytes, ) auth_session = google.auth.transport.requests.AuthorizedSession( credentials=mock.Mock() ) with mock.patch.dict( os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"} ): auth_session.configure_mtls_channel() assert auth_session.is_mtls assert isinstance( auth_session.adapters["https://"], google.auth.transport.requests._MutualTlsAdapter, ) @mock.patch.object(google.auth.transport.requests._MutualTlsAdapter, "__init__") @mock.patch( "google.auth.transport._mtls_helper.get_client_cert_and_key", autospec=True ) def test_configure_mtls_channel_non_mtls( self, mock_get_client_cert_and_key, mock_adapter_ctor ): mock_get_client_cert_and_key.return_value = (False, None, None) auth_session = google.auth.transport.requests.AuthorizedSession( credentials=mock.Mock() ) with mock.patch.dict( os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"} ): auth_session.configure_mtls_channel() assert not auth_session.is_mtls # Assert _MutualTlsAdapter constructor is not called. mock_adapter_ctor.assert_not_called() @mock.patch( "google.auth.transport._mtls_helper.get_client_cert_and_key", autospec=True ) def test_configure_mtls_channel_exceptions(self, mock_get_client_cert_and_key): mock_get_client_cert_and_key.side_effect = exceptions.ClientCertError() auth_session = google.auth.transport.requests.AuthorizedSession( credentials=mock.Mock() ) with pytest.raises(exceptions.MutualTLSChannelError): with mock.patch.dict( os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"} ): auth_session.configure_mtls_channel() mock_get_client_cert_and_key.return_value = (False, None, None) with mock.patch.dict("sys.modules"): sys.modules["OpenSSL"] = None with pytest.raises(exceptions.MutualTLSChannelError): with mock.patch.dict( os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"}, ): auth_session.configure_mtls_channel() @mock.patch( "google.auth.transport._mtls_helper.get_client_cert_and_key", autospec=True ) def test_configure_mtls_channel_without_client_cert_env( self, get_client_cert_and_key ): # Test client cert won't be used if GOOGLE_API_USE_CLIENT_CERTIFICATE # environment variable is not set. auth_session = google.auth.transport.requests.AuthorizedSession( credentials=mock.Mock() ) auth_session.configure_mtls_channel() assert not auth_session.is_mtls get_client_cert_and_key.assert_not_called() mock_callback = mock.Mock() auth_session.configure_mtls_channel(mock_callback) assert not auth_session.is_mtls mock_callback.assert_not_called() def test_close_wo_passed_in_auth_request(self): authed_session = google.auth.transport.requests.AuthorizedSession( mock.sentinel.credentials ) authed_session._auth_request_session = mock.Mock(spec=["close"]) authed_session.close() authed_session._auth_request_session.close.assert_called_once_with() def test_close_w_passed_in_auth_request(self): http = mock.create_autospec(requests.Session) auth_request = google.auth.transport.requests.Request(http) authed_session = google.auth.transport.requests.AuthorizedSession( mock.sentinel.credentials, auth_request=auth_request ) authed_session.close() # no raise class TestMutualTlsOffloadAdapter(object): @mock.patch.object(requests.adapters.HTTPAdapter, "init_poolmanager") @mock.patch.object(requests.adapters.HTTPAdapter, "proxy_manager_for") @mock.patch.object( google.auth.transport._custom_tls_signer.CustomTlsSigner, "load_libraries" ) @mock.patch.object( google.auth.transport._custom_tls_signer.CustomTlsSigner, "set_up_custom_key" ) @mock.patch.object( google.auth.transport._custom_tls_signer.CustomTlsSigner, "attach_to_ssl_context", ) def test_success( self, mock_attach_to_ssl_context, mock_set_up_custom_key, mock_load_libraries, mock_proxy_manager_for, mock_init_poolmanager, ): enterprise_cert_file_path = "/path/to/enterprise/cert/json" adapter = google.auth.transport.requests._MutualTlsOffloadAdapter( enterprise_cert_file_path ) mock_load_libraries.assert_called_once() mock_set_up_custom_key.assert_called_once() assert mock_attach_to_ssl_context.call_count == 2 adapter.init_poolmanager() mock_init_poolmanager.assert_called_with(ssl_context=adapter._ctx_poolmanager) adapter.proxy_manager_for() mock_proxy_manager_for.assert_called_with(ssl_context=adapter._ctx_proxymanager) transport/test_urllib3.py 0000644 00000026360 15025175543 0011603 0 ustar 00 # Copyright 2016 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import http.client as http_client import os import sys import mock import OpenSSL import pytest # type: ignore import urllib3 # type: ignore from google.auth import environment_vars from google.auth import exceptions import google.auth.credentials import google.auth.transport._mtls_helper import google.auth.transport.urllib3 from google.oauth2 import service_account from tests.transport import compliance class TestRequestResponse(compliance.RequestResponseTests): def make_request(self): http = urllib3.PoolManager() return google.auth.transport.urllib3.Request(http) def test_timeout(self): http = mock.create_autospec(urllib3.PoolManager) request = google.auth.transport.urllib3.Request(http) request(url="http://example.com", method="GET", timeout=5) assert http.request.call_args[1]["timeout"] == 5 def test__make_default_http_with_certifi(): http = google.auth.transport.urllib3._make_default_http() assert "cert_reqs" in http.connection_pool_kw @mock.patch.object(google.auth.transport.urllib3, "certifi", new=None) def test__make_default_http_without_certifi(): http = google.auth.transport.urllib3._make_default_http() assert "cert_reqs" not in http.connection_pool_kw class CredentialsStub(google.auth.credentials.Credentials): def __init__(self, token="token"): super(CredentialsStub, self).__init__() self.token = token def apply(self, headers, token=None): headers["authorization"] = self.token def before_request(self, request, method, url, headers): self.apply(headers) 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): self.responses = responses self.requests = [] self.headers = headers or {} def urlopen(self, method, url, body=None, headers=None, **kwargs): self.requests.append((method, url, body, headers, kwargs)) return self.responses.pop(0) def clear(self): pass class ResponseStub(object): def __init__(self, status=http_client.OK, data=None): self.status = status self.data = data class TestMakeMutualTlsHttp(object): def test_success(self): http = google.auth.transport.urllib3._make_mutual_tls_http( pytest.public_cert_bytes, pytest.private_key_bytes ) assert isinstance(http, urllib3.PoolManager) def test_crypto_error(self): with pytest.raises(OpenSSL.crypto.Error): google.auth.transport.urllib3._make_mutual_tls_http( b"invalid cert", b"invalid key" ) @mock.patch.dict("sys.modules", {"OpenSSL.crypto": None}) def test_import_error(self): with pytest.raises(ImportError): google.auth.transport.urllib3._make_mutual_tls_http( pytest.public_cert_bytes, pytest.private_key_bytes ) class TestAuthorizedHttp(object): TEST_URL = "http://example.com" def test_authed_http_defaults(self): authed_http = google.auth.transport.urllib3.AuthorizedHttp( mock.sentinel.credentials ) assert authed_http.credentials == mock.sentinel.credentials assert isinstance(authed_http.http, urllib3.PoolManager) def test_urlopen_no_refresh(self): credentials = mock.Mock(wraps=CredentialsStub()) response = ResponseStub() http = HttpStub([response]) authed_http = google.auth.transport.urllib3.AuthorizedHttp( credentials, http=http ) result = authed_http.urlopen("GET", self.TEST_URL) assert result == response assert credentials.before_request.called assert not credentials.refresh.called assert http.requests == [ ("GET", self.TEST_URL, None, {"authorization": "token"}, {}) ] def test_urlopen_refresh(self): credentials = mock.Mock(wraps=CredentialsStub()) final_response = ResponseStub(status=http_client.OK) # First request will 401, second request will succeed. http = HttpStub([ResponseStub(status=http_client.UNAUTHORIZED), final_response]) authed_http = google.auth.transport.urllib3.AuthorizedHttp( credentials, http=http ) authed_http = authed_http.urlopen("GET", "http://example.com") assert authed_http == final_response assert credentials.before_request.call_count == 2 assert credentials.refresh.called assert http.requests == [ ("GET", self.TEST_URL, None, {"authorization": "token"}, {}), ("GET", self.TEST_URL, None, {"authorization": "token1"}, {}), ] def test_urlopen_no_default_host(self): credentials = mock.create_autospec(service_account.Credentials) authed_http = google.auth.transport.urllib3.AuthorizedHttp(credentials) authed_http.credentials._create_self_signed_jwt.assert_called_once_with(None) def test_urlopen_with_default_host(self): default_host = "pubsub.googleapis.com" credentials = mock.create_autospec(service_account.Credentials) authed_http = google.auth.transport.urllib3.AuthorizedHttp( credentials, default_host=default_host ) authed_http.credentials._create_self_signed_jwt.assert_called_once_with( "https://{}/".format(default_host) ) def test_proxies(self): http = mock.create_autospec(urllib3.PoolManager) authed_http = google.auth.transport.urllib3.AuthorizedHttp(None, http=http) with authed_http: pass assert http.__enter__.called assert http.__exit__.called authed_http.headers = mock.sentinel.headers assert authed_http.headers == http.headers @mock.patch("google.auth.transport.urllib3._make_mutual_tls_http", autospec=True) def test_configure_mtls_channel_with_callback(self, mock_make_mutual_tls_http): callback = mock.Mock() callback.return_value = (pytest.public_cert_bytes, pytest.private_key_bytes) authed_http = google.auth.transport.urllib3.AuthorizedHttp( credentials=mock.Mock(), http=mock.Mock() ) with pytest.warns(UserWarning): with mock.patch.dict( os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"} ): is_mtls = authed_http.configure_mtls_channel(callback) assert is_mtls mock_make_mutual_tls_http.assert_called_once_with( cert=pytest.public_cert_bytes, key=pytest.private_key_bytes ) @mock.patch("google.auth.transport.urllib3._make_mutual_tls_http", autospec=True) @mock.patch( "google.auth.transport._mtls_helper.get_client_cert_and_key", autospec=True ) def test_configure_mtls_channel_with_metadata( self, mock_get_client_cert_and_key, mock_make_mutual_tls_http ): authed_http = google.auth.transport.urllib3.AuthorizedHttp( credentials=mock.Mock() ) mock_get_client_cert_and_key.return_value = ( True, pytest.public_cert_bytes, pytest.private_key_bytes, ) with mock.patch.dict( os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"} ): is_mtls = authed_http.configure_mtls_channel() assert is_mtls mock_get_client_cert_and_key.assert_called_once() mock_make_mutual_tls_http.assert_called_once_with( cert=pytest.public_cert_bytes, key=pytest.private_key_bytes ) @mock.patch("google.auth.transport.urllib3._make_mutual_tls_http", autospec=True) @mock.patch( "google.auth.transport._mtls_helper.get_client_cert_and_key", autospec=True ) def test_configure_mtls_channel_non_mtls( self, mock_get_client_cert_and_key, mock_make_mutual_tls_http ): authed_http = google.auth.transport.urllib3.AuthorizedHttp( credentials=mock.Mock() ) mock_get_client_cert_and_key.return_value = (False, None, None) with mock.patch.dict( os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"} ): is_mtls = authed_http.configure_mtls_channel() assert not is_mtls mock_get_client_cert_and_key.assert_called_once() mock_make_mutual_tls_http.assert_not_called() @mock.patch( "google.auth.transport._mtls_helper.get_client_cert_and_key", autospec=True ) def test_configure_mtls_channel_exceptions(self, mock_get_client_cert_and_key): authed_http = google.auth.transport.urllib3.AuthorizedHttp( credentials=mock.Mock() ) mock_get_client_cert_and_key.side_effect = exceptions.ClientCertError() with pytest.raises(exceptions.MutualTLSChannelError): with mock.patch.dict( os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"} ): authed_http.configure_mtls_channel() mock_get_client_cert_and_key.return_value = (False, None, None) with mock.patch.dict("sys.modules"): sys.modules["OpenSSL"] = None with pytest.raises(exceptions.MutualTLSChannelError): with mock.patch.dict( os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"}, ): authed_http.configure_mtls_channel() @mock.patch( "google.auth.transport._mtls_helper.get_client_cert_and_key", autospec=True ) def test_configure_mtls_channel_without_client_cert_env( self, get_client_cert_and_key ): callback = mock.Mock() authed_http = google.auth.transport.urllib3.AuthorizedHttp( credentials=mock.Mock(), http=mock.Mock() ) # Test the callback is not called if GOOGLE_API_USE_CLIENT_CERTIFICATE is not set. is_mtls = authed_http.configure_mtls_channel(callback) assert not is_mtls callback.assert_not_called() # Test ADC client cert is not used if GOOGLE_API_USE_CLIENT_CERTIFICATE is not set. is_mtls = authed_http.configure_mtls_channel(callback) assert not is_mtls get_client_cert_and_key.assert_not_called() def test_clear_pool_on_del(self): http = mock.create_autospec(urllib3.PoolManager) authed_http = google.auth.transport.urllib3.AuthorizedHttp( mock.sentinel.credentials, http=http ) authed_http.__del__() http.clear.assert_called_with() authed_http.http = None authed_http.__del__() # Expect it to not crash transport/test_mtls.py 0000644 00000006502 15025175543 0011202 0 ustar 00 # Copyright 2020 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import mock import pytest # type: ignore from google.auth import exceptions from google.auth.transport import mtls @mock.patch( "google.auth.transport._mtls_helper._check_dca_metadata_path", autospec=True ) def test_has_default_client_cert_source(check_dca_metadata_path): check_dca_metadata_path.return_value = mock.Mock() assert mtls.has_default_client_cert_source() check_dca_metadata_path.return_value = None assert not mtls.has_default_client_cert_source() @mock.patch("google.auth.transport._mtls_helper.get_client_cert_and_key", autospec=True) @mock.patch("google.auth.transport.mtls.has_default_client_cert_source", autospec=True) def test_default_client_cert_source( has_default_client_cert_source, get_client_cert_and_key ): # Test default client cert source doesn't exist. has_default_client_cert_source.return_value = False with pytest.raises(exceptions.MutualTLSChannelError): mtls.default_client_cert_source() # The following tests will assume default client cert source exists. has_default_client_cert_source.return_value = True # Test good callback. get_client_cert_and_key.return_value = (True, b"cert", b"key") callback = mtls.default_client_cert_source() assert callback() == (b"cert", b"key") # Test bad callback which throws exception. get_client_cert_and_key.side_effect = ValueError() callback = mtls.default_client_cert_source() with pytest.raises(exceptions.MutualTLSChannelError): callback() @mock.patch( "google.auth.transport._mtls_helper.get_client_ssl_credentials", autospec=True ) @mock.patch("google.auth.transport.mtls.has_default_client_cert_source", autospec=True) def test_default_client_encrypted_cert_source( has_default_client_cert_source, get_client_ssl_credentials ): # Test default client cert source doesn't exist. has_default_client_cert_source.return_value = False with pytest.raises(exceptions.MutualTLSChannelError): mtls.default_client_encrypted_cert_source("cert_path", "key_path") # The following tests will assume default client cert source exists. has_default_client_cert_source.return_value = True # Test good callback. get_client_ssl_credentials.return_value = (True, b"cert", b"key", b"passphrase") callback = mtls.default_client_encrypted_cert_source("cert_path", "key_path") with mock.patch("{}.open".format(__name__), return_value=mock.MagicMock()): assert callback() == ("cert_path", "key_path", b"passphrase") # Test bad callback which throws exception. get_client_ssl_credentials.side_effect = exceptions.ClientCertError() callback = mtls.default_client_encrypted_cert_source("cert_path", "key_path") with pytest.raises(exceptions.MutualTLSChannelError): callback() transport/test__custom_tls_signer.py 0000644 00000020762 15025175543 0014131 0 ustar 00 # Copyright 2022 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import base64 import ctypes import os import mock import pytest # type: ignore from requests.packages.urllib3.util.ssl_ import create_urllib3_context # type: ignore import urllib3.contrib.pyopenssl # type: ignore from google.auth import exceptions from google.auth.transport import _custom_tls_signer urllib3.contrib.pyopenssl.inject_into_urllib3() FAKE_ENTERPRISE_CERT_FILE_PATH = "/path/to/enterprise/cert/file" ENTERPRISE_CERT_FILE = os.path.join( os.path.dirname(__file__), "../data/enterprise_cert_valid.json" ) INVALID_ENTERPRISE_CERT_FILE = os.path.join( os.path.dirname(__file__), "../data/enterprise_cert_invalid.json" ) def test_load_offload_lib(): with mock.patch("ctypes.CDLL", return_value=mock.MagicMock()): lib = _custom_tls_signer.load_offload_lib("/path/to/offload/lib") assert lib.ConfigureSslContext.argtypes == [ _custom_tls_signer.SIGN_CALLBACK_CTYPE, ctypes.c_char_p, ctypes.c_void_p, ] assert lib.ConfigureSslContext.restype == ctypes.c_int def test_load_signer_lib(): with mock.patch("ctypes.CDLL", return_value=mock.MagicMock()): lib = _custom_tls_signer.load_signer_lib("/path/to/signer/lib") assert lib.SignForPython.restype == ctypes.c_int assert lib.SignForPython.argtypes == [ ctypes.c_char_p, ctypes.c_char_p, ctypes.c_int, ctypes.c_char_p, ctypes.c_int, ] assert lib.GetCertPemForPython.restype == ctypes.c_int assert lib.GetCertPemForPython.argtypes == [ ctypes.c_char_p, ctypes.c_char_p, ctypes.c_int, ] def test__compute_sha256_digest(): to_be_signed = ctypes.create_string_buffer(b"foo") sig = _custom_tls_signer._compute_sha256_digest(to_be_signed, 4) assert ( base64.b64encode(sig).decode() == "RG5gyEH8CAAh3lxgbt2PLPAHPO8p6i9+cn5dqHfUUYM=" ) def test_get_sign_callback(): # mock signer lib's SignForPython function mock_sig_len = 10 mock_signer_lib = mock.MagicMock() mock_signer_lib.SignForPython.return_value = mock_sig_len # create a sign callback. The callback calls signer lib's SignForPython method sign_callback = _custom_tls_signer.get_sign_callback( mock_signer_lib, FAKE_ENTERPRISE_CERT_FILE_PATH ) # mock the parameters used to call the sign callback to_be_signed = ctypes.POINTER(ctypes.c_ubyte)() to_be_signed_len = 4 returned_sig_array = ctypes.c_ubyte() mock_sig_array = ctypes.byref(returned_sig_array) returned_sign_len = ctypes.c_ulong() mock_sig_len_array = ctypes.byref(returned_sign_len) # call the callback, make sure the signature len is returned via mock_sig_len_array[0] assert sign_callback( mock_sig_array, mock_sig_len_array, to_be_signed, to_be_signed_len ) assert returned_sign_len.value == mock_sig_len def test_get_sign_callback_failed_to_sign(): # mock signer lib's SignForPython function. Set the sig len to be 0 to # indicate the signing failed. mock_sig_len = 0 mock_signer_lib = mock.MagicMock() mock_signer_lib.SignForPython.return_value = mock_sig_len # create a sign callback. The callback calls signer lib's SignForPython method sign_callback = _custom_tls_signer.get_sign_callback( mock_signer_lib, FAKE_ENTERPRISE_CERT_FILE_PATH ) # mock the parameters used to call the sign callback to_be_signed = ctypes.POINTER(ctypes.c_ubyte)() to_be_signed_len = 4 returned_sig_array = ctypes.c_ubyte() mock_sig_array = ctypes.byref(returned_sig_array) returned_sign_len = ctypes.c_ulong() mock_sig_len_array = ctypes.byref(returned_sign_len) sign_callback(mock_sig_array, mock_sig_len_array, to_be_signed, to_be_signed_len) # sign callback should return 0 assert not sign_callback( mock_sig_array, mock_sig_len_array, to_be_signed, to_be_signed_len ) def test_get_cert_no_cert(): # mock signer lib's GetCertPemForPython function to return 0 to indicts # the cert doesn't exit (cert len = 0) mock_signer_lib = mock.MagicMock() mock_signer_lib.GetCertPemForPython.return_value = 0 # call the get cert method with pytest.raises(exceptions.MutualTLSChannelError) as excinfo: _custom_tls_signer.get_cert(mock_signer_lib, FAKE_ENTERPRISE_CERT_FILE_PATH) assert excinfo.match("failed to get certificate") def test_get_cert(): # mock signer lib's GetCertPemForPython function mock_cert_len = 10 mock_signer_lib = mock.MagicMock() mock_signer_lib.GetCertPemForPython.return_value = mock_cert_len # call the get cert method mock_cert = _custom_tls_signer.get_cert( mock_signer_lib, FAKE_ENTERPRISE_CERT_FILE_PATH ) # make sure the signer lib's GetCertPemForPython is called twice, and the # mock_cert has length mock_cert_len assert mock_signer_lib.GetCertPemForPython.call_count == 2 assert len(mock_cert) == mock_cert_len def test_custom_tls_signer(): offload_lib = mock.MagicMock() signer_lib = mock.MagicMock() # Test load_libraries method with mock.patch( "google.auth.transport._custom_tls_signer.load_signer_lib" ) as load_signer_lib: with mock.patch( "google.auth.transport._custom_tls_signer.load_offload_lib" ) as load_offload_lib: load_offload_lib.return_value = offload_lib load_signer_lib.return_value = signer_lib signer_object = _custom_tls_signer.CustomTlsSigner(ENTERPRISE_CERT_FILE) signer_object.load_libraries() assert signer_object._cert is None assert signer_object._enterprise_cert_file_path == ENTERPRISE_CERT_FILE assert signer_object._offload_lib == offload_lib assert signer_object._signer_lib == signer_lib load_signer_lib.assert_called_with("/path/to/signer/lib") load_offload_lib.assert_called_with("/path/to/offload/lib") # Test set_up_custom_key and set_up_ssl_context methods with mock.patch("google.auth.transport._custom_tls_signer.get_cert") as get_cert: with mock.patch( "google.auth.transport._custom_tls_signer.get_sign_callback" ) as get_sign_callback: get_cert.return_value = b"mock_cert" signer_object.set_up_custom_key() signer_object.attach_to_ssl_context(create_urllib3_context()) get_cert.assert_called_once() get_sign_callback.assert_called_once() offload_lib.ConfigureSslContext.assert_called_once() def test_custom_tls_signer_failed_to_load_libraries(): # Test load_libraries method with pytest.raises(exceptions.MutualTLSChannelError) as excinfo: signer_object = _custom_tls_signer.CustomTlsSigner(INVALID_ENTERPRISE_CERT_FILE) signer_object.load_libraries() assert excinfo.match("enterprise cert file is invalid") def test_custom_tls_signer_fail_to_offload(): offload_lib = mock.MagicMock() signer_lib = mock.MagicMock() with mock.patch( "google.auth.transport._custom_tls_signer.load_signer_lib" ) as load_signer_lib: with mock.patch( "google.auth.transport._custom_tls_signer.load_offload_lib" ) as load_offload_lib: load_offload_lib.return_value = offload_lib load_signer_lib.return_value = signer_lib signer_object = _custom_tls_signer.CustomTlsSigner(ENTERPRISE_CERT_FILE) signer_object.load_libraries() # set the return value to be 0 which indicts offload fails offload_lib.ConfigureSslContext.return_value = 0 with pytest.raises(exceptions.MutualTLSChannelError) as excinfo: with mock.patch( "google.auth.transport._custom_tls_signer.get_cert" ) as get_cert: with mock.patch( "google.auth.transport._custom_tls_signer.get_sign_callback" ): get_cert.return_value = b"mock_cert" signer_object.set_up_custom_key() signer_object.attach_to_ssl_context(create_urllib3_context()) assert excinfo.match("failed to configure SSL context") transport/__init__.py 0000644 00000000000 15025175543 0010706 0 ustar 00 transport/test__mtls_helper.py 0000644 00000041601 15025175543 0012677 0 ustar 00 # Copyright 2020 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import os import re import mock from OpenSSL import crypto import pytest # type: ignore from google.auth import exceptions from google.auth.transport import _mtls_helper CONTEXT_AWARE_METADATA = {"cert_provider_command": ["some command"]} ENCRYPTED_EC_PRIVATE_KEY = b"""-----BEGIN ENCRYPTED PRIVATE KEY----- MIHkME8GCSqGSIb3DQEFDTBCMCkGCSqGSIb3DQEFDDAcBAgl2/yVgs1h3QICCAAw DAYIKoZIhvcNAgkFADAVBgkrBgEEAZdVAQIECJk2GRrvxOaJBIGQXIBnMU4wmciT uA6yD8q0FxuIzjG7E2S6tc5VRgSbhRB00eBO3jWmO2pBybeQW+zVioDcn50zp2ts wYErWC+LCm1Zg3r+EGnT1E1GgNoODbVQ3AEHlKh1CGCYhEovxtn3G+Fjh7xOBrNB saVVeDb4tHD4tMkiVVUBrUcTZPndP73CtgyGHYEphasYPzEz3+AU -----END ENCRYPTED PRIVATE KEY-----""" EC_PUBLIC_KEY = b"""-----BEGIN PUBLIC KEY----- MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEvCNi1NoDY1oMqPHIgXI8RBbTYGi/ brEjbre1nSiQW11xRTJbVeETdsuP0EAu2tG3PcRhhwDfeJ8zXREgTBurNw== -----END PUBLIC KEY-----""" PASSPHRASE = b"""-----BEGIN PASSPHRASE----- password -----END PASSPHRASE-----""" PASSPHRASE_VALUE = b"password" def check_cert_and_key(content, expected_cert, expected_key): success = True cert_match = re.findall(_mtls_helper._CERT_REGEX, content) success = success and len(cert_match) == 1 and cert_match[0] == expected_cert key_match = re.findall(_mtls_helper._KEY_REGEX, content) success = success and len(key_match) == 1 and key_match[0] == expected_key return success class TestCertAndKeyRegex(object): def test_cert_and_key(self): # Test single cert and single key check_cert_and_key( pytest.public_cert_bytes + pytest.private_key_bytes, pytest.public_cert_bytes, pytest.private_key_bytes, ) check_cert_and_key( pytest.private_key_bytes + pytest.public_cert_bytes, pytest.public_cert_bytes, pytest.private_key_bytes, ) # Test cert chain and single key check_cert_and_key( pytest.public_cert_bytes + pytest.public_cert_bytes + pytest.private_key_bytes, pytest.public_cert_bytes + pytest.public_cert_bytes, pytest.private_key_bytes, ) check_cert_and_key( pytest.private_key_bytes + pytest.public_cert_bytes + pytest.public_cert_bytes, pytest.public_cert_bytes + pytest.public_cert_bytes, pytest.private_key_bytes, ) def test_key(self): # Create some fake keys for regex check. KEY = b"""-----BEGIN PRIVATE KEY----- MIIBCgKCAQEA4ej0p7bQ7L/r4rVGUz9RN4VQWoej1Bg1mYWIDYslvKrk1gpj7wZg /fy3ZpsL7WqgsZS7Q+0VRK8gKfqkxg5OYQIDAQAB -----END PRIVATE KEY-----""" RSA_KEY = b"""-----BEGIN RSA PRIVATE KEY----- MIIBCgKCAQEA4ej0p7bQ7L/r4rVGUz9RN4VQWoej1Bg1mYWIDYslvKrk1gpj7wZg /fy3ZpsL7WqgsZS7Q+0VRK8gKfqkxg5OYQIDAQAB -----END RSA PRIVATE KEY-----""" EC_KEY = b"""-----BEGIN EC PRIVATE KEY----- MIIBCgKCAQEA4ej0p7bQ7L/r4rVGUz9RN4VQWoej1Bg1mYWIDYslvKrk1gpj7wZg /fy3ZpsL7WqgsZS7Q+0VRK8gKfqkxg5OYQIDAQAB -----END EC PRIVATE KEY-----""" check_cert_and_key( pytest.public_cert_bytes + KEY, pytest.public_cert_bytes, KEY ) check_cert_and_key( pytest.public_cert_bytes + RSA_KEY, pytest.public_cert_bytes, RSA_KEY ) check_cert_and_key( pytest.public_cert_bytes + EC_KEY, pytest.public_cert_bytes, EC_KEY ) class TestCheckaMetadataPath(object): def test_success(self): metadata_path = os.path.join(pytest.data_dir, "context_aware_metadata.json") returned_path = _mtls_helper._check_dca_metadata_path(metadata_path) assert returned_path is not None def test_failure(self): metadata_path = os.path.join(pytest.data_dir, "not_exists.json") returned_path = _mtls_helper._check_dca_metadata_path(metadata_path) assert returned_path is None class TestReadMetadataFile(object): def test_success(self): metadata_path = os.path.join(pytest.data_dir, "context_aware_metadata.json") metadata = _mtls_helper._read_dca_metadata_file(metadata_path) assert "cert_provider_command" in metadata def test_file_not_json(self): # read a file which is not json format. metadata_path = os.path.join(pytest.data_dir, "privatekey.pem") with pytest.raises(exceptions.ClientCertError): _mtls_helper._read_dca_metadata_file(metadata_path) class TestRunCertProviderCommand(object): def create_mock_process(self, output, error): # There are two steps to execute a script with subprocess.Popen. # (1) process = subprocess.Popen([comannds]) # (2) stdout, stderr = process.communicate() # This function creates a mock process which can be returned by a mock # subprocess.Popen. The mock process returns the given output and error # when mock_process.communicate() is called. mock_process = mock.Mock() attrs = {"communicate.return_value": (output, error), "returncode": 0} mock_process.configure_mock(**attrs) return mock_process @mock.patch("subprocess.Popen", autospec=True) def test_success(self, mock_popen): mock_popen.return_value = self.create_mock_process( pytest.public_cert_bytes + pytest.private_key_bytes, b"" ) cert, key, passphrase = _mtls_helper._run_cert_provider_command(["command"]) assert cert == pytest.public_cert_bytes assert key == pytest.private_key_bytes assert passphrase is None mock_popen.return_value = self.create_mock_process( pytest.public_cert_bytes + ENCRYPTED_EC_PRIVATE_KEY + PASSPHRASE, b"" ) cert, key, passphrase = _mtls_helper._run_cert_provider_command( ["command"], expect_encrypted_key=True ) assert cert == pytest.public_cert_bytes assert key == ENCRYPTED_EC_PRIVATE_KEY assert passphrase == PASSPHRASE_VALUE @mock.patch("subprocess.Popen", autospec=True) def test_success_with_cert_chain(self, mock_popen): PUBLIC_CERT_CHAIN_BYTES = pytest.public_cert_bytes + pytest.public_cert_bytes mock_popen.return_value = self.create_mock_process( PUBLIC_CERT_CHAIN_BYTES + pytest.private_key_bytes, b"" ) cert, key, passphrase = _mtls_helper._run_cert_provider_command(["command"]) assert cert == PUBLIC_CERT_CHAIN_BYTES assert key == pytest.private_key_bytes assert passphrase is None mock_popen.return_value = self.create_mock_process( PUBLIC_CERT_CHAIN_BYTES + ENCRYPTED_EC_PRIVATE_KEY + PASSPHRASE, b"" ) cert, key, passphrase = _mtls_helper._run_cert_provider_command( ["command"], expect_encrypted_key=True ) assert cert == PUBLIC_CERT_CHAIN_BYTES assert key == ENCRYPTED_EC_PRIVATE_KEY assert passphrase == PASSPHRASE_VALUE @mock.patch("subprocess.Popen", autospec=True) def test_missing_cert(self, mock_popen): mock_popen.return_value = self.create_mock_process( pytest.private_key_bytes, b"" ) with pytest.raises(exceptions.ClientCertError): _mtls_helper._run_cert_provider_command(["command"]) mock_popen.return_value = self.create_mock_process( ENCRYPTED_EC_PRIVATE_KEY + PASSPHRASE, b"" ) with pytest.raises(exceptions.ClientCertError): _mtls_helper._run_cert_provider_command( ["command"], expect_encrypted_key=True ) @mock.patch("subprocess.Popen", autospec=True) def test_missing_key(self, mock_popen): mock_popen.return_value = self.create_mock_process( pytest.public_cert_bytes, b"" ) with pytest.raises(exceptions.ClientCertError): _mtls_helper._run_cert_provider_command(["command"]) mock_popen.return_value = self.create_mock_process( pytest.public_cert_bytes + PASSPHRASE, b"" ) with pytest.raises(exceptions.ClientCertError): _mtls_helper._run_cert_provider_command( ["command"], expect_encrypted_key=True ) @mock.patch("subprocess.Popen", autospec=True) def test_missing_passphrase(self, mock_popen): mock_popen.return_value = self.create_mock_process( pytest.public_cert_bytes + ENCRYPTED_EC_PRIVATE_KEY, b"" ) with pytest.raises(exceptions.ClientCertError): _mtls_helper._run_cert_provider_command( ["command"], expect_encrypted_key=True ) @mock.patch("subprocess.Popen", autospec=True) def test_passphrase_not_expected(self, mock_popen): mock_popen.return_value = self.create_mock_process( pytest.public_cert_bytes + pytest.private_key_bytes + PASSPHRASE, b"" ) with pytest.raises(exceptions.ClientCertError): _mtls_helper._run_cert_provider_command(["command"]) @mock.patch("subprocess.Popen", autospec=True) def test_encrypted_key_expected(self, mock_popen): mock_popen.return_value = self.create_mock_process( pytest.public_cert_bytes + pytest.private_key_bytes + PASSPHRASE, b"" ) with pytest.raises(exceptions.ClientCertError): _mtls_helper._run_cert_provider_command( ["command"], expect_encrypted_key=True ) @mock.patch("subprocess.Popen", autospec=True) def test_unencrypted_key_expected(self, mock_popen): mock_popen.return_value = self.create_mock_process( pytest.public_cert_bytes + ENCRYPTED_EC_PRIVATE_KEY, b"" ) with pytest.raises(exceptions.ClientCertError): _mtls_helper._run_cert_provider_command(["command"]) @mock.patch("subprocess.Popen", autospec=True) def test_cert_provider_returns_error(self, mock_popen): mock_popen.return_value = self.create_mock_process(b"", b"some error") mock_popen.return_value.returncode = 1 with pytest.raises(exceptions.ClientCertError): _mtls_helper._run_cert_provider_command(["command"]) @mock.patch("subprocess.Popen", autospec=True) def test_popen_raise_exception(self, mock_popen): mock_popen.side_effect = OSError() with pytest.raises(exceptions.ClientCertError): _mtls_helper._run_cert_provider_command(["command"]) class TestGetClientSslCredentials(object): @mock.patch( "google.auth.transport._mtls_helper._run_cert_provider_command", autospec=True ) @mock.patch( "google.auth.transport._mtls_helper._read_dca_metadata_file", autospec=True ) @mock.patch( "google.auth.transport._mtls_helper._check_dca_metadata_path", autospec=True ) def test_success( self, mock_check_dca_metadata_path, mock_read_dca_metadata_file, mock_run_cert_provider_command, ): mock_check_dca_metadata_path.return_value = True mock_read_dca_metadata_file.return_value = { "cert_provider_command": ["command"] } mock_run_cert_provider_command.return_value = (b"cert", b"key", None) has_cert, cert, key, passphrase = _mtls_helper.get_client_ssl_credentials() assert has_cert assert cert == b"cert" assert key == b"key" assert passphrase is None @mock.patch( "google.auth.transport._mtls_helper._check_dca_metadata_path", autospec=True ) def test_success_without_metadata(self, mock_check_dca_metadata_path): mock_check_dca_metadata_path.return_value = False has_cert, cert, key, passphrase = _mtls_helper.get_client_ssl_credentials() assert not has_cert assert cert is None assert key is None assert passphrase is None @mock.patch( "google.auth.transport._mtls_helper._run_cert_provider_command", autospec=True ) @mock.patch( "google.auth.transport._mtls_helper._read_dca_metadata_file", autospec=True ) @mock.patch( "google.auth.transport._mtls_helper._check_dca_metadata_path", autospec=True ) def test_success_with_encrypted_key( self, mock_check_dca_metadata_path, mock_read_dca_metadata_file, mock_run_cert_provider_command, ): mock_check_dca_metadata_path.return_value = True mock_read_dca_metadata_file.return_value = { "cert_provider_command": ["command"] } mock_run_cert_provider_command.return_value = (b"cert", b"key", b"passphrase") has_cert, cert, key, passphrase = _mtls_helper.get_client_ssl_credentials( generate_encrypted_key=True ) assert has_cert assert cert == b"cert" assert key == b"key" assert passphrase == b"passphrase" mock_run_cert_provider_command.assert_called_once_with( ["command", "--with_passphrase"], expect_encrypted_key=True ) @mock.patch( "google.auth.transport._mtls_helper._read_dca_metadata_file", autospec=True ) @mock.patch( "google.auth.transport._mtls_helper._check_dca_metadata_path", autospec=True ) def test_missing_cert_command( self, mock_check_dca_metadata_path, mock_read_dca_metadata_file ): mock_check_dca_metadata_path.return_value = True mock_read_dca_metadata_file.return_value = {} with pytest.raises(exceptions.ClientCertError): _mtls_helper.get_client_ssl_credentials() @mock.patch( "google.auth.transport._mtls_helper._run_cert_provider_command", autospec=True ) @mock.patch( "google.auth.transport._mtls_helper._read_dca_metadata_file", autospec=True ) @mock.patch( "google.auth.transport._mtls_helper._check_dca_metadata_path", autospec=True ) def test_customize_context_aware_metadata_path( self, mock_check_dca_metadata_path, mock_read_dca_metadata_file, mock_run_cert_provider_command, ): context_aware_metadata_path = "/path/to/metata/data" mock_check_dca_metadata_path.return_value = context_aware_metadata_path mock_read_dca_metadata_file.return_value = { "cert_provider_command": ["command"] } mock_run_cert_provider_command.return_value = (b"cert", b"key", None) has_cert, cert, key, passphrase = _mtls_helper.get_client_ssl_credentials( context_aware_metadata_path=context_aware_metadata_path ) assert has_cert assert cert == b"cert" assert key == b"key" assert passphrase is None mock_check_dca_metadata_path.assert_called_with(context_aware_metadata_path) mock_read_dca_metadata_file.assert_called_with(context_aware_metadata_path) class TestGetClientCertAndKey(object): def test_callback_success(self): callback = mock.Mock() callback.return_value = (pytest.public_cert_bytes, pytest.private_key_bytes) found_cert_key, cert, key = _mtls_helper.get_client_cert_and_key(callback) assert found_cert_key assert cert == pytest.public_cert_bytes assert key == pytest.private_key_bytes @mock.patch( "google.auth.transport._mtls_helper.get_client_ssl_credentials", autospec=True ) def test_use_metadata(self, mock_get_client_ssl_credentials): mock_get_client_ssl_credentials.return_value = ( True, pytest.public_cert_bytes, pytest.private_key_bytes, None, ) found_cert_key, cert, key = _mtls_helper.get_client_cert_and_key() assert found_cert_key assert cert == pytest.public_cert_bytes assert key == pytest.private_key_bytes class TestDecryptPrivateKey(object): def test_success(self): decrypted_key = _mtls_helper.decrypt_private_key( ENCRYPTED_EC_PRIVATE_KEY, PASSPHRASE_VALUE ) private_key = crypto.load_privatekey(crypto.FILETYPE_PEM, decrypted_key) public_key = crypto.load_publickey(crypto.FILETYPE_PEM, EC_PUBLIC_KEY) x509 = crypto.X509() x509.set_pubkey(public_key) # Test the decrypted key works by signing and verification. signature = crypto.sign(private_key, b"data", "sha256") crypto.verify(x509, signature, b"data", "sha256") def test_crypto_error(self): with pytest.raises(crypto.Error): _mtls_helper.decrypt_private_key( ENCRYPTED_EC_PRIVATE_KEY, b"wrong_password" ) test_downscoped.py 0000644 00000065337 15025175543 0010347 0 ustar 00 # Copyright 2021 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import datetime import http.client as http_client import json import urllib import mock import pytest # type: ignore from google.auth import _helpers from google.auth import credentials from google.auth import downscoped from google.auth import exceptions from google.auth import transport EXPRESSION = ( "resource.name.startsWith('projects/_/buckets/example-bucket/objects/customer-a')" ) TITLE = "customer-a-objects" DESCRIPTION = ( "Condition to make permissions available for objects starting with customer-a" ) AVAILABLE_RESOURCE = "//storage.googleapis.com/projects/_/buckets/example-bucket" AVAILABLE_PERMISSIONS = ["inRole:roles/storage.objectViewer"] OTHER_EXPRESSION = ( "resource.name.startsWith('projects/_/buckets/example-bucket/objects/customer-b')" ) OTHER_TITLE = "customer-b-objects" OTHER_DESCRIPTION = ( "Condition to make permissions available for objects starting with customer-b" ) OTHER_AVAILABLE_RESOURCE = "//storage.googleapis.com/projects/_/buckets/other-bucket" OTHER_AVAILABLE_PERMISSIONS = ["inRole:roles/storage.objectCreator"] QUOTA_PROJECT_ID = "QUOTA_PROJECT_ID" GRANT_TYPE = "urn:ietf:params:oauth:grant-type:token-exchange" REQUESTED_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:access_token" TOKEN_EXCHANGE_ENDPOINT = "https://sts.googleapis.com/v1/token" SUBJECT_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:access_token" SUCCESS_RESPONSE = { "access_token": "ACCESS_TOKEN", "issued_token_type": "urn:ietf:params:oauth:token-type:access_token", "token_type": "Bearer", "expires_in": 3600, } ERROR_RESPONSE = { "error": "invalid_grant", "error_description": "Subject token is invalid.", "error_uri": "https://tools.ietf.org/html/rfc6749", } CREDENTIAL_ACCESS_BOUNDARY_JSON = { "accessBoundary": { "accessBoundaryRules": [ { "availablePermissions": AVAILABLE_PERMISSIONS, "availableResource": AVAILABLE_RESOURCE, "availabilityCondition": { "expression": EXPRESSION, "title": TITLE, "description": DESCRIPTION, }, } ] } } class SourceCredentials(credentials.Credentials): def __init__(self, raise_error=False, expires_in=3600): super(SourceCredentials, self).__init__() self._counter = 0 self._raise_error = raise_error self._expires_in = expires_in def refresh(self, request): if self._raise_error: raise exceptions.RefreshError( "Failed to refresh access token in source credentials." ) now = _helpers.utcnow() self._counter += 1 self.token = "ACCESS_TOKEN_{}".format(self._counter) self.expiry = now + datetime.timedelta(seconds=self._expires_in) def make_availability_condition(expression, title=None, description=None): return downscoped.AvailabilityCondition(expression, title, description) def make_access_boundary_rule( available_resource, available_permissions, availability_condition=None ): return downscoped.AccessBoundaryRule( available_resource, available_permissions, availability_condition ) def make_credential_access_boundary(rules): return downscoped.CredentialAccessBoundary(rules) class TestAvailabilityCondition(object): def test_constructor(self): availability_condition = make_availability_condition( EXPRESSION, TITLE, DESCRIPTION ) assert availability_condition.expression == EXPRESSION assert availability_condition.title == TITLE assert availability_condition.description == DESCRIPTION def test_constructor_required_params_only(self): availability_condition = make_availability_condition(EXPRESSION) assert availability_condition.expression == EXPRESSION assert availability_condition.title is None assert availability_condition.description is None def test_setters(self): availability_condition = make_availability_condition( EXPRESSION, TITLE, DESCRIPTION ) availability_condition.expression = OTHER_EXPRESSION availability_condition.title = OTHER_TITLE availability_condition.description = OTHER_DESCRIPTION assert availability_condition.expression == OTHER_EXPRESSION assert availability_condition.title == OTHER_TITLE assert availability_condition.description == OTHER_DESCRIPTION def test_invalid_expression_type(self): with pytest.raises(TypeError) as excinfo: make_availability_condition([EXPRESSION], TITLE, DESCRIPTION) assert excinfo.match("The provided expression is not a string.") def test_invalid_title_type(self): with pytest.raises(TypeError) as excinfo: make_availability_condition(EXPRESSION, False, DESCRIPTION) assert excinfo.match("The provided title is not a string or None.") def test_invalid_description_type(self): with pytest.raises(TypeError) as excinfo: make_availability_condition(EXPRESSION, TITLE, False) assert excinfo.match("The provided description is not a string or None.") def test_to_json_required_params_only(self): availability_condition = make_availability_condition(EXPRESSION) assert availability_condition.to_json() == {"expression": EXPRESSION} def test_to_json_(self): availability_condition = make_availability_condition( EXPRESSION, TITLE, DESCRIPTION ) assert availability_condition.to_json() == { "expression": EXPRESSION, "title": TITLE, "description": DESCRIPTION, } class TestAccessBoundaryRule(object): def test_constructor(self): availability_condition = make_availability_condition( EXPRESSION, TITLE, DESCRIPTION ) access_boundary_rule = make_access_boundary_rule( AVAILABLE_RESOURCE, AVAILABLE_PERMISSIONS, availability_condition ) assert access_boundary_rule.available_resource == AVAILABLE_RESOURCE assert access_boundary_rule.available_permissions == tuple( AVAILABLE_PERMISSIONS ) assert access_boundary_rule.availability_condition == availability_condition def test_constructor_required_params_only(self): access_boundary_rule = make_access_boundary_rule( AVAILABLE_RESOURCE, AVAILABLE_PERMISSIONS ) assert access_boundary_rule.available_resource == AVAILABLE_RESOURCE assert access_boundary_rule.available_permissions == tuple( AVAILABLE_PERMISSIONS ) assert access_boundary_rule.availability_condition is None def test_setters(self): availability_condition = make_availability_condition( EXPRESSION, TITLE, DESCRIPTION ) other_availability_condition = make_availability_condition( OTHER_EXPRESSION, OTHER_TITLE, OTHER_DESCRIPTION ) access_boundary_rule = make_access_boundary_rule( AVAILABLE_RESOURCE, AVAILABLE_PERMISSIONS, availability_condition ) access_boundary_rule.available_resource = OTHER_AVAILABLE_RESOURCE access_boundary_rule.available_permissions = OTHER_AVAILABLE_PERMISSIONS access_boundary_rule.availability_condition = other_availability_condition assert access_boundary_rule.available_resource == OTHER_AVAILABLE_RESOURCE assert access_boundary_rule.available_permissions == tuple( OTHER_AVAILABLE_PERMISSIONS ) assert ( access_boundary_rule.availability_condition == other_availability_condition ) def test_invalid_available_resource_type(self): availability_condition = make_availability_condition( EXPRESSION, TITLE, DESCRIPTION ) with pytest.raises(TypeError) as excinfo: make_access_boundary_rule( None, AVAILABLE_PERMISSIONS, availability_condition ) assert excinfo.match("The provided available_resource is not a string.") def test_invalid_available_permissions_type(self): availability_condition = make_availability_condition( EXPRESSION, TITLE, DESCRIPTION ) with pytest.raises(TypeError) as excinfo: make_access_boundary_rule( AVAILABLE_RESOURCE, [0, 1, 2], availability_condition ) assert excinfo.match( "Provided available_permissions are not a list of strings." ) def test_invalid_available_permissions_value(self): availability_condition = make_availability_condition( EXPRESSION, TITLE, DESCRIPTION ) with pytest.raises(ValueError) as excinfo: make_access_boundary_rule( AVAILABLE_RESOURCE, ["roles/storage.objectViewer"], availability_condition, ) assert excinfo.match("available_permissions must be prefixed with 'inRole:'.") def test_invalid_availability_condition_type(self): with pytest.raises(TypeError) as excinfo: make_access_boundary_rule( AVAILABLE_RESOURCE, AVAILABLE_PERMISSIONS, {"foo": "bar"} ) assert excinfo.match( "The provided availability_condition is not a 'google.auth.downscoped.AvailabilityCondition' or None." ) def test_to_json(self): availability_condition = make_availability_condition( EXPRESSION, TITLE, DESCRIPTION ) access_boundary_rule = make_access_boundary_rule( AVAILABLE_RESOURCE, AVAILABLE_PERMISSIONS, availability_condition ) assert access_boundary_rule.to_json() == { "availablePermissions": AVAILABLE_PERMISSIONS, "availableResource": AVAILABLE_RESOURCE, "availabilityCondition": { "expression": EXPRESSION, "title": TITLE, "description": DESCRIPTION, }, } def test_to_json_required_params_only(self): access_boundary_rule = make_access_boundary_rule( AVAILABLE_RESOURCE, AVAILABLE_PERMISSIONS ) assert access_boundary_rule.to_json() == { "availablePermissions": AVAILABLE_PERMISSIONS, "availableResource": AVAILABLE_RESOURCE, } class TestCredentialAccessBoundary(object): def test_constructor(self): availability_condition = make_availability_condition( EXPRESSION, TITLE, DESCRIPTION ) access_boundary_rule = make_access_boundary_rule( AVAILABLE_RESOURCE, AVAILABLE_PERMISSIONS, availability_condition ) rules = [access_boundary_rule] credential_access_boundary = make_credential_access_boundary(rules) assert credential_access_boundary.rules == tuple(rules) def test_setters(self): availability_condition = make_availability_condition( EXPRESSION, TITLE, DESCRIPTION ) access_boundary_rule = make_access_boundary_rule( AVAILABLE_RESOURCE, AVAILABLE_PERMISSIONS, availability_condition ) rules = [access_boundary_rule] other_availability_condition = make_availability_condition( OTHER_EXPRESSION, OTHER_TITLE, OTHER_DESCRIPTION ) other_access_boundary_rule = make_access_boundary_rule( OTHER_AVAILABLE_RESOURCE, OTHER_AVAILABLE_PERMISSIONS, other_availability_condition, ) other_rules = [other_access_boundary_rule] credential_access_boundary = make_credential_access_boundary(rules) credential_access_boundary.rules = other_rules assert credential_access_boundary.rules == tuple(other_rules) def test_add_rule(self): availability_condition = make_availability_condition( EXPRESSION, TITLE, DESCRIPTION ) access_boundary_rule = make_access_boundary_rule( AVAILABLE_RESOURCE, AVAILABLE_PERMISSIONS, availability_condition ) rules = [access_boundary_rule] * 9 credential_access_boundary = make_credential_access_boundary(rules) # Add one more rule. This should not raise an error. additional_access_boundary_rule = make_access_boundary_rule( OTHER_AVAILABLE_RESOURCE, OTHER_AVAILABLE_PERMISSIONS ) credential_access_boundary.add_rule(additional_access_boundary_rule) assert len(credential_access_boundary.rules) == 10 assert credential_access_boundary.rules[9] == additional_access_boundary_rule def test_add_rule_invalid_value(self): availability_condition = make_availability_condition( EXPRESSION, TITLE, DESCRIPTION ) access_boundary_rule = make_access_boundary_rule( AVAILABLE_RESOURCE, AVAILABLE_PERMISSIONS, availability_condition ) rules = [access_boundary_rule] * 10 credential_access_boundary = make_credential_access_boundary(rules) # Add one more rule to exceed maximum allowed rules. with pytest.raises(ValueError) as excinfo: credential_access_boundary.add_rule(access_boundary_rule) assert excinfo.match( "Credential access boundary rules can have a maximum of 10 rules." ) assert len(credential_access_boundary.rules) == 10 def test_add_rule_invalid_type(self): availability_condition = make_availability_condition( EXPRESSION, TITLE, DESCRIPTION ) access_boundary_rule = make_access_boundary_rule( AVAILABLE_RESOURCE, AVAILABLE_PERMISSIONS, availability_condition ) rules = [access_boundary_rule] credential_access_boundary = make_credential_access_boundary(rules) # Add an invalid rule to exceed maximum allowed rules. with pytest.raises(TypeError) as excinfo: credential_access_boundary.add_rule("invalid") assert excinfo.match( "The provided rule does not contain a valid 'google.auth.downscoped.AccessBoundaryRule'." ) assert len(credential_access_boundary.rules) == 1 assert credential_access_boundary.rules[0] == access_boundary_rule def test_invalid_rules_type(self): with pytest.raises(TypeError) as excinfo: make_credential_access_boundary(["invalid"]) assert excinfo.match( "List of rules provided do not contain a valid 'google.auth.downscoped.AccessBoundaryRule'." ) def test_invalid_rules_value(self): availability_condition = make_availability_condition( EXPRESSION, TITLE, DESCRIPTION ) access_boundary_rule = make_access_boundary_rule( AVAILABLE_RESOURCE, AVAILABLE_PERMISSIONS, availability_condition ) too_many_rules = [access_boundary_rule] * 11 with pytest.raises(ValueError) as excinfo: make_credential_access_boundary(too_many_rules) assert excinfo.match( "Credential access boundary rules can have a maximum of 10 rules." ) def test_to_json(self): availability_condition = make_availability_condition( EXPRESSION, TITLE, DESCRIPTION ) access_boundary_rule = make_access_boundary_rule( AVAILABLE_RESOURCE, AVAILABLE_PERMISSIONS, availability_condition ) rules = [access_boundary_rule] credential_access_boundary = make_credential_access_boundary(rules) assert credential_access_boundary.to_json() == { "accessBoundary": { "accessBoundaryRules": [ { "availablePermissions": AVAILABLE_PERMISSIONS, "availableResource": AVAILABLE_RESOURCE, "availabilityCondition": { "expression": EXPRESSION, "title": TITLE, "description": DESCRIPTION, }, } ] } } class TestCredentials(object): @staticmethod def make_credentials(source_credentials=SourceCredentials(), quota_project_id=None): availability_condition = make_availability_condition( EXPRESSION, TITLE, DESCRIPTION ) access_boundary_rule = make_access_boundary_rule( AVAILABLE_RESOURCE, AVAILABLE_PERMISSIONS, availability_condition ) rules = [access_boundary_rule] credential_access_boundary = make_credential_access_boundary(rules) return downscoped.Credentials( source_credentials, credential_access_boundary, quota_project_id ) @staticmethod def make_mock_request(data, status=http_client.OK): response = mock.create_autospec(transport.Response, instance=True) response.status = status response.data = json.dumps(data).encode("utf-8") request = mock.create_autospec(transport.Request) request.return_value = response return request @staticmethod def assert_request_kwargs(request_kwargs, headers, request_data): """Asserts the request was called with the expected parameters. """ assert request_kwargs["url"] == TOKEN_EXCHANGE_ENDPOINT assert request_kwargs["method"] == "POST" assert request_kwargs["headers"] == headers assert request_kwargs["body"] is not None body_tuples = urllib.parse.parse_qsl(request_kwargs["body"]) for (k, v) in body_tuples: assert v.decode("utf-8") == request_data[k.decode("utf-8")] assert len(body_tuples) == len(request_data.keys()) def test_default_state(self): credentials = self.make_credentials() # No token acquired yet. assert not credentials.token assert not credentials.valid # Expiration hasn't been set yet. assert not credentials.expiry assert not credentials.expired # No quota project ID set. assert not credentials.quota_project_id def test_with_quota_project(self): credentials = self.make_credentials() assert not credentials.quota_project_id quota_project_creds = credentials.with_quota_project("project-foo") assert quota_project_creds.quota_project_id == "project-foo" @mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min) def test_refresh(self, unused_utcnow): response = SUCCESS_RESPONSE.copy() # Test custom expiration to confirm expiry is set correctly. response["expires_in"] = 2800 expected_expiry = datetime.datetime.min + datetime.timedelta( seconds=response["expires_in"] ) headers = {"Content-Type": "application/x-www-form-urlencoded"} request_data = { "grant_type": GRANT_TYPE, "subject_token": "ACCESS_TOKEN_1", "subject_token_type": SUBJECT_TOKEN_TYPE, "requested_token_type": REQUESTED_TOKEN_TYPE, "options": urllib.parse.quote(json.dumps(CREDENTIAL_ACCESS_BOUNDARY_JSON)), } request = self.make_mock_request(status=http_client.OK, data=response) source_credentials = SourceCredentials() credentials = self.make_credentials(source_credentials=source_credentials) # Spy on calls to source credentials refresh to confirm the expected request # instance is used. with mock.patch.object( source_credentials, "refresh", wraps=source_credentials.refresh ) as wrapped_souce_cred_refresh: credentials.refresh(request) self.assert_request_kwargs(request.call_args[1], headers, request_data) assert credentials.valid assert credentials.expiry == expected_expiry assert not credentials.expired assert credentials.token == response["access_token"] # Confirm source credentials called with the same request instance. wrapped_souce_cred_refresh.assert_called_with(request) @mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min) def test_refresh_without_response_expires_in(self, unused_utcnow): response = SUCCESS_RESPONSE.copy() # Simulate the response is missing the expires_in field. # The downscoped token expiration should match the source credentials # expiration. del response["expires_in"] expected_expires_in = 1800 # Simulate the source credentials generates a token with 1800 second # expiration time. The generated downscoped token should have the same # expiration time. source_credentials = SourceCredentials(expires_in=expected_expires_in) expected_expiry = datetime.datetime.min + datetime.timedelta( seconds=expected_expires_in ) headers = {"Content-Type": "application/x-www-form-urlencoded"} request_data = { "grant_type": GRANT_TYPE, "subject_token": "ACCESS_TOKEN_1", "subject_token_type": SUBJECT_TOKEN_TYPE, "requested_token_type": REQUESTED_TOKEN_TYPE, "options": urllib.parse.quote(json.dumps(CREDENTIAL_ACCESS_BOUNDARY_JSON)), } request = self.make_mock_request(status=http_client.OK, data=response) credentials = self.make_credentials(source_credentials=source_credentials) # Spy on calls to source credentials refresh to confirm the expected request # instance is used. with mock.patch.object( source_credentials, "refresh", wraps=source_credentials.refresh ) as wrapped_souce_cred_refresh: credentials.refresh(request) self.assert_request_kwargs(request.call_args[1], headers, request_data) assert credentials.valid assert credentials.expiry == expected_expiry assert not credentials.expired assert credentials.token == response["access_token"] # Confirm source credentials called with the same request instance. wrapped_souce_cred_refresh.assert_called_with(request) def test_refresh_token_exchange_error(self): request = self.make_mock_request( status=http_client.BAD_REQUEST, data=ERROR_RESPONSE ) credentials = self.make_credentials() with pytest.raises(exceptions.OAuthError) as excinfo: credentials.refresh(request) assert excinfo.match( r"Error code invalid_grant: Subject token is invalid. - https://tools.ietf.org/html/rfc6749" ) assert not credentials.expired assert credentials.token is None def test_refresh_source_credentials_refresh_error(self): # Initialize downscoped credentials with source credentials that raise # an error on refresh. credentials = self.make_credentials( source_credentials=SourceCredentials(raise_error=True) ) with pytest.raises(exceptions.RefreshError) as excinfo: credentials.refresh(mock.sentinel.request) assert excinfo.match(r"Failed to refresh access token in source credentials.") assert not credentials.expired assert credentials.token is None def test_apply_without_quota_project_id(self): headers = {} request = self.make_mock_request(status=http_client.OK, data=SUCCESS_RESPONSE) credentials = self.make_credentials() credentials.refresh(request) credentials.apply(headers) assert headers == { "authorization": "Bearer {}".format(SUCCESS_RESPONSE["access_token"]) } def test_apply_with_quota_project_id(self): headers = {"other": "header-value"} request = self.make_mock_request(status=http_client.OK, data=SUCCESS_RESPONSE) credentials = self.make_credentials(quota_project_id=QUOTA_PROJECT_ID) credentials.refresh(request) credentials.apply(headers) assert headers == { "other": "header-value", "authorization": "Bearer {}".format(SUCCESS_RESPONSE["access_token"]), "x-goog-user-project": QUOTA_PROJECT_ID, } def test_before_request(self): headers = {"other": "header-value"} request = self.make_mock_request(status=http_client.OK, data=SUCCESS_RESPONSE) credentials = self.make_credentials() # First call should call refresh, setting the token. credentials.before_request(request, "POST", "https://example.com/api", headers) assert headers == { "other": "header-value", "authorization": "Bearer {}".format(SUCCESS_RESPONSE["access_token"]), } # Second call shouldn't call refresh (request should be untouched). credentials.before_request( mock.sentinel.request, "POST", "https://example.com/api", headers ) assert headers == { "other": "header-value", "authorization": "Bearer {}".format(SUCCESS_RESPONSE["access_token"]), } @mock.patch("google.auth._helpers.utcnow") def test_before_request_expired(self, utcnow): headers = {} request = self.make_mock_request(status=http_client.OK, data=SUCCESS_RESPONSE) credentials = self.make_credentials() credentials.token = "token" utcnow.return_value = datetime.datetime.min # Set the expiration to one second more than now plus the clock skew # accommodation. These credentials should be valid. credentials.expiry = ( datetime.datetime.min + _helpers.REFRESH_THRESHOLD + datetime.timedelta(seconds=1) ) assert credentials.valid assert not credentials.expired credentials.before_request(request, "POST", "https://example.com/api", headers) # Cached token should be used. assert headers == {"authorization": "Bearer token"} # Next call should simulate 1 second passed. utcnow.return_value = datetime.datetime.min + datetime.timedelta(seconds=1) assert not credentials.valid assert credentials.expired credentials.before_request(request, "POST", "https://example.com/api", headers) # New token should be retrieved. assert headers == { "authorization": "Bearer {}".format(SUCCESS_RESPONSE["access_token"]) } test_api_key.py 0000644 00000002535 15025175543 0007612 0 ustar 00 # Copyright 2022 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import pytest # type: ignore from google.auth import api_key def test_credentials_constructor(): with pytest.raises(ValueError) as excinfo: api_key.Credentials("") assert excinfo.match(r"Token must be a non-empty API key string") def test_expired_and_valid(): credentials = api_key.Credentials("api-key") assert credentials.valid assert credentials.token == "api-key" assert not credentials.expired credentials.refresh(None) assert credentials.valid assert credentials.token == "api-key" assert not credentials.expired def test_before_request(): credentials = api_key.Credentials("api-key") headers = {} credentials.before_request(None, "http://example.com", "GET", headers) assert headers["x-goog-api-key"] == "api-key" test_iam.py 0000644 00000006311 15025175543 0006733 0 ustar 00 # Copyright 2017 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import base64 import datetime import http.client as http_client import json import mock import pytest # type: ignore from google.auth import _helpers from google.auth import exceptions from google.auth import iam from google.auth import transport import google.auth.credentials def make_request(status, data=None): response = mock.create_autospec(transport.Response, instance=True) response.status = status if data is not None: response.data = json.dumps(data).encode("utf-8") request = mock.create_autospec(transport.Request) request.return_value = response return request def make_credentials(): class CredentialsImpl(google.auth.credentials.Credentials): def __init__(self): super(CredentialsImpl, self).__init__() self.token = "token" # Force refresh self.expiry = datetime.datetime.min + _helpers.REFRESH_THRESHOLD def refresh(self, request): pass def with_quota_project(self, quota_project_id): raise NotImplementedError() return CredentialsImpl() class TestSigner(object): def test_constructor(self): request = mock.sentinel.request credentials = mock.create_autospec( google.auth.credentials.Credentials, instance=True ) signer = iam.Signer(request, credentials, mock.sentinel.service_account_email) assert signer._request == mock.sentinel.request assert signer._credentials == credentials assert signer._service_account_email == mock.sentinel.service_account_email def test_key_id(self): signer = iam.Signer( mock.sentinel.request, mock.sentinel.credentials, mock.sentinel.service_account_email, ) assert signer.key_id is None def test_sign_bytes(self): signature = b"DEADBEEF" encoded_signature = base64.b64encode(signature).decode("utf-8") request = make_request(http_client.OK, data={"signedBlob": encoded_signature}) credentials = make_credentials() signer = iam.Signer(request, credentials, mock.sentinel.service_account_email) returned_signature = signer.sign("123") assert returned_signature == signature kwargs = request.call_args[1] assert kwargs["headers"]["Content-Type"] == "application/json" def test_sign_bytes_failure(self): request = make_request(http_client.UNAUTHORIZED) credentials = make_credentials() signer = iam.Signer(request, credentials, mock.sentinel.service_account_email) with pytest.raises(exceptions.TransportError): signer.sign("123") crypt/test__cryptography_rsa.py 0000644 00000015627 15025175543 0013077 0 ustar 00 # Copyright 2016 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import json import os import pickle from cryptography.hazmat.primitives.asymmetric import rsa import pytest # type: ignore from google.auth import _helpers from google.auth.crypt import _cryptography_rsa from google.auth.crypt import base DATA_DIR = os.path.join(os.path.dirname(__file__), "..", "data") # To generate privatekey.pem, privatekey.pub, and public_cert.pem: # $ openssl req -new -newkey rsa:1024 -x509 -nodes -out public_cert.pem \ # > -keyout privatekey.pem # $ openssl rsa -in privatekey.pem -pubout -out privatekey.pub with open(os.path.join(DATA_DIR, "privatekey.pem"), "rb") as fh: PRIVATE_KEY_BYTES = fh.read() PKCS1_KEY_BYTES = PRIVATE_KEY_BYTES with open(os.path.join(DATA_DIR, "privatekey.pub"), "rb") as fh: PUBLIC_KEY_BYTES = fh.read() with open(os.path.join(DATA_DIR, "public_cert.pem"), "rb") as fh: PUBLIC_CERT_BYTES = fh.read() # To generate pem_from_pkcs12.pem and privatekey.p12: # $ openssl pkcs12 -export -out privatekey.p12 -inkey privatekey.pem \ # > -in public_cert.pem # $ openssl pkcs12 -in privatekey.p12 -nocerts -nodes \ # > -out pem_from_pkcs12.pem with open(os.path.join(DATA_DIR, "pem_from_pkcs12.pem"), "rb") as fh: PKCS8_KEY_BYTES = fh.read() with open(os.path.join(DATA_DIR, "privatekey.p12"), "rb") as fh: PKCS12_KEY_BYTES = fh.read() # The service account JSON file can be generated from the Google Cloud Console. SERVICE_ACCOUNT_JSON_FILE = os.path.join(DATA_DIR, "service_account.json") with open(SERVICE_ACCOUNT_JSON_FILE, "rb") as fh: SERVICE_ACCOUNT_INFO = json.load(fh) class TestRSAVerifier(object): def test_verify_success(self): to_sign = b"foo" signer = _cryptography_rsa.RSASigner.from_string(PRIVATE_KEY_BYTES) actual_signature = signer.sign(to_sign) verifier = _cryptography_rsa.RSAVerifier.from_string(PUBLIC_KEY_BYTES) assert verifier.verify(to_sign, actual_signature) def test_verify_unicode_success(self): to_sign = u"foo" signer = _cryptography_rsa.RSASigner.from_string(PRIVATE_KEY_BYTES) actual_signature = signer.sign(to_sign) verifier = _cryptography_rsa.RSAVerifier.from_string(PUBLIC_KEY_BYTES) assert verifier.verify(to_sign, actual_signature) def test_verify_failure(self): verifier = _cryptography_rsa.RSAVerifier.from_string(PUBLIC_KEY_BYTES) bad_signature1 = b"" assert not verifier.verify(b"foo", bad_signature1) bad_signature2 = b"a" assert not verifier.verify(b"foo", bad_signature2) def test_from_string_pub_key(self): verifier = _cryptography_rsa.RSAVerifier.from_string(PUBLIC_KEY_BYTES) assert isinstance(verifier, _cryptography_rsa.RSAVerifier) assert isinstance(verifier._pubkey, rsa.RSAPublicKey) def test_from_string_pub_key_unicode(self): public_key = _helpers.from_bytes(PUBLIC_KEY_BYTES) verifier = _cryptography_rsa.RSAVerifier.from_string(public_key) assert isinstance(verifier, _cryptography_rsa.RSAVerifier) assert isinstance(verifier._pubkey, rsa.RSAPublicKey) def test_from_string_pub_cert(self): verifier = _cryptography_rsa.RSAVerifier.from_string(PUBLIC_CERT_BYTES) assert isinstance(verifier, _cryptography_rsa.RSAVerifier) assert isinstance(verifier._pubkey, rsa.RSAPublicKey) def test_from_string_pub_cert_unicode(self): public_cert = _helpers.from_bytes(PUBLIC_CERT_BYTES) verifier = _cryptography_rsa.RSAVerifier.from_string(public_cert) assert isinstance(verifier, _cryptography_rsa.RSAVerifier) assert isinstance(verifier._pubkey, rsa.RSAPublicKey) class TestRSASigner(object): def test_from_string_pkcs1(self): signer = _cryptography_rsa.RSASigner.from_string(PKCS1_KEY_BYTES) assert isinstance(signer, _cryptography_rsa.RSASigner) assert isinstance(signer._key, rsa.RSAPrivateKey) def test_from_string_pkcs1_unicode(self): key_bytes = _helpers.from_bytes(PKCS1_KEY_BYTES) signer = _cryptography_rsa.RSASigner.from_string(key_bytes) assert isinstance(signer, _cryptography_rsa.RSASigner) assert isinstance(signer._key, rsa.RSAPrivateKey) def test_from_string_pkcs8(self): signer = _cryptography_rsa.RSASigner.from_string(PKCS8_KEY_BYTES) assert isinstance(signer, _cryptography_rsa.RSASigner) assert isinstance(signer._key, rsa.RSAPrivateKey) def test_from_string_pkcs8_unicode(self): key_bytes = _helpers.from_bytes(PKCS8_KEY_BYTES) signer = _cryptography_rsa.RSASigner.from_string(key_bytes) assert isinstance(signer, _cryptography_rsa.RSASigner) assert isinstance(signer._key, rsa.RSAPrivateKey) def test_from_string_pkcs12(self): with pytest.raises(ValueError): _cryptography_rsa.RSASigner.from_string(PKCS12_KEY_BYTES) def test_from_string_bogus_key(self): key_bytes = "bogus-key" with pytest.raises(ValueError): _cryptography_rsa.RSASigner.from_string(key_bytes) def test_from_service_account_info(self): signer = _cryptography_rsa.RSASigner.from_service_account_info( SERVICE_ACCOUNT_INFO ) assert signer.key_id == SERVICE_ACCOUNT_INFO[base._JSON_FILE_PRIVATE_KEY_ID] assert isinstance(signer._key, rsa.RSAPrivateKey) def test_from_service_account_info_missing_key(self): with pytest.raises(ValueError) as excinfo: _cryptography_rsa.RSASigner.from_service_account_info({}) assert excinfo.match(base._JSON_FILE_PRIVATE_KEY) def test_from_service_account_file(self): signer = _cryptography_rsa.RSASigner.from_service_account_file( SERVICE_ACCOUNT_JSON_FILE ) assert signer.key_id == SERVICE_ACCOUNT_INFO[base._JSON_FILE_PRIVATE_KEY_ID] assert isinstance(signer._key, rsa.RSAPrivateKey) def test_pickle(self): signer = _cryptography_rsa.RSASigner.from_service_account_file( SERVICE_ACCOUNT_JSON_FILE ) assert signer.key_id == SERVICE_ACCOUNT_INFO[base._JSON_FILE_PRIVATE_KEY_ID] assert isinstance(signer._key, rsa.RSAPrivateKey) pickled_signer = pickle.dumps(signer) signer = pickle.loads(pickled_signer) assert signer.key_id == SERVICE_ACCOUNT_INFO[base._JSON_FILE_PRIVATE_KEY_ID] assert isinstance(signer._key, rsa.RSAPrivateKey) crypt/test_crypt.py 0000644 00000003572 15025175543 0010475 0 ustar 00 # Copyright 2016 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import os from google.auth import crypt DATA_DIR = os.path.join(os.path.dirname(__file__), "..", "data") # To generate privatekey.pem, privatekey.pub, and public_cert.pem: # $ openssl req -new -newkey rsa:1024 -x509 -nodes -out public_cert.pem \ # > -keyout privatekey.pem # $ openssl rsa -in privatekey.pem -pubout -out privatekey.pub with open(os.path.join(DATA_DIR, "privatekey.pem"), "rb") as fh: PRIVATE_KEY_BYTES = fh.read() with open(os.path.join(DATA_DIR, "public_cert.pem"), "rb") as fh: PUBLIC_CERT_BYTES = fh.read() # To generate other_cert.pem: # $ openssl req -new -newkey rsa:1024 -x509 -nodes -out other_cert.pem with open(os.path.join(DATA_DIR, "other_cert.pem"), "rb") as fh: OTHER_CERT_BYTES = fh.read() def test_verify_signature(): to_sign = b"foo" signer = crypt.RSASigner.from_string(PRIVATE_KEY_BYTES) signature = signer.sign(to_sign) assert crypt.verify_signature(to_sign, signature, PUBLIC_CERT_BYTES) # List of certs assert crypt.verify_signature( to_sign, signature, [OTHER_CERT_BYTES, PUBLIC_CERT_BYTES] ) def test_verify_signature_failure(): to_sign = b"foo" signer = crypt.RSASigner.from_string(PRIVATE_KEY_BYTES) signature = signer.sign(to_sign) assert not crypt.verify_signature(to_sign, signature, OTHER_CERT_BYTES) crypt/test__python_rsa.py 0000644 00000016654 15025175543 0011666 0 ustar 00 # Copyright 2016 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import io import json import os import mock from pyasn1_modules import pem # type: ignore import pytest # type: ignore import rsa # type: ignore from google.auth import _helpers from google.auth.crypt import _python_rsa from google.auth.crypt import base DATA_DIR = os.path.join(os.path.dirname(__file__), "..", "data") # To generate privatekey.pem, privatekey.pub, and public_cert.pem: # $ openssl req -new -newkey rsa:1024 -x509 -nodes -out public_cert.pem \ # > -keyout privatekey.pem # $ openssl rsa -in privatekey.pem -pubout -out privatekey.pub with open(os.path.join(DATA_DIR, "privatekey.pem"), "rb") as fh: PRIVATE_KEY_BYTES = fh.read() PKCS1_KEY_BYTES = PRIVATE_KEY_BYTES with open(os.path.join(DATA_DIR, "privatekey.pub"), "rb") as fh: PUBLIC_KEY_BYTES = fh.read() with open(os.path.join(DATA_DIR, "public_cert.pem"), "rb") as fh: PUBLIC_CERT_BYTES = fh.read() # To generate pem_from_pkcs12.pem and privatekey.p12: # $ openssl pkcs12 -export -out privatekey.p12 -inkey privatekey.pem \ # > -in public_cert.pem # $ openssl pkcs12 -in privatekey.p12 -nocerts -nodes \ # > -out pem_from_pkcs12.pem with open(os.path.join(DATA_DIR, "pem_from_pkcs12.pem"), "rb") as fh: PKCS8_KEY_BYTES = fh.read() with open(os.path.join(DATA_DIR, "privatekey.p12"), "rb") as fh: PKCS12_KEY_BYTES = fh.read() # The service account JSON file can be generated from the Google Cloud Console. SERVICE_ACCOUNT_JSON_FILE = os.path.join(DATA_DIR, "service_account.json") with open(SERVICE_ACCOUNT_JSON_FILE, "rb") as fh: SERVICE_ACCOUNT_INFO = json.load(fh) class TestRSAVerifier(object): def test_verify_success(self): to_sign = b"foo" signer = _python_rsa.RSASigner.from_string(PRIVATE_KEY_BYTES) actual_signature = signer.sign(to_sign) verifier = _python_rsa.RSAVerifier.from_string(PUBLIC_KEY_BYTES) assert verifier.verify(to_sign, actual_signature) def test_verify_unicode_success(self): to_sign = u"foo" signer = _python_rsa.RSASigner.from_string(PRIVATE_KEY_BYTES) actual_signature = signer.sign(to_sign) verifier = _python_rsa.RSAVerifier.from_string(PUBLIC_KEY_BYTES) assert verifier.verify(to_sign, actual_signature) def test_verify_failure(self): verifier = _python_rsa.RSAVerifier.from_string(PUBLIC_KEY_BYTES) bad_signature1 = b"" assert not verifier.verify(b"foo", bad_signature1) bad_signature2 = b"a" assert not verifier.verify(b"foo", bad_signature2) def test_from_string_pub_key(self): verifier = _python_rsa.RSAVerifier.from_string(PUBLIC_KEY_BYTES) assert isinstance(verifier, _python_rsa.RSAVerifier) assert isinstance(verifier._pubkey, rsa.key.PublicKey) def test_from_string_pub_key_unicode(self): public_key = _helpers.from_bytes(PUBLIC_KEY_BYTES) verifier = _python_rsa.RSAVerifier.from_string(public_key) assert isinstance(verifier, _python_rsa.RSAVerifier) assert isinstance(verifier._pubkey, rsa.key.PublicKey) def test_from_string_pub_cert(self): verifier = _python_rsa.RSAVerifier.from_string(PUBLIC_CERT_BYTES) assert isinstance(verifier, _python_rsa.RSAVerifier) assert isinstance(verifier._pubkey, rsa.key.PublicKey) def test_from_string_pub_cert_unicode(self): public_cert = _helpers.from_bytes(PUBLIC_CERT_BYTES) verifier = _python_rsa.RSAVerifier.from_string(public_cert) assert isinstance(verifier, _python_rsa.RSAVerifier) assert isinstance(verifier._pubkey, rsa.key.PublicKey) def test_from_string_pub_cert_failure(self): cert_bytes = PUBLIC_CERT_BYTES true_der = rsa.pem.load_pem(cert_bytes, "CERTIFICATE") load_pem_patch = mock.patch( "rsa.pem.load_pem", return_value=true_der + b"extra", autospec=True ) with load_pem_patch as load_pem: with pytest.raises(ValueError): _python_rsa.RSAVerifier.from_string(cert_bytes) load_pem.assert_called_once_with(cert_bytes, "CERTIFICATE") class TestRSASigner(object): def test_from_string_pkcs1(self): signer = _python_rsa.RSASigner.from_string(PKCS1_KEY_BYTES) assert isinstance(signer, _python_rsa.RSASigner) assert isinstance(signer._key, rsa.key.PrivateKey) def test_from_string_pkcs1_unicode(self): key_bytes = _helpers.from_bytes(PKCS1_KEY_BYTES) signer = _python_rsa.RSASigner.from_string(key_bytes) assert isinstance(signer, _python_rsa.RSASigner) assert isinstance(signer._key, rsa.key.PrivateKey) def test_from_string_pkcs8(self): signer = _python_rsa.RSASigner.from_string(PKCS8_KEY_BYTES) assert isinstance(signer, _python_rsa.RSASigner) assert isinstance(signer._key, rsa.key.PrivateKey) def test_from_string_pkcs8_extra_bytes(self): key_bytes = PKCS8_KEY_BYTES _, pem_bytes = pem.readPemBlocksFromFile( io.StringIO(_helpers.from_bytes(key_bytes)), _python_rsa._PKCS8_MARKER ) key_info, remaining = None, "extra" decode_patch = mock.patch( "pyasn1.codec.der.decoder.decode", return_value=(key_info, remaining), autospec=True, ) with decode_patch as decode: with pytest.raises(ValueError): _python_rsa.RSASigner.from_string(key_bytes) # Verify mock was called. decode.assert_called_once_with(pem_bytes, asn1Spec=_python_rsa._PKCS8_SPEC) def test_from_string_pkcs8_unicode(self): key_bytes = _helpers.from_bytes(PKCS8_KEY_BYTES) signer = _python_rsa.RSASigner.from_string(key_bytes) assert isinstance(signer, _python_rsa.RSASigner) assert isinstance(signer._key, rsa.key.PrivateKey) def test_from_string_pkcs12(self): with pytest.raises(ValueError): _python_rsa.RSASigner.from_string(PKCS12_KEY_BYTES) def test_from_string_bogus_key(self): key_bytes = "bogus-key" with pytest.raises(ValueError): _python_rsa.RSASigner.from_string(key_bytes) def test_from_service_account_info(self): signer = _python_rsa.RSASigner.from_service_account_info(SERVICE_ACCOUNT_INFO) assert signer.key_id == SERVICE_ACCOUNT_INFO[base._JSON_FILE_PRIVATE_KEY_ID] assert isinstance(signer._key, rsa.key.PrivateKey) def test_from_service_account_info_missing_key(self): with pytest.raises(ValueError) as excinfo: _python_rsa.RSASigner.from_service_account_info({}) assert excinfo.match(base._JSON_FILE_PRIVATE_KEY) def test_from_service_account_file(self): signer = _python_rsa.RSASigner.from_service_account_file( SERVICE_ACCOUNT_JSON_FILE ) assert signer.key_id == SERVICE_ACCOUNT_INFO[base._JSON_FILE_PRIVATE_KEY_ID] assert isinstance(signer._key, rsa.key.PrivateKey) crypt/__init__.py 0000644 00000000000 15025175543 0010013 0 ustar 00 crypt/test_es256.py 0000644 00000014147 15025175543 0010200 0 ustar 00 # Copyright 2016 Google Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import base64 import json import os import pickle from cryptography.hazmat.primitives.asymmetric import ec import pytest # type: ignore from google.auth import _helpers from google.auth.crypt import base from google.auth.crypt import es256 DATA_DIR = os.path.join(os.path.dirname(__file__), "..", "data") # To generate es256_privatekey.pem, es256_privatekey.pub, and # es256_public_cert.pem: # $ openssl ecparam -genkey -name prime256v1 -noout -out es256_privatekey.pem # $ openssl ec -in es256-private-key.pem -pubout -out es256-publickey.pem # $ openssl req -new -x509 -key es256_privatekey.pem -out \ # > es256_public_cert.pem with open(os.path.join(DATA_DIR, "es256_privatekey.pem"), "rb") as fh: PRIVATE_KEY_BYTES = fh.read() PKCS1_KEY_BYTES = PRIVATE_KEY_BYTES with open(os.path.join(DATA_DIR, "es256_publickey.pem"), "rb") as fh: PUBLIC_KEY_BYTES = fh.read() with open(os.path.join(DATA_DIR, "es256_public_cert.pem"), "rb") as fh: PUBLIC_CERT_BYTES = fh.read() SERVICE_ACCOUNT_JSON_FILE = os.path.join(DATA_DIR, "es256_service_account.json") with open(SERVICE_ACCOUNT_JSON_FILE, "rb") as fh: SERVICE_ACCOUNT_INFO = json.load(fh) class TestES256Verifier(object): def test_verify_success(self): to_sign = b"foo" signer = es256.ES256Signer.from_string(PRIVATE_KEY_BYTES) actual_signature = signer.sign(to_sign) verifier = es256.ES256Verifier.from_string(PUBLIC_KEY_BYTES) assert verifier.verify(to_sign, actual_signature) def test_verify_unicode_success(self): to_sign = u"foo" signer = es256.ES256Signer.from_string(PRIVATE_KEY_BYTES) actual_signature = signer.sign(to_sign) verifier = es256.ES256Verifier.from_string(PUBLIC_KEY_BYTES) assert verifier.verify(to_sign, actual_signature) def test_verify_failure(self): verifier = es256.ES256Verifier.from_string(PUBLIC_KEY_BYTES) bad_signature1 = b"" assert not verifier.verify(b"foo", bad_signature1) bad_signature2 = b"a" assert not verifier.verify(b"foo", bad_signature2) def test_verify_failure_with_wrong_raw_signature(self): to_sign = b"foo" # This signature has a wrong "r" value in the "(r,s)" raw signature. wrong_signature = base64.urlsafe_b64decode( b"m7oaRxUDeYqjZ8qiMwo0PZLTMZWKJLFQREpqce1StMIa_yXQQ-C5WgeIRHW7OqlYSDL0XbUrj_uAw9i-QhfOJQ==" ) verifier = es256.ES256Verifier.from_string(PUBLIC_KEY_BYTES) assert not verifier.verify(to_sign, wrong_signature) def test_from_string_pub_key(self): verifier = es256.ES256Verifier.from_string(PUBLIC_KEY_BYTES) assert isinstance(verifier, es256.ES256Verifier) assert isinstance(verifier._pubkey, ec.EllipticCurvePublicKey) def test_from_string_pub_key_unicode(self): public_key = _helpers.from_bytes(PUBLIC_KEY_BYTES) verifier = es256.ES256Verifier.from_string(public_key) assert isinstance(verifier, es256.ES256Verifier) assert isinstance(verifier._pubkey, ec.EllipticCurvePublicKey) def test_from_string_pub_cert(self): verifier = es256.ES256Verifier.from_string(PUBLIC_CERT_BYTES) assert isinstance(verifier, es256.ES256Verifier) assert isinstance(verifier._pubkey, ec.EllipticCurvePublicKey) def test_from_string_pub_cert_unicode(self): public_cert = _helpers.from_bytes(PUBLIC_CERT_BYTES) verifier = es256.ES256Verifier.from_string(public_cert) assert isinstance(verifier, es256.ES256Verifier) assert isinstance(verifier._pubkey, ec.EllipticCurvePublicKey) class TestES256Signer(object): def test_from_string_pkcs1(self): signer = es256.ES256Signer.from_string(PKCS1_KEY_BYTES) assert isinstance(signer, es256.ES256Signer) assert isinstance(signer._key, ec.EllipticCurvePrivateKey) def test_from_string_pkcs1_unicode(self): key_bytes = _helpers.from_bytes(PKCS1_KEY_BYTES) signer = es256.ES256Signer.from_string(key_bytes) assert isinstance(signer, es256.ES256Signer) assert isinstance(signer._key, ec.EllipticCurvePrivateKey) def test_from_string_bogus_key(self): key_bytes = "bogus-key" with pytest.raises(ValueError): es256.ES256Signer.from_string(key_bytes) def test_from_service_account_info(self): signer = es256.ES256Signer.from_service_account_info(SERVICE_ACCOUNT_INFO) assert signer.key_id == SERVICE_ACCOUNT_INFO[base._JSON_FILE_PRIVATE_KEY_ID] assert isinstance(signer._key, ec.EllipticCurvePrivateKey) def test_from_service_account_info_missing_key(self): with pytest.raises(ValueError) as excinfo: es256.ES256Signer.from_service_account_info({}) assert excinfo.match(base._JSON_FILE_PRIVATE_KEY) def test_from_service_account_file(self): signer = es256.ES256Signer.from_service_account_file(SERVICE_ACCOUNT_JSON_FILE) assert signer.key_id == SERVICE_ACCOUNT_INFO[base._JSON_FILE_PRIVATE_KEY_ID] assert isinstance(signer._key, ec.EllipticCurvePrivateKey) def test_pickle(self): signer = es256.ES256Signer.from_service_account_file(SERVICE_ACCOUNT_JSON_FILE) assert signer.key_id == SERVICE_ACCOUNT_INFO[base._JSON_FILE_PRIVATE_KEY_ID] assert isinstance(signer._key, ec.EllipticCurvePrivateKey) pickled_signer = pickle.dumps(signer) signer = pickle.loads(pickled_signer) assert signer.key_id == SERVICE_ACCOUNT_INFO[base._JSON_FILE_PRIVATE_KEY_ID] assert isinstance(signer._key, ec.EllipticCurvePrivateKey) test__default.py 0000644 00000141642 15025175543 0007757 0 ustar 00 # Copyright 2016 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import json import os import mock import pytest # type: ignore from google.auth import _default from google.auth import api_key from google.auth import app_engine from google.auth import aws from google.auth import compute_engine from google.auth import credentials from google.auth import environment_vars from google.auth import exceptions from google.auth import external_account from google.auth import external_account_authorized_user from google.auth import identity_pool from google.auth import impersonated_credentials from google.auth import pluggable from google.oauth2 import gdch_credentials from google.oauth2 import service_account import google.oauth2.credentials DATA_DIR = os.path.join(os.path.dirname(__file__), "data") AUTHORIZED_USER_FILE = os.path.join(DATA_DIR, "authorized_user.json") with open(AUTHORIZED_USER_FILE) as fh: AUTHORIZED_USER_FILE_DATA = json.load(fh) AUTHORIZED_USER_CLOUD_SDK_FILE = os.path.join( DATA_DIR, "authorized_user_cloud_sdk.json" ) AUTHORIZED_USER_CLOUD_SDK_WITH_QUOTA_PROJECT_ID_FILE = os.path.join( DATA_DIR, "authorized_user_cloud_sdk_with_quota_project_id.json" ) SERVICE_ACCOUNT_FILE = os.path.join(DATA_DIR, "service_account.json") CLIENT_SECRETS_FILE = os.path.join(DATA_DIR, "client_secrets.json") GDCH_SERVICE_ACCOUNT_FILE = os.path.join(DATA_DIR, "gdch_service_account.json") with open(SERVICE_ACCOUNT_FILE) as fh: SERVICE_ACCOUNT_FILE_DATA = json.load(fh) SUBJECT_TOKEN_TEXT_FILE = os.path.join(DATA_DIR, "external_subject_token.txt") TOKEN_URL = "https://sts.googleapis.com/v1/token" AUDIENCE = "//iam.googleapis.com/projects/123456/locations/global/workloadIdentityPools/POOL_ID/providers/PROVIDER_ID" WORKFORCE_AUDIENCE = ( "//iam.googleapis.com/locations/global/workforcePools/POOL_ID/providers/PROVIDER_ID" ) WORKFORCE_POOL_USER_PROJECT = "WORKFORCE_POOL_USER_PROJECT_NUMBER" REGION_URL = "http://169.254.169.254/latest/meta-data/placement/availability-zone" SECURITY_CREDS_URL = "http://169.254.169.254/latest/meta-data/iam/security-credentials" CRED_VERIFICATION_URL = ( "https://sts.{region}.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15" ) IDENTITY_POOL_DATA = { "type": "external_account", "audience": AUDIENCE, "subject_token_type": "urn:ietf:params:oauth:token-type:jwt", "token_url": TOKEN_URL, "credential_source": {"file": SUBJECT_TOKEN_TEXT_FILE}, } PLUGGABLE_DATA = { "type": "external_account", "audience": AUDIENCE, "subject_token_type": "urn:ietf:params:oauth:token-type:jwt", "token_url": TOKEN_URL, "credential_source": {"executable": {"command": "command"}}, } AWS_DATA = { "type": "external_account", "audience": AUDIENCE, "subject_token_type": "urn:ietf:params:aws:token-type:aws4_request", "token_url": TOKEN_URL, "credential_source": { "environment_id": "aws1", "region_url": REGION_URL, "url": SECURITY_CREDS_URL, "regional_cred_verification_url": CRED_VERIFICATION_URL, }, } SERVICE_ACCOUNT_EMAIL = "service-1234@service-name.iam.gserviceaccount.com" SERVICE_ACCOUNT_IMPERSONATION_URL = ( "https://us-east1-iamcredentials.googleapis.com/v1/projects/-" + "/serviceAccounts/{}:generateAccessToken".format(SERVICE_ACCOUNT_EMAIL) ) IMPERSONATED_IDENTITY_POOL_DATA = { "type": "external_account", "audience": AUDIENCE, "subject_token_type": "urn:ietf:params:oauth:token-type:jwt", "token_url": TOKEN_URL, "credential_source": {"file": SUBJECT_TOKEN_TEXT_FILE}, "service_account_impersonation_url": SERVICE_ACCOUNT_IMPERSONATION_URL, } IMPERSONATED_AWS_DATA = { "type": "external_account", "audience": AUDIENCE, "subject_token_type": "urn:ietf:params:aws:token-type:aws4_request", "token_url": TOKEN_URL, "credential_source": { "environment_id": "aws1", "region_url": REGION_URL, "url": SECURITY_CREDS_URL, "regional_cred_verification_url": CRED_VERIFICATION_URL, }, "service_account_impersonation_url": SERVICE_ACCOUNT_IMPERSONATION_URL, } IDENTITY_POOL_WORKFORCE_DATA = { "type": "external_account", "audience": WORKFORCE_AUDIENCE, "subject_token_type": "urn:ietf:params:oauth:token-type:id_token", "token_url": TOKEN_URL, "credential_source": {"file": SUBJECT_TOKEN_TEXT_FILE}, "workforce_pool_user_project": WORKFORCE_POOL_USER_PROJECT, } IMPERSONATED_IDENTITY_POOL_WORKFORCE_DATA = { "type": "external_account", "audience": WORKFORCE_AUDIENCE, "subject_token_type": "urn:ietf:params:oauth:token-type:id_token", "token_url": TOKEN_URL, "credential_source": {"file": SUBJECT_TOKEN_TEXT_FILE}, "service_account_impersonation_url": SERVICE_ACCOUNT_IMPERSONATION_URL, "workforce_pool_user_project": WORKFORCE_POOL_USER_PROJECT, } IMPERSONATED_SERVICE_ACCOUNT_AUTHORIZED_USER_SOURCE_FILE = os.path.join( DATA_DIR, "impersonated_service_account_authorized_user_source.json" ) IMPERSONATED_SERVICE_ACCOUNT_WITH_QUOTA_PROJECT_FILE = os.path.join( DATA_DIR, "impersonated_service_account_with_quota_project.json" ) IMPERSONATED_SERVICE_ACCOUNT_SERVICE_ACCOUNT_SOURCE_FILE = os.path.join( DATA_DIR, "impersonated_service_account_service_account_source.json" ) EXTERNAL_ACCOUNT_AUTHORIZED_USER_FILE = os.path.join( DATA_DIR, "external_account_authorized_user.json" ) MOCK_CREDENTIALS = mock.Mock(spec=credentials.CredentialsWithQuotaProject) MOCK_CREDENTIALS.with_quota_project.return_value = MOCK_CREDENTIALS def get_project_id_side_effect(self, request=None): # If no scopes are set, this will always return None. if not self.scopes: return None return mock.sentinel.project_id LOAD_FILE_PATCH = mock.patch( "google.auth._default.load_credentials_from_file", return_value=(MOCK_CREDENTIALS, mock.sentinel.project_id), autospec=True, ) EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH = mock.patch.object( external_account.Credentials, "get_project_id", side_effect=get_project_id_side_effect, autospec=True, ) def test_load_credentials_from_missing_file(): with pytest.raises(exceptions.DefaultCredentialsError) as excinfo: _default.load_credentials_from_file("") assert excinfo.match(r"not found") def test_load_credentials_from_dict_non_dict_object(): with pytest.raises(exceptions.DefaultCredentialsError) as excinfo: _default.load_credentials_from_dict("") assert excinfo.match(r"dict type was expected") with pytest.raises(exceptions.DefaultCredentialsError) as excinfo: _default.load_credentials_from_dict(None) assert excinfo.match(r"dict type was expected") with pytest.raises(exceptions.DefaultCredentialsError) as excinfo: _default.load_credentials_from_dict(1) assert excinfo.match(r"dict type was expected") def test_load_credentials_from_dict_authorized_user(): credentials, project_id = _default.load_credentials_from_dict( AUTHORIZED_USER_FILE_DATA ) assert isinstance(credentials, google.oauth2.credentials.Credentials) assert project_id is None def test_load_credentials_from_file_invalid_json(tmpdir): jsonfile = tmpdir.join("invalid.json") jsonfile.write("{") with pytest.raises(exceptions.DefaultCredentialsError) as excinfo: _default.load_credentials_from_file(str(jsonfile)) assert excinfo.match(r"not a valid json file") def test_load_credentials_from_file_invalid_type(tmpdir): jsonfile = tmpdir.join("invalid.json") jsonfile.write(json.dumps({"type": "not-a-real-type"})) with pytest.raises(exceptions.DefaultCredentialsError) as excinfo: _default.load_credentials_from_file(str(jsonfile)) assert excinfo.match(r"does not have a valid type") def test_load_credentials_from_file_authorized_user(): credentials, project_id = _default.load_credentials_from_file(AUTHORIZED_USER_FILE) assert isinstance(credentials, google.oauth2.credentials.Credentials) assert project_id is None def test_load_credentials_from_file_no_type(tmpdir): # use the client_secrets.json, which is valid json but not a # loadable credentials type with pytest.raises(exceptions.DefaultCredentialsError) as excinfo: _default.load_credentials_from_file(CLIENT_SECRETS_FILE) assert excinfo.match(r"does not have a valid type") assert excinfo.match(r"Type is None") def test_load_credentials_from_file_authorized_user_bad_format(tmpdir): filename = tmpdir.join("authorized_user_bad.json") filename.write(json.dumps({"type": "authorized_user"})) with pytest.raises(exceptions.DefaultCredentialsError) as excinfo: _default.load_credentials_from_file(str(filename)) assert excinfo.match(r"Failed to load authorized user") assert excinfo.match(r"missing fields") def test_load_credentials_from_file_authorized_user_cloud_sdk(): with pytest.warns(UserWarning, match="Cloud SDK"): credentials, project_id = _default.load_credentials_from_file( AUTHORIZED_USER_CLOUD_SDK_FILE ) assert isinstance(credentials, google.oauth2.credentials.Credentials) assert project_id is None # No warning if the json file has quota project id. credentials, project_id = _default.load_credentials_from_file( AUTHORIZED_USER_CLOUD_SDK_WITH_QUOTA_PROJECT_ID_FILE ) assert isinstance(credentials, google.oauth2.credentials.Credentials) assert project_id is None def test_load_credentials_from_file_authorized_user_cloud_sdk_with_scopes(): with pytest.warns(UserWarning, match="Cloud SDK"): credentials, project_id = _default.load_credentials_from_file( AUTHORIZED_USER_CLOUD_SDK_FILE, scopes=["https://www.google.com/calendar/feeds"], ) assert isinstance(credentials, google.oauth2.credentials.Credentials) assert project_id is None 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) assert project_id == SERVICE_ACCOUNT_FILE_DATA["project_id"] def test_load_credentials_from_file_service_account_with_scopes(): credentials, project_id = _default.load_credentials_from_file( SERVICE_ACCOUNT_FILE, scopes=["https://www.google.com/calendar/feeds"] ) assert isinstance(credentials, service_account.Credentials) assert project_id == SERVICE_ACCOUNT_FILE_DATA["project_id"] assert credentials.scopes == ["https://www.google.com/calendar/feeds"] def test_load_credentials_from_file_service_account_with_quota_project(): credentials, project_id = _default.load_credentials_from_file( SERVICE_ACCOUNT_FILE, quota_project_id="project-foo" ) assert isinstance(credentials, service_account.Credentials) assert project_id == SERVICE_ACCOUNT_FILE_DATA["project_id"] assert credentials.quota_project_id == "project-foo" def test_load_credentials_from_file_service_account_bad_format(tmpdir): filename = tmpdir.join("serivce_account_bad.json") filename.write(json.dumps({"type": "service_account"})) with pytest.raises(exceptions.DefaultCredentialsError) as excinfo: _default.load_credentials_from_file(str(filename)) assert excinfo.match(r"Failed to load service account") assert excinfo.match(r"missing fields") def test_load_credentials_from_file_impersonated_with_authorized_user_source(): credentials, project_id = _default.load_credentials_from_file( IMPERSONATED_SERVICE_ACCOUNT_AUTHORIZED_USER_SOURCE_FILE ) assert isinstance(credentials, impersonated_credentials.Credentials) assert isinstance( credentials._source_credentials, google.oauth2.credentials.Credentials ) assert credentials.service_account_email == "service-account-target@example.com" assert credentials._delegates == ["service-account-delegate@example.com"] assert not credentials._quota_project_id assert not credentials._target_scopes assert project_id is None def test_load_credentials_from_file_impersonated_with_quota_project(): credentials, _ = _default.load_credentials_from_file( IMPERSONATED_SERVICE_ACCOUNT_WITH_QUOTA_PROJECT_FILE ) assert isinstance(credentials, impersonated_credentials.Credentials) assert credentials._quota_project_id == "quota_project" def test_load_credentials_from_file_impersonated_with_service_account_source(): credentials, _ = _default.load_credentials_from_file( IMPERSONATED_SERVICE_ACCOUNT_SERVICE_ACCOUNT_SOURCE_FILE ) assert isinstance(credentials, impersonated_credentials.Credentials) assert isinstance(credentials._source_credentials, service_account.Credentials) assert not credentials._quota_project_id def test_load_credentials_from_file_impersonated_passing_quota_project(): credentials, _ = _default.load_credentials_from_file( IMPERSONATED_SERVICE_ACCOUNT_SERVICE_ACCOUNT_SOURCE_FILE, quota_project_id="new_quota_project", ) assert credentials._quota_project_id == "new_quota_project" def test_load_credentials_from_file_impersonated_passing_scopes(): credentials, _ = _default.load_credentials_from_file( IMPERSONATED_SERVICE_ACCOUNT_SERVICE_ACCOUNT_SOURCE_FILE, scopes=["scope1", "scope2"], ) assert credentials._target_scopes == ["scope1", "scope2"] def test_load_credentials_from_file_impersonated_wrong_target_principal(tmpdir): with open(IMPERSONATED_SERVICE_ACCOUNT_AUTHORIZED_USER_SOURCE_FILE) as fh: impersonated_credentials_info = json.load(fh) impersonated_credentials_info[ "service_account_impersonation_url" ] = "something_wrong" jsonfile = tmpdir.join("invalid.json") jsonfile.write(json.dumps(impersonated_credentials_info)) with pytest.raises(exceptions.DefaultCredentialsError) as excinfo: _default.load_credentials_from_file(str(jsonfile)) assert excinfo.match(r"Cannot extract target principal") def test_load_credentials_from_file_impersonated_wrong_source_type(tmpdir): with open(IMPERSONATED_SERVICE_ACCOUNT_AUTHORIZED_USER_SOURCE_FILE) as fh: impersonated_credentials_info = json.load(fh) impersonated_credentials_info["source_credentials"]["type"] = "external_account" jsonfile = tmpdir.join("invalid.json") jsonfile.write(json.dumps(impersonated_credentials_info)) with pytest.raises(exceptions.DefaultCredentialsError) as excinfo: _default.load_credentials_from_file(str(jsonfile)) assert excinfo.match(r"source credential of type external_account is not supported") @EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH def test_load_credentials_from_file_external_account_identity_pool( get_project_id, tmpdir ): config_file = tmpdir.join("config.json") config_file.write(json.dumps(IDENTITY_POOL_DATA)) credentials, project_id = _default.load_credentials_from_file(str(config_file)) assert isinstance(credentials, identity_pool.Credentials) # Since no scopes are specified, the project ID cannot be determined. assert project_id is None assert get_project_id.called @EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH def test_load_credentials_from_file_external_account_aws(get_project_id, tmpdir): config_file = tmpdir.join("config.json") config_file.write(json.dumps(AWS_DATA)) credentials, project_id = _default.load_credentials_from_file(str(config_file)) assert isinstance(credentials, aws.Credentials) # Since no scopes are specified, the project ID cannot be determined. assert project_id is None assert get_project_id.called @EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH def test_load_credentials_from_file_external_account_identity_pool_impersonated( get_project_id, tmpdir ): config_file = tmpdir.join("config.json") config_file.write(json.dumps(IMPERSONATED_IDENTITY_POOL_DATA)) credentials, project_id = _default.load_credentials_from_file(str(config_file)) assert isinstance(credentials, identity_pool.Credentials) assert not credentials.is_user assert not credentials.is_workforce_pool # Since no scopes are specified, the project ID cannot be determined. assert project_id is None assert get_project_id.called @EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH def test_load_credentials_from_file_external_account_aws_impersonated( get_project_id, tmpdir ): config_file = tmpdir.join("config.json") config_file.write(json.dumps(IMPERSONATED_AWS_DATA)) credentials, project_id = _default.load_credentials_from_file(str(config_file)) assert isinstance(credentials, aws.Credentials) assert not credentials.is_user assert not credentials.is_workforce_pool # Since no scopes are specified, the project ID cannot be determined. assert project_id is None assert get_project_id.called @EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH def test_load_credentials_from_file_external_account_workforce(get_project_id, tmpdir): config_file = tmpdir.join("config.json") config_file.write(json.dumps(IDENTITY_POOL_WORKFORCE_DATA)) credentials, project_id = _default.load_credentials_from_file(str(config_file)) assert isinstance(credentials, identity_pool.Credentials) assert credentials.is_user assert credentials.is_workforce_pool # Since no scopes are specified, the project ID cannot be determined. assert project_id is None assert get_project_id.called @EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH def test_load_credentials_from_file_external_account_workforce_impersonated( get_project_id, tmpdir ): config_file = tmpdir.join("config.json") config_file.write(json.dumps(IMPERSONATED_IDENTITY_POOL_WORKFORCE_DATA)) credentials, project_id = _default.load_credentials_from_file(str(config_file)) assert isinstance(credentials, identity_pool.Credentials) assert not credentials.is_user assert credentials.is_workforce_pool # Since no scopes are specified, the project ID cannot be determined. assert project_id is None assert get_project_id.called @EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH def test_load_credentials_from_file_external_account_with_user_and_default_scopes( get_project_id, tmpdir ): config_file = tmpdir.join("config.json") config_file.write(json.dumps(IDENTITY_POOL_DATA)) credentials, project_id = _default.load_credentials_from_file( str(config_file), scopes=["https://www.google.com/calendar/feeds"], default_scopes=["https://www.googleapis.com/auth/cloud-platform"], ) assert isinstance(credentials, identity_pool.Credentials) # Since scopes are specified, the project ID can be determined. assert project_id is mock.sentinel.project_id assert credentials.scopes == ["https://www.google.com/calendar/feeds"] assert credentials.default_scopes == [ "https://www.googleapis.com/auth/cloud-platform" ] @EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH def test_load_credentials_from_file_external_account_with_quota_project( get_project_id, tmpdir ): config_file = tmpdir.join("config.json") config_file.write(json.dumps(IDENTITY_POOL_DATA)) credentials, project_id = _default.load_credentials_from_file( str(config_file), quota_project_id="project-foo" ) assert isinstance(credentials, identity_pool.Credentials) # Since no scopes are specified, the project ID cannot be determined. assert project_id is None assert credentials.quota_project_id == "project-foo" def test_load_credentials_from_file_external_account_bad_format(tmpdir): filename = tmpdir.join("external_account_bad.json") filename.write(json.dumps({"type": "external_account"})) with pytest.raises(exceptions.DefaultCredentialsError) as excinfo: _default.load_credentials_from_file(str(filename)) assert excinfo.match( "Failed to load external account credentials from {}".format(str(filename)) ) @EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH def test_load_credentials_from_file_external_account_explicit_request( get_project_id, tmpdir ): config_file = tmpdir.join("config.json") config_file.write(json.dumps(IDENTITY_POOL_DATA)) credentials, project_id = _default.load_credentials_from_file( str(config_file), request=mock.sentinel.request, scopes=["https://www.googleapis.com/auth/cloud-platform"], ) assert isinstance(credentials, identity_pool.Credentials) # Since scopes are specified, the project ID can be determined. assert project_id is mock.sentinel.project_id get_project_id.assert_called_with(credentials, request=mock.sentinel.request) @mock.patch.dict(os.environ, {}, clear=True) def test__get_explicit_environ_credentials_no_env(): assert _default._get_explicit_environ_credentials() == (None, None) def test_load_credentials_from_file_external_account_authorized_user(): credentials, project_id = _default.load_credentials_from_file( EXTERNAL_ACCOUNT_AUTHORIZED_USER_FILE, request=mock.sentinel.request ) assert isinstance(credentials, external_account_authorized_user.Credentials) assert project_id is None def test_load_credentials_from_file_external_account_authorized_user_bad_format(tmpdir): filename = tmpdir.join("external_account_authorized_user_bad.json") filename.write(json.dumps({"type": "external_account_authorized_user"})) with pytest.raises(exceptions.DefaultCredentialsError) as excinfo: _default.load_credentials_from_file(str(filename)) assert excinfo.match( "Failed to load external account authorized user credentials from {}".format( str(filename) ) ) @pytest.mark.parametrize("quota_project_id", [None, "project-foo"]) @LOAD_FILE_PATCH def test__get_explicit_environ_credentials(load, quota_project_id, monkeypatch): monkeypatch.setenv(environment_vars.CREDENTIALS, "filename") credentials, project_id = _default._get_explicit_environ_credentials( quota_project_id=quota_project_id ) assert credentials is MOCK_CREDENTIALS assert project_id is mock.sentinel.project_id load.assert_called_with("filename", quota_project_id=quota_project_id) @LOAD_FILE_PATCH def test__get_explicit_environ_credentials_no_project_id(load, monkeypatch): load.return_value = MOCK_CREDENTIALS, None monkeypatch.setenv(environment_vars.CREDENTIALS, "filename") credentials, project_id = _default._get_explicit_environ_credentials() assert credentials is MOCK_CREDENTIALS assert project_id is None @pytest.mark.parametrize("quota_project_id", [None, "project-foo"]) @mock.patch( "google.auth._cloud_sdk.get_application_default_credentials_path", autospec=True ) @mock.patch("google.auth._default._get_gcloud_sdk_credentials", autospec=True) def test__get_explicit_environ_credentials_fallback_to_gcloud( get_gcloud_creds, get_adc_path, quota_project_id, monkeypatch ): # Set explicit credentials path to cloud sdk credentials path. get_adc_path.return_value = "filename" monkeypatch.setenv(environment_vars.CREDENTIALS, "filename") _default._get_explicit_environ_credentials(quota_project_id=quota_project_id) # Check we fall back to cloud sdk flow since explicit credentials path is # cloud sdk credentials path get_gcloud_creds.assert_called_with(quota_project_id=quota_project_id) @pytest.mark.parametrize("quota_project_id", [None, "project-foo"]) @LOAD_FILE_PATCH @mock.patch( "google.auth._cloud_sdk.get_application_default_credentials_path", autospec=True ) def test__get_gcloud_sdk_credentials(get_adc_path, load, quota_project_id): get_adc_path.return_value = SERVICE_ACCOUNT_FILE credentials, project_id = _default._get_gcloud_sdk_credentials( quota_project_id=quota_project_id ) assert credentials is MOCK_CREDENTIALS assert project_id is mock.sentinel.project_id load.assert_called_with(SERVICE_ACCOUNT_FILE, quota_project_id=quota_project_id) @mock.patch( "google.auth._cloud_sdk.get_application_default_credentials_path", autospec=True ) def test__get_gcloud_sdk_credentials_non_existent(get_adc_path, tmpdir): non_existent = tmpdir.join("non-existent") get_adc_path.return_value = str(non_existent) credentials, project_id = _default._get_gcloud_sdk_credentials() assert credentials is None assert project_id is None @mock.patch( "google.auth._cloud_sdk.get_project_id", return_value=mock.sentinel.project_id, autospec=True, ) @mock.patch("os.path.isfile", return_value=True, autospec=True) @LOAD_FILE_PATCH 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_CREDENTIALS, None credentials, project_id = _default._get_gcloud_sdk_credentials() assert credentials == MOCK_CREDENTIALS assert project_id == mock.sentinel.project_id assert get_project_id.called @mock.patch("google.auth._cloud_sdk.get_project_id", return_value=None, autospec=True) @mock.patch("os.path.isfile", return_value=True) @LOAD_FILE_PATCH 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_CREDENTIALS, None credentials, project_id = _default._get_gcloud_sdk_credentials() assert credentials == MOCK_CREDENTIALS assert project_id is None assert get_project_id.called def test__get_gdch_service_account_credentials_invalid_format_version(): with pytest.raises(exceptions.DefaultCredentialsError) as excinfo: _default._get_gdch_service_account_credentials( "file_name", {"format_version": "2"} ) assert excinfo.match("Failed to load GDCH service account credentials") def test_get_api_key_credentials(): creds = _default.get_api_key_credentials("api_key") assert isinstance(creds, api_key.Credentials) assert creds.token == "api_key" class _AppIdentityModule(object): """The interface of the App Idenity app engine module. See https://cloud.google.com/appengine/docs/standard/python/refdocs\ /google.appengine.api.app_identity.app_identity """ def get_application_id(self): raise NotImplementedError() @pytest.fixture def app_identity(monkeypatch): """Mocks the app_identity module for google.auth.app_engine.""" app_identity_module = mock.create_autospec(_AppIdentityModule, instance=True) monkeypatch.setattr(app_engine, "app_identity", app_identity_module) yield app_identity_module @mock.patch.dict(os.environ) def test__get_gae_credentials_gen1(app_identity): os.environ[environment_vars.LEGACY_APPENGINE_RUNTIME] = "python27" app_identity.get_application_id.return_value = mock.sentinel.project credentials, project_id = _default._get_gae_credentials() assert isinstance(credentials, app_engine.Credentials) assert project_id == mock.sentinel.project @mock.patch.dict(os.environ) def test__get_gae_credentials_gen2(): os.environ["GAE_RUNTIME"] = "python37" credentials, project_id = _default._get_gae_credentials() assert credentials is None assert project_id is None @mock.patch.dict(os.environ) def test__get_gae_credentials_gen2_backwards_compat(): # compat helpers may copy GAE_RUNTIME to APPENGINE_RUNTIME # for backwards compatibility with code that relies on it os.environ[environment_vars.LEGACY_APPENGINE_RUNTIME] = "python37" os.environ["GAE_RUNTIME"] = "python37" credentials, project_id = _default._get_gae_credentials() assert credentials is None assert project_id is None def test__get_gae_credentials_env_unset(): assert environment_vars.LEGACY_APPENGINE_RUNTIME not in os.environ assert "GAE_RUNTIME" not in os.environ credentials, project_id = _default._get_gae_credentials() assert credentials is None assert project_id is None @mock.patch.dict(os.environ) def test__get_gae_credentials_no_app_engine(): # test both with and without LEGACY_APPENGINE_RUNTIME setting assert environment_vars.LEGACY_APPENGINE_RUNTIME not in os.environ import sys with mock.patch.dict(sys.modules, {"google.auth.app_engine": None}): credentials, project_id = _default._get_gae_credentials() assert credentials is None assert project_id is None os.environ[environment_vars.LEGACY_APPENGINE_RUNTIME] = "python27" credentials, project_id = _default._get_gae_credentials() assert credentials is None assert project_id is None @mock.patch.dict(os.environ) @mock.patch.object(app_engine, "app_identity", new=None) def test__get_gae_credentials_no_apis(): # test both with and without LEGACY_APPENGINE_RUNTIME setting assert environment_vars.LEGACY_APPENGINE_RUNTIME not in os.environ credentials, project_id = _default._get_gae_credentials() assert credentials is None assert project_id is None os.environ[environment_vars.LEGACY_APPENGINE_RUNTIME] = "python27" credentials, project_id = _default._get_gae_credentials() assert credentials is None assert project_id is None @mock.patch( "google.auth.compute_engine._metadata.is_on_gce", return_value=True, autospec=True ) @mock.patch( "google.auth.compute_engine._metadata.get_project_id", return_value="example-project", autospec=True, ) def test__get_gce_credentials(unused_get, unused_ping): credentials, project_id = _default._get_gce_credentials() assert isinstance(credentials, compute_engine.Credentials) assert project_id == "example-project" @mock.patch( "google.auth.compute_engine._metadata.is_on_gce", return_value=False, autospec=True ) def test__get_gce_credentials_no_ping(unused_ping): credentials, project_id = _default._get_gce_credentials() assert credentials is None assert project_id is None @mock.patch( "google.auth.compute_engine._metadata.is_on_gce", return_value=True, autospec=True ) @mock.patch( "google.auth.compute_engine._metadata.get_project_id", side_effect=exceptions.TransportError(), autospec=True, ) def test__get_gce_credentials_no_project_id(unused_get, unused_ping): credentials, project_id = _default._get_gce_credentials() assert isinstance(credentials, compute_engine.Credentials) assert project_id is None def test__get_gce_credentials_no_compute_engine(): import sys with mock.patch.dict("sys.modules"): sys.modules["google.auth.compute_engine"] = None credentials, project_id = _default._get_gce_credentials() assert credentials is None assert project_id is None @mock.patch( "google.auth.compute_engine._metadata.is_on_gce", return_value=False, autospec=True ) def test__get_gce_credentials_explicit_request(ping): _default._get_gce_credentials(mock.sentinel.request) ping.assert_called_with(request=mock.sentinel.request) @mock.patch( "google.auth._default._get_explicit_environ_credentials", return_value=(MOCK_CREDENTIALS, mock.sentinel.project_id), autospec=True, ) def test_default_early_out(unused_get): assert _default.default() == (MOCK_CREDENTIALS, mock.sentinel.project_id) @mock.patch( "google.auth._default._get_explicit_environ_credentials", 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_CREDENTIALS, "explicit-env") @mock.patch( "google.auth._default._get_explicit_environ_credentials", 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_CREDENTIALS, "explicit-env") @mock.patch("logging.Logger.warning", autospec=True) @mock.patch( "google.auth._default._get_explicit_environ_credentials", return_value=(MOCK_CREDENTIALS, None), autospec=True, ) @mock.patch( "google.auth._default._get_gcloud_sdk_credentials", return_value=(MOCK_CREDENTIALS, None), autospec=True, ) @mock.patch( "google.auth._default._get_gae_credentials", return_value=(MOCK_CREDENTIALS, None), autospec=True, ) @mock.patch( "google.auth._default._get_gce_credentials", 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_CREDENTIALS, None) logger_warning.assert_called_with(mock.ANY, mock.ANY, mock.ANY) @mock.patch( "google.auth._default._get_explicit_environ_credentials", return_value=(None, None), autospec=True, ) @mock.patch( "google.auth._default._get_gcloud_sdk_credentials", return_value=(None, None), autospec=True, ) @mock.patch( "google.auth._default._get_gae_credentials", return_value=(None, None), autospec=True, ) @mock.patch( "google.auth._default._get_gce_credentials", return_value=(None, None), autospec=True, ) def test_default_fail(unused_gce, unused_gae, unused_sdk, unused_explicit): with pytest.raises(exceptions.DefaultCredentialsError) as excinfo: assert _default.default() assert excinfo.match(_default._CLOUD_SDK_MISSING_CREDENTIALS) @mock.patch( "google.auth._default._get_explicit_environ_credentials", return_value=(MOCK_CREDENTIALS, mock.sentinel.project_id), autospec=True, ) @mock.patch( "google.auth.credentials.with_scopes_if_required", return_value=MOCK_CREDENTIALS, autospec=True, ) def test_default_scoped(with_scopes, unused_get): scopes = ["one", "two"] credentials, project_id = _default.default(scopes=scopes) assert credentials == with_scopes.return_value assert project_id == mock.sentinel.project_id with_scopes.assert_called_once_with(MOCK_CREDENTIALS, scopes, default_scopes=None) @mock.patch( "google.auth._default._get_explicit_environ_credentials", return_value=(MOCK_CREDENTIALS, mock.sentinel.project_id), autospec=True, ) def test_default_quota_project(with_quota_project): credentials, project_id = _default.default(quota_project_id="project-foo") MOCK_CREDENTIALS.with_quota_project.assert_called_once_with("project-foo") assert project_id == mock.sentinel.project_id @mock.patch( "google.auth._default._get_explicit_environ_credentials", return_value=(MOCK_CREDENTIALS, mock.sentinel.project_id), autospec=True, ) def test_default_no_app_engine_compute_engine_module(unused_get): """ google.auth.compute_engine and google.auth.app_engine are both optional to allow not including them when using this package. This verifies that default fails gracefully if these modules are absent """ import sys with mock.patch.dict("sys.modules"): sys.modules["google.auth.compute_engine"] = None sys.modules["google.auth.app_engine"] = None assert _default.default() == (MOCK_CREDENTIALS, mock.sentinel.project_id) @EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH def test_default_environ_external_credentials_identity_pool( get_project_id, monkeypatch, tmpdir ): config_file = tmpdir.join("config.json") config_file.write(json.dumps(IDENTITY_POOL_DATA)) monkeypatch.setenv(environment_vars.CREDENTIALS, str(config_file)) credentials, project_id = _default.default() assert isinstance(credentials, identity_pool.Credentials) assert not credentials.is_user assert not credentials.is_workforce_pool # Without scopes, project ID cannot be determined. assert project_id is None @EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH def test_default_environ_external_credentials_identity_pool_impersonated( get_project_id, monkeypatch, tmpdir ): config_file = tmpdir.join("config.json") config_file.write(json.dumps(IMPERSONATED_IDENTITY_POOL_DATA)) monkeypatch.setenv(environment_vars.CREDENTIALS, str(config_file)) credentials, project_id = _default.default( scopes=["https://www.google.com/calendar/feeds"] ) assert isinstance(credentials, identity_pool.Credentials) assert not credentials.is_user assert not credentials.is_workforce_pool assert project_id is mock.sentinel.project_id assert credentials.scopes == ["https://www.google.com/calendar/feeds"] # The credential.get_project_id should have been used in _get_external_account_credentials and default assert get_project_id.call_count == 2 @EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH @mock.patch.dict(os.environ) def test_default_environ_external_credentials_project_from_env( get_project_id, monkeypatch, tmpdir ): project_from_env = "project_from_env" os.environ[environment_vars.PROJECT] = project_from_env config_file = tmpdir.join("config.json") config_file.write(json.dumps(IMPERSONATED_IDENTITY_POOL_DATA)) monkeypatch.setenv(environment_vars.CREDENTIALS, str(config_file)) credentials, project_id = _default.default( scopes=["https://www.google.com/calendar/feeds"] ) assert isinstance(credentials, identity_pool.Credentials) assert not credentials.is_user assert not credentials.is_workforce_pool assert project_id == project_from_env assert credentials.scopes == ["https://www.google.com/calendar/feeds"] # The credential.get_project_id should have been used only in _get_external_account_credentials assert get_project_id.call_count == 1 @EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH @mock.patch.dict(os.environ) def test_default_environ_external_credentials_legacy_project_from_env( get_project_id, monkeypatch, tmpdir ): project_from_env = "project_from_env" os.environ[environment_vars.LEGACY_PROJECT] = project_from_env config_file = tmpdir.join("config.json") config_file.write(json.dumps(IMPERSONATED_IDENTITY_POOL_DATA)) monkeypatch.setenv(environment_vars.CREDENTIALS, str(config_file)) credentials, project_id = _default.default( scopes=["https://www.google.com/calendar/feeds"] ) assert isinstance(credentials, identity_pool.Credentials) assert not credentials.is_user assert not credentials.is_workforce_pool assert project_id == project_from_env assert credentials.scopes == ["https://www.google.com/calendar/feeds"] # The credential.get_project_id should have been used only in _get_external_account_credentials assert get_project_id.call_count == 1 @EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH def test_default_environ_external_credentials_aws_impersonated( get_project_id, monkeypatch, tmpdir ): config_file = tmpdir.join("config.json") config_file.write(json.dumps(IMPERSONATED_AWS_DATA)) monkeypatch.setenv(environment_vars.CREDENTIALS, str(config_file)) credentials, project_id = _default.default( scopes=["https://www.google.com/calendar/feeds"] ) assert isinstance(credentials, aws.Credentials) assert not credentials.is_user assert not credentials.is_workforce_pool assert project_id is mock.sentinel.project_id assert credentials.scopes == ["https://www.google.com/calendar/feeds"] @EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH def test_default_environ_external_credentials_workforce( get_project_id, monkeypatch, tmpdir ): config_file = tmpdir.join("config.json") config_file.write(json.dumps(IDENTITY_POOL_WORKFORCE_DATA)) monkeypatch.setenv(environment_vars.CREDENTIALS, str(config_file)) credentials, project_id = _default.default( scopes=["https://www.google.com/calendar/feeds"] ) assert isinstance(credentials, identity_pool.Credentials) assert credentials.is_user assert credentials.is_workforce_pool assert project_id is mock.sentinel.project_id assert credentials.scopes == ["https://www.google.com/calendar/feeds"] @EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH def test_default_environ_external_credentials_workforce_impersonated( get_project_id, monkeypatch, tmpdir ): config_file = tmpdir.join("config.json") config_file.write(json.dumps(IMPERSONATED_IDENTITY_POOL_WORKFORCE_DATA)) monkeypatch.setenv(environment_vars.CREDENTIALS, str(config_file)) credentials, project_id = _default.default( scopes=["https://www.google.com/calendar/feeds"] ) assert isinstance(credentials, identity_pool.Credentials) assert not credentials.is_user assert credentials.is_workforce_pool assert project_id is mock.sentinel.project_id assert credentials.scopes == ["https://www.google.com/calendar/feeds"] @EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH def test_default_environ_external_credentials_with_user_and_default_scopes_and_quota_project_id( get_project_id, monkeypatch, tmpdir ): config_file = tmpdir.join("config.json") config_file.write(json.dumps(IDENTITY_POOL_DATA)) monkeypatch.setenv(environment_vars.CREDENTIALS, str(config_file)) credentials, project_id = _default.default( scopes=["https://www.google.com/calendar/feeds"], default_scopes=["https://www.googleapis.com/auth/cloud-platform"], quota_project_id="project-foo", ) assert isinstance(credentials, identity_pool.Credentials) assert project_id is mock.sentinel.project_id assert credentials.quota_project_id == "project-foo" assert credentials.scopes == ["https://www.google.com/calendar/feeds"] assert credentials.default_scopes == [ "https://www.googleapis.com/auth/cloud-platform" ] @EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH def test_default_environ_external_credentials_explicit_request_with_scopes( get_project_id, monkeypatch, tmpdir ): config_file = tmpdir.join("config.json") config_file.write(json.dumps(IDENTITY_POOL_DATA)) monkeypatch.setenv(environment_vars.CREDENTIALS, str(config_file)) credentials, project_id = _default.default( request=mock.sentinel.request, scopes=["https://www.googleapis.com/auth/cloud-platform"], ) assert isinstance(credentials, identity_pool.Credentials) assert project_id is mock.sentinel.project_id # default() will initialize new credentials via with_scopes_if_required # and potentially with_quota_project. # As a result the caller of get_project_id() will not match the returned # credentials. get_project_id.assert_called_with(mock.ANY, request=mock.sentinel.request) def test_default_environ_external_credentials_bad_format(monkeypatch, tmpdir): filename = tmpdir.join("external_account_bad.json") filename.write(json.dumps({"type": "external_account"})) monkeypatch.setenv(environment_vars.CREDENTIALS, str(filename)) with pytest.raises(exceptions.DefaultCredentialsError) as excinfo: _default.default() assert excinfo.match( "Failed to load external account credentials from {}".format(str(filename)) ) @mock.patch( "google.auth._cloud_sdk.get_application_default_credentials_path", autospec=True ) def test_default_warning_without_quota_project_id_for_user_creds(get_adc_path): get_adc_path.return_value = AUTHORIZED_USER_CLOUD_SDK_FILE with pytest.warns(UserWarning, match=_default._CLOUD_SDK_CREDENTIALS_WARNING): credentials, project_id = _default.default(quota_project_id=None) @mock.patch( "google.auth._cloud_sdk.get_application_default_credentials_path", autospec=True ) def test_default_no_warning_with_quota_project_id_for_user_creds(get_adc_path): get_adc_path.return_value = AUTHORIZED_USER_CLOUD_SDK_FILE credentials, project_id = _default.default(quota_project_id="project-foo") @mock.patch( "google.auth._cloud_sdk.get_application_default_credentials_path", autospec=True ) def test_default_impersonated_service_account(get_adc_path): get_adc_path.return_value = IMPERSONATED_SERVICE_ACCOUNT_AUTHORIZED_USER_SOURCE_FILE credentials, _ = _default.default() assert isinstance(credentials, impersonated_credentials.Credentials) assert isinstance( credentials._source_credentials, google.oauth2.credentials.Credentials ) assert credentials.service_account_email == "service-account-target@example.com" assert credentials._delegates == ["service-account-delegate@example.com"] assert not credentials._quota_project_id assert not credentials._target_scopes @mock.patch( "google.auth._cloud_sdk.get_application_default_credentials_path", autospec=True ) def test_default_impersonated_service_account_set_scopes(get_adc_path): get_adc_path.return_value = IMPERSONATED_SERVICE_ACCOUNT_AUTHORIZED_USER_SOURCE_FILE scopes = ["scope1", "scope2"] credentials, _ = _default.default(scopes=scopes) assert credentials._target_scopes == scopes @mock.patch( "google.auth._cloud_sdk.get_application_default_credentials_path", autospec=True ) def test_default_impersonated_service_account_set_default_scopes(get_adc_path): get_adc_path.return_value = IMPERSONATED_SERVICE_ACCOUNT_AUTHORIZED_USER_SOURCE_FILE default_scopes = ["scope1", "scope2"] credentials, _ = _default.default(default_scopes=default_scopes) assert credentials._target_scopes == default_scopes @mock.patch( "google.auth._cloud_sdk.get_application_default_credentials_path", autospec=True ) def test_default_impersonated_service_account_set_both_scopes_and_default_scopes( get_adc_path ): get_adc_path.return_value = IMPERSONATED_SERVICE_ACCOUNT_AUTHORIZED_USER_SOURCE_FILE scopes = ["scope1", "scope2"] default_scopes = ["scope3", "scope4"] credentials, _ = _default.default(scopes=scopes, default_scopes=default_scopes) assert credentials._target_scopes == scopes @EXTERNAL_ACCOUNT_GET_PROJECT_ID_PATCH def test_load_credentials_from_external_account_pluggable(get_project_id, tmpdir): config_file = tmpdir.join("config.json") config_file.write(json.dumps(PLUGGABLE_DATA)) credentials, project_id = _default.load_credentials_from_file(str(config_file)) assert isinstance(credentials, pluggable.Credentials) # Since no scopes are specified, the project ID cannot be determined. assert project_id is None assert get_project_id.called @mock.patch( "google.auth._cloud_sdk.get_application_default_credentials_path", autospec=True ) def test_default_gdch_service_account_credentials(get_adc_path): get_adc_path.return_value = GDCH_SERVICE_ACCOUNT_FILE creds, project = _default.default(quota_project_id="project-foo") assert isinstance(creds, gdch_credentials.ServiceAccountCredentials) assert creds._service_identity_name == "service_identity_name" assert creds._audience is None assert creds._token_uri == "https://service-identity.<Domain>/authenticate" assert creds._ca_cert_path == "/path/to/ca/cert" assert project == "project_foo" @mock.patch.dict(os.environ) @mock.patch( "google.auth._cloud_sdk.get_application_default_credentials_path", autospec=True ) def test_quota_project_from_environment(get_adc_path): get_adc_path.return_value = AUTHORIZED_USER_CLOUD_SDK_WITH_QUOTA_PROJECT_ID_FILE credentials, _ = _default.default(quota_project_id=None) assert credentials.quota_project_id == "quota_project_id" quota_from_env = "quota_from_env" os.environ[environment_vars.GOOGLE_CLOUD_QUOTA_PROJECT] = quota_from_env credentials, _ = _default.default(quota_project_id=None) assert credentials.quota_project_id == quota_from_env explicit_quota = "explicit_quota" credentials, _ = _default.default(quota_project_id=explicit_quota) assert credentials.quota_project_id == explicit_quota @mock.patch( "google.auth.compute_engine._metadata.is_on_gce", return_value=True, autospec=True ) @mock.patch( "google.auth.compute_engine._metadata.get_project_id", return_value="example-project", autospec=True, ) @mock.patch.dict(os.environ) def test_quota_gce_credentials(unused_get, unused_ping): # No quota credentials, project_id = _default._get_gce_credentials() assert project_id == "example-project" assert credentials.quota_project_id is None # Quota from environment quota_from_env = "quota_from_env" os.environ[environment_vars.GOOGLE_CLOUD_QUOTA_PROJECT] = quota_from_env credentials, project_id = _default._get_gce_credentials() assert credentials.quota_project_id == quota_from_env # Explicit quota explicit_quota = "explicit_quota" credentials, project_id = _default._get_gce_credentials( quota_project_id=explicit_quota ) assert credentials.quota_project_id == explicit_quota oauth2/test_sts.py 0000644 00000043777 15025175543 0010221 0 ustar 00 # Copyright 2020 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import http.client as http_client import json import urllib import mock import pytest # type: ignore from google.auth import exceptions from google.auth import transport from google.oauth2 import sts from google.oauth2 import utils CLIENT_ID = "username" CLIENT_SECRET = "password" # Base64 encoding of "username:password" BASIC_AUTH_ENCODING = "dXNlcm5hbWU6cGFzc3dvcmQ=" class TestStsClient(object): GRANT_TYPE = "urn:ietf:params:oauth:grant-type:token-exchange" RESOURCE = "https://api.example.com/" AUDIENCE = "urn:example:cooperation-context" SCOPES = ["scope1", "scope2"] REQUESTED_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:access_token" SUBJECT_TOKEN = "HEADER.SUBJECT_TOKEN_PAYLOAD.SIGNATURE" SUBJECT_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:jwt" ACTOR_TOKEN = "HEADER.ACTOR_TOKEN_PAYLOAD.SIGNATURE" ACTOR_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:jwt" TOKEN_EXCHANGE_ENDPOINT = "https://example.com/token.oauth2" ADDON_HEADERS = {"x-client-version": "0.1.2"} ADDON_OPTIONS = {"additional": {"non-standard": ["options"], "other": "some-value"}} SUCCESS_RESPONSE = { "access_token": "ACCESS_TOKEN", "issued_token_type": "urn:ietf:params:oauth:token-type:access_token", "token_type": "Bearer", "expires_in": 3600, "scope": "scope1 scope2", } SUCCESS_RESPONSE_WITH_REFRESH = { "access_token": "abc", "refresh_token": "xyz", "expires_in": 3600, } ERROR_RESPONSE = { "error": "invalid_request", "error_description": "Invalid subject token", "error_uri": "https://tools.ietf.org/html/rfc6749", } CLIENT_AUTH_BASIC = utils.ClientAuthentication( utils.ClientAuthType.basic, CLIENT_ID, CLIENT_SECRET ) CLIENT_AUTH_REQUEST_BODY = utils.ClientAuthentication( utils.ClientAuthType.request_body, CLIENT_ID, CLIENT_SECRET ) @classmethod def make_client(cls, client_auth=None): return sts.Client(cls.TOKEN_EXCHANGE_ENDPOINT, client_auth) @classmethod def make_mock_request(cls, data, status=http_client.OK): response = mock.create_autospec(transport.Response, instance=True) response.status = status response.data = json.dumps(data).encode("utf-8") request = mock.create_autospec(transport.Request) request.return_value = response return request @classmethod def assert_request_kwargs(cls, request_kwargs, headers, request_data): """Asserts the request was called with the expected parameters. """ assert request_kwargs["url"] == cls.TOKEN_EXCHANGE_ENDPOINT assert request_kwargs["method"] == "POST" assert request_kwargs["headers"] == headers assert request_kwargs["body"] is not None body_tuples = urllib.parse.parse_qsl(request_kwargs["body"]) for (k, v) in body_tuples: assert v.decode("utf-8") == request_data[k.decode("utf-8")] assert len(body_tuples) == len(request_data.keys()) def test_exchange_token_full_success_without_auth(self): """Test token exchange success without client authentication using full parameters. """ client = self.make_client() headers = self.ADDON_HEADERS.copy() headers["Content-Type"] = "application/x-www-form-urlencoded" request_data = { "grant_type": self.GRANT_TYPE, "resource": self.RESOURCE, "audience": self.AUDIENCE, "scope": " ".join(self.SCOPES), "requested_token_type": self.REQUESTED_TOKEN_TYPE, "subject_token": self.SUBJECT_TOKEN, "subject_token_type": self.SUBJECT_TOKEN_TYPE, "actor_token": self.ACTOR_TOKEN, "actor_token_type": self.ACTOR_TOKEN_TYPE, "options": urllib.parse.quote(json.dumps(self.ADDON_OPTIONS)), } request = self.make_mock_request( status=http_client.OK, data=self.SUCCESS_RESPONSE ) response = client.exchange_token( request, self.GRANT_TYPE, self.SUBJECT_TOKEN, self.SUBJECT_TOKEN_TYPE, self.RESOURCE, self.AUDIENCE, self.SCOPES, self.REQUESTED_TOKEN_TYPE, self.ACTOR_TOKEN, self.ACTOR_TOKEN_TYPE, self.ADDON_OPTIONS, self.ADDON_HEADERS, ) self.assert_request_kwargs(request.call_args[1], headers, request_data) assert response == self.SUCCESS_RESPONSE def test_exchange_token_partial_success_without_auth(self): """Test token exchange success without client authentication using partial (required only) parameters. """ client = self.make_client() headers = {"Content-Type": "application/x-www-form-urlencoded"} request_data = { "grant_type": self.GRANT_TYPE, "audience": self.AUDIENCE, "requested_token_type": self.REQUESTED_TOKEN_TYPE, "subject_token": self.SUBJECT_TOKEN, "subject_token_type": self.SUBJECT_TOKEN_TYPE, } request = self.make_mock_request( status=http_client.OK, data=self.SUCCESS_RESPONSE ) response = client.exchange_token( request, grant_type=self.GRANT_TYPE, subject_token=self.SUBJECT_TOKEN, subject_token_type=self.SUBJECT_TOKEN_TYPE, audience=self.AUDIENCE, requested_token_type=self.REQUESTED_TOKEN_TYPE, ) self.assert_request_kwargs(request.call_args[1], headers, request_data) assert response == self.SUCCESS_RESPONSE def test_exchange_token_non200_without_auth(self): """Test token exchange without client auth responding with non-200 status. """ client = self.make_client() request = self.make_mock_request( status=http_client.BAD_REQUEST, data=self.ERROR_RESPONSE ) with pytest.raises(exceptions.OAuthError) as excinfo: client.exchange_token( request, self.GRANT_TYPE, self.SUBJECT_TOKEN, self.SUBJECT_TOKEN_TYPE, self.RESOURCE, self.AUDIENCE, self.SCOPES, self.REQUESTED_TOKEN_TYPE, self.ACTOR_TOKEN, self.ACTOR_TOKEN_TYPE, self.ADDON_OPTIONS, self.ADDON_HEADERS, ) assert excinfo.match( r"Error code invalid_request: Invalid subject token - https://tools.ietf.org/html/rfc6749" ) def test_exchange_token_full_success_with_basic_auth(self): """Test token exchange success with basic client authentication using full parameters. """ client = self.make_client(self.CLIENT_AUTH_BASIC) headers = self.ADDON_HEADERS.copy() headers["Content-Type"] = "application/x-www-form-urlencoded" headers["Authorization"] = "Basic {}".format(BASIC_AUTH_ENCODING) request_data = { "grant_type": self.GRANT_TYPE, "resource": self.RESOURCE, "audience": self.AUDIENCE, "scope": " ".join(self.SCOPES), "requested_token_type": self.REQUESTED_TOKEN_TYPE, "subject_token": self.SUBJECT_TOKEN, "subject_token_type": self.SUBJECT_TOKEN_TYPE, "actor_token": self.ACTOR_TOKEN, "actor_token_type": self.ACTOR_TOKEN_TYPE, "options": urllib.parse.quote(json.dumps(self.ADDON_OPTIONS)), } request = self.make_mock_request( status=http_client.OK, data=self.SUCCESS_RESPONSE ) response = client.exchange_token( request, self.GRANT_TYPE, self.SUBJECT_TOKEN, self.SUBJECT_TOKEN_TYPE, self.RESOURCE, self.AUDIENCE, self.SCOPES, self.REQUESTED_TOKEN_TYPE, self.ACTOR_TOKEN, self.ACTOR_TOKEN_TYPE, self.ADDON_OPTIONS, self.ADDON_HEADERS, ) self.assert_request_kwargs(request.call_args[1], headers, request_data) assert response == self.SUCCESS_RESPONSE def test_exchange_token_partial_success_with_basic_auth(self): """Test token exchange success with basic client authentication using partial (required only) parameters. """ client = self.make_client(self.CLIENT_AUTH_BASIC) headers = { "Content-Type": "application/x-www-form-urlencoded", "Authorization": "Basic {}".format(BASIC_AUTH_ENCODING), } request_data = { "grant_type": self.GRANT_TYPE, "audience": self.AUDIENCE, "requested_token_type": self.REQUESTED_TOKEN_TYPE, "subject_token": self.SUBJECT_TOKEN, "subject_token_type": self.SUBJECT_TOKEN_TYPE, } request = self.make_mock_request( status=http_client.OK, data=self.SUCCESS_RESPONSE ) response = client.exchange_token( request, grant_type=self.GRANT_TYPE, subject_token=self.SUBJECT_TOKEN, subject_token_type=self.SUBJECT_TOKEN_TYPE, audience=self.AUDIENCE, requested_token_type=self.REQUESTED_TOKEN_TYPE, ) self.assert_request_kwargs(request.call_args[1], headers, request_data) assert response == self.SUCCESS_RESPONSE def test_exchange_token_non200_with_basic_auth(self): """Test token exchange with basic client auth responding with non-200 status. """ client = self.make_client(self.CLIENT_AUTH_BASIC) request = self.make_mock_request( status=http_client.BAD_REQUEST, data=self.ERROR_RESPONSE ) with pytest.raises(exceptions.OAuthError) as excinfo: client.exchange_token( request, self.GRANT_TYPE, self.SUBJECT_TOKEN, self.SUBJECT_TOKEN_TYPE, self.RESOURCE, self.AUDIENCE, self.SCOPES, self.REQUESTED_TOKEN_TYPE, self.ACTOR_TOKEN, self.ACTOR_TOKEN_TYPE, self.ADDON_OPTIONS, self.ADDON_HEADERS, ) assert excinfo.match( r"Error code invalid_request: Invalid subject token - https://tools.ietf.org/html/rfc6749" ) def test_exchange_token_full_success_with_reqbody_auth(self): """Test token exchange success with request body client authenticaiton using full parameters. """ client = self.make_client(self.CLIENT_AUTH_REQUEST_BODY) headers = self.ADDON_HEADERS.copy() headers["Content-Type"] = "application/x-www-form-urlencoded" request_data = { "grant_type": self.GRANT_TYPE, "resource": self.RESOURCE, "audience": self.AUDIENCE, "scope": " ".join(self.SCOPES), "requested_token_type": self.REQUESTED_TOKEN_TYPE, "subject_token": self.SUBJECT_TOKEN, "subject_token_type": self.SUBJECT_TOKEN_TYPE, "actor_token": self.ACTOR_TOKEN, "actor_token_type": self.ACTOR_TOKEN_TYPE, "options": urllib.parse.quote(json.dumps(self.ADDON_OPTIONS)), "client_id": CLIENT_ID, "client_secret": CLIENT_SECRET, } request = self.make_mock_request( status=http_client.OK, data=self.SUCCESS_RESPONSE ) response = client.exchange_token( request, self.GRANT_TYPE, self.SUBJECT_TOKEN, self.SUBJECT_TOKEN_TYPE, self.RESOURCE, self.AUDIENCE, self.SCOPES, self.REQUESTED_TOKEN_TYPE, self.ACTOR_TOKEN, self.ACTOR_TOKEN_TYPE, self.ADDON_OPTIONS, self.ADDON_HEADERS, ) self.assert_request_kwargs(request.call_args[1], headers, request_data) assert response == self.SUCCESS_RESPONSE def test_exchange_token_partial_success_with_reqbody_auth(self): """Test token exchange success with request body client authentication using partial (required only) parameters. """ client = self.make_client(self.CLIENT_AUTH_REQUEST_BODY) headers = {"Content-Type": "application/x-www-form-urlencoded"} request_data = { "grant_type": self.GRANT_TYPE, "audience": self.AUDIENCE, "requested_token_type": self.REQUESTED_TOKEN_TYPE, "subject_token": self.SUBJECT_TOKEN, "subject_token_type": self.SUBJECT_TOKEN_TYPE, "client_id": CLIENT_ID, "client_secret": CLIENT_SECRET, } request = self.make_mock_request( status=http_client.OK, data=self.SUCCESS_RESPONSE ) response = client.exchange_token( request, grant_type=self.GRANT_TYPE, subject_token=self.SUBJECT_TOKEN, subject_token_type=self.SUBJECT_TOKEN_TYPE, audience=self.AUDIENCE, requested_token_type=self.REQUESTED_TOKEN_TYPE, ) self.assert_request_kwargs(request.call_args[1], headers, request_data) assert response == self.SUCCESS_RESPONSE def test_exchange_token_non200_with_reqbody_auth(self): """Test token exchange with POST request body client auth responding with non-200 status. """ client = self.make_client(self.CLIENT_AUTH_REQUEST_BODY) request = self.make_mock_request( status=http_client.BAD_REQUEST, data=self.ERROR_RESPONSE ) with pytest.raises(exceptions.OAuthError) as excinfo: client.exchange_token( request, self.GRANT_TYPE, self.SUBJECT_TOKEN, self.SUBJECT_TOKEN_TYPE, self.RESOURCE, self.AUDIENCE, self.SCOPES, self.REQUESTED_TOKEN_TYPE, self.ACTOR_TOKEN, self.ACTOR_TOKEN_TYPE, self.ADDON_OPTIONS, self.ADDON_HEADERS, ) assert excinfo.match( r"Error code invalid_request: Invalid subject token - https://tools.ietf.org/html/rfc6749" ) def test_refresh_token_success(self): """Test refresh token with successful response.""" client = self.make_client(self.CLIENT_AUTH_BASIC) request = self.make_mock_request( status=http_client.OK, data=self.SUCCESS_RESPONSE ) response = client.refresh_token(request, "refreshtoken") headers = { "Authorization": "Basic dXNlcm5hbWU6cGFzc3dvcmQ=", "Content-Type": "application/x-www-form-urlencoded", } request_data = {"grant_type": "refresh_token", "refresh_token": "refreshtoken"} self.assert_request_kwargs(request.call_args[1], headers, request_data) assert response == self.SUCCESS_RESPONSE def test_refresh_token_success_with_refresh(self): """Test refresh token with successful response.""" client = self.make_client(self.CLIENT_AUTH_BASIC) request = self.make_mock_request( status=http_client.OK, data=self.SUCCESS_RESPONSE_WITH_REFRESH ) response = client.refresh_token(request, "refreshtoken") headers = { "Authorization": "Basic dXNlcm5hbWU6cGFzc3dvcmQ=", "Content-Type": "application/x-www-form-urlencoded", } request_data = {"grant_type": "refresh_token", "refresh_token": "refreshtoken"} self.assert_request_kwargs(request.call_args[1], headers, request_data) assert response == self.SUCCESS_RESPONSE_WITH_REFRESH def test_refresh_token_failure(self): """Test refresh token with failure response.""" client = self.make_client(self.CLIENT_AUTH_BASIC) request = self.make_mock_request( status=http_client.BAD_REQUEST, data=self.ERROR_RESPONSE ) with pytest.raises(exceptions.OAuthError) as excinfo: client.refresh_token(request, "refreshtoken") assert excinfo.match( r"Error code invalid_request: Invalid subject token - https://tools.ietf.org/html/rfc6749" ) def test__make_request_success(self): """Test base method with successful response.""" client = self.make_client(self.CLIENT_AUTH_BASIC) request = self.make_mock_request( status=http_client.OK, data=self.SUCCESS_RESPONSE ) response = client._make_request(request, {"a": "b"}, {"c": "d"}) headers = { "Authorization": "Basic dXNlcm5hbWU6cGFzc3dvcmQ=", "Content-Type": "application/x-www-form-urlencoded", "a": "b", } request_data = {"c": "d"} self.assert_request_kwargs(request.call_args[1], headers, request_data) assert response == self.SUCCESS_RESPONSE def test_make_request_failure(self): """Test refresh token with failure response.""" client = self.make_client(self.CLIENT_AUTH_BASIC) request = self.make_mock_request( status=http_client.BAD_REQUEST, data=self.ERROR_RESPONSE ) with pytest.raises(exceptions.OAuthError) as excinfo: client._make_request(request, {"a": "b"}, {"c": "d"}) assert excinfo.match( r"Error code invalid_request: Invalid subject token - https://tools.ietf.org/html/rfc6749" ) oauth2/test_reauth.py 0000644 00000033345 15025175543 0010666 0 ustar 00 # Copyright 2021 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import copy import mock import pytest # type: ignore from google.auth import exceptions from google.oauth2 import reauth MOCK_REQUEST = mock.Mock() CHALLENGES_RESPONSE_TEMPLATE = { "status": "CHALLENGE_REQUIRED", "sessionId": "123", "challenges": [ { "status": "READY", "challengeId": 1, "challengeType": "PASSWORD", "securityKey": {}, } ], } CHALLENGES_RESPONSE_AUTHENTICATED = { "status": "AUTHENTICATED", "sessionId": "123", "encodedProofOfReauthToken": "new_rapt_token", } REAUTH_START_METRICS_HEADER_VALUE = "gl-python/3.7 auth/1.1 auth-request-type/re-start" REAUTH_CONTINUE_METRICS_HEADER_VALUE = ( "gl-python/3.7 auth/1.1 auth-request-type/re-cont" ) TOKEN_REQUEST_METRICS_HEADER_VALUE = "gl-python/3.7 auth/1.1 cred-type/u" class MockChallenge(object): def __init__(self, name, locally_eligible, challenge_input): self.name = name self.is_locally_eligible = locally_eligible self.challenge_input = challenge_input def obtain_challenge_input(self, metadata): return self.challenge_input def test_is_interactive(): with mock.patch("sys.stdin.isatty", return_value=True): assert reauth.is_interactive() @mock.patch( "google.auth.metrics.reauth_start", return_value=REAUTH_START_METRICS_HEADER_VALUE ) def test__get_challenges(mock_metrics_header_value): with mock.patch( "google.oauth2._client._token_endpoint_request" ) as mock_token_endpoint_request: reauth._get_challenges(MOCK_REQUEST, ["SAML"], "token") mock_token_endpoint_request.assert_called_with( MOCK_REQUEST, reauth._REAUTH_API + ":start", {"supportedChallengeTypes": ["SAML"]}, access_token="token", use_json=True, headers={"x-goog-api-client": REAUTH_START_METRICS_HEADER_VALUE}, ) @mock.patch( "google.auth.metrics.reauth_start", return_value=REAUTH_START_METRICS_HEADER_VALUE ) def test__get_challenges_with_scopes(mock_metrics_header_value): with mock.patch( "google.oauth2._client._token_endpoint_request" ) as mock_token_endpoint_request: reauth._get_challenges( MOCK_REQUEST, ["SAML"], "token", requested_scopes=["scope"] ) mock_token_endpoint_request.assert_called_with( MOCK_REQUEST, reauth._REAUTH_API + ":start", { "supportedChallengeTypes": ["SAML"], "oauthScopesForDomainPolicyLookup": ["scope"], }, access_token="token", use_json=True, headers={"x-goog-api-client": REAUTH_START_METRICS_HEADER_VALUE}, ) @mock.patch( "google.auth.metrics.reauth_continue", return_value=REAUTH_CONTINUE_METRICS_HEADER_VALUE, ) def test__send_challenge_result(mock_metrics_header_value): with mock.patch( "google.oauth2._client._token_endpoint_request" ) as mock_token_endpoint_request: reauth._send_challenge_result( MOCK_REQUEST, "123", "1", {"credential": "password"}, "token" ) mock_token_endpoint_request.assert_called_with( MOCK_REQUEST, reauth._REAUTH_API + "/123:continue", { "sessionId": "123", "challengeId": "1", "action": "RESPOND", "proposalResponse": {"credential": "password"}, }, access_token="token", use_json=True, headers={"x-goog-api-client": REAUTH_CONTINUE_METRICS_HEADER_VALUE}, ) def test__run_next_challenge_not_ready(): challenges_response = copy.deepcopy(CHALLENGES_RESPONSE_TEMPLATE) challenges_response["challenges"][0]["status"] = "STATUS_UNSPECIFIED" assert ( reauth._run_next_challenge(challenges_response, MOCK_REQUEST, "token") is None ) def test__run_next_challenge_not_supported(): challenges_response = copy.deepcopy(CHALLENGES_RESPONSE_TEMPLATE) challenges_response["challenges"][0]["challengeType"] = "CHALLENGE_TYPE_UNSPECIFIED" with pytest.raises(exceptions.ReauthFailError) as excinfo: reauth._run_next_challenge(challenges_response, MOCK_REQUEST, "token") assert excinfo.match(r"Unsupported challenge type CHALLENGE_TYPE_UNSPECIFIED") def test__run_next_challenge_not_locally_eligible(): mock_challenge = MockChallenge("PASSWORD", False, "challenge_input") with mock.patch( "google.oauth2.challenges.AVAILABLE_CHALLENGES", {"PASSWORD": mock_challenge} ): with pytest.raises(exceptions.ReauthFailError) as excinfo: reauth._run_next_challenge( CHALLENGES_RESPONSE_TEMPLATE, MOCK_REQUEST, "token" ) assert excinfo.match(r"Challenge PASSWORD is not locally eligible") def test__run_next_challenge_no_challenge_input(): mock_challenge = MockChallenge("PASSWORD", True, None) with mock.patch( "google.oauth2.challenges.AVAILABLE_CHALLENGES", {"PASSWORD": mock_challenge} ): assert ( reauth._run_next_challenge( CHALLENGES_RESPONSE_TEMPLATE, MOCK_REQUEST, "token" ) is None ) def test__run_next_challenge_success(): mock_challenge = MockChallenge("PASSWORD", True, {"credential": "password"}) with mock.patch( "google.oauth2.challenges.AVAILABLE_CHALLENGES", {"PASSWORD": mock_challenge} ): with mock.patch( "google.oauth2.reauth._send_challenge_result" ) as mock_send_challenge_result: reauth._run_next_challenge( CHALLENGES_RESPONSE_TEMPLATE, MOCK_REQUEST, "token" ) mock_send_challenge_result.assert_called_with( MOCK_REQUEST, "123", 1, {"credential": "password"}, "token" ) def test__obtain_rapt_authenticated(): with mock.patch( "google.oauth2.reauth._get_challenges", return_value=CHALLENGES_RESPONSE_AUTHENTICATED, ): assert reauth._obtain_rapt(MOCK_REQUEST, "token", None) == "new_rapt_token" def test__obtain_rapt_authenticated_after_run_next_challenge(): with mock.patch( "google.oauth2.reauth._get_challenges", return_value=CHALLENGES_RESPONSE_TEMPLATE, ): with mock.patch( "google.oauth2.reauth._run_next_challenge", side_effect=[ CHALLENGES_RESPONSE_TEMPLATE, CHALLENGES_RESPONSE_AUTHENTICATED, ], ): with mock.patch("google.oauth2.reauth.is_interactive", return_value=True): assert ( reauth._obtain_rapt(MOCK_REQUEST, "token", None) == "new_rapt_token" ) def test__obtain_rapt_unsupported_status(): challenges_response = copy.deepcopy(CHALLENGES_RESPONSE_TEMPLATE) challenges_response["status"] = "STATUS_UNSPECIFIED" with mock.patch( "google.oauth2.reauth._get_challenges", return_value=challenges_response ): with pytest.raises(exceptions.ReauthFailError) as excinfo: reauth._obtain_rapt(MOCK_REQUEST, "token", None) assert excinfo.match(r"API error: STATUS_UNSPECIFIED") def test__obtain_rapt_no_challenge_output(): challenges_response = copy.deepcopy(CHALLENGES_RESPONSE_TEMPLATE) with mock.patch( "google.oauth2.reauth._get_challenges", return_value=challenges_response ): with mock.patch("google.oauth2.reauth.is_interactive", return_value=True): with mock.patch( "google.oauth2.reauth._run_next_challenge", return_value=None ): with pytest.raises(exceptions.ReauthFailError) as excinfo: reauth._obtain_rapt(MOCK_REQUEST, "token", None) assert excinfo.match(r"Failed to obtain rapt token") def test__obtain_rapt_not_interactive(): with mock.patch( "google.oauth2.reauth._get_challenges", return_value=CHALLENGES_RESPONSE_TEMPLATE, ): with mock.patch("google.oauth2.reauth.is_interactive", return_value=False): with pytest.raises(exceptions.ReauthFailError) as excinfo: reauth._obtain_rapt(MOCK_REQUEST, "token", None) assert excinfo.match(r"not in an interactive session") def test__obtain_rapt_not_authenticated(): with mock.patch( "google.oauth2.reauth._get_challenges", return_value=CHALLENGES_RESPONSE_TEMPLATE, ): with mock.patch("google.oauth2.reauth.RUN_CHALLENGE_RETRY_LIMIT", 0): with pytest.raises(exceptions.ReauthFailError) as excinfo: reauth._obtain_rapt(MOCK_REQUEST, "token", None) assert excinfo.match(r"Reauthentication failed") def test_get_rapt_token(): with mock.patch( "google.oauth2._client.refresh_grant", return_value=("token", None, None, None) ) as mock_refresh_grant: with mock.patch( "google.oauth2.reauth._obtain_rapt", return_value="new_rapt_token" ) as mock_obtain_rapt: assert ( reauth.get_rapt_token( MOCK_REQUEST, "client_id", "client_secret", "refresh_token", "token_uri", ) == "new_rapt_token" ) mock_refresh_grant.assert_called_with( request=MOCK_REQUEST, client_id="client_id", client_secret="client_secret", refresh_token="refresh_token", token_uri="token_uri", scopes=[reauth._REAUTH_SCOPE], ) mock_obtain_rapt.assert_called_with( MOCK_REQUEST, "token", requested_scopes=None ) @mock.patch( "google.auth.metrics.token_request_user", return_value=TOKEN_REQUEST_METRICS_HEADER_VALUE, ) def test_refresh_grant_failed(mock_metrics_header_value): with mock.patch( "google.oauth2._client._token_endpoint_request_no_throw" ) as mock_token_request: mock_token_request.return_value = (False, {"error": "Bad request"}, False) with pytest.raises(exceptions.RefreshError) as excinfo: reauth.refresh_grant( MOCK_REQUEST, "token_uri", "refresh_token", "client_id", "client_secret", scopes=["foo", "bar"], rapt_token="rapt_token", enable_reauth_refresh=True, ) assert excinfo.match(r"Bad request") assert not excinfo.value.retryable mock_token_request.assert_called_with( MOCK_REQUEST, "token_uri", { "grant_type": "refresh_token", "client_id": "client_id", "client_secret": "client_secret", "refresh_token": "refresh_token", "scope": "foo bar", "rapt": "rapt_token", }, headers={"x-goog-api-client": TOKEN_REQUEST_METRICS_HEADER_VALUE}, ) def test_refresh_grant_failed_with_string_type_response(): with mock.patch( "google.oauth2._client._token_endpoint_request_no_throw" ) as mock_token_request: mock_token_request.return_value = (False, "string type error", False) with pytest.raises(exceptions.RefreshError) as excinfo: reauth.refresh_grant( MOCK_REQUEST, "token_uri", "refresh_token", "client_id", "client_secret", scopes=["foo", "bar"], rapt_token="rapt_token", enable_reauth_refresh=True, ) assert excinfo.match(r"string type error") assert not excinfo.value.retryable def test_refresh_grant_success(): with mock.patch( "google.oauth2._client._token_endpoint_request_no_throw" ) as mock_token_request: mock_token_request.side_effect = [ (False, {"error": "invalid_grant", "error_subtype": "rapt_required"}, True), (True, {"access_token": "access_token"}, None), ] with mock.patch( "google.oauth2.reauth.get_rapt_token", return_value="new_rapt_token" ): assert reauth.refresh_grant( MOCK_REQUEST, "token_uri", "refresh_token", "client_id", "client_secret", enable_reauth_refresh=True, ) == ( "access_token", "refresh_token", None, {"access_token": "access_token"}, "new_rapt_token", ) def test_refresh_grant_reauth_refresh_disabled(): with mock.patch( "google.oauth2._client._token_endpoint_request_no_throw" ) as mock_token_request: mock_token_request.side_effect = [ (False, {"error": "invalid_grant", "error_subtype": "rapt_required"}, True), (True, {"access_token": "access_token"}, None), ] with pytest.raises(exceptions.RefreshError) as excinfo: reauth.refresh_grant( MOCK_REQUEST, "token_uri", "refresh_token", "client_id", "client_secret" ) assert excinfo.match(r"Reauthentication is needed") oauth2/test_credentials.py 0000644 00000106131 15025175543 0011665 0 ustar 00 # Copyright 2016 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import datetime import json import os import pickle import sys import mock import pytest # type: ignore from google.auth import _helpers from google.auth import exceptions from google.auth import transport from google.oauth2 import credentials DATA_DIR = os.path.join(os.path.dirname(__file__), "..", "data") AUTH_USER_JSON_FILE = os.path.join(DATA_DIR, "authorized_user.json") with open(AUTH_USER_JSON_FILE, "r") as fh: AUTH_USER_INFO = json.load(fh) class TestCredentials(object): TOKEN_URI = "https://example.com/oauth2/token" REFRESH_TOKEN = "refresh_token" RAPT_TOKEN = "rapt_token" CLIENT_ID = "client_id" CLIENT_SECRET = "client_secret" @classmethod def make_credentials(cls): return credentials.Credentials( token=None, refresh_token=cls.REFRESH_TOKEN, token_uri=cls.TOKEN_URI, client_id=cls.CLIENT_ID, client_secret=cls.CLIENT_SECRET, rapt_token=cls.RAPT_TOKEN, enable_reauth_refresh=True, ) def test_default_state(self): credentials = self.make_credentials() assert not credentials.valid # Expiration hasn't been set yet assert not credentials.expired # Scopes aren't required for these credentials assert not credentials.requires_scopes # Test properties assert credentials.refresh_token == self.REFRESH_TOKEN assert credentials.token_uri == self.TOKEN_URI assert credentials.client_id == self.CLIENT_ID assert credentials.client_secret == self.CLIENT_SECRET assert credentials.rapt_token == self.RAPT_TOKEN assert credentials.refresh_handler is None def test_token_usage_metrics(self): credentials = self.make_credentials() credentials.token = "token" credentials.expiry = None headers = {} credentials.before_request(mock.Mock(), None, None, headers) assert headers["authorization"] == "Bearer token" assert headers["x-goog-api-client"] == "cred-type/u" def test_refresh_handler_setter_and_getter(self): scopes = ["email", "profile"] original_refresh_handler = mock.Mock(return_value=("ACCESS_TOKEN_1", None)) updated_refresh_handler = mock.Mock(return_value=("ACCESS_TOKEN_2", None)) creds = credentials.Credentials( token=None, refresh_token=None, token_uri=None, client_id=None, client_secret=None, rapt_token=None, scopes=scopes, default_scopes=None, refresh_handler=original_refresh_handler, ) assert creds.refresh_handler is original_refresh_handler creds.refresh_handler = updated_refresh_handler assert creds.refresh_handler is updated_refresh_handler creds.refresh_handler = None assert creds.refresh_handler is None def test_invalid_refresh_handler(self): scopes = ["email", "profile"] with pytest.raises(TypeError) as excinfo: credentials.Credentials( token=None, refresh_token=None, token_uri=None, client_id=None, client_secret=None, rapt_token=None, scopes=scopes, default_scopes=None, refresh_handler=object(), ) assert excinfo.match("The provided refresh_handler is not a callable or None.") @mock.patch("google.oauth2.reauth.refresh_grant", autospec=True) @mock.patch( "google.auth._helpers.utcnow", return_value=datetime.datetime.min + _helpers.REFRESH_THRESHOLD, ) def test_refresh_success(self, unused_utcnow, refresh_grant): token = "token" new_rapt_token = "new_rapt_token" expiry = _helpers.utcnow() + datetime.timedelta(seconds=500) grant_response = {"id_token": mock.sentinel.id_token} refresh_grant.return_value = ( # Access token token, # New refresh token None, # Expiry, expiry, # Extra data grant_response, # rapt_token new_rapt_token, ) request = mock.create_autospec(transport.Request) credentials = self.make_credentials() # Refresh credentials credentials.refresh(request) # Check jwt grant call. refresh_grant.assert_called_with( request, self.TOKEN_URI, self.REFRESH_TOKEN, self.CLIENT_ID, self.CLIENT_SECRET, None, self.RAPT_TOKEN, True, ) # Check that the credentials have the token and expiry assert credentials.token == token assert credentials.expiry == expiry assert credentials.id_token == mock.sentinel.id_token assert credentials.rapt_token == new_rapt_token # Check that the credentials are valid (have a token and are not # expired) assert credentials.valid def test_refresh_no_refresh_token(self): request = mock.create_autospec(transport.Request) credentials_ = credentials.Credentials(token=None, refresh_token=None) with pytest.raises(exceptions.RefreshError, match="necessary fields"): credentials_.refresh(request) request.assert_not_called() @mock.patch("google.oauth2.reauth.refresh_grant", autospec=True) @mock.patch( "google.auth._helpers.utcnow", return_value=datetime.datetime.min + _helpers.REFRESH_THRESHOLD, ) def test_refresh_with_refresh_token_and_refresh_handler( self, unused_utcnow, refresh_grant ): token = "token" new_rapt_token = "new_rapt_token" expiry = _helpers.utcnow() + datetime.timedelta(seconds=500) grant_response = {"id_token": mock.sentinel.id_token} refresh_grant.return_value = ( # Access token token, # New refresh token None, # Expiry, expiry, # Extra data grant_response, # rapt_token new_rapt_token, ) refresh_handler = mock.Mock() request = mock.create_autospec(transport.Request) creds = credentials.Credentials( token=None, refresh_token=self.REFRESH_TOKEN, token_uri=self.TOKEN_URI, client_id=self.CLIENT_ID, client_secret=self.CLIENT_SECRET, rapt_token=self.RAPT_TOKEN, refresh_handler=refresh_handler, ) # Refresh credentials creds.refresh(request) # Check jwt grant call. refresh_grant.assert_called_with( request, self.TOKEN_URI, self.REFRESH_TOKEN, self.CLIENT_ID, self.CLIENT_SECRET, None, self.RAPT_TOKEN, False, ) # Check that the credentials have the token and expiry assert creds.token == token assert creds.expiry == expiry assert creds.id_token == mock.sentinel.id_token assert creds.rapt_token == new_rapt_token # Check that the credentials are valid (have a token and are not # expired) assert creds.valid # Assert refresh handler not called as the refresh token has # higher priority. refresh_handler.assert_not_called() @mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min) def test_refresh_with_refresh_handler_success_scopes(self, unused_utcnow): expected_expiry = datetime.datetime.min + datetime.timedelta(seconds=2800) refresh_handler = mock.Mock(return_value=("ACCESS_TOKEN", expected_expiry)) scopes = ["email", "profile"] default_scopes = ["https://www.googleapis.com/auth/cloud-platform"] request = mock.create_autospec(transport.Request) creds = credentials.Credentials( token=None, refresh_token=None, token_uri=None, client_id=None, client_secret=None, rapt_token=None, scopes=scopes, default_scopes=default_scopes, refresh_handler=refresh_handler, ) creds.refresh(request) assert creds.token == "ACCESS_TOKEN" assert creds.expiry == expected_expiry assert creds.valid assert not creds.expired # Confirm refresh handler called with the expected arguments. refresh_handler.assert_called_with(request, scopes=scopes) @mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min) def test_refresh_with_refresh_handler_success_default_scopes(self, unused_utcnow): expected_expiry = datetime.datetime.min + datetime.timedelta(seconds=2800) original_refresh_handler = mock.Mock( return_value=("UNUSED_TOKEN", expected_expiry) ) refresh_handler = mock.Mock(return_value=("ACCESS_TOKEN", expected_expiry)) default_scopes = ["https://www.googleapis.com/auth/cloud-platform"] request = mock.create_autospec(transport.Request) creds = credentials.Credentials( token=None, refresh_token=None, token_uri=None, client_id=None, client_secret=None, rapt_token=None, scopes=None, default_scopes=default_scopes, refresh_handler=original_refresh_handler, ) # Test newly set refresh_handler is used instead of the original one. creds.refresh_handler = refresh_handler creds.refresh(request) assert creds.token == "ACCESS_TOKEN" assert creds.expiry == expected_expiry assert creds.valid assert not creds.expired # default_scopes should be used since no developer provided scopes # are provided. refresh_handler.assert_called_with(request, scopes=default_scopes) @mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min) def test_refresh_with_refresh_handler_invalid_token(self, unused_utcnow): expected_expiry = datetime.datetime.min + datetime.timedelta(seconds=2800) # Simulate refresh handler does not return a valid token. refresh_handler = mock.Mock(return_value=(None, expected_expiry)) scopes = ["email", "profile"] default_scopes = ["https://www.googleapis.com/auth/cloud-platform"] request = mock.create_autospec(transport.Request) creds = credentials.Credentials( token=None, refresh_token=None, token_uri=None, client_id=None, client_secret=None, rapt_token=None, scopes=scopes, default_scopes=default_scopes, refresh_handler=refresh_handler, ) with pytest.raises( exceptions.RefreshError, match="returned token is not a string" ): creds.refresh(request) assert creds.token is None assert creds.expiry is None assert not creds.valid # Confirm refresh handler called with the expected arguments. refresh_handler.assert_called_with(request, scopes=scopes) def test_refresh_with_refresh_handler_invalid_expiry(self): # Simulate refresh handler returns expiration time in an invalid unit. refresh_handler = mock.Mock(return_value=("TOKEN", 2800)) scopes = ["email", "profile"] default_scopes = ["https://www.googleapis.com/auth/cloud-platform"] request = mock.create_autospec(transport.Request) creds = credentials.Credentials( token=None, refresh_token=None, token_uri=None, client_id=None, client_secret=None, rapt_token=None, scopes=scopes, default_scopes=default_scopes, refresh_handler=refresh_handler, ) with pytest.raises( exceptions.RefreshError, match="returned expiry is not a datetime object" ): creds.refresh(request) assert creds.token is None assert creds.expiry is None assert not creds.valid # Confirm refresh handler called with the expected arguments. refresh_handler.assert_called_with(request, scopes=scopes) @mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min) def test_refresh_with_refresh_handler_expired_token(self, unused_utcnow): expected_expiry = datetime.datetime.min + _helpers.REFRESH_THRESHOLD # Simulate refresh handler returns an expired token. refresh_handler = mock.Mock(return_value=("TOKEN", expected_expiry)) scopes = ["email", "profile"] default_scopes = ["https://www.googleapis.com/auth/cloud-platform"] request = mock.create_autospec(transport.Request) creds = credentials.Credentials( token=None, refresh_token=None, token_uri=None, client_id=None, client_secret=None, rapt_token=None, scopes=scopes, default_scopes=default_scopes, refresh_handler=refresh_handler, ) with pytest.raises(exceptions.RefreshError, match="already expired"): creds.refresh(request) assert creds.token is None assert creds.expiry is None assert not creds.valid # Confirm refresh handler called with the expected arguments. refresh_handler.assert_called_with(request, scopes=scopes) @mock.patch("google.oauth2.reauth.refresh_grant", autospec=True) @mock.patch( "google.auth._helpers.utcnow", return_value=datetime.datetime.min + _helpers.REFRESH_THRESHOLD, ) def test_credentials_with_scopes_requested_refresh_success( self, unused_utcnow, refresh_grant ): scopes = ["email", "profile"] default_scopes = ["https://www.googleapis.com/auth/cloud-platform"] token = "token" new_rapt_token = "new_rapt_token" expiry = _helpers.utcnow() + datetime.timedelta(seconds=500) grant_response = {"id_token": mock.sentinel.id_token, "scope": "email profile"} refresh_grant.return_value = ( # Access token token, # New refresh token None, # Expiry, expiry, # Extra data grant_response, # rapt token new_rapt_token, ) request = mock.create_autospec(transport.Request) creds = credentials.Credentials( token=None, refresh_token=self.REFRESH_TOKEN, token_uri=self.TOKEN_URI, client_id=self.CLIENT_ID, client_secret=self.CLIENT_SECRET, scopes=scopes, default_scopes=default_scopes, rapt_token=self.RAPT_TOKEN, enable_reauth_refresh=True, ) # Refresh credentials creds.refresh(request) # Check jwt grant call. refresh_grant.assert_called_with( request, self.TOKEN_URI, self.REFRESH_TOKEN, self.CLIENT_ID, self.CLIENT_SECRET, scopes, self.RAPT_TOKEN, True, ) # Check that the credentials have the token and expiry assert creds.token == token assert creds.expiry == expiry assert creds.id_token == mock.sentinel.id_token assert creds.has_scopes(scopes) assert creds.rapt_token == new_rapt_token assert creds.granted_scopes == scopes # Check that the credentials are valid (have a token and are not # expired.) assert creds.valid @mock.patch("google.oauth2.reauth.refresh_grant", autospec=True) @mock.patch( "google.auth._helpers.utcnow", return_value=datetime.datetime.min + _helpers.REFRESH_THRESHOLD, ) def test_credentials_with_only_default_scopes_requested( self, unused_utcnow, refresh_grant ): default_scopes = ["email", "profile"] token = "token" new_rapt_token = "new_rapt_token" expiry = _helpers.utcnow() + datetime.timedelta(seconds=500) grant_response = {"id_token": mock.sentinel.id_token, "scope": "email profile"} refresh_grant.return_value = ( # Access token token, # New refresh token None, # Expiry, expiry, # Extra data grant_response, # rapt token new_rapt_token, ) request = mock.create_autospec(transport.Request) creds = credentials.Credentials( token=None, refresh_token=self.REFRESH_TOKEN, token_uri=self.TOKEN_URI, client_id=self.CLIENT_ID, client_secret=self.CLIENT_SECRET, default_scopes=default_scopes, rapt_token=self.RAPT_TOKEN, enable_reauth_refresh=True, ) # Refresh credentials creds.refresh(request) # Check jwt grant call. refresh_grant.assert_called_with( request, self.TOKEN_URI, self.REFRESH_TOKEN, self.CLIENT_ID, self.CLIENT_SECRET, default_scopes, self.RAPT_TOKEN, True, ) # Check that the credentials have the token and expiry assert creds.token == token assert creds.expiry == expiry assert creds.id_token == mock.sentinel.id_token assert creds.has_scopes(default_scopes) assert creds.rapt_token == new_rapt_token assert creds.granted_scopes == default_scopes # Check that the credentials are valid (have a token and are not # expired.) assert creds.valid @mock.patch("google.oauth2.reauth.refresh_grant", autospec=True) @mock.patch( "google.auth._helpers.utcnow", return_value=datetime.datetime.min + _helpers.REFRESH_THRESHOLD, ) def test_credentials_with_scopes_returned_refresh_success( self, unused_utcnow, refresh_grant ): scopes = ["email", "profile"] token = "token" new_rapt_token = "new_rapt_token" expiry = _helpers.utcnow() + datetime.timedelta(seconds=500) grant_response = {"id_token": mock.sentinel.id_token, "scope": " ".join(scopes)} refresh_grant.return_value = ( # Access token token, # New refresh token None, # Expiry, expiry, # Extra data grant_response, # rapt token new_rapt_token, ) request = mock.create_autospec(transport.Request) creds = credentials.Credentials( token=None, refresh_token=self.REFRESH_TOKEN, token_uri=self.TOKEN_URI, client_id=self.CLIENT_ID, client_secret=self.CLIENT_SECRET, scopes=scopes, rapt_token=self.RAPT_TOKEN, enable_reauth_refresh=True, ) # Refresh credentials creds.refresh(request) # Check jwt grant call. refresh_grant.assert_called_with( request, self.TOKEN_URI, self.REFRESH_TOKEN, self.CLIENT_ID, self.CLIENT_SECRET, scopes, self.RAPT_TOKEN, True, ) # Check that the credentials have the token and expiry assert creds.token == token assert creds.expiry == expiry assert creds.id_token == mock.sentinel.id_token assert creds.has_scopes(scopes) assert creds.rapt_token == new_rapt_token assert creds.granted_scopes == scopes # Check that the credentials are valid (have a token and are not # expired.) assert creds.valid @mock.patch("google.oauth2.reauth.refresh_grant", autospec=True) @mock.patch( "google.auth._helpers.utcnow", return_value=datetime.datetime.min + _helpers.REFRESH_THRESHOLD, ) def test_credentials_with_only_default_scopes_requested_different_granted_scopes( self, unused_utcnow, refresh_grant ): default_scopes = ["email", "profile"] token = "token" new_rapt_token = "new_rapt_token" expiry = _helpers.utcnow() + datetime.timedelta(seconds=500) grant_response = {"id_token": mock.sentinel.id_token, "scope": "email"} refresh_grant.return_value = ( # Access token token, # New refresh token None, # Expiry, expiry, # Extra data grant_response, # rapt token new_rapt_token, ) request = mock.create_autospec(transport.Request) creds = credentials.Credentials( token=None, refresh_token=self.REFRESH_TOKEN, token_uri=self.TOKEN_URI, client_id=self.CLIENT_ID, client_secret=self.CLIENT_SECRET, default_scopes=default_scopes, rapt_token=self.RAPT_TOKEN, enable_reauth_refresh=True, ) # Refresh credentials creds.refresh(request) # Check jwt grant call. refresh_grant.assert_called_with( request, self.TOKEN_URI, self.REFRESH_TOKEN, self.CLIENT_ID, self.CLIENT_SECRET, default_scopes, self.RAPT_TOKEN, True, ) # Check that the credentials have the token and expiry assert creds.token == token assert creds.expiry == expiry assert creds.id_token == mock.sentinel.id_token assert creds.has_scopes(default_scopes) assert creds.rapt_token == new_rapt_token assert creds.granted_scopes == ["email"] # Check that the credentials are valid (have a token and are not # expired.) assert creds.valid @mock.patch("google.oauth2.reauth.refresh_grant", autospec=True) @mock.patch( "google.auth._helpers.utcnow", return_value=datetime.datetime.min + _helpers.REFRESH_THRESHOLD, ) def test_credentials_with_scopes_refresh_different_granted_scopes( self, unused_utcnow, refresh_grant ): scopes = ["email", "profile"] scopes_returned = ["email"] token = "token" new_rapt_token = "new_rapt_token" expiry = _helpers.utcnow() + datetime.timedelta(seconds=500) grant_response = { "id_token": mock.sentinel.id_token, "scope": " ".join(scopes_returned), } refresh_grant.return_value = ( # Access token token, # New refresh token None, # Expiry, expiry, # Extra data grant_response, # rapt token new_rapt_token, ) request = mock.create_autospec(transport.Request) creds = credentials.Credentials( token=None, refresh_token=self.REFRESH_TOKEN, token_uri=self.TOKEN_URI, client_id=self.CLIENT_ID, client_secret=self.CLIENT_SECRET, scopes=scopes, rapt_token=self.RAPT_TOKEN, enable_reauth_refresh=True, ) # Refresh credentials creds.refresh(request) # Check jwt grant call. refresh_grant.assert_called_with( request, self.TOKEN_URI, self.REFRESH_TOKEN, self.CLIENT_ID, self.CLIENT_SECRET, scopes, self.RAPT_TOKEN, True, ) # Check that the credentials have the token and expiry assert creds.token == token assert creds.expiry == expiry assert creds.id_token == mock.sentinel.id_token assert creds.has_scopes(scopes) assert creds.rapt_token == new_rapt_token assert creds.granted_scopes == scopes_returned # Check that the credentials are valid (have a token and are not # expired.) assert creds.valid def test_apply_with_quota_project_id(self): creds = credentials.Credentials( token="token", refresh_token=self.REFRESH_TOKEN, token_uri=self.TOKEN_URI, client_id=self.CLIENT_ID, client_secret=self.CLIENT_SECRET, quota_project_id="quota-project-123", ) headers = {} creds.apply(headers) assert headers["x-goog-user-project"] == "quota-project-123" assert "token" in headers["authorization"] def test_apply_with_no_quota_project_id(self): creds = credentials.Credentials( token="token", refresh_token=self.REFRESH_TOKEN, token_uri=self.TOKEN_URI, client_id=self.CLIENT_ID, client_secret=self.CLIENT_SECRET, ) headers = {} creds.apply(headers) assert "x-goog-user-project" not in headers assert "token" in headers["authorization"] def test_with_quota_project(self): creds = credentials.Credentials( token="token", refresh_token=self.REFRESH_TOKEN, token_uri=self.TOKEN_URI, client_id=self.CLIENT_ID, client_secret=self.CLIENT_SECRET, quota_project_id="quota-project-123", ) new_creds = creds.with_quota_project("new-project-456") assert new_creds.quota_project_id == "new-project-456" headers = {} 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() creds = credentials.Credentials.from_authorized_user_info(info) assert creds.client_secret == info["client_secret"] assert creds.client_id == info["client_id"] assert creds.refresh_token == info["refresh_token"] assert creds.token_uri == credentials._GOOGLE_OAUTH2_TOKEN_ENDPOINT assert creds.scopes is None scopes = ["email", "profile"] creds = credentials.Credentials.from_authorized_user_info(info, scopes) assert creds.client_secret == info["client_secret"] assert creds.client_id == info["client_id"] assert creds.refresh_token == info["refresh_token"] assert creds.token_uri == credentials._GOOGLE_OAUTH2_TOKEN_ENDPOINT assert creds.scopes == scopes info["scopes"] = "email" # single non-array scope from file creds = credentials.Credentials.from_authorized_user_info(info) assert creds.scopes == [info["scopes"]] info["scopes"] = ["email", "profile"] # array scope from file creds = credentials.Credentials.from_authorized_user_info(info) assert creds.scopes == info["scopes"] expiry = datetime.datetime(2020, 8, 14, 15, 54, 1) info["expiry"] = expiry.isoformat() + "Z" creds = credentials.Credentials.from_authorized_user_info(info) assert creds.expiry == expiry assert creds.expired def test_from_authorized_user_file(self): info = AUTH_USER_INFO.copy() creds = credentials.Credentials.from_authorized_user_file(AUTH_USER_JSON_FILE) assert creds.client_secret == info["client_secret"] assert creds.client_id == info["client_id"] assert creds.refresh_token == info["refresh_token"] assert creds.token_uri == credentials._GOOGLE_OAUTH2_TOKEN_ENDPOINT assert creds.scopes is None assert creds.rapt_token is None scopes = ["email", "profile"] creds = credentials.Credentials.from_authorized_user_file( AUTH_USER_JSON_FILE, scopes ) assert creds.client_secret == info["client_secret"] assert creds.client_id == info["client_id"] assert creds.refresh_token == info["refresh_token"] assert creds.token_uri == credentials._GOOGLE_OAUTH2_TOKEN_ENDPOINT assert creds.scopes == scopes def test_from_authorized_user_file_with_rapt_token(self): info = AUTH_USER_INFO.copy() file_path = os.path.join(DATA_DIR, "authorized_user_with_rapt_token.json") creds = credentials.Credentials.from_authorized_user_file(file_path) assert creds.client_secret == info["client_secret"] assert creds.client_id == info["client_id"] assert creds.refresh_token == info["refresh_token"] assert creds.token_uri == credentials._GOOGLE_OAUTH2_TOKEN_ENDPOINT assert creds.scopes is None assert creds.rapt_token == "rapt" def test_to_json(self): info = AUTH_USER_INFO.copy() expiry = datetime.datetime(2020, 8, 14, 15, 54, 1) info["expiry"] = expiry.isoformat() + "Z" creds = credentials.Credentials.from_authorized_user_info(info) assert creds.expiry == expiry # Test with no `strip` arg json_output = creds.to_json() json_asdict = json.loads(json_output) assert json_asdict.get("token") == creds.token assert json_asdict.get("refresh_token") == creds.refresh_token assert json_asdict.get("token_uri") == creds.token_uri assert json_asdict.get("client_id") == creds.client_id assert json_asdict.get("scopes") == creds.scopes assert json_asdict.get("client_secret") == creds.client_secret assert json_asdict.get("expiry") == info["expiry"] # Test with a `strip` arg json_output = creds.to_json(strip=["client_secret"]) json_asdict = json.loads(json_output) assert json_asdict.get("token") == creds.token assert json_asdict.get("refresh_token") == creds.refresh_token assert json_asdict.get("token_uri") == creds.token_uri assert json_asdict.get("client_id") == creds.client_id assert json_asdict.get("scopes") == creds.scopes assert json_asdict.get("client_secret") is None # Test with no expiry creds.expiry = None json_output = creds.to_json() json_asdict = json.loads(json_output) assert json_asdict.get("expiry") is None def test_pickle_and_unpickle(self): creds = self.make_credentials() unpickled = pickle.loads(pickle.dumps(creds)) # make sure attributes aren't lost during pickling assert list(creds.__dict__).sort() == list(unpickled.__dict__).sort() for attr in list(creds.__dict__): assert getattr(creds, attr) == getattr(unpickled, attr) def test_pickle_and_unpickle_with_refresh_handler(self): expected_expiry = _helpers.utcnow() + datetime.timedelta(seconds=2800) refresh_handler = mock.Mock(return_value=("TOKEN", expected_expiry)) creds = credentials.Credentials( token=None, refresh_token=None, token_uri=None, client_id=None, client_secret=None, rapt_token=None, refresh_handler=refresh_handler, ) unpickled = pickle.loads(pickle.dumps(creds)) # make sure attributes aren't lost during pickling assert list(creds.__dict__).sort() == list(unpickled.__dict__).sort() for attr in list(creds.__dict__): # For the _refresh_handler property, the unpickled creds should be # set to None. if attr == "_refresh_handler": assert getattr(unpickled, attr) is None else: assert getattr(creds, attr) == getattr(unpickled, attr) def test_pickle_with_missing_attribute(self): creds = self.make_credentials() # remove an optional attribute before pickling # this mimics a pickle created with a previous class definition with # fewer attributes del creds.__dict__["_quota_project_id"] unpickled = pickle.loads(pickle.dumps(creds)) # Attribute should be initialized by `__setstate__` assert unpickled.quota_project_id is None # pickles are not compatible across versions @pytest.mark.skipif( sys.version_info < (3, 5), reason="pickle file can only be loaded with Python >= 3.5", ) def test_unpickle_old_credentials_pickle(self): # make sure a credentials file pickled with an older # library version (google-auth==1.5.1) can be unpickled with open( os.path.join(DATA_DIR, "old_oauth_credentials_py3.pickle"), "rb" ) as f: credentials = pickle.load(f) assert credentials.quota_project_id is None class TestUserAccessTokenCredentials(object): def test_instance(self): with pytest.warns( UserWarning, match="UserAccessTokenCredentials is deprecated" ): cred = credentials.UserAccessTokenCredentials() assert cred._account is None cred = cred.with_account("account") assert cred._account == "account" @mock.patch("google.auth._cloud_sdk.get_auth_access_token", autospec=True) def test_refresh(self, get_auth_access_token): with pytest.warns( UserWarning, match="UserAccessTokenCredentials is deprecated" ): get_auth_access_token.return_value = "access_token" cred = credentials.UserAccessTokenCredentials() cred.refresh(None) assert cred.token == "access_token" def test_with_quota_project(self): with pytest.warns( UserWarning, match="UserAccessTokenCredentials is deprecated" ): 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 ) @mock.patch( "google.oauth2.credentials.UserAccessTokenCredentials.refresh", autospec=True ) def test_before_request(self, refresh, apply): with pytest.warns( UserWarning, match="UserAccessTokenCredentials is deprecated" ): cred = credentials.UserAccessTokenCredentials() cred.before_request(mock.Mock(), "GET", "https://example.com", {}) refresh.assert_called() apply.assert_called() oauth2/test_utils.py 0000644 00000022275 15025175543 0010536 0 ustar 00 # Copyright 2020 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import json import pytest # type: ignore from google.auth import exceptions from google.oauth2 import utils CLIENT_ID = "username" CLIENT_SECRET = "password" # Base64 encoding of "username:password" BASIC_AUTH_ENCODING = "dXNlcm5hbWU6cGFzc3dvcmQ=" # Base64 encoding of "username:" BASIC_AUTH_ENCODING_SECRETLESS = "dXNlcm5hbWU6" class AuthHandler(utils.OAuthClientAuthHandler): def __init__(self, client_auth=None): super(AuthHandler, self).__init__(client_auth) def apply_client_authentication_options( self, headers, request_body=None, bearer_token=None ): return super(AuthHandler, self).apply_client_authentication_options( headers, request_body, bearer_token ) class TestClientAuthentication(object): @classmethod def make_client_auth(cls, client_secret=None): return utils.ClientAuthentication( utils.ClientAuthType.basic, CLIENT_ID, client_secret ) def test_initialization_with_client_secret(self): client_auth = self.make_client_auth(CLIENT_SECRET) assert client_auth.client_auth_type == utils.ClientAuthType.basic assert client_auth.client_id == CLIENT_ID assert client_auth.client_secret == CLIENT_SECRET def test_initialization_no_client_secret(self): client_auth = self.make_client_auth() assert client_auth.client_auth_type == utils.ClientAuthType.basic assert client_auth.client_id == CLIENT_ID assert client_auth.client_secret is None class TestOAuthClientAuthHandler(object): CLIENT_AUTH_BASIC = utils.ClientAuthentication( utils.ClientAuthType.basic, CLIENT_ID, CLIENT_SECRET ) CLIENT_AUTH_BASIC_SECRETLESS = utils.ClientAuthentication( utils.ClientAuthType.basic, CLIENT_ID ) CLIENT_AUTH_REQUEST_BODY = utils.ClientAuthentication( utils.ClientAuthType.request_body, CLIENT_ID, CLIENT_SECRET ) CLIENT_AUTH_REQUEST_BODY_SECRETLESS = utils.ClientAuthentication( utils.ClientAuthType.request_body, CLIENT_ID ) @classmethod def make_oauth_client_auth_handler(cls, client_auth=None): return AuthHandler(client_auth) def test_apply_client_authentication_options_none(self): headers = {"Content-Type": "application/json"} request_body = {"foo": "bar"} auth_handler = self.make_oauth_client_auth_handler() auth_handler.apply_client_authentication_options(headers, request_body) assert headers == {"Content-Type": "application/json"} assert request_body == {"foo": "bar"} def test_apply_client_authentication_options_basic(self): headers = {"Content-Type": "application/json"} request_body = {"foo": "bar"} auth_handler = self.make_oauth_client_auth_handler(self.CLIENT_AUTH_BASIC) auth_handler.apply_client_authentication_options(headers, request_body) assert headers == { "Content-Type": "application/json", "Authorization": "Basic {}".format(BASIC_AUTH_ENCODING), } assert request_body == {"foo": "bar"} def test_apply_client_authentication_options_basic_nosecret(self): headers = {"Content-Type": "application/json"} request_body = {"foo": "bar"} auth_handler = self.make_oauth_client_auth_handler( self.CLIENT_AUTH_BASIC_SECRETLESS ) auth_handler.apply_client_authentication_options(headers, request_body) assert headers == { "Content-Type": "application/json", "Authorization": "Basic {}".format(BASIC_AUTH_ENCODING_SECRETLESS), } assert request_body == {"foo": "bar"} def test_apply_client_authentication_options_request_body(self): headers = {"Content-Type": "application/json"} request_body = {"foo": "bar"} auth_handler = self.make_oauth_client_auth_handler( self.CLIENT_AUTH_REQUEST_BODY ) auth_handler.apply_client_authentication_options(headers, request_body) assert headers == {"Content-Type": "application/json"} assert request_body == { "foo": "bar", "client_id": CLIENT_ID, "client_secret": CLIENT_SECRET, } def test_apply_client_authentication_options_request_body_nosecret(self): headers = {"Content-Type": "application/json"} request_body = {"foo": "bar"} auth_handler = self.make_oauth_client_auth_handler( self.CLIENT_AUTH_REQUEST_BODY_SECRETLESS ) auth_handler.apply_client_authentication_options(headers, request_body) assert headers == {"Content-Type": "application/json"} assert request_body == { "foo": "bar", "client_id": CLIENT_ID, "client_secret": "", } def test_apply_client_authentication_options_request_body_no_body(self): headers = {"Content-Type": "application/json"} auth_handler = self.make_oauth_client_auth_handler( self.CLIENT_AUTH_REQUEST_BODY ) with pytest.raises(exceptions.OAuthError) as excinfo: auth_handler.apply_client_authentication_options(headers) assert excinfo.match(r"HTTP request does not support request-body") def test_apply_client_authentication_options_bearer_token(self): bearer_token = "ACCESS_TOKEN" headers = {"Content-Type": "application/json"} request_body = {"foo": "bar"} auth_handler = self.make_oauth_client_auth_handler() auth_handler.apply_client_authentication_options( headers, request_body, bearer_token ) assert headers == { "Content-Type": "application/json", "Authorization": "Bearer {}".format(bearer_token), } assert request_body == {"foo": "bar"} def test_apply_client_authentication_options_bearer_and_basic(self): bearer_token = "ACCESS_TOKEN" headers = {"Content-Type": "application/json"} request_body = {"foo": "bar"} auth_handler = self.make_oauth_client_auth_handler(self.CLIENT_AUTH_BASIC) auth_handler.apply_client_authentication_options( headers, request_body, bearer_token ) # Bearer token should have higher priority. assert headers == { "Content-Type": "application/json", "Authorization": "Bearer {}".format(bearer_token), } assert request_body == {"foo": "bar"} def test_apply_client_authentication_options_bearer_and_request_body(self): bearer_token = "ACCESS_TOKEN" headers = {"Content-Type": "application/json"} request_body = {"foo": "bar"} auth_handler = self.make_oauth_client_auth_handler( self.CLIENT_AUTH_REQUEST_BODY ) auth_handler.apply_client_authentication_options( headers, request_body, bearer_token ) # Bearer token should have higher priority. assert headers == { "Content-Type": "application/json", "Authorization": "Bearer {}".format(bearer_token), } assert request_body == {"foo": "bar"} def test__handle_error_response_code_only(): error_resp = {"error": "unsupported_grant_type"} response_data = json.dumps(error_resp) with pytest.raises(exceptions.OAuthError) as excinfo: utils.handle_error_response(response_data) assert excinfo.match(r"Error code unsupported_grant_type") def test__handle_error_response_code_description(): error_resp = { "error": "unsupported_grant_type", "error_description": "The provided grant_type is unsupported", } response_data = json.dumps(error_resp) with pytest.raises(exceptions.OAuthError) as excinfo: utils.handle_error_response(response_data) assert excinfo.match( r"Error code unsupported_grant_type: The provided grant_type is unsupported" ) def test__handle_error_response_code_description_uri(): error_resp = { "error": "unsupported_grant_type", "error_description": "The provided grant_type is unsupported", "error_uri": "https://tools.ietf.org/html/rfc6749", } response_data = json.dumps(error_resp) with pytest.raises(exceptions.OAuthError) as excinfo: utils.handle_error_response(response_data) assert excinfo.match( r"Error code unsupported_grant_type: The provided grant_type is unsupported - https://tools.ietf.org/html/rfc6749" ) def test__handle_error_response_non_json(): response_data = "Oops, something wrong happened" with pytest.raises(exceptions.OAuthError) as excinfo: utils.handle_error_response(response_data) assert excinfo.match(r"Oops, something wrong happened") oauth2/test_challenges.py 0000644 00000017654 15025175543 0011510 0 ustar 00 # Copyright 2021 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Tests for the reauth module.""" import base64 import sys import mock import pytest # type: ignore import pyu2f # type: ignore from google.auth import exceptions from google.oauth2 import challenges def test_get_user_password(): with mock.patch("getpass.getpass", return_value="foo"): assert challenges.get_user_password("") == "foo" def test_security_key(): metadata = { "status": "READY", "challengeId": 2, "challengeType": "SECURITY_KEY", "securityKey": { "applicationId": "security_key_application_id", "challenges": [ { "keyHandle": "some_key", "challenge": base64.urlsafe_b64encode( "some_challenge".encode("ascii") ).decode("ascii"), } ], "relyingPartyId": "security_key_application_id", }, } mock_key = mock.Mock() challenge = challenges.SecurityKeyChallenge() # Test the case that security key challenge is passed with applicationId and # relyingPartyId the same. with mock.patch("pyu2f.model.RegisteredKey", return_value=mock_key): with mock.patch( "pyu2f.convenience.authenticator.CompositeAuthenticator.Authenticate" ) as mock_authenticate: mock_authenticate.return_value = "security key response" assert challenge.name == "SECURITY_KEY" assert challenge.is_locally_eligible assert challenge.obtain_challenge_input(metadata) == { "securityKey": "security key response" } mock_authenticate.assert_called_with( "security_key_application_id", [{"key": mock_key, "challenge": b"some_challenge"}], print_callback=sys.stderr.write, ) # Test the case that security key challenge is passed with applicationId and # relyingPartyId different, first call works. metadata["securityKey"]["relyingPartyId"] = "security_key_relying_party_id" sys.stderr.write("metadata=" + str(metadata) + "\n") with mock.patch("pyu2f.model.RegisteredKey", return_value=mock_key): with mock.patch( "pyu2f.convenience.authenticator.CompositeAuthenticator.Authenticate" ) as mock_authenticate: mock_authenticate.return_value = "security key response" assert challenge.name == "SECURITY_KEY" assert challenge.is_locally_eligible assert challenge.obtain_challenge_input(metadata) == { "securityKey": "security key response" } mock_authenticate.assert_called_with( "security_key_relying_party_id", [{"key": mock_key, "challenge": b"some_challenge"}], print_callback=sys.stderr.write, ) # Test the case that security key challenge is passed with applicationId and # relyingPartyId different, first call fails, requires retry. metadata["securityKey"]["relyingPartyId"] = "security_key_relying_party_id" with mock.patch("pyu2f.model.RegisteredKey", return_value=mock_key): with mock.patch( "pyu2f.convenience.authenticator.CompositeAuthenticator.Authenticate" ) as mock_authenticate: assert challenge.name == "SECURITY_KEY" assert challenge.is_locally_eligible mock_authenticate.side_effect = [ pyu2f.errors.U2FError(pyu2f.errors.U2FError.DEVICE_INELIGIBLE), "security key response", ] assert challenge.obtain_challenge_input(metadata) == { "securityKey": "security key response" } calls = [ mock.call( "security_key_relying_party_id", [{"key": mock_key, "challenge": b"some_challenge"}], print_callback=sys.stderr.write, ), mock.call( "security_key_application_id", [{"key": mock_key, "challenge": b"some_challenge"}], print_callback=sys.stderr.write, ), ] mock_authenticate.assert_has_calls(calls) # Test various types of exceptions. with mock.patch("pyu2f.model.RegisteredKey", return_value=mock_key): with mock.patch( "pyu2f.convenience.authenticator.CompositeAuthenticator.Authenticate" ) as mock_authenticate: mock_authenticate.side_effect = pyu2f.errors.U2FError( pyu2f.errors.U2FError.DEVICE_INELIGIBLE ) assert challenge.obtain_challenge_input(metadata) is None with mock.patch( "pyu2f.convenience.authenticator.CompositeAuthenticator.Authenticate" ) as mock_authenticate: mock_authenticate.side_effect = pyu2f.errors.U2FError( pyu2f.errors.U2FError.TIMEOUT ) assert challenge.obtain_challenge_input(metadata) is None with mock.patch( "pyu2f.convenience.authenticator.CompositeAuthenticator.Authenticate" ) as mock_authenticate: mock_authenticate.side_effect = pyu2f.errors.PluginError() assert challenge.obtain_challenge_input(metadata) is None with mock.patch( "pyu2f.convenience.authenticator.CompositeAuthenticator.Authenticate" ) as mock_authenticate: mock_authenticate.side_effect = pyu2f.errors.U2FError( pyu2f.errors.U2FError.BAD_REQUEST ) with pytest.raises(pyu2f.errors.U2FError): challenge.obtain_challenge_input(metadata) with mock.patch( "pyu2f.convenience.authenticator.CompositeAuthenticator.Authenticate" ) as mock_authenticate: mock_authenticate.side_effect = pyu2f.errors.NoDeviceFoundError() assert challenge.obtain_challenge_input(metadata) is None with mock.patch( "pyu2f.convenience.authenticator.CompositeAuthenticator.Authenticate" ) as mock_authenticate: mock_authenticate.side_effect = pyu2f.errors.UnsupportedVersionException() with pytest.raises(pyu2f.errors.UnsupportedVersionException): challenge.obtain_challenge_input(metadata) with mock.patch.dict("sys.modules"): sys.modules["pyu2f"] = None with pytest.raises(exceptions.ReauthFailError) as excinfo: challenge.obtain_challenge_input(metadata) assert excinfo.match(r"pyu2f dependency is required") @mock.patch("getpass.getpass", return_value="foo") def test_password_challenge(getpass_mock): challenge = challenges.PasswordChallenge() with mock.patch("getpass.getpass", return_value="foo"): assert challenge.is_locally_eligible assert challenge.name == "PASSWORD" assert challenges.PasswordChallenge().obtain_challenge_input({}) == { "credential": "foo" } with mock.patch("getpass.getpass", return_value=None): assert challenges.PasswordChallenge().obtain_challenge_input({}) == { "credential": " " } def test_saml_challenge(): challenge = challenges.SamlChallenge() assert challenge.is_locally_eligible assert challenge.name == "SAML" with pytest.raises(exceptions.ReauthSamlChallengeFailError): challenge.obtain_challenge_input(None) oauth2/test__client.py 0000644 00000046724 15025175543 0011020 0 ustar 00 # Copyright 2016 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import datetime import http.client as http_client import json import os import urllib import mock import pytest # type: ignore from google.auth import _helpers from google.auth import crypt from google.auth import exceptions from google.auth import jwt from google.auth import transport from google.oauth2 import _client DATA_DIR = os.path.join(os.path.dirname(__file__), "..", "data") with open(os.path.join(DATA_DIR, "privatekey.pem"), "rb") as fh: PRIVATE_KEY_BYTES = fh.read() SIGNER = crypt.RSASigner.from_string(PRIVATE_KEY_BYTES, "1") SCOPES_AS_LIST = [ "https://www.googleapis.com/auth/pubsub", "https://www.googleapis.com/auth/logging.write", ] SCOPES_AS_STRING = ( "https://www.googleapis.com/auth/pubsub" " https://www.googleapis.com/auth/logging.write" ) ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE = ( "gl-python/3.7 auth/1.1 auth-request-type/at cred-type/sa" ) ID_TOKEN_REQUEST_METRICS_HEADER_VALUE = ( "gl-python/3.7 auth/1.1 auth-request-type/it cred-type/sa" ) @pytest.mark.parametrize("retryable", [True, False]) def test__handle_error_response(retryable): response_data = {"error": "help", "error_description": "I'm alive"} with pytest.raises(exceptions.RefreshError) as excinfo: _client._handle_error_response(response_data, retryable) assert excinfo.value.retryable == retryable assert excinfo.match(r"help: I\'m alive") def test__handle_error_response_no_error(): response_data = {"foo": "bar"} with pytest.raises(exceptions.RefreshError) as excinfo: _client._handle_error_response(response_data, False) assert not excinfo.value.retryable assert excinfo.match(r"{\"foo\": \"bar\"}") def test__handle_error_response_not_json(): response_data = "this is an error message" with pytest.raises(exceptions.RefreshError) as excinfo: _client._handle_error_response(response_data, False) assert not excinfo.value.retryable assert excinfo.match(response_data) def test__can_retry_retryable(): retryable_codes = transport.DEFAULT_RETRYABLE_STATUS_CODES for status_code in range(100, 600): if status_code in retryable_codes: assert _client._can_retry(status_code, {"error": "invalid_scope"}) else: assert not _client._can_retry(status_code, {"error": "invalid_scope"}) @pytest.mark.parametrize( "response_data", [{"error": "internal_failure"}, {"error": "server_error"}] ) def test__can_retry_message(response_data): assert _client._can_retry(http_client.OK, response_data) @pytest.mark.parametrize( "response_data", [ {"error": "invalid_scope"}, {"error": {"foo": "bar"}}, {"error_description": {"foo", "bar"}}, ], ) def test__can_retry_no_retry_message(response_data): assert not _client._can_retry(http_client.OK, response_data) @pytest.mark.parametrize("mock_expires_in", [500, "500"]) @mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min) def test__parse_expiry(unused_utcnow, mock_expires_in): result = _client._parse_expiry({"expires_in": mock_expires_in}) assert result == datetime.datetime.min + datetime.timedelta(seconds=500) def test__parse_expiry_none(): assert _client._parse_expiry({}) is None def make_request(response_data, status=http_client.OK): response = mock.create_autospec(transport.Response, instance=True) response.status = status response.data = json.dumps(response_data).encode("utf-8") request = mock.create_autospec(transport.Request) request.return_value = response return request def test__token_endpoint_request(): request = make_request({"test": "response"}) result = _client._token_endpoint_request( request, "http://example.com", {"test": "params"} ) # Check request call request.assert_called_with( method="POST", url="http://example.com", headers={"Content-Type": "application/x-www-form-urlencoded"}, body="test=params".encode("utf-8"), ) # Check result assert result == {"test": "response"} def test__token_endpoint_request_use_json(): request = make_request({"test": "response"}) result = _client._token_endpoint_request( request, "http://example.com", {"test": "params"}, access_token="access_token", use_json=True, ) # Check request call request.assert_called_with( method="POST", url="http://example.com", headers={ "Content-Type": "application/json", "Authorization": "Bearer access_token", }, body=b'{"test": "params"}', ) # Check result assert result == {"test": "response"} def test__token_endpoint_request_error(): request = make_request({}, status=http_client.BAD_REQUEST) with pytest.raises(exceptions.RefreshError): _client._token_endpoint_request(request, "http://example.com", {}) def test__token_endpoint_request_internal_failure_error(): request = make_request( {"error_description": "internal_failure"}, status=http_client.BAD_REQUEST ) with pytest.raises(exceptions.RefreshError): _client._token_endpoint_request( request, "http://example.com", {"error_description": "internal_failure"} ) # request should be called once and then with 3 retries assert request.call_count == 4 request = make_request( {"error": "internal_failure"}, status=http_client.BAD_REQUEST ) with pytest.raises(exceptions.RefreshError): _client._token_endpoint_request( request, "http://example.com", {"error": "internal_failure"} ) # request should be called once and then with 3 retries assert request.call_count == 4 def test__token_endpoint_request_internal_failure_and_retry_failure_error(): retryable_error = mock.create_autospec(transport.Response, instance=True) retryable_error.status = http_client.BAD_REQUEST retryable_error.data = json.dumps({"error_description": "internal_failure"}).encode( "utf-8" ) unretryable_error = mock.create_autospec(transport.Response, instance=True) unretryable_error.status = http_client.BAD_REQUEST unretryable_error.data = json.dumps({"error_description": "invalid_scope"}).encode( "utf-8" ) request = mock.create_autospec(transport.Request) request.side_effect = [retryable_error, retryable_error, unretryable_error] with pytest.raises(exceptions.RefreshError): _client._token_endpoint_request( request, "http://example.com", {"error_description": "invalid_scope"} ) # request should be called three times. Two retryable errors and one # unretryable error to break the retry loop. assert request.call_count == 3 def test__token_endpoint_request_internal_failure_and_retry_succeeds(): retryable_error = mock.create_autospec(transport.Response, instance=True) retryable_error.status = http_client.BAD_REQUEST retryable_error.data = json.dumps({"error_description": "internal_failure"}).encode( "utf-8" ) response = mock.create_autospec(transport.Response, instance=True) response.status = http_client.OK response.data = json.dumps({"hello": "world"}).encode("utf-8") request = mock.create_autospec(transport.Request) request.side_effect = [retryable_error, response] _ = _client._token_endpoint_request( request, "http://example.com", {"test": "params"} ) assert request.call_count == 2 def test__token_endpoint_request_string_error(): response = mock.create_autospec(transport.Response, instance=True) response.status = http_client.BAD_REQUEST response.data = "this is an error message" request = mock.create_autospec(transport.Request) request.return_value = response with pytest.raises(exceptions.RefreshError) as excinfo: _client._token_endpoint_request(request, "http://example.com", {}) assert excinfo.match("this is an error message") def verify_request_params(request, params): request_body = request.call_args[1]["body"].decode("utf-8") request_params = urllib.parse.parse_qs(request_body) for key, value in params.items(): assert request_params[key][0] == value @mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min) def test_jwt_grant(utcnow): request = make_request( {"access_token": "token", "expires_in": 500, "extra": "data"} ) token, expiry, extra_data = _client.jwt_grant( request, "http://example.com", "assertion_value" ) # Check request call verify_request_params( request, {"grant_type": _client._JWT_GRANT_TYPE, "assertion": "assertion_value"} ) # Check result assert token == "token" assert expiry == utcnow() + datetime.timedelta(seconds=500) assert extra_data["extra"] == "data" def test_jwt_grant_no_access_token(): request = make_request( { # No access token. "expires_in": 500, "extra": "data", } ) with pytest.raises(exceptions.RefreshError) as excinfo: _client.jwt_grant(request, "http://example.com", "assertion_value") assert not excinfo.value.retryable def test_call_iam_generate_id_token_endpoint(): now = _helpers.utcnow() id_token_expiry = _helpers.datetime_to_secs(now) id_token = jwt.encode(SIGNER, {"exp": id_token_expiry}).decode("utf-8") request = make_request({"token": id_token}) token, expiry = _client.call_iam_generate_id_token_endpoint( request, "fake_email", "fake_audience", "fake_access_token" ) assert ( request.call_args[1]["url"] == "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/fake_email:generateIdToken" ) assert request.call_args[1]["headers"]["Content-Type"] == "application/json" assert ( request.call_args[1]["headers"]["Authorization"] == "Bearer fake_access_token" ) response_body = json.loads(request.call_args[1]["body"]) assert response_body["audience"] == "fake_audience" assert response_body["includeEmail"] == "true" assert response_body["useEmailAzp"] == "true" # Check result assert token == id_token # JWT does not store microseconds now = now.replace(microsecond=0) assert expiry == now def test_call_iam_generate_id_token_endpoint_no_id_token(): request = make_request( { # No access token. "error": "no token" } ) with pytest.raises(exceptions.RefreshError) as excinfo: _client.call_iam_generate_id_token_endpoint( request, "fake_email", "fake_audience", "fake_access_token" ) assert excinfo.match("No ID token in response") def test_id_token_jwt_grant(): now = _helpers.utcnow() id_token_expiry = _helpers.datetime_to_secs(now) id_token = jwt.encode(SIGNER, {"exp": id_token_expiry}).decode("utf-8") request = make_request({"id_token": id_token, "extra": "data"}) token, expiry, extra_data = _client.id_token_jwt_grant( request, "http://example.com", "assertion_value" ) # Check request call verify_request_params( request, {"grant_type": _client._JWT_GRANT_TYPE, "assertion": "assertion_value"} ) # Check result assert token == id_token # JWT does not store microseconds now = now.replace(microsecond=0) assert expiry == now assert extra_data["extra"] == "data" def test_id_token_jwt_grant_no_access_token(): request = make_request( { # No access token. "expires_in": 500, "extra": "data", } ) with pytest.raises(exceptions.RefreshError) as excinfo: _client.id_token_jwt_grant(request, "http://example.com", "assertion_value") assert not excinfo.value.retryable @mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min) def test_refresh_grant(unused_utcnow): request = make_request( { "access_token": "token", "refresh_token": "new_refresh_token", "expires_in": 500, "extra": "data", } ) token, refresh_token, expiry, extra_data = _client.refresh_grant( request, "http://example.com", "refresh_token", "client_id", "client_secret", rapt_token="rapt_token", ) # Check request call verify_request_params( request, { "grant_type": _client._REFRESH_GRANT_TYPE, "refresh_token": "refresh_token", "client_id": "client_id", "client_secret": "client_secret", "rapt": "rapt_token", }, ) # Check result assert token == "token" assert refresh_token == "new_refresh_token" assert expiry == datetime.datetime.min + datetime.timedelta(seconds=500) assert extra_data["extra"] == "data" @mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min) def test_refresh_grant_with_scopes(unused_utcnow): request = make_request( { "access_token": "token", "refresh_token": "new_refresh_token", "expires_in": 500, "extra": "data", "scope": SCOPES_AS_STRING, } ) token, refresh_token, expiry, extra_data = _client.refresh_grant( request, "http://example.com", "refresh_token", "client_id", "client_secret", SCOPES_AS_LIST, ) # Check request call. verify_request_params( request, { "grant_type": _client._REFRESH_GRANT_TYPE, "refresh_token": "refresh_token", "client_id": "client_id", "client_secret": "client_secret", "scope": SCOPES_AS_STRING, }, ) # Check result. assert token == "token" assert refresh_token == "new_refresh_token" assert expiry == datetime.datetime.min + datetime.timedelta(seconds=500) assert extra_data["extra"] == "data" def test_refresh_grant_no_access_token(): request = make_request( { # No access token. "refresh_token": "new_refresh_token", "expires_in": 500, "extra": "data", } ) with pytest.raises(exceptions.RefreshError) as excinfo: _client.refresh_grant( request, "http://example.com", "refresh_token", "client_id", "client_secret" ) assert not excinfo.value.retryable @mock.patch( "google.auth.metrics.token_request_access_token_sa_assertion", return_value=ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE, ) @mock.patch("google.oauth2._client._parse_expiry", return_value=None) @mock.patch.object(_client, "_token_endpoint_request", autospec=True) def test_jwt_grant_retry_default( mock_token_endpoint_request, mock_expiry, mock_metrics_header_value ): _client.jwt_grant(mock.Mock(), mock.Mock(), mock.Mock()) mock_token_endpoint_request.assert_called_with( mock.ANY, mock.ANY, mock.ANY, can_retry=True, headers={"x-goog-api-client": ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE}, ) @pytest.mark.parametrize("can_retry", [True, False]) @mock.patch( "google.auth.metrics.token_request_access_token_sa_assertion", return_value=ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE, ) @mock.patch("google.oauth2._client._parse_expiry", return_value=None) @mock.patch.object(_client, "_token_endpoint_request", autospec=True) def test_jwt_grant_retry_with_retry( mock_token_endpoint_request, mock_expiry, mock_metrics_header_value, can_retry ): _client.jwt_grant(mock.Mock(), mock.Mock(), mock.Mock(), can_retry=can_retry) mock_token_endpoint_request.assert_called_with( mock.ANY, mock.ANY, mock.ANY, can_retry=can_retry, headers={"x-goog-api-client": ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE}, ) @mock.patch( "google.auth.metrics.token_request_id_token_sa_assertion", return_value=ID_TOKEN_REQUEST_METRICS_HEADER_VALUE, ) @mock.patch("google.auth.jwt.decode", return_value={"exp": 0}) @mock.patch.object(_client, "_token_endpoint_request", autospec=True) def test_id_token_jwt_grant_retry_default( mock_token_endpoint_request, mock_jwt_decode, mock_metrics_header_value ): _client.id_token_jwt_grant(mock.Mock(), mock.Mock(), mock.Mock()) mock_token_endpoint_request.assert_called_with( mock.ANY, mock.ANY, mock.ANY, can_retry=True, headers={"x-goog-api-client": ID_TOKEN_REQUEST_METRICS_HEADER_VALUE}, ) @pytest.mark.parametrize("can_retry", [True, False]) @mock.patch( "google.auth.metrics.token_request_id_token_sa_assertion", return_value=ID_TOKEN_REQUEST_METRICS_HEADER_VALUE, ) @mock.patch("google.auth.jwt.decode", return_value={"exp": 0}) @mock.patch.object(_client, "_token_endpoint_request", autospec=True) def test_id_token_jwt_grant_retry_with_retry( mock_token_endpoint_request, mock_jwt_decode, mock_metrics_header_value, can_retry ): _client.id_token_jwt_grant( mock.Mock(), mock.Mock(), mock.Mock(), can_retry=can_retry ) mock_token_endpoint_request.assert_called_with( mock.ANY, mock.ANY, mock.ANY, can_retry=can_retry, headers={"x-goog-api-client": ID_TOKEN_REQUEST_METRICS_HEADER_VALUE}, ) @mock.patch("google.oauth2._client._parse_expiry", return_value=None) @mock.patch.object(_client, "_token_endpoint_request", autospec=True) def test_refresh_grant_retry_default(mock_token_endpoint_request, mock_parse_expiry): _client.refresh_grant( mock.Mock(), mock.Mock(), mock.Mock(), mock.Mock(), mock.Mock() ) mock_token_endpoint_request.assert_called_with( mock.ANY, mock.ANY, mock.ANY, can_retry=True ) @pytest.mark.parametrize("can_retry", [True, False]) @mock.patch("google.oauth2._client._parse_expiry", return_value=None) @mock.patch.object(_client, "_token_endpoint_request", autospec=True) def test_refresh_grant_retry_with_retry( mock_token_endpoint_request, mock_parse_expiry, can_retry ): _client.refresh_grant( mock.Mock(), mock.Mock(), mock.Mock(), mock.Mock(), mock.Mock(), can_retry=can_retry, ) mock_token_endpoint_request.assert_called_with( mock.ANY, mock.ANY, mock.ANY, can_retry=can_retry ) @pytest.mark.parametrize("can_retry", [True, False]) def test__token_endpoint_request_no_throw_with_retry(can_retry): response_data = {"error": "help", "error_description": "I'm alive"} body = "dummy body" mock_response = mock.create_autospec(transport.Response, instance=True) mock_response.status = http_client.INTERNAL_SERVER_ERROR mock_response.data = json.dumps(response_data).encode("utf-8") mock_request = mock.create_autospec(transport.Request) mock_request.return_value = mock_response _client._token_endpoint_request_no_throw( mock_request, mock.Mock(), body, mock.Mock(), mock.Mock(), can_retry=can_retry ) if can_retry: assert mock_request.call_count == 4 else: assert mock_request.call_count == 1 oauth2/test_gdch_credentials.py 0000644 00000014743 15025175543 0012661 0 ustar 00 # Copyright 2022 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import copy import datetime import json import os import mock import pytest # type: ignore import requests from google.auth import exceptions from google.auth import jwt import google.auth.transport.requests from google.oauth2 import gdch_credentials from google.oauth2.gdch_credentials import ServiceAccountCredentials class TestServiceAccountCredentials(object): AUDIENCE = "https://service-identity.<Domain>/authenticate" PROJECT = "project_foo" PRIVATE_KEY_ID = "key_foo" NAME = "service_identity_name" CA_CERT_PATH = "/path/to/ca/cert" TOKEN_URI = "https://service-identity.<Domain>/authenticate" JSON_PATH = os.path.join( os.path.dirname(__file__), "..", "data", "gdch_service_account.json" ) with open(JSON_PATH, "rb") as fh: INFO = json.load(fh) def test_with_gdch_audience(self): mock_signer = mock.Mock() creds = ServiceAccountCredentials._from_signer_and_info(mock_signer, self.INFO) assert creds._signer == mock_signer assert creds._service_identity_name == self.NAME assert creds._audience is None assert creds._token_uri == self.TOKEN_URI assert creds._ca_cert_path == self.CA_CERT_PATH new_creds = creds.with_gdch_audience(self.AUDIENCE) assert new_creds._signer == mock_signer assert new_creds._service_identity_name == self.NAME assert new_creds._audience == self.AUDIENCE assert new_creds._token_uri == self.TOKEN_URI assert new_creds._ca_cert_path == self.CA_CERT_PATH def test__create_jwt(self): creds = ServiceAccountCredentials.from_service_account_file(self.JSON_PATH) with mock.patch("google.auth._helpers.utcnow") as utcnow: utcnow.return_value = datetime.datetime.now() jwt_token = creds._create_jwt() header, payload, _, _ = jwt._unverified_decode(jwt_token) expected_iss_sub_value = ( "system:serviceaccount:project_foo:service_identity_name" ) assert isinstance(jwt_token, str) assert header["alg"] == "ES256" assert header["kid"] == self.PRIVATE_KEY_ID assert payload["iss"] == expected_iss_sub_value assert payload["sub"] == expected_iss_sub_value assert payload["aud"] == self.AUDIENCE assert payload["exp"] == (payload["iat"] + 3600) @mock.patch( "google.oauth2.gdch_credentials.ServiceAccountCredentials._create_jwt", autospec=True, ) @mock.patch("google.oauth2._client._token_endpoint_request", autospec=True) def test_refresh(self, token_endpoint_request, create_jwt): creds = ServiceAccountCredentials.from_service_account_info(self.INFO) creds = creds.with_gdch_audience(self.AUDIENCE) req = google.auth.transport.requests.Request() mock_jwt_token = "jwt token" create_jwt.return_value = mock_jwt_token sts_token = "STS token" token_endpoint_request.return_value = { "access_token": sts_token, "issued_token_type": "urn:ietf:params:oauth:token-type:access_token", "token_type": "Bearer", "expires_in": 3600, } creds.refresh(req) token_endpoint_request.assert_called_with( req, self.TOKEN_URI, { "grant_type": gdch_credentials.TOKEN_EXCHANGE_TYPE, "audience": self.AUDIENCE, "requested_token_type": gdch_credentials.ACCESS_TOKEN_TOKEN_TYPE, "subject_token": mock_jwt_token, "subject_token_type": gdch_credentials.SERVICE_ACCOUNT_TOKEN_TYPE, }, access_token=None, use_json=True, verify=self.CA_CERT_PATH, ) assert creds.token == sts_token def test_refresh_wrong_requests_object(self): creds = ServiceAccountCredentials.from_service_account_info(self.INFO) creds = creds.with_gdch_audience(self.AUDIENCE) req = requests.Request() with pytest.raises(exceptions.RefreshError) as excinfo: creds.refresh(req) assert excinfo.match( "request must be a google.auth.transport.requests.Request object" ) def test__from_signer_and_info_wrong_format_version(self): with pytest.raises(ValueError) as excinfo: ServiceAccountCredentials._from_signer_and_info( mock.Mock(), {"format_version": "2"} ) assert excinfo.match("Only format version 1 is supported") def test_from_service_account_info_miss_field(self): for field in [ "format_version", "private_key_id", "private_key", "name", "project", "token_uri", ]: info_with_missing_field = copy.deepcopy(self.INFO) del info_with_missing_field[field] with pytest.raises(ValueError) as excinfo: ServiceAccountCredentials.from_service_account_info( info_with_missing_field ) assert excinfo.match("missing fields") @mock.patch("google.auth._service_account_info.from_filename") def test_from_service_account_file(self, from_filename): mock_signer = mock.Mock() from_filename.return_value = (self.INFO, mock_signer) creds = ServiceAccountCredentials.from_service_account_file(self.JSON_PATH) from_filename.assert_called_with( self.JSON_PATH, require=[ "format_version", "private_key_id", "private_key", "name", "project", "token_uri", ], use_rsa_signer=False, ) assert creds._signer == mock_signer assert creds._service_identity_name == self.NAME assert creds._audience is None assert creds._token_uri == self.TOKEN_URI assert creds._ca_cert_path == self.CA_CERT_PATH oauth2/test_id_token.py 0000644 00000025540 15025175543 0011170 0 ustar 00 # Copyright 2014 Google Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import json import os import mock import pytest # type: ignore from google.auth import environment_vars from google.auth import exceptions from google.auth import transport from google.oauth2 import id_token from google.oauth2 import service_account SERVICE_ACCOUNT_FILE = os.path.join( os.path.dirname(__file__), "../data/service_account.json" ) ID_TOKEN_AUDIENCE = "https://pubsub.googleapis.com" def make_request(status, data=None): response = mock.create_autospec(transport.Response, instance=True) response.status = status if data is not None: response.data = json.dumps(data).encode("utf-8") request = mock.create_autospec(transport.Request) request.return_value = response return request def test__fetch_certs_success(): certs = {"1": "cert"} request = make_request(200, certs) returned_certs = id_token._fetch_certs(request, mock.sentinel.cert_url) request.assert_called_once_with(mock.sentinel.cert_url, method="GET") assert returned_certs == certs def test__fetch_certs_failure(): request = make_request(404) with pytest.raises(exceptions.TransportError): id_token._fetch_certs(request, mock.sentinel.cert_url) request.assert_called_once_with(mock.sentinel.cert_url, method="GET") @mock.patch("google.auth.jwt.decode", autospec=True) @mock.patch("google.oauth2.id_token._fetch_certs", autospec=True) def test_verify_token(_fetch_certs, decode): result = id_token.verify_token(mock.sentinel.token, mock.sentinel.request) assert result == decode.return_value _fetch_certs.assert_called_once_with( mock.sentinel.request, id_token._GOOGLE_OAUTH2_CERTS_URL ) decode.assert_called_once_with( mock.sentinel.token, certs=_fetch_certs.return_value, audience=None, clock_skew_in_seconds=0, ) @mock.patch("google.auth.jwt.decode", autospec=True) @mock.patch("google.oauth2.id_token._fetch_certs", autospec=True) def test_verify_token_args(_fetch_certs, decode): result = id_token.verify_token( mock.sentinel.token, mock.sentinel.request, audience=mock.sentinel.audience, certs_url=mock.sentinel.certs_url, ) assert result == decode.return_value _fetch_certs.assert_called_once_with(mock.sentinel.request, mock.sentinel.certs_url) decode.assert_called_once_with( mock.sentinel.token, certs=_fetch_certs.return_value, audience=mock.sentinel.audience, clock_skew_in_seconds=0, ) @mock.patch("google.auth.jwt.decode", autospec=True) @mock.patch("google.oauth2.id_token._fetch_certs", autospec=True) def test_verify_token_clock_skew(_fetch_certs, decode): result = id_token.verify_token( mock.sentinel.token, mock.sentinel.request, audience=mock.sentinel.audience, certs_url=mock.sentinel.certs_url, clock_skew_in_seconds=10, ) assert result == decode.return_value _fetch_certs.assert_called_once_with(mock.sentinel.request, mock.sentinel.certs_url) decode.assert_called_once_with( mock.sentinel.token, certs=_fetch_certs.return_value, audience=mock.sentinel.audience, clock_skew_in_seconds=10, ) @mock.patch("google.oauth2.id_token.verify_token", autospec=True) def test_verify_oauth2_token(verify_token): verify_token.return_value = {"iss": "accounts.google.com"} result = id_token.verify_oauth2_token( mock.sentinel.token, mock.sentinel.request, audience=mock.sentinel.audience ) assert result == verify_token.return_value verify_token.assert_called_once_with( mock.sentinel.token, mock.sentinel.request, audience=mock.sentinel.audience, certs_url=id_token._GOOGLE_OAUTH2_CERTS_URL, clock_skew_in_seconds=0, ) @mock.patch("google.oauth2.id_token.verify_token", autospec=True) def test_verify_oauth2_token_clock_skew(verify_token): verify_token.return_value = {"iss": "accounts.google.com"} result = id_token.verify_oauth2_token( mock.sentinel.token, mock.sentinel.request, audience=mock.sentinel.audience, clock_skew_in_seconds=10, ) assert result == verify_token.return_value verify_token.assert_called_once_with( mock.sentinel.token, mock.sentinel.request, audience=mock.sentinel.audience, certs_url=id_token._GOOGLE_OAUTH2_CERTS_URL, clock_skew_in_seconds=10, ) @mock.patch("google.oauth2.id_token.verify_token", autospec=True) def test_verify_oauth2_token_invalid_iss(verify_token): verify_token.return_value = {"iss": "invalid_issuer"} with pytest.raises(exceptions.GoogleAuthError): id_token.verify_oauth2_token( mock.sentinel.token, mock.sentinel.request, audience=mock.sentinel.audience ) @mock.patch("google.oauth2.id_token.verify_token", autospec=True) def test_verify_firebase_token(verify_token): result = id_token.verify_firebase_token( mock.sentinel.token, mock.sentinel.request, audience=mock.sentinel.audience ) assert result == verify_token.return_value verify_token.assert_called_once_with( mock.sentinel.token, mock.sentinel.request, audience=mock.sentinel.audience, certs_url=id_token._GOOGLE_APIS_CERTS_URL, clock_skew_in_seconds=0, ) @mock.patch("google.oauth2.id_token.verify_token", autospec=True) def test_verify_firebase_token_clock_skew(verify_token): result = id_token.verify_firebase_token( mock.sentinel.token, mock.sentinel.request, audience=mock.sentinel.audience, clock_skew_in_seconds=10, ) assert result == verify_token.return_value verify_token.assert_called_once_with( mock.sentinel.token, mock.sentinel.request, audience=mock.sentinel.audience, certs_url=id_token._GOOGLE_APIS_CERTS_URL, clock_skew_in_seconds=10, ) def test_fetch_id_token_credentials_optional_request(monkeypatch): monkeypatch.delenv(environment_vars.CREDENTIALS, raising=False) # Test a request object is created if not provided with mock.patch("google.auth.compute_engine._metadata.ping", return_value=True): with mock.patch( "google.auth.compute_engine.IDTokenCredentials.__init__", return_value=None ): with mock.patch( "google.auth.transport.requests.Request.__init__", return_value=None ) as mock_request: id_token.fetch_id_token_credentials(ID_TOKEN_AUDIENCE) mock_request.assert_called() def test_fetch_id_token_credentials_from_metadata_server(monkeypatch): monkeypatch.delenv(environment_vars.CREDENTIALS, raising=False) mock_req = mock.Mock() with mock.patch("google.auth.compute_engine._metadata.ping", return_value=True): with mock.patch( "google.auth.compute_engine.IDTokenCredentials.__init__", return_value=None ) as mock_init: id_token.fetch_id_token_credentials(ID_TOKEN_AUDIENCE, request=mock_req) mock_init.assert_called_once_with( mock_req, ID_TOKEN_AUDIENCE, use_metadata_identity_endpoint=True ) def test_fetch_id_token_credentials_from_explicit_cred_json_file(monkeypatch): monkeypatch.setenv(environment_vars.CREDENTIALS, SERVICE_ACCOUNT_FILE) cred = id_token.fetch_id_token_credentials(ID_TOKEN_AUDIENCE) assert isinstance(cred, service_account.IDTokenCredentials) assert cred._target_audience == ID_TOKEN_AUDIENCE def test_fetch_id_token_credentials_no_cred_exists(monkeypatch): monkeypatch.delenv(environment_vars.CREDENTIALS, raising=False) with mock.patch( "google.auth.compute_engine._metadata.ping", side_effect=exceptions.TransportError(), ): with pytest.raises(exceptions.DefaultCredentialsError) as excinfo: id_token.fetch_id_token_credentials(ID_TOKEN_AUDIENCE) assert excinfo.match( r"Neither metadata server or valid service account credentials are found." ) with mock.patch("google.auth.compute_engine._metadata.ping", return_value=False): with pytest.raises(exceptions.DefaultCredentialsError) as excinfo: id_token.fetch_id_token_credentials(ID_TOKEN_AUDIENCE) assert excinfo.match( r"Neither metadata server or valid service account credentials are found." ) def test_fetch_id_token_credentials_invalid_cred_file_type(monkeypatch): user_credentials_file = os.path.join( os.path.dirname(__file__), "../data/authorized_user.json" ) monkeypatch.setenv(environment_vars.CREDENTIALS, user_credentials_file) with mock.patch("google.auth.compute_engine._metadata.ping", return_value=False): with pytest.raises(exceptions.DefaultCredentialsError) as excinfo: id_token.fetch_id_token_credentials(ID_TOKEN_AUDIENCE) assert excinfo.match( r"Neither metadata server or valid service account credentials are found." ) def test_fetch_id_token_credentials_invalid_json(monkeypatch): not_json_file = os.path.join(os.path.dirname(__file__), "../data/public_cert.pem") monkeypatch.setenv(environment_vars.CREDENTIALS, not_json_file) with pytest.raises(exceptions.DefaultCredentialsError) as excinfo: id_token.fetch_id_token_credentials(ID_TOKEN_AUDIENCE) assert excinfo.match( r"GOOGLE_APPLICATION_CREDENTIALS is not valid service account credentials." ) def test_fetch_id_token_credentials_invalid_cred_path(monkeypatch): not_json_file = os.path.join(os.path.dirname(__file__), "../data/not_exists.json") monkeypatch.setenv(environment_vars.CREDENTIALS, not_json_file) with pytest.raises(exceptions.DefaultCredentialsError) as excinfo: id_token.fetch_id_token_credentials(ID_TOKEN_AUDIENCE) assert excinfo.match( r"GOOGLE_APPLICATION_CREDENTIALS path is either not found or invalid." ) def test_fetch_id_token(monkeypatch): mock_cred = mock.MagicMock() mock_cred.token = "token" mock_req = mock.Mock() with mock.patch( "google.oauth2.id_token.fetch_id_token_credentials", return_value=mock_cred ) as mock_fetch: token = id_token.fetch_id_token(mock_req, ID_TOKEN_AUDIENCE) mock_fetch.assert_called_once_with(ID_TOKEN_AUDIENCE, request=mock_req) mock_cred.refresh.assert_called_once_with(mock_req) assert token == "token" oauth2/test_service_account.py 0000644 00000075142 15025175543 0012553 0 ustar 00 # Copyright 2016 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import datetime import json import os import mock import pytest # type: ignore from google.auth import _helpers from google.auth import crypt from google.auth import exceptions from google.auth import jwt from google.auth import transport from google.oauth2 import service_account DATA_DIR = os.path.join(os.path.dirname(__file__), "..", "data") with open(os.path.join(DATA_DIR, "privatekey.pem"), "rb") as fh: PRIVATE_KEY_BYTES = fh.read() with open(os.path.join(DATA_DIR, "public_cert.pem"), "rb") as fh: PUBLIC_CERT_BYTES = fh.read() with open(os.path.join(DATA_DIR, "other_cert.pem"), "rb") as fh: OTHER_CERT_BYTES = fh.read() SERVICE_ACCOUNT_JSON_FILE = os.path.join(DATA_DIR, "service_account.json") SERVICE_ACCOUNT_NON_GDU_JSON_FILE = os.path.join( DATA_DIR, "service_account_non_gdu.json" ) FAKE_UNIVERSE_DOMAIN = "universe.foo" with open(SERVICE_ACCOUNT_JSON_FILE, "rb") as fh: SERVICE_ACCOUNT_INFO = json.load(fh) with open(SERVICE_ACCOUNT_NON_GDU_JSON_FILE, "rb") as fh: SERVICE_ACCOUNT_INFO_NON_GDU = json.load(fh) SIGNER = crypt.RSASigner.from_string(PRIVATE_KEY_BYTES, "1") class TestCredentials(object): SERVICE_ACCOUNT_EMAIL = "service-account@example.com" TOKEN_URI = "https://example.com/oauth2/token" @classmethod def make_credentials(cls, universe_domain=service_account._DEFAULT_UNIVERSE_DOMAIN): return service_account.Credentials( SIGNER, cls.SERVICE_ACCOUNT_EMAIL, cls.TOKEN_URI, universe_domain=universe_domain, ) def test_constructor_no_universe_domain(self): credentials = service_account.Credentials( SIGNER, self.SERVICE_ACCOUNT_EMAIL, self.TOKEN_URI, universe_domain=None ) assert credentials.universe_domain == service_account._DEFAULT_UNIVERSE_DOMAIN def test_from_service_account_info(self): credentials = service_account.Credentials.from_service_account_info( SERVICE_ACCOUNT_INFO ) assert credentials._signer.key_id == SERVICE_ACCOUNT_INFO["private_key_id"] assert credentials.service_account_email == SERVICE_ACCOUNT_INFO["client_email"] assert credentials._token_uri == SERVICE_ACCOUNT_INFO["token_uri"] assert credentials._universe_domain == service_account._DEFAULT_UNIVERSE_DOMAIN assert not credentials._always_use_jwt_access def test_from_service_account_info_non_gdu(self): credentials = service_account.Credentials.from_service_account_info( SERVICE_ACCOUNT_INFO_NON_GDU ) assert credentials.universe_domain == FAKE_UNIVERSE_DOMAIN assert credentials._always_use_jwt_access def test_from_service_account_info_args(self): info = SERVICE_ACCOUNT_INFO.copy() scopes = ["email", "profile"] subject = "subject" additional_claims = {"meta": "data"} credentials = service_account.Credentials.from_service_account_info( info, scopes=scopes, subject=subject, additional_claims=additional_claims ) assert credentials.service_account_email == info["client_email"] assert credentials.project_id == info["project_id"] assert credentials._signer.key_id == info["private_key_id"] assert credentials._token_uri == info["token_uri"] assert credentials._scopes == scopes assert credentials._subject == subject assert credentials._additional_claims == additional_claims assert not credentials._always_use_jwt_access def test_from_service_account_file(self): info = SERVICE_ACCOUNT_INFO.copy() credentials = service_account.Credentials.from_service_account_file( SERVICE_ACCOUNT_JSON_FILE ) assert credentials.service_account_email == info["client_email"] assert credentials.project_id == info["project_id"] assert credentials._signer.key_id == info["private_key_id"] assert credentials._token_uri == info["token_uri"] def test_from_service_account_file_non_gdu(self): info = SERVICE_ACCOUNT_INFO_NON_GDU.copy() credentials = service_account.Credentials.from_service_account_file( SERVICE_ACCOUNT_NON_GDU_JSON_FILE ) assert credentials.service_account_email == info["client_email"] assert credentials.project_id == info["project_id"] assert credentials._signer.key_id == info["private_key_id"] assert credentials._token_uri == info["token_uri"] assert credentials._universe_domain == FAKE_UNIVERSE_DOMAIN assert credentials._always_use_jwt_access def test_from_service_account_file_args(self): info = SERVICE_ACCOUNT_INFO.copy() scopes = ["email", "profile"] subject = "subject" additional_claims = {"meta": "data"} credentials = service_account.Credentials.from_service_account_file( SERVICE_ACCOUNT_JSON_FILE, subject=subject, scopes=scopes, additional_claims=additional_claims, ) assert credentials.service_account_email == info["client_email"] assert credentials.project_id == info["project_id"] assert credentials._signer.key_id == info["private_key_id"] assert credentials._token_uri == info["token_uri"] assert credentials._scopes == scopes assert credentials._subject == subject assert credentials._additional_claims == additional_claims def test_default_state(self): credentials = self.make_credentials() assert not credentials.valid # Expiration hasn't been set yet assert not credentials.expired # Scopes haven't been specified yet assert credentials.requires_scopes def test_sign_bytes(self): credentials = self.make_credentials() to_sign = b"123" signature = credentials.sign_bytes(to_sign) assert crypt.verify_signature(to_sign, signature, PUBLIC_CERT_BYTES) def test_signer(self): credentials = self.make_credentials() assert isinstance(credentials.signer, crypt.Signer) def test_signer_email(self): credentials = self.make_credentials() assert credentials.signer_email == self.SERVICE_ACCOUNT_EMAIL def test_create_scoped(self): credentials = self.make_credentials() scopes = ["email", "profile"] credentials = credentials.with_scopes(scopes) assert credentials._scopes == scopes def test_with_claims(self): credentials = self.make_credentials() new_credentials = credentials.with_claims({"meep": "moop"}) assert new_credentials._additional_claims == {"meep": "moop"} def test_with_quota_project(self): credentials = self.make_credentials() new_credentials = credentials.with_quota_project("new-project-456") assert new_credentials.quota_project_id == "new-project-456" hdrs = {} 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 new_credentials = credentials.with_always_use_jwt_access(True) assert new_credentials._always_use_jwt_access def test__with_always_use_jwt_access_non_default_universe_domain(self): credentials = self.make_credentials(universe_domain=FAKE_UNIVERSE_DOMAIN) with pytest.raises(exceptions.InvalidValue) as excinfo: credentials.with_always_use_jwt_access(False) assert excinfo.match( "always_use_jwt_access should be True for non-default universe domain" ) def test__make_authorization_grant_assertion(self): credentials = self.make_credentials() token = credentials._make_authorization_grant_assertion() payload = jwt.decode(token, PUBLIC_CERT_BYTES) assert payload["iss"] == self.SERVICE_ACCOUNT_EMAIL assert payload["aud"] == service_account._GOOGLE_OAUTH2_TOKEN_ENDPOINT def test__make_authorization_grant_assertion_scoped(self): credentials = self.make_credentials() scopes = ["email", "profile"] credentials = credentials.with_scopes(scopes) token = credentials._make_authorization_grant_assertion() payload = jwt.decode(token, PUBLIC_CERT_BYTES) assert payload["scope"] == "email profile" def test__make_authorization_grant_assertion_subject(self): credentials = self.make_credentials() subject = "user@example.com" credentials = credentials.with_subject(subject) token = credentials._make_authorization_grant_assertion() payload = jwt.decode(token, PUBLIC_CERT_BYTES) assert payload["sub"] == subject def test_apply_with_quota_project_id(self): credentials = service_account.Credentials( SIGNER, self.SERVICE_ACCOUNT_EMAIL, self.TOKEN_URI, quota_project_id="quota-project-123", ) headers = {} credentials.apply(headers, token="token") assert headers["x-goog-user-project"] == "quota-project-123" assert "token" in headers["authorization"] def test_apply_with_no_quota_project_id(self): credentials = service_account.Credentials( SIGNER, self.SERVICE_ACCOUNT_EMAIL, self.TOKEN_URI ) headers = {} credentials.apply(headers, token="token") assert "x-goog-user-project" not in headers assert "token" in headers["authorization"] @mock.patch("google.auth.jwt.Credentials", instance=True, autospec=True) def test__create_self_signed_jwt(self, jwt): credentials = service_account.Credentials( SIGNER, self.SERVICE_ACCOUNT_EMAIL, self.TOKEN_URI ) audience = "https://pubsub.googleapis.com" credentials._create_self_signed_jwt(audience) jwt.from_signing_credentials.assert_called_once_with(credentials, audience) @mock.patch("google.auth.jwt.Credentials", instance=True, autospec=True) def test__create_self_signed_jwt_with_user_scopes(self, jwt): credentials = service_account.Credentials( SIGNER, self.SERVICE_ACCOUNT_EMAIL, self.TOKEN_URI, scopes=["foo"] ) audience = "https://pubsub.googleapis.com" credentials._create_self_signed_jwt(audience) # JWT should not be created if there are user-defined scopes jwt.from_signing_credentials.assert_not_called() @mock.patch("google.auth.jwt.Credentials", instance=True, autospec=True) def test__create_self_signed_jwt_always_use_jwt_access_with_audience(self, jwt): credentials = service_account.Credentials( SIGNER, self.SERVICE_ACCOUNT_EMAIL, self.TOKEN_URI, default_scopes=["bar", "foo"], always_use_jwt_access=True, ) audience = "https://pubsub.googleapis.com" credentials._create_self_signed_jwt(audience) jwt.from_signing_credentials.assert_called_once_with(credentials, audience) @mock.patch("google.auth.jwt.Credentials", instance=True, autospec=True) def test__create_self_signed_jwt_always_use_jwt_access_with_audience_similar_jwt_is_reused( self, jwt ): credentials = service_account.Credentials( SIGNER, self.SERVICE_ACCOUNT_EMAIL, self.TOKEN_URI, default_scopes=["bar", "foo"], always_use_jwt_access=True, ) audience = "https://pubsub.googleapis.com" credentials._create_self_signed_jwt(audience) credentials._jwt_credentials._audience = audience credentials._create_self_signed_jwt(audience) jwt.from_signing_credentials.assert_called_once_with(credentials, audience) @mock.patch("google.auth.jwt.Credentials", instance=True, autospec=True) def test__create_self_signed_jwt_always_use_jwt_access_with_scopes(self, jwt): credentials = service_account.Credentials( SIGNER, self.SERVICE_ACCOUNT_EMAIL, self.TOKEN_URI, scopes=["bar", "foo"], always_use_jwt_access=True, ) audience = "https://pubsub.googleapis.com" credentials._create_self_signed_jwt(audience) jwt.from_signing_credentials.assert_called_once_with( credentials, None, additional_claims={"scope": "bar foo"} ) @mock.patch("google.auth.jwt.Credentials", instance=True, autospec=True) def test__create_self_signed_jwt_always_use_jwt_access_with_scopes_similar_jwt_is_reused( self, jwt ): credentials = service_account.Credentials( SIGNER, self.SERVICE_ACCOUNT_EMAIL, self.TOKEN_URI, scopes=["bar", "foo"], always_use_jwt_access=True, ) audience = "https://pubsub.googleapis.com" credentials._create_self_signed_jwt(audience) credentials._jwt_credentials.additional_claims = {"scope": "bar foo"} credentials._create_self_signed_jwt(audience) jwt.from_signing_credentials.assert_called_once_with( credentials, None, additional_claims={"scope": "bar foo"} ) @mock.patch("google.auth.jwt.Credentials", instance=True, autospec=True) def test__create_self_signed_jwt_always_use_jwt_access_with_default_scopes( self, jwt ): credentials = service_account.Credentials( SIGNER, self.SERVICE_ACCOUNT_EMAIL, self.TOKEN_URI, default_scopes=["bar", "foo"], always_use_jwt_access=True, ) credentials._create_self_signed_jwt(None) jwt.from_signing_credentials.assert_called_once_with( credentials, None, additional_claims={"scope": "bar foo"} ) @mock.patch("google.auth.jwt.Credentials", instance=True, autospec=True) def test__create_self_signed_jwt_always_use_jwt_access_with_default_scopes_similar_jwt_is_reused( self, jwt ): credentials = service_account.Credentials( SIGNER, self.SERVICE_ACCOUNT_EMAIL, self.TOKEN_URI, default_scopes=["bar", "foo"], always_use_jwt_access=True, ) credentials._create_self_signed_jwt(None) credentials._jwt_credentials.additional_claims = {"scope": "bar foo"} credentials._create_self_signed_jwt(None) jwt.from_signing_credentials.assert_called_once_with( credentials, None, additional_claims={"scope": "bar foo"} ) @mock.patch("google.auth.jwt.Credentials", instance=True, autospec=True) def test__create_self_signed_jwt_always_use_jwt_access(self, jwt): credentials = service_account.Credentials( SIGNER, self.SERVICE_ACCOUNT_EMAIL, self.TOKEN_URI, always_use_jwt_access=True, ) credentials._create_self_signed_jwt(None) jwt.from_signing_credentials.assert_not_called() def test_token_usage_metrics_assertion(self): credentials = service_account.Credentials( SIGNER, self.SERVICE_ACCOUNT_EMAIL, self.TOKEN_URI, always_use_jwt_access=False, ) credentials.token = "token" credentials.expiry = None headers = {} credentials.before_request(mock.Mock(), None, None, headers) assert headers["authorization"] == "Bearer token" assert headers["x-goog-api-client"] == "cred-type/sa" def test_token_usage_metrics_self_signed_jwt(self): credentials = service_account.Credentials( SIGNER, self.SERVICE_ACCOUNT_EMAIL, self.TOKEN_URI, always_use_jwt_access=True, ) credentials._create_self_signed_jwt("foo.googleapis.com") credentials.token = "token" credentials.expiry = None headers = {} credentials.before_request(mock.Mock(), None, None, headers) assert headers["authorization"] == "Bearer token" assert headers["x-goog-api-client"] == "cred-type/jwt" @mock.patch("google.oauth2._client.jwt_grant", autospec=True) def test_refresh_success(self, jwt_grant): credentials = self.make_credentials() token = "token" jwt_grant.return_value = ( token, _helpers.utcnow() + datetime.timedelta(seconds=500), {}, ) request = mock.create_autospec(transport.Request, instance=True) # Refresh credentials credentials.refresh(request) # Check jwt grant call. assert jwt_grant.called called_request, token_uri, assertion = jwt_grant.call_args[0] assert called_request == request assert token_uri == credentials._token_uri assert jwt.decode(assertion, PUBLIC_CERT_BYTES) # No further assertion done on the token, as there are separate tests # for checking the authorization grant assertion. # Check that the credentials have the token. assert credentials.token == token # Check that the credentials are valid (have a token and are not # expired) assert credentials.valid @mock.patch("google.oauth2._client.jwt_grant", autospec=True) def test_before_request_refreshes(self, jwt_grant): credentials = self.make_credentials() token = "token" jwt_grant.return_value = ( token, _helpers.utcnow() + datetime.timedelta(seconds=500), None, ) request = mock.create_autospec(transport.Request, instance=True) # Credentials should start as invalid assert not credentials.valid # before_request should cause a refresh credentials.before_request(request, "GET", "http://example.com?a=1#3", {}) # The refresh endpoint should've been called. assert jwt_grant.called # Credentials should now be valid. assert credentials.valid @mock.patch("google.auth.jwt.Credentials._make_jwt") def test_refresh_with_jwt_credentials(self, make_jwt): credentials = self.make_credentials() credentials._create_self_signed_jwt("https://pubsub.googleapis.com") request = mock.create_autospec(transport.Request, instance=True) token = "token" expiry = _helpers.utcnow() + datetime.timedelta(seconds=500) make_jwt.return_value = (b"token", expiry) # Credentials should start as invalid assert not credentials.valid # before_request should cause a refresh credentials.before_request(request, "GET", "http://example.com?a=1#3", {}) # Credentials should now be valid. assert credentials.valid # Assert make_jwt was called assert make_jwt.call_count == 1 assert credentials.token == token assert credentials.expiry == expiry def test_refresh_with_jwt_credentials_token_type_check(self): credentials = self.make_credentials() credentials._create_self_signed_jwt("https://pubsub.googleapis.com") credentials.refresh(mock.Mock()) # Credentials token should be a JWT string. assert isinstance(credentials.token, str) payload = jwt.decode(credentials.token, verify=False) assert payload["aud"] == "https://pubsub.googleapis.com" @mock.patch("google.oauth2._client.jwt_grant", autospec=True) @mock.patch("google.auth.jwt.Credentials.refresh", autospec=True) def test_refresh_jwt_not_used_for_domain_wide_delegation( self, self_signed_jwt_refresh, jwt_grant ): # Create a domain wide delegation credentials by setting the subject. credentials = service_account.Credentials( SIGNER, self.SERVICE_ACCOUNT_EMAIL, self.TOKEN_URI, always_use_jwt_access=True, subject="subject", ) credentials._create_self_signed_jwt("https://pubsub.googleapis.com") jwt_grant.return_value = ( "token", _helpers.utcnow() + datetime.timedelta(seconds=500), {}, ) request = mock.create_autospec(transport.Request, instance=True) # Refresh credentials credentials.refresh(request) # Make sure we are using jwt_grant and not self signed JWT refresh # method to obtain the token. assert jwt_grant.called assert not self_signed_jwt_refresh.called def test_refresh_missing_jwt_credentials(self): credentials = self.make_credentials() credentials = credentials.with_scopes(["foo", "bar"]) credentials = credentials.with_always_use_jwt_access(True) assert not credentials._jwt_credentials credentials.refresh(mock.Mock()) # jwt credentials should have been automatically created with scopes assert credentials._jwt_credentials is not None def test_refresh_non_gdu_domain_wide_delegation_not_supported(self): credentials = self.make_credentials(universe_domain="foo") credentials._subject = "bar@example.com" credentials._create_self_signed_jwt("https://pubsub.googleapis.com") with pytest.raises(exceptions.RefreshError) as excinfo: credentials.refresh(None) assert excinfo.match("domain wide delegation is not supported") class TestIDTokenCredentials(object): SERVICE_ACCOUNT_EMAIL = "service-account@example.com" TOKEN_URI = "https://example.com/oauth2/token" TARGET_AUDIENCE = "https://example.com" @classmethod def make_credentials(cls, universe_domain=service_account._DEFAULT_UNIVERSE_DOMAIN): return service_account.IDTokenCredentials( SIGNER, cls.SERVICE_ACCOUNT_EMAIL, cls.TOKEN_URI, cls.TARGET_AUDIENCE, universe_domain=universe_domain, ) def test_constructor_no_universe_domain(self): credentials = service_account.IDTokenCredentials( SIGNER, self.SERVICE_ACCOUNT_EMAIL, self.TOKEN_URI, self.TARGET_AUDIENCE, universe_domain=None, ) assert credentials._universe_domain == service_account._DEFAULT_UNIVERSE_DOMAIN def test_from_service_account_info(self): credentials = service_account.IDTokenCredentials.from_service_account_info( SERVICE_ACCOUNT_INFO, target_audience=self.TARGET_AUDIENCE ) assert credentials._signer.key_id == SERVICE_ACCOUNT_INFO["private_key_id"] assert credentials.service_account_email == SERVICE_ACCOUNT_INFO["client_email"] assert credentials._token_uri == SERVICE_ACCOUNT_INFO["token_uri"] assert credentials._target_audience == self.TARGET_AUDIENCE assert not credentials._use_iam_endpoint def test_from_service_account_info_non_gdu(self): credentials = service_account.IDTokenCredentials.from_service_account_info( SERVICE_ACCOUNT_INFO_NON_GDU, target_audience=self.TARGET_AUDIENCE ) assert ( credentials._signer.key_id == SERVICE_ACCOUNT_INFO_NON_GDU["private_key_id"] ) assert ( credentials.service_account_email == SERVICE_ACCOUNT_INFO_NON_GDU["client_email"] ) assert credentials._token_uri == SERVICE_ACCOUNT_INFO_NON_GDU["token_uri"] assert credentials._target_audience == self.TARGET_AUDIENCE assert credentials._use_iam_endpoint def test_from_service_account_file(self): info = SERVICE_ACCOUNT_INFO.copy() credentials = service_account.IDTokenCredentials.from_service_account_file( SERVICE_ACCOUNT_JSON_FILE, target_audience=self.TARGET_AUDIENCE ) assert credentials.service_account_email == info["client_email"] assert credentials._signer.key_id == info["private_key_id"] assert credentials._token_uri == info["token_uri"] assert credentials._target_audience == self.TARGET_AUDIENCE assert not credentials._use_iam_endpoint def test_from_service_account_file_non_gdu(self): info = SERVICE_ACCOUNT_INFO_NON_GDU.copy() credentials = service_account.IDTokenCredentials.from_service_account_file( SERVICE_ACCOUNT_NON_GDU_JSON_FILE, target_audience=self.TARGET_AUDIENCE ) assert credentials.service_account_email == info["client_email"] assert credentials._signer.key_id == info["private_key_id"] assert credentials._token_uri == info["token_uri"] assert credentials._target_audience == self.TARGET_AUDIENCE assert credentials._use_iam_endpoint def test_default_state(self): credentials = self.make_credentials() assert not credentials.valid # Expiration hasn't been set yet assert not credentials.expired def test_sign_bytes(self): credentials = self.make_credentials() to_sign = b"123" signature = credentials.sign_bytes(to_sign) assert crypt.verify_signature(to_sign, signature, PUBLIC_CERT_BYTES) def test_signer(self): credentials = self.make_credentials() assert isinstance(credentials.signer, crypt.Signer) def test_signer_email(self): credentials = self.make_credentials() assert credentials.signer_email == self.SERVICE_ACCOUNT_EMAIL def test_with_target_audience(self): credentials = self.make_credentials() new_credentials = credentials.with_target_audience("https://new.example.com") assert new_credentials._target_audience == "https://new.example.com" def test__with_use_iam_endpoint(self): credentials = self.make_credentials() new_credentials = credentials._with_use_iam_endpoint(True) assert new_credentials._use_iam_endpoint def test__with_use_iam_endpoint_non_default_universe_domain(self): credentials = self.make_credentials(universe_domain=FAKE_UNIVERSE_DOMAIN) with pytest.raises(exceptions.InvalidValue) as excinfo: credentials._with_use_iam_endpoint(False) assert excinfo.match( "use_iam_endpoint should be True for non-default universe domain" ) 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_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() payload = jwt.decode(token, PUBLIC_CERT_BYTES) assert payload["iss"] == self.SERVICE_ACCOUNT_EMAIL assert payload["aud"] == service_account._GOOGLE_OAUTH2_TOKEN_ENDPOINT assert payload["target_audience"] == self.TARGET_AUDIENCE @mock.patch("google.oauth2._client.id_token_jwt_grant", autospec=True) def test_refresh_success(self, id_token_jwt_grant): credentials = self.make_credentials() token = "token" id_token_jwt_grant.return_value = ( token, _helpers.utcnow() + datetime.timedelta(seconds=500), {}, ) request = mock.create_autospec(transport.Request, instance=True) # Refresh credentials credentials.refresh(request) # Check jwt grant call. assert id_token_jwt_grant.called called_request, token_uri, assertion = id_token_jwt_grant.call_args[0] assert called_request == request assert token_uri == credentials._token_uri assert jwt.decode(assertion, PUBLIC_CERT_BYTES) # No further assertion done on the token, as there are separate tests # for checking the authorization grant assertion. # Check that the credentials have the token. assert credentials.token == token # Check that the credentials are valid (have a token and are not # expired) assert credentials.valid @mock.patch( "google.oauth2._client.call_iam_generate_id_token_endpoint", autospec=True ) def test_refresh_iam_flow(self, call_iam_generate_id_token_endpoint): credentials = self.make_credentials() credentials._use_iam_endpoint = True token = "id_token" call_iam_generate_id_token_endpoint.return_value = ( token, _helpers.utcnow() + datetime.timedelta(seconds=500), ) request = mock.Mock() credentials.refresh(request) req, signer_email, target_audience, access_token = call_iam_generate_id_token_endpoint.call_args[ 0 ] assert req == request assert signer_email == "service-account@example.com" assert target_audience == "https://example.com" decoded_access_token = jwt.decode(access_token, verify=False) assert decoded_access_token["scope"] == "https://www.googleapis.com/auth/iam" @mock.patch("google.oauth2._client.id_token_jwt_grant", autospec=True) def test_before_request_refreshes(self, id_token_jwt_grant): credentials = self.make_credentials() token = "token" id_token_jwt_grant.return_value = ( token, _helpers.utcnow() + datetime.timedelta(seconds=500), None, ) request = mock.create_autospec(transport.Request, instance=True) # Credentials should start as invalid assert not credentials.valid # before_request should cause a refresh credentials.before_request(request, "GET", "http://example.com?a=1#3", {}) # The refresh endpoint should've been called. assert id_token_jwt_grant.called # Credentials should now be valid. assert credentials.valid oauth2/__init__.py 0000644 00000000000 15025175543 0010054 0 ustar 00 test_aws.py 0000644 00000256710 15025175543 0006771 0 ustar 00 # Copyright 2020 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import datetime import http.client as http_client import json import os import urllib.parse import mock import pytest # type: ignore from google.auth import _helpers from google.auth import aws from google.auth import environment_vars from google.auth import exceptions from google.auth import transport IMPERSONATE_ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE = ( "gl-python/3.7 auth/1.1 auth-request-type/at cred-type/imp" ) LANG_LIBRARY_METRICS_HEADER_VALUE = "gl-python/3.7 auth/1.1" CLIENT_ID = "username" CLIENT_SECRET = "password" # Base64 encoding of "username:password". BASIC_AUTH_ENCODING = "dXNlcm5hbWU6cGFzc3dvcmQ=" SERVICE_ACCOUNT_EMAIL = "service-1234@service-name.iam.gserviceaccount.com" SERVICE_ACCOUNT_IMPERSONATION_URL_BASE = ( "https://us-east1-iamcredentials.googleapis.com" ) SERVICE_ACCOUNT_IMPERSONATION_URL_ROUTE = "/v1/projects/-/serviceAccounts/{}:generateAccessToken".format( SERVICE_ACCOUNT_EMAIL ) SERVICE_ACCOUNT_IMPERSONATION_URL = ( SERVICE_ACCOUNT_IMPERSONATION_URL_BASE + SERVICE_ACCOUNT_IMPERSONATION_URL_ROUTE ) QUOTA_PROJECT_ID = "QUOTA_PROJECT_ID" SCOPES = ["scope1", "scope2"] TOKEN_URL = "https://sts.googleapis.com/v1/token" TOKEN_INFO_URL = "https://sts.googleapis.com/v1/introspect" SUBJECT_TOKEN_TYPE = "urn:ietf:params:aws:token-type:aws4_request" AUDIENCE = "//iam.googleapis.com/projects/123456/locations/global/workloadIdentityPools/POOL_ID/providers/PROVIDER_ID" REGION_URL = "http://169.254.169.254/latest/meta-data/placement/availability-zone" IMDSV2_SESSION_TOKEN_URL = "http://169.254.169.254/latest/api/token" SECURITY_CREDS_URL = "http://169.254.169.254/latest/meta-data/iam/security-credentials" REGION_URL_IPV6 = "http://[fd00:ec2::254]/latest/meta-data/placement/availability-zone" IMDSV2_SESSION_TOKEN_URL_IPV6 = "http://[fd00:ec2::254]/latest/api/token" SECURITY_CREDS_URL_IPV6 = ( "http://[fd00:ec2::254]/latest/meta-data/iam/security-credentials" ) CRED_VERIFICATION_URL = ( "https://sts.{region}.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15" ) # Sample fictitious AWS security credentials to be used with tests that require a session token. ACCESS_KEY_ID = "AKIAIOSFODNN7EXAMPLE" SECRET_ACCESS_KEY = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" TOKEN = "AQoEXAMPLEH4aoAH0gNCAPyJxz4BlCFFxWNE1OPTgk5TthT+FvwqnKwRcOIfrRh3c/LTo6UDdyJwOOvEVPvLXCrrrUtdnniCEXAMPLE/IvU1dYUg2RVAJBanLiHb4IgRmpRV3zrkuWJOgQs8IZZaIv2BXIa2R4OlgkBN9bkUDNCJiBeb/AXlzBBko7b15fjrBs2+cTQtpZ3CYWFXG8C5zqx37wnOE49mRl/+OtkIKGO7fAE" # To avoid json.dumps() differing behavior from one version to other, # the JSON payload is hardcoded. REQUEST_PARAMS = '{"KeySchema":[{"KeyType":"HASH","AttributeName":"Id"}],"TableName":"TestTable","AttributeDefinitions":[{"AttributeName":"Id","AttributeType":"S"}],"ProvisionedThroughput":{"WriteCapacityUnits":5,"ReadCapacityUnits":5}}' # Each tuple contains the following entries: # region, time, credentials, original_request, signed_request DEFAULT_UNIVERSE_DOMAIN = "googleapis.com" VALID_TOKEN_URLS = [ "https://sts.googleapis.com", "https://us-east-1.sts.googleapis.com", "https://US-EAST-1.sts.googleapis.com", "https://sts.us-east-1.googleapis.com", "https://sts.US-WEST-1.googleapis.com", "https://us-east-1-sts.googleapis.com", "https://US-WEST-1-sts.googleapis.com", "https://us-west-1-sts.googleapis.com/path?query", "https://sts-us-east-1.p.googleapis.com", ] INVALID_TOKEN_URLS = [ "https://iamcredentials.googleapis.com", "sts.googleapis.com", "https://", "http://sts.googleapis.com", "https://st.s.googleapis.com", "https://us-eas\t-1.sts.googleapis.com", "https:/us-east-1.sts.googleapis.com", "https://US-WE/ST-1-sts.googleapis.com", "https://sts-us-east-1.googleapis.com", "https://sts-US-WEST-1.googleapis.com", "testhttps://us-east-1.sts.googleapis.com", "https://us-east-1.sts.googleapis.comevil.com", "https://us-east-1.us-east-1.sts.googleapis.com", "https://us-ea.s.t.sts.googleapis.com", "https://sts.googleapis.comevil.com", "hhttps://us-east-1.sts.googleapis.com", "https://us- -1.sts.googleapis.com", "https://-sts.googleapis.com", "https://us-east-1.sts.googleapis.com.evil.com", "https://sts.pgoogleapis.com", "https://p.googleapis.com", "https://sts.p.com", "http://sts.p.googleapis.com", "https://xyz-sts.p.googleapis.com", "https://sts-xyz.123.p.googleapis.com", "https://sts-xyz.p1.googleapis.com", "https://sts-xyz.p.foo.com", "https://sts-xyz.p.foo.googleapis.com", ] VALID_SERVICE_ACCOUNT_IMPERSONATION_URLS = [ "https://iamcredentials.googleapis.com", "https://us-east-1.iamcredentials.googleapis.com", "https://US-EAST-1.iamcredentials.googleapis.com", "https://iamcredentials.us-east-1.googleapis.com", "https://iamcredentials.US-WEST-1.googleapis.com", "https://us-east-1-iamcredentials.googleapis.com", "https://US-WEST-1-iamcredentials.googleapis.com", "https://us-west-1-iamcredentials.googleapis.com/path?query", "https://iamcredentials-us-east-1.p.googleapis.com", ] INVALID_SERVICE_ACCOUNT_IMPERSONATION_URLS = [ "https://sts.googleapis.com", "iamcredentials.googleapis.com", "https://", "http://iamcredentials.googleapis.com", "https://iamcre.dentials.googleapis.com", "https://us-eas\t-1.iamcredentials.googleapis.com", "https:/us-east-1.iamcredentials.googleapis.com", "https://US-WE/ST-1-iamcredentials.googleapis.com", "https://iamcredentials-us-east-1.googleapis.com", "https://iamcredentials-US-WEST-1.googleapis.com", "testhttps://us-east-1.iamcredentials.googleapis.com", "https://us-east-1.iamcredentials.googleapis.comevil.com", "https://us-east-1.us-east-1.iamcredentials.googleapis.com", "https://us-ea.s.t.iamcredentials.googleapis.com", "https://iamcredentials.googleapis.comevil.com", "hhttps://us-east-1.iamcredentials.googleapis.com", "https://us- -1.iamcredentials.googleapis.com", "https://-iamcredentials.googleapis.com", "https://us-east-1.iamcredentials.googleapis.com.evil.com", "https://iamcredentials.pgoogleapis.com", "https://p.googleapis.com", "https://iamcredentials.p.com", "http://iamcredentials.p.googleapis.com", "https://xyz-iamcredentials.p.googleapis.com", "https://iamcredentials-xyz.123.p.googleapis.com", "https://iamcredentials-xyz.p1.googleapis.com", "https://iamcredentials-xyz.p.foo.com", "https://iamcredentials-xyz.p.foo.googleapis.com", ] TEST_FIXTURES = [ # GET request (AWS botocore tests). # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-vanilla.req # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-vanilla.sreq ( "us-east-1", "2011-09-09T23:36:00Z", { "access_key_id": "AKIDEXAMPLE", "secret_access_key": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY", }, { "method": "GET", "url": "https://host.foo.com", "headers": {"date": "Mon, 09 Sep 2011 23:36:00 GMT"}, }, { "url": "https://host.foo.com", "method": "GET", "headers": { "Authorization": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=date;host, Signature=b27ccfbfa7df52a200ff74193ca6e32d4b48b8856fab7ebf1c595d0670a7e470", "host": "host.foo.com", "date": "Mon, 09 Sep 2011 23:36:00 GMT", }, }, ), # GET request with relative path (AWS botocore tests). # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-relative-relative.req # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-relative-relative.sreq ( "us-east-1", "2011-09-09T23:36:00Z", { "access_key_id": "AKIDEXAMPLE", "secret_access_key": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY", }, { "method": "GET", "url": "https://host.foo.com/foo/bar/../..", "headers": {"date": "Mon, 09 Sep 2011 23:36:00 GMT"}, }, { "url": "https://host.foo.com/foo/bar/../..", "method": "GET", "headers": { "Authorization": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=date;host, Signature=b27ccfbfa7df52a200ff74193ca6e32d4b48b8856fab7ebf1c595d0670a7e470", "host": "host.foo.com", "date": "Mon, 09 Sep 2011 23:36:00 GMT", }, }, ), # GET request with /./ path (AWS botocore tests). # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-slash-dot-slash.req # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-slash-dot-slash.sreq ( "us-east-1", "2011-09-09T23:36:00Z", { "access_key_id": "AKIDEXAMPLE", "secret_access_key": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY", }, { "method": "GET", "url": "https://host.foo.com/./", "headers": {"date": "Mon, 09 Sep 2011 23:36:00 GMT"}, }, { "url": "https://host.foo.com/./", "method": "GET", "headers": { "Authorization": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=date;host, Signature=b27ccfbfa7df52a200ff74193ca6e32d4b48b8856fab7ebf1c595d0670a7e470", "host": "host.foo.com", "date": "Mon, 09 Sep 2011 23:36:00 GMT", }, }, ), # GET request with pointless dot path (AWS botocore tests). # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-slash-pointless-dot.req # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-slash-pointless-dot.sreq ( "us-east-1", "2011-09-09T23:36:00Z", { "access_key_id": "AKIDEXAMPLE", "secret_access_key": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY", }, { "method": "GET", "url": "https://host.foo.com/./foo", "headers": {"date": "Mon, 09 Sep 2011 23:36:00 GMT"}, }, { "url": "https://host.foo.com/./foo", "method": "GET", "headers": { "Authorization": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=date;host, Signature=910e4d6c9abafaf87898e1eb4c929135782ea25bb0279703146455745391e63a", "host": "host.foo.com", "date": "Mon, 09 Sep 2011 23:36:00 GMT", }, }, ), # GET request with utf8 path (AWS botocore tests). # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-utf8.req # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-utf8.sreq ( "us-east-1", "2011-09-09T23:36:00Z", { "access_key_id": "AKIDEXAMPLE", "secret_access_key": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY", }, { "method": "GET", "url": "https://host.foo.com/%E1%88%B4", "headers": {"date": "Mon, 09 Sep 2011 23:36:00 GMT"}, }, { "url": "https://host.foo.com/%E1%88%B4", "method": "GET", "headers": { "Authorization": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=date;host, Signature=8d6634c189aa8c75c2e51e106b6b5121bed103fdb351f7d7d4381c738823af74", "host": "host.foo.com", "date": "Mon, 09 Sep 2011 23:36:00 GMT", }, }, ), # GET request with duplicate query key (AWS botocore tests). # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-vanilla-query-order-key-case.req # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-vanilla-query-order-key-case.sreq ( "us-east-1", "2011-09-09T23:36:00Z", { "access_key_id": "AKIDEXAMPLE", "secret_access_key": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY", }, { "method": "GET", "url": "https://host.foo.com/?foo=Zoo&foo=aha", "headers": {"date": "Mon, 09 Sep 2011 23:36:00 GMT"}, }, { "url": "https://host.foo.com/?foo=Zoo&foo=aha", "method": "GET", "headers": { "Authorization": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=date;host, Signature=be7148d34ebccdc6423b19085378aa0bee970bdc61d144bd1a8c48c33079ab09", "host": "host.foo.com", "date": "Mon, 09 Sep 2011 23:36:00 GMT", }, }, ), # GET request with duplicate out of order query key (AWS botocore tests). # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-vanilla-query-order-value.req # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-vanilla-query-order-value.sreq ( "us-east-1", "2011-09-09T23:36:00Z", { "access_key_id": "AKIDEXAMPLE", "secret_access_key": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY", }, { "method": "GET", "url": "https://host.foo.com/?foo=b&foo=a", "headers": {"date": "Mon, 09 Sep 2011 23:36:00 GMT"}, }, { "url": "https://host.foo.com/?foo=b&foo=a", "method": "GET", "headers": { "Authorization": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=date;host, Signature=feb926e49e382bec75c9d7dcb2a1b6dc8aa50ca43c25d2bc51143768c0875acc", "host": "host.foo.com", "date": "Mon, 09 Sep 2011 23:36:00 GMT", }, }, ), # GET request with utf8 query (AWS botocore tests). # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-vanilla-ut8-query.req # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-vanilla-ut8-query.sreq ( "us-east-1", "2011-09-09T23:36:00Z", { "access_key_id": "AKIDEXAMPLE", "secret_access_key": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY", }, { "method": "GET", "url": "https://host.foo.com/?{}=bar".format( urllib.parse.unquote("%E1%88%B4") ), "headers": {"date": "Mon, 09 Sep 2011 23:36:00 GMT"}, }, { "url": "https://host.foo.com/?{}=bar".format( urllib.parse.unquote("%E1%88%B4") ), "method": "GET", "headers": { "Authorization": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=date;host, Signature=6fb359e9a05394cc7074e0feb42573a2601abc0c869a953e8c5c12e4e01f1a8c", "host": "host.foo.com", "date": "Mon, 09 Sep 2011 23:36:00 GMT", }, }, ), # POST request with sorted headers (AWS botocore tests). # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/post-header-key-sort.req # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/post-header-key-sort.sreq ( "us-east-1", "2011-09-09T23:36:00Z", { "access_key_id": "AKIDEXAMPLE", "secret_access_key": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY", }, { "method": "POST", "url": "https://host.foo.com/", "headers": {"date": "Mon, 09 Sep 2011 23:36:00 GMT", "ZOO": "zoobar"}, }, { "url": "https://host.foo.com/", "method": "POST", "headers": { "Authorization": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=date;host;zoo, Signature=b7a95a52518abbca0964a999a880429ab734f35ebbf1235bd79a5de87756dc4a", "host": "host.foo.com", "date": "Mon, 09 Sep 2011 23:36:00 GMT", "ZOO": "zoobar", }, }, ), # POST request with upper case header value from AWS Python test harness. # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/post-header-value-case.req # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/post-header-value-case.sreq ( "us-east-1", "2011-09-09T23:36:00Z", { "access_key_id": "AKIDEXAMPLE", "secret_access_key": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY", }, { "method": "POST", "url": "https://host.foo.com/", "headers": {"date": "Mon, 09 Sep 2011 23:36:00 GMT", "zoo": "ZOOBAR"}, }, { "url": "https://host.foo.com/", "method": "POST", "headers": { "Authorization": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=date;host;zoo, Signature=273313af9d0c265c531e11db70bbd653f3ba074c1009239e8559d3987039cad7", "host": "host.foo.com", "date": "Mon, 09 Sep 2011 23:36:00 GMT", "zoo": "ZOOBAR", }, }, ), # POST request with header and no body (AWS botocore tests). # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-header-value-trim.req # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/get-header-value-trim.sreq ( "us-east-1", "2011-09-09T23:36:00Z", { "access_key_id": "AKIDEXAMPLE", "secret_access_key": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY", }, { "method": "POST", "url": "https://host.foo.com/", "headers": {"date": "Mon, 09 Sep 2011 23:36:00 GMT", "p": "phfft"}, }, { "url": "https://host.foo.com/", "method": "POST", "headers": { "Authorization": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=date;host;p, Signature=debf546796015d6f6ded8626f5ce98597c33b47b9164cf6b17b4642036fcb592", "host": "host.foo.com", "date": "Mon, 09 Sep 2011 23:36:00 GMT", "p": "phfft", }, }, ), # POST request with body and no header (AWS botocore tests). # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/post-x-www-form-urlencoded.req # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/post-x-www-form-urlencoded.sreq ( "us-east-1", "2011-09-09T23:36:00Z", { "access_key_id": "AKIDEXAMPLE", "secret_access_key": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY", }, { "method": "POST", "url": "https://host.foo.com/", "headers": { "Content-Type": "application/x-www-form-urlencoded", "date": "Mon, 09 Sep 2011 23:36:00 GMT", }, "data": "foo=bar", }, { "url": "https://host.foo.com/", "method": "POST", "headers": { "Authorization": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=content-type;date;host, Signature=5a15b22cf462f047318703b92e6f4f38884e4a7ab7b1d6426ca46a8bd1c26cbc", "host": "host.foo.com", "Content-Type": "application/x-www-form-urlencoded", "date": "Mon, 09 Sep 2011 23:36:00 GMT", }, "data": "foo=bar", }, ), # POST request with querystring (AWS botocore tests). # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/post-vanilla-query.req # https://github.com/boto/botocore/blob/879f8440a4e9ace5d3cf145ce8b3d5e5ffb892ef/tests/unit/auth/aws4_testsuite/post-vanilla-query.sreq ( "us-east-1", "2011-09-09T23:36:00Z", { "access_key_id": "AKIDEXAMPLE", "secret_access_key": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY", }, { "method": "POST", "url": "https://host.foo.com/?foo=bar", "headers": {"date": "Mon, 09 Sep 2011 23:36:00 GMT"}, }, { "url": "https://host.foo.com/?foo=bar", "method": "POST", "headers": { "Authorization": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/host/aws4_request, SignedHeaders=date;host, Signature=b6e3b79003ce0743a491606ba1035a804593b0efb1e20a11cba83f8c25a57a92", "host": "host.foo.com", "date": "Mon, 09 Sep 2011 23:36:00 GMT", }, }, ), # GET request with session token credentials. ( "us-east-2", "2020-08-11T06:55:22Z", { "access_key_id": ACCESS_KEY_ID, "secret_access_key": SECRET_ACCESS_KEY, "security_token": TOKEN, }, { "method": "GET", "url": "https://ec2.us-east-2.amazonaws.com?Action=DescribeRegions&Version=2013-10-15", }, { "url": "https://ec2.us-east-2.amazonaws.com?Action=DescribeRegions&Version=2013-10-15", "method": "GET", "headers": { "Authorization": "AWS4-HMAC-SHA256 Credential=" + ACCESS_KEY_ID + "/20200811/us-east-2/ec2/aws4_request, SignedHeaders=host;x-amz-date;x-amz-security-token, Signature=41e226f997bf917ec6c9b2b14218df0874225f13bb153236c247881e614fafc9", "host": "ec2.us-east-2.amazonaws.com", "x-amz-date": "20200811T065522Z", "x-amz-security-token": TOKEN, }, }, ), # POST request with session token credentials. ( "us-east-2", "2020-08-11T06:55:22Z", { "access_key_id": ACCESS_KEY_ID, "secret_access_key": SECRET_ACCESS_KEY, "security_token": TOKEN, }, { "method": "POST", "url": "https://sts.us-east-2.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15", }, { "url": "https://sts.us-east-2.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15", "method": "POST", "headers": { "Authorization": "AWS4-HMAC-SHA256 Credential=" + ACCESS_KEY_ID + "/20200811/us-east-2/sts/aws4_request, SignedHeaders=host;x-amz-date;x-amz-security-token, Signature=596aa990b792d763465d73703e684ca273c45536c6d322c31be01a41d02e5b60", "host": "sts.us-east-2.amazonaws.com", "x-amz-date": "20200811T065522Z", "x-amz-security-token": TOKEN, }, }, ), # POST request with computed x-amz-date and no data. ( "us-east-2", "2020-08-11T06:55:22Z", {"access_key_id": ACCESS_KEY_ID, "secret_access_key": SECRET_ACCESS_KEY}, { "method": "POST", "url": "https://sts.us-east-2.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15", }, { "url": "https://sts.us-east-2.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15", "method": "POST", "headers": { "Authorization": "AWS4-HMAC-SHA256 Credential=" + ACCESS_KEY_ID + "/20200811/us-east-2/sts/aws4_request, SignedHeaders=host;x-amz-date, Signature=9e722e5b7bfa163447e2a14df118b45ebd283c5aea72019bdf921d6e7dc01a9a", "host": "sts.us-east-2.amazonaws.com", "x-amz-date": "20200811T065522Z", }, }, ), # POST request with session token and additional headers/data. ( "us-east-2", "2020-08-11T06:55:22Z", { "access_key_id": ACCESS_KEY_ID, "secret_access_key": SECRET_ACCESS_KEY, "security_token": TOKEN, }, { "method": "POST", "url": "https://dynamodb.us-east-2.amazonaws.com/", "headers": { "Content-Type": "application/x-amz-json-1.0", "x-amz-target": "DynamoDB_20120810.CreateTable", }, "data": REQUEST_PARAMS, }, { "url": "https://dynamodb.us-east-2.amazonaws.com/", "method": "POST", "headers": { "Authorization": "AWS4-HMAC-SHA256 Credential=" + ACCESS_KEY_ID + "/20200811/us-east-2/dynamodb/aws4_request, SignedHeaders=content-type;host;x-amz-date;x-amz-security-token;x-amz-target, Signature=eb8bce0e63654bba672d4a8acb07e72d69210c1797d56ce024dbbc31beb2a2c7", "host": "dynamodb.us-east-2.amazonaws.com", "x-amz-date": "20200811T065522Z", "Content-Type": "application/x-amz-json-1.0", "x-amz-target": "DynamoDB_20120810.CreateTable", "x-amz-security-token": TOKEN, }, "data": REQUEST_PARAMS, }, ), ] class TestRequestSigner(object): @pytest.mark.parametrize( "region, time, credentials, original_request, signed_request", TEST_FIXTURES ) @mock.patch("google.auth._helpers.utcnow") def test_get_request_options( self, utcnow, region, time, credentials, original_request, signed_request ): utcnow.return_value = datetime.datetime.strptime(time, "%Y-%m-%dT%H:%M:%SZ") request_signer = aws.RequestSigner(region) actual_signed_request = request_signer.get_request_options( credentials, original_request.get("url"), original_request.get("method"), original_request.get("data"), original_request.get("headers"), ) assert actual_signed_request == signed_request def test_get_request_options_with_missing_scheme_url(self): request_signer = aws.RequestSigner("us-east-2") with pytest.raises(ValueError) as excinfo: request_signer.get_request_options( { "access_key_id": ACCESS_KEY_ID, "secret_access_key": SECRET_ACCESS_KEY, }, "invalid", "POST", ) assert excinfo.match(r"Invalid AWS service URL") def test_get_request_options_with_invalid_scheme_url(self): request_signer = aws.RequestSigner("us-east-2") with pytest.raises(ValueError) as excinfo: request_signer.get_request_options( { "access_key_id": ACCESS_KEY_ID, "secret_access_key": SECRET_ACCESS_KEY, }, "http://invalid", "POST", ) assert excinfo.match(r"Invalid AWS service URL") def test_get_request_options_with_missing_hostname_url(self): request_signer = aws.RequestSigner("us-east-2") with pytest.raises(ValueError) as excinfo: request_signer.get_request_options( { "access_key_id": ACCESS_KEY_ID, "secret_access_key": SECRET_ACCESS_KEY, }, "https://", "POST", ) assert excinfo.match(r"Invalid AWS service URL") class TestCredentials(object): AWS_REGION = "us-east-2" AWS_ROLE = "gcp-aws-role" AWS_SECURITY_CREDENTIALS_RESPONSE = { "AccessKeyId": ACCESS_KEY_ID, "SecretAccessKey": SECRET_ACCESS_KEY, "Token": TOKEN, } AWS_IMDSV2_SESSION_TOKEN = "awsimdsv2sessiontoken" AWS_SIGNATURE_TIME = "2020-08-11T06:55:22Z" CREDENTIAL_SOURCE = { "environment_id": "aws1", "region_url": REGION_URL, "url": SECURITY_CREDS_URL, "regional_cred_verification_url": CRED_VERIFICATION_URL, } CREDENTIAL_SOURCE_IPV6 = { "environment_id": "aws1", "region_url": REGION_URL_IPV6, "url": SECURITY_CREDS_URL_IPV6, "regional_cred_verification_url": CRED_VERIFICATION_URL, "imdsv2_session_token_url": IMDSV2_SESSION_TOKEN_URL_IPV6, } SUCCESS_RESPONSE = { "access_token": "ACCESS_TOKEN", "issued_token_type": "urn:ietf:params:oauth:token-type:access_token", "token_type": "Bearer", "expires_in": 3600, "scope": " ".join(SCOPES), } @classmethod def make_serialized_aws_signed_request( cls, aws_security_credentials, region_name="us-east-2", url="https://sts.us-east-2.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15", ): """Utility to generate serialize AWS signed requests. This makes it easy to assert generated subject tokens based on the provided AWS security credentials, regions and AWS STS endpoint. """ request_signer = aws.RequestSigner(region_name) signed_request = request_signer.get_request_options( aws_security_credentials, url, "POST" ) reformatted_signed_request = { "url": signed_request.get("url"), "method": signed_request.get("method"), "headers": [ { "key": "Authorization", "value": signed_request.get("headers").get("Authorization"), }, {"key": "host", "value": signed_request.get("headers").get("host")}, { "key": "x-amz-date", "value": signed_request.get("headers").get("x-amz-date"), }, ], } # Include security token if available. if "security_token" in aws_security_credentials: reformatted_signed_request.get("headers").append( { "key": "x-amz-security-token", "value": signed_request.get("headers").get("x-amz-security-token"), } ) # Append x-goog-cloud-target-resource header. reformatted_signed_request.get("headers").append( {"key": "x-goog-cloud-target-resource", "value": AUDIENCE} ), return urllib.parse.quote( json.dumps( reformatted_signed_request, separators=(",", ":"), sort_keys=True ) ) @classmethod def make_mock_request( cls, region_status=None, region_name=None, role_status=None, role_name=None, security_credentials_status=None, security_credentials_data=None, token_status=None, token_data=None, impersonation_status=None, impersonation_data=None, imdsv2_session_token_status=None, imdsv2_session_token_data=None, ): """Utility function to generate a mock HTTP request object. This will facilitate testing various edge cases by specify how the various endpoints will respond while generating a Google Access token in an AWS environment. """ responses = [] if imdsv2_session_token_status: # AWS session token request imdsv2_session_response = mock.create_autospec( transport.Response, instance=True ) imdsv2_session_response.status = imdsv2_session_token_status imdsv2_session_response.data = imdsv2_session_token_data responses.append(imdsv2_session_response) if region_status: # AWS region request. region_response = mock.create_autospec(transport.Response, instance=True) region_response.status = region_status if region_name: region_response.data = "{}b".format(region_name).encode("utf-8") responses.append(region_response) if role_status: # AWS role name request. role_response = mock.create_autospec(transport.Response, instance=True) role_response.status = role_status if role_name: role_response.data = role_name.encode("utf-8") responses.append(role_response) if security_credentials_status: # AWS security credentials request. security_credentials_response = mock.create_autospec( transport.Response, instance=True ) security_credentials_response.status = security_credentials_status if security_credentials_data: security_credentials_response.data = json.dumps( security_credentials_data ).encode("utf-8") responses.append(security_credentials_response) if token_status: # GCP token exchange request. token_response = mock.create_autospec(transport.Response, instance=True) token_response.status = token_status token_response.data = json.dumps(token_data).encode("utf-8") responses.append(token_response) if impersonation_status: # Service account impersonation request. impersonation_response = mock.create_autospec( transport.Response, instance=True ) impersonation_response.status = impersonation_status impersonation_response.data = json.dumps(impersonation_data).encode("utf-8") responses.append(impersonation_response) request = mock.create_autospec(transport.Request) request.side_effect = responses return request @classmethod def make_credentials( cls, credential_source, token_url=TOKEN_URL, token_info_url=TOKEN_INFO_URL, client_id=None, client_secret=None, quota_project_id=None, scopes=None, default_scopes=None, service_account_impersonation_url=None, ): return aws.Credentials( audience=AUDIENCE, subject_token_type=SUBJECT_TOKEN_TYPE, token_url=token_url, token_info_url=token_info_url, service_account_impersonation_url=service_account_impersonation_url, credential_source=credential_source, client_id=client_id, client_secret=client_secret, quota_project_id=quota_project_id, scopes=scopes, default_scopes=default_scopes, ) @classmethod def assert_aws_metadata_request_kwargs( cls, request_kwargs, url, headers=None, method="GET" ): assert request_kwargs["url"] == url # All used AWS metadata server endpoints use GET HTTP method. assert request_kwargs["method"] == method if headers: assert request_kwargs["headers"] == headers else: assert "headers" not in request_kwargs or request_kwargs["headers"] is None # None of the endpoints used require any data in request. assert "body" not in request_kwargs @classmethod def assert_token_request_kwargs( cls, request_kwargs, headers, request_data, token_url=TOKEN_URL ): assert request_kwargs["url"] == token_url assert request_kwargs["method"] == "POST" assert request_kwargs["headers"] == headers assert request_kwargs["body"] is not None body_tuples = urllib.parse.parse_qsl(request_kwargs["body"]) assert len(body_tuples) == len(request_data.keys()) for (k, v) in body_tuples: assert v.decode("utf-8") == request_data[k.decode("utf-8")] @classmethod def assert_impersonation_request_kwargs( cls, request_kwargs, headers, request_data, service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL, ): assert request_kwargs["url"] == service_account_impersonation_url assert request_kwargs["method"] == "POST" assert request_kwargs["headers"] == headers assert request_kwargs["body"] is not None body_json = json.loads(request_kwargs["body"].decode("utf-8")) assert body_json == request_data @mock.patch.object(aws.Credentials, "__init__", return_value=None) def test_from_info_full_options(self, mock_init): credentials = aws.Credentials.from_info( { "audience": AUDIENCE, "subject_token_type": SUBJECT_TOKEN_TYPE, "token_url": TOKEN_URL, "token_info_url": TOKEN_INFO_URL, "service_account_impersonation_url": SERVICE_ACCOUNT_IMPERSONATION_URL, "service_account_impersonation": {"token_lifetime_seconds": 2800}, "client_id": CLIENT_ID, "client_secret": CLIENT_SECRET, "quota_project_id": QUOTA_PROJECT_ID, "credential_source": self.CREDENTIAL_SOURCE, } ) # Confirm aws.Credentials instance initialized with the expected parameters. assert isinstance(credentials, aws.Credentials) mock_init.assert_called_once_with( audience=AUDIENCE, subject_token_type=SUBJECT_TOKEN_TYPE, token_url=TOKEN_URL, token_info_url=TOKEN_INFO_URL, service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL, service_account_impersonation_options={"token_lifetime_seconds": 2800}, client_id=CLIENT_ID, client_secret=CLIENT_SECRET, credential_source=self.CREDENTIAL_SOURCE, quota_project_id=QUOTA_PROJECT_ID, workforce_pool_user_project=None, universe_domain=DEFAULT_UNIVERSE_DOMAIN, ) @mock.patch.object(aws.Credentials, "__init__", return_value=None) def test_from_info_required_options_only(self, mock_init): credentials = aws.Credentials.from_info( { "audience": AUDIENCE, "subject_token_type": SUBJECT_TOKEN_TYPE, "token_url": TOKEN_URL, "credential_source": self.CREDENTIAL_SOURCE, } ) # Confirm aws.Credentials instance initialized with the expected parameters. assert isinstance(credentials, aws.Credentials) mock_init.assert_called_once_with( audience=AUDIENCE, subject_token_type=SUBJECT_TOKEN_TYPE, token_url=TOKEN_URL, token_info_url=None, service_account_impersonation_url=None, service_account_impersonation_options={}, client_id=None, client_secret=None, credential_source=self.CREDENTIAL_SOURCE, quota_project_id=None, workforce_pool_user_project=None, universe_domain=DEFAULT_UNIVERSE_DOMAIN, ) @mock.patch.object(aws.Credentials, "__init__", return_value=None) def test_from_file_full_options(self, mock_init, tmpdir): info = { "audience": AUDIENCE, "subject_token_type": SUBJECT_TOKEN_TYPE, "token_url": TOKEN_URL, "token_info_url": TOKEN_INFO_URL, "service_account_impersonation_url": SERVICE_ACCOUNT_IMPERSONATION_URL, "service_account_impersonation": {"token_lifetime_seconds": 2800}, "client_id": CLIENT_ID, "client_secret": CLIENT_SECRET, "quota_project_id": QUOTA_PROJECT_ID, "credential_source": self.CREDENTIAL_SOURCE, "universe_domain": DEFAULT_UNIVERSE_DOMAIN, } config_file = tmpdir.join("config.json") config_file.write(json.dumps(info)) credentials = aws.Credentials.from_file(str(config_file)) # Confirm aws.Credentials instance initialized with the expected parameters. assert isinstance(credentials, aws.Credentials) mock_init.assert_called_once_with( audience=AUDIENCE, subject_token_type=SUBJECT_TOKEN_TYPE, token_url=TOKEN_URL, token_info_url=TOKEN_INFO_URL, service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL, service_account_impersonation_options={"token_lifetime_seconds": 2800}, client_id=CLIENT_ID, client_secret=CLIENT_SECRET, credential_source=self.CREDENTIAL_SOURCE, quota_project_id=QUOTA_PROJECT_ID, workforce_pool_user_project=None, universe_domain=DEFAULT_UNIVERSE_DOMAIN, ) @mock.patch.object(aws.Credentials, "__init__", return_value=None) def test_from_file_required_options_only(self, mock_init, tmpdir): info = { "audience": AUDIENCE, "subject_token_type": SUBJECT_TOKEN_TYPE, "token_url": TOKEN_URL, "credential_source": self.CREDENTIAL_SOURCE, } config_file = tmpdir.join("config.json") config_file.write(json.dumps(info)) credentials = aws.Credentials.from_file(str(config_file)) # Confirm aws.Credentials instance initialized with the expected parameters. assert isinstance(credentials, aws.Credentials) mock_init.assert_called_once_with( audience=AUDIENCE, subject_token_type=SUBJECT_TOKEN_TYPE, token_url=TOKEN_URL, token_info_url=None, service_account_impersonation_url=None, service_account_impersonation_options={}, client_id=None, client_secret=None, credential_source=self.CREDENTIAL_SOURCE, quota_project_id=None, workforce_pool_user_project=None, universe_domain=DEFAULT_UNIVERSE_DOMAIN, ) def test_constructor_invalid_credential_source(self): # Provide invalid credential source. credential_source = {"unsupported": "value"} with pytest.raises(ValueError) as excinfo: self.make_credentials(credential_source=credential_source) assert excinfo.match(r"No valid AWS 'credential_source' provided") def test_constructor_invalid_environment_id(self): # Provide invalid environment_id. credential_source = self.CREDENTIAL_SOURCE.copy() credential_source["environment_id"] = "azure1" with pytest.raises(ValueError) as excinfo: self.make_credentials(credential_source=credential_source) assert excinfo.match(r"No valid AWS 'credential_source' provided") def test_constructor_missing_cred_verification_url(self): # regional_cred_verification_url is a required field. credential_source = self.CREDENTIAL_SOURCE.copy() credential_source.pop("regional_cred_verification_url") with pytest.raises(ValueError) as excinfo: self.make_credentials(credential_source=credential_source) assert excinfo.match(r"No valid AWS 'credential_source' provided") def test_constructor_invalid_environment_id_version(self): # Provide an unsupported version. credential_source = self.CREDENTIAL_SOURCE.copy() credential_source["environment_id"] = "aws3" with pytest.raises(ValueError) as excinfo: self.make_credentials(credential_source=credential_source) assert excinfo.match(r"aws version '3' is not supported in the current build.") def test_info(self): credentials = self.make_credentials( credential_source=self.CREDENTIAL_SOURCE.copy() ) assert credentials.info == { "type": "external_account", "audience": AUDIENCE, "subject_token_type": SUBJECT_TOKEN_TYPE, "token_url": TOKEN_URL, "token_info_url": TOKEN_INFO_URL, "credential_source": self.CREDENTIAL_SOURCE, "universe_domain": DEFAULT_UNIVERSE_DOMAIN, } def test_token_info_url(self): credentials = self.make_credentials( credential_source=self.CREDENTIAL_SOURCE.copy() ) assert credentials.token_info_url == TOKEN_INFO_URL def test_token_info_url_custom(self): for url in VALID_TOKEN_URLS: credentials = self.make_credentials( credential_source=self.CREDENTIAL_SOURCE.copy(), token_info_url=(url + "/introspect"), ) assert credentials.token_info_url == (url + "/introspect") def test_token_info_url_negative(self): credentials = self.make_credentials( credential_source=self.CREDENTIAL_SOURCE.copy(), token_info_url=None ) assert not credentials.token_info_url def test_token_url_custom(self): for url in VALID_TOKEN_URLS: credentials = self.make_credentials( credential_source=self.CREDENTIAL_SOURCE.copy(), token_url=(url + "/token"), ) assert credentials._token_url == (url + "/token") def test_service_account_impersonation_url_custom(self): for url in VALID_SERVICE_ACCOUNT_IMPERSONATION_URLS: credentials = self.make_credentials( credential_source=self.CREDENTIAL_SOURCE.copy(), service_account_impersonation_url=( url + SERVICE_ACCOUNT_IMPERSONATION_URL_ROUTE ), ) assert credentials._service_account_impersonation_url == ( url + SERVICE_ACCOUNT_IMPERSONATION_URL_ROUTE ) def test_retrieve_subject_token_missing_region_url(self): # When AWS_REGION envvar is not available, region_url is required for # determining the current AWS region. credential_source = self.CREDENTIAL_SOURCE.copy() credential_source.pop("region_url") credentials = self.make_credentials(credential_source=credential_source) with pytest.raises(exceptions.RefreshError) as excinfo: credentials.retrieve_subject_token(None) assert excinfo.match(r"Unable to determine AWS region") @mock.patch("google.auth._helpers.utcnow") def test_retrieve_subject_token_success_temp_creds_no_environment_vars( self, utcnow ): utcnow.return_value = datetime.datetime.strptime( self.AWS_SIGNATURE_TIME, "%Y-%m-%dT%H:%M:%SZ" ) request = self.make_mock_request( region_status=http_client.OK, region_name=self.AWS_REGION, role_status=http_client.OK, role_name=self.AWS_ROLE, security_credentials_status=http_client.OK, security_credentials_data=self.AWS_SECURITY_CREDENTIALS_RESPONSE, ) credentials = self.make_credentials(credential_source=self.CREDENTIAL_SOURCE) subject_token = credentials.retrieve_subject_token(request) assert subject_token == self.make_serialized_aws_signed_request( { "access_key_id": ACCESS_KEY_ID, "secret_access_key": SECRET_ACCESS_KEY, "security_token": TOKEN, } ) # Assert region request. self.assert_aws_metadata_request_kwargs( request.call_args_list[0][1], REGION_URL ) # Assert role request. self.assert_aws_metadata_request_kwargs( request.call_args_list[1][1], SECURITY_CREDS_URL ) # Assert security credentials request. self.assert_aws_metadata_request_kwargs( request.call_args_list[2][1], "{}/{}".format(SECURITY_CREDS_URL, self.AWS_ROLE), {"Content-Type": "application/json"}, ) # Retrieve subject_token again. Region should not be queried again. new_request = self.make_mock_request( role_status=http_client.OK, role_name=self.AWS_ROLE, security_credentials_status=http_client.OK, security_credentials_data=self.AWS_SECURITY_CREDENTIALS_RESPONSE, ) credentials.retrieve_subject_token(new_request) # Only 3 requests should be sent as the region is cached. assert len(new_request.call_args_list) == 2 # Assert role request. self.assert_aws_metadata_request_kwargs( new_request.call_args_list[0][1], SECURITY_CREDS_URL ) # Assert security credentials request. self.assert_aws_metadata_request_kwargs( new_request.call_args_list[1][1], "{}/{}".format(SECURITY_CREDS_URL, self.AWS_ROLE), {"Content-Type": "application/json"}, ) @mock.patch("google.auth._helpers.utcnow") @mock.patch.dict(os.environ, {}) def test_retrieve_subject_token_success_temp_creds_no_environment_vars_idmsv2( self, utcnow ): utcnow.return_value = datetime.datetime.strptime( self.AWS_SIGNATURE_TIME, "%Y-%m-%dT%H:%M:%SZ" ) request = self.make_mock_request( region_status=http_client.OK, region_name=self.AWS_REGION, role_status=http_client.OK, role_name=self.AWS_ROLE, security_credentials_status=http_client.OK, security_credentials_data=self.AWS_SECURITY_CREDENTIALS_RESPONSE, imdsv2_session_token_status=http_client.OK, imdsv2_session_token_data=self.AWS_IMDSV2_SESSION_TOKEN, ) credential_source_token_url = self.CREDENTIAL_SOURCE.copy() credential_source_token_url[ "imdsv2_session_token_url" ] = IMDSV2_SESSION_TOKEN_URL credentials = self.make_credentials( credential_source=credential_source_token_url ) subject_token = credentials.retrieve_subject_token(request) assert subject_token == self.make_serialized_aws_signed_request( { "access_key_id": ACCESS_KEY_ID, "secret_access_key": SECRET_ACCESS_KEY, "security_token": TOKEN, } ) # Assert session token request self.assert_aws_metadata_request_kwargs( request.call_args_list[0][1], IMDSV2_SESSION_TOKEN_URL, {"X-aws-ec2-metadata-token-ttl-seconds": "300"}, "PUT", ) # Assert region request. self.assert_aws_metadata_request_kwargs( request.call_args_list[1][1], REGION_URL, {"X-aws-ec2-metadata-token": self.AWS_IMDSV2_SESSION_TOKEN}, ) # Assert role request. self.assert_aws_metadata_request_kwargs( request.call_args_list[2][1], SECURITY_CREDS_URL, {"X-aws-ec2-metadata-token": self.AWS_IMDSV2_SESSION_TOKEN}, ) # Assert security credentials request. self.assert_aws_metadata_request_kwargs( request.call_args_list[3][1], "{}/{}".format(SECURITY_CREDS_URL, self.AWS_ROLE), { "Content-Type": "application/json", "X-aws-ec2-metadata-token": self.AWS_IMDSV2_SESSION_TOKEN, }, ) # Retrieve subject_token again. Region should not be queried again. new_request = self.make_mock_request( role_status=http_client.OK, role_name=self.AWS_ROLE, security_credentials_status=http_client.OK, security_credentials_data=self.AWS_SECURITY_CREDENTIALS_RESPONSE, imdsv2_session_token_status=http_client.OK, imdsv2_session_token_data=self.AWS_IMDSV2_SESSION_TOKEN, ) credentials.retrieve_subject_token(new_request) # Only 3 requests should be sent as the region is cached. assert len(new_request.call_args_list) == 3 # Assert session token request. self.assert_aws_metadata_request_kwargs( request.call_args_list[0][1], IMDSV2_SESSION_TOKEN_URL, {"X-aws-ec2-metadata-token-ttl-seconds": "300"}, "PUT", ) # Assert role request. self.assert_aws_metadata_request_kwargs( new_request.call_args_list[1][1], SECURITY_CREDS_URL, {"X-aws-ec2-metadata-token": self.AWS_IMDSV2_SESSION_TOKEN}, ) # Assert security credentials request. self.assert_aws_metadata_request_kwargs( new_request.call_args_list[2][1], "{}/{}".format(SECURITY_CREDS_URL, self.AWS_ROLE), { "Content-Type": "application/json", "X-aws-ec2-metadata-token": self.AWS_IMDSV2_SESSION_TOKEN, }, ) @mock.patch("google.auth._helpers.utcnow") @mock.patch.dict( os.environ, { environment_vars.AWS_REGION: AWS_REGION, environment_vars.AWS_ACCESS_KEY_ID: ACCESS_KEY_ID, }, ) def test_retrieve_subject_token_success_temp_creds_environment_vars_missing_secret_access_key_idmsv2( self, utcnow ): utcnow.return_value = datetime.datetime.strptime( self.AWS_SIGNATURE_TIME, "%Y-%m-%dT%H:%M:%SZ" ) request = self.make_mock_request( role_status=http_client.OK, role_name=self.AWS_ROLE, security_credentials_status=http_client.OK, security_credentials_data=self.AWS_SECURITY_CREDENTIALS_RESPONSE, imdsv2_session_token_status=http_client.OK, imdsv2_session_token_data=self.AWS_IMDSV2_SESSION_TOKEN, ) credential_source_token_url = self.CREDENTIAL_SOURCE.copy() credential_source_token_url[ "imdsv2_session_token_url" ] = IMDSV2_SESSION_TOKEN_URL credentials = self.make_credentials( credential_source=credential_source_token_url ) subject_token = credentials.retrieve_subject_token(request) assert subject_token == self.make_serialized_aws_signed_request( { "access_key_id": ACCESS_KEY_ID, "secret_access_key": SECRET_ACCESS_KEY, "security_token": TOKEN, } ) # Assert session token request. self.assert_aws_metadata_request_kwargs( request.call_args_list[0][1], IMDSV2_SESSION_TOKEN_URL, {"X-aws-ec2-metadata-token-ttl-seconds": "300"}, "PUT", ) # Assert role request. self.assert_aws_metadata_request_kwargs( request.call_args_list[1][1], SECURITY_CREDS_URL, {"X-aws-ec2-metadata-token": self.AWS_IMDSV2_SESSION_TOKEN}, ) # Assert security credentials request. self.assert_aws_metadata_request_kwargs( request.call_args_list[2][1], "{}/{}".format(SECURITY_CREDS_URL, self.AWS_ROLE), { "Content-Type": "application/json", "X-aws-ec2-metadata-token": self.AWS_IMDSV2_SESSION_TOKEN, }, ) @mock.patch("google.auth._helpers.utcnow") @mock.patch.dict( os.environ, { environment_vars.AWS_REGION: AWS_REGION, environment_vars.AWS_SECRET_ACCESS_KEY: SECRET_ACCESS_KEY, }, ) def test_retrieve_subject_token_success_temp_creds_environment_vars_missing_access_key_id_idmsv2( self, utcnow ): utcnow.return_value = datetime.datetime.strptime( self.AWS_SIGNATURE_TIME, "%Y-%m-%dT%H:%M:%SZ" ) request = self.make_mock_request( role_status=http_client.OK, role_name=self.AWS_ROLE, security_credentials_status=http_client.OK, security_credentials_data=self.AWS_SECURITY_CREDENTIALS_RESPONSE, imdsv2_session_token_status=http_client.OK, imdsv2_session_token_data=self.AWS_IMDSV2_SESSION_TOKEN, ) credential_source_token_url = self.CREDENTIAL_SOURCE.copy() credential_source_token_url[ "imdsv2_session_token_url" ] = IMDSV2_SESSION_TOKEN_URL credentials = self.make_credentials( credential_source=credential_source_token_url ) subject_token = credentials.retrieve_subject_token(request) assert subject_token == self.make_serialized_aws_signed_request( { "access_key_id": ACCESS_KEY_ID, "secret_access_key": SECRET_ACCESS_KEY, "security_token": TOKEN, } ) # Assert session token request. self.assert_aws_metadata_request_kwargs( request.call_args_list[0][1], IMDSV2_SESSION_TOKEN_URL, {"X-aws-ec2-metadata-token-ttl-seconds": "300"}, "PUT", ) # Assert role request. self.assert_aws_metadata_request_kwargs( request.call_args_list[1][1], SECURITY_CREDS_URL, {"X-aws-ec2-metadata-token": self.AWS_IMDSV2_SESSION_TOKEN}, ) # Assert security credentials request. self.assert_aws_metadata_request_kwargs( request.call_args_list[2][1], "{}/{}".format(SECURITY_CREDS_URL, self.AWS_ROLE), { "Content-Type": "application/json", "X-aws-ec2-metadata-token": self.AWS_IMDSV2_SESSION_TOKEN, }, ) @mock.patch("google.auth._helpers.utcnow") @mock.patch.dict(os.environ, {environment_vars.AWS_REGION: AWS_REGION}) def test_retrieve_subject_token_success_temp_creds_environment_vars_missing_creds_idmsv2( self, utcnow ): utcnow.return_value = datetime.datetime.strptime( self.AWS_SIGNATURE_TIME, "%Y-%m-%dT%H:%M:%SZ" ) request = self.make_mock_request( role_status=http_client.OK, role_name=self.AWS_ROLE, security_credentials_status=http_client.OK, security_credentials_data=self.AWS_SECURITY_CREDENTIALS_RESPONSE, imdsv2_session_token_status=http_client.OK, imdsv2_session_token_data=self.AWS_IMDSV2_SESSION_TOKEN, ) credential_source_token_url = self.CREDENTIAL_SOURCE.copy() credential_source_token_url[ "imdsv2_session_token_url" ] = IMDSV2_SESSION_TOKEN_URL credentials = self.make_credentials( credential_source=credential_source_token_url ) subject_token = credentials.retrieve_subject_token(request) assert subject_token == self.make_serialized_aws_signed_request( { "access_key_id": ACCESS_KEY_ID, "secret_access_key": SECRET_ACCESS_KEY, "security_token": TOKEN, } ) # Assert session token request. self.assert_aws_metadata_request_kwargs( request.call_args_list[0][1], IMDSV2_SESSION_TOKEN_URL, {"X-aws-ec2-metadata-token-ttl-seconds": "300"}, "PUT", ) # Assert role request. self.assert_aws_metadata_request_kwargs( request.call_args_list[1][1], SECURITY_CREDS_URL, {"X-aws-ec2-metadata-token": self.AWS_IMDSV2_SESSION_TOKEN}, ) # Assert security credentials request. self.assert_aws_metadata_request_kwargs( request.call_args_list[2][1], "{}/{}".format(SECURITY_CREDS_URL, self.AWS_ROLE), { "Content-Type": "application/json", "X-aws-ec2-metadata-token": self.AWS_IMDSV2_SESSION_TOKEN, }, ) @mock.patch("google.auth._helpers.utcnow") @mock.patch.dict( os.environ, { environment_vars.AWS_REGION: AWS_REGION, environment_vars.AWS_ACCESS_KEY_ID: ACCESS_KEY_ID, environment_vars.AWS_SECRET_ACCESS_KEY: SECRET_ACCESS_KEY, }, ) def test_retrieve_subject_token_success_temp_creds_idmsv2(self, utcnow): utcnow.return_value = datetime.datetime.strptime( self.AWS_SIGNATURE_TIME, "%Y-%m-%dT%H:%M:%SZ" ) request = self.make_mock_request( role_status=http_client.OK, role_name=self.AWS_ROLE ) credential_source_token_url = self.CREDENTIAL_SOURCE.copy() credential_source_token_url[ "imdsv2_session_token_url" ] = IMDSV2_SESSION_TOKEN_URL credentials = self.make_credentials( credential_source=credential_source_token_url ) credentials.retrieve_subject_token(request) assert not request.called @mock.patch("google.auth._helpers.utcnow") def test_retrieve_subject_token_success_ipv6(self, utcnow): utcnow.return_value = datetime.datetime.strptime( self.AWS_SIGNATURE_TIME, "%Y-%m-%dT%H:%M:%SZ" ) request = self.make_mock_request( region_status=http_client.OK, region_name=self.AWS_REGION, role_status=http_client.OK, role_name=self.AWS_ROLE, security_credentials_status=http_client.OK, security_credentials_data=self.AWS_SECURITY_CREDENTIALS_RESPONSE, imdsv2_session_token_status=http_client.OK, imdsv2_session_token_data=self.AWS_IMDSV2_SESSION_TOKEN, ) credential_source_token_url = self.CREDENTIAL_SOURCE_IPV6.copy() credentials = self.make_credentials( credential_source=credential_source_token_url ) subject_token = credentials.retrieve_subject_token(request) assert subject_token == self.make_serialized_aws_signed_request( { "access_key_id": ACCESS_KEY_ID, "secret_access_key": SECRET_ACCESS_KEY, "security_token": TOKEN, } ) # Assert session token request. self.assert_aws_metadata_request_kwargs( request.call_args_list[0][1], IMDSV2_SESSION_TOKEN_URL_IPV6, {"X-aws-ec2-metadata-token-ttl-seconds": "300"}, "PUT", ) # Assert region request. self.assert_aws_metadata_request_kwargs( request.call_args_list[1][1], REGION_URL_IPV6, {"X-aws-ec2-metadata-token": self.AWS_IMDSV2_SESSION_TOKEN}, ) # Assert role request. self.assert_aws_metadata_request_kwargs( request.call_args_list[2][1], SECURITY_CREDS_URL_IPV6, {"X-aws-ec2-metadata-token": self.AWS_IMDSV2_SESSION_TOKEN}, ) # Assert security credentials request. self.assert_aws_metadata_request_kwargs( request.call_args_list[3][1], "{}/{}".format(SECURITY_CREDS_URL_IPV6, self.AWS_ROLE), { "Content-Type": "application/json", "X-aws-ec2-metadata-token": self.AWS_IMDSV2_SESSION_TOKEN, }, ) @mock.patch("google.auth._helpers.utcnow") def test_retrieve_subject_token_session_error_idmsv2(self, utcnow): utcnow.return_value = datetime.datetime.strptime( self.AWS_SIGNATURE_TIME, "%Y-%m-%dT%H:%M:%SZ" ) request = self.make_mock_request( imdsv2_session_token_status=http_client.UNAUTHORIZED, imdsv2_session_token_data="unauthorized", ) credential_source_token_url = self.CREDENTIAL_SOURCE.copy() credential_source_token_url[ "imdsv2_session_token_url" ] = IMDSV2_SESSION_TOKEN_URL credentials = self.make_credentials( credential_source=credential_source_token_url ) with pytest.raises(exceptions.RefreshError) as excinfo: credentials.retrieve_subject_token(request) assert excinfo.match(r"Unable to retrieve AWS Session Token") # Assert session token request self.assert_aws_metadata_request_kwargs( request.call_args_list[0][1], IMDSV2_SESSION_TOKEN_URL, {"X-aws-ec2-metadata-token-ttl-seconds": "300"}, "PUT", ) @mock.patch("google.auth._helpers.utcnow") def test_retrieve_subject_token_success_permanent_creds_no_environment_vars( self, utcnow ): # Simualte a permanent credential without a session token is # returned by the security-credentials endpoint. security_creds_response = self.AWS_SECURITY_CREDENTIALS_RESPONSE.copy() security_creds_response.pop("Token") utcnow.return_value = datetime.datetime.strptime( self.AWS_SIGNATURE_TIME, "%Y-%m-%dT%H:%M:%SZ" ) request = self.make_mock_request( region_status=http_client.OK, region_name=self.AWS_REGION, role_status=http_client.OK, role_name=self.AWS_ROLE, security_credentials_status=http_client.OK, security_credentials_data=security_creds_response, ) credentials = self.make_credentials(credential_source=self.CREDENTIAL_SOURCE) subject_token = credentials.retrieve_subject_token(request) assert subject_token == self.make_serialized_aws_signed_request( {"access_key_id": ACCESS_KEY_ID, "secret_access_key": SECRET_ACCESS_KEY} ) @mock.patch("google.auth._helpers.utcnow") def test_retrieve_subject_token_success_environment_vars(self, utcnow, monkeypatch): monkeypatch.setenv(environment_vars.AWS_ACCESS_KEY_ID, ACCESS_KEY_ID) monkeypatch.setenv(environment_vars.AWS_SECRET_ACCESS_KEY, SECRET_ACCESS_KEY) monkeypatch.setenv(environment_vars.AWS_SESSION_TOKEN, TOKEN) monkeypatch.setenv(environment_vars.AWS_REGION, self.AWS_REGION) utcnow.return_value = datetime.datetime.strptime( self.AWS_SIGNATURE_TIME, "%Y-%m-%dT%H:%M:%SZ" ) credentials = self.make_credentials(credential_source=self.CREDENTIAL_SOURCE) subject_token = credentials.retrieve_subject_token(None) assert subject_token == self.make_serialized_aws_signed_request( { "access_key_id": ACCESS_KEY_ID, "secret_access_key": SECRET_ACCESS_KEY, "security_token": TOKEN, } ) @mock.patch("google.auth._helpers.utcnow") def test_retrieve_subject_token_success_environment_vars_with_default_region( self, utcnow, monkeypatch ): monkeypatch.setenv(environment_vars.AWS_ACCESS_KEY_ID, ACCESS_KEY_ID) monkeypatch.setenv(environment_vars.AWS_SECRET_ACCESS_KEY, SECRET_ACCESS_KEY) monkeypatch.setenv(environment_vars.AWS_SESSION_TOKEN, TOKEN) monkeypatch.setenv(environment_vars.AWS_DEFAULT_REGION, self.AWS_REGION) utcnow.return_value = datetime.datetime.strptime( self.AWS_SIGNATURE_TIME, "%Y-%m-%dT%H:%M:%SZ" ) credentials = self.make_credentials(credential_source=self.CREDENTIAL_SOURCE) subject_token = credentials.retrieve_subject_token(None) assert subject_token == self.make_serialized_aws_signed_request( { "access_key_id": ACCESS_KEY_ID, "secret_access_key": SECRET_ACCESS_KEY, "security_token": TOKEN, } ) @mock.patch("google.auth._helpers.utcnow") def test_retrieve_subject_token_success_environment_vars_with_both_regions_set( self, utcnow, monkeypatch ): monkeypatch.setenv(environment_vars.AWS_ACCESS_KEY_ID, ACCESS_KEY_ID) monkeypatch.setenv(environment_vars.AWS_SECRET_ACCESS_KEY, SECRET_ACCESS_KEY) monkeypatch.setenv(environment_vars.AWS_SESSION_TOKEN, TOKEN) monkeypatch.setenv(environment_vars.AWS_DEFAULT_REGION, "Malformed AWS Region") # This test makes sure that the AWS_REGION gets used over AWS_DEFAULT_REGION, # So, AWS_DEFAULT_REGION is set to something that would cause the test to fail, # And AWS_REGION is set to the a valid value, and it should succeed monkeypatch.setenv(environment_vars.AWS_REGION, self.AWS_REGION) utcnow.return_value = datetime.datetime.strptime( self.AWS_SIGNATURE_TIME, "%Y-%m-%dT%H:%M:%SZ" ) credentials = self.make_credentials(credential_source=self.CREDENTIAL_SOURCE) subject_token = credentials.retrieve_subject_token(None) assert subject_token == self.make_serialized_aws_signed_request( { "access_key_id": ACCESS_KEY_ID, "secret_access_key": SECRET_ACCESS_KEY, "security_token": TOKEN, } ) @mock.patch("google.auth._helpers.utcnow") def test_retrieve_subject_token_success_environment_vars_no_session_token( self, utcnow, monkeypatch ): monkeypatch.setenv(environment_vars.AWS_ACCESS_KEY_ID, ACCESS_KEY_ID) monkeypatch.setenv(environment_vars.AWS_SECRET_ACCESS_KEY, SECRET_ACCESS_KEY) monkeypatch.setenv(environment_vars.AWS_REGION, self.AWS_REGION) utcnow.return_value = datetime.datetime.strptime( self.AWS_SIGNATURE_TIME, "%Y-%m-%dT%H:%M:%SZ" ) credentials = self.make_credentials(credential_source=self.CREDENTIAL_SOURCE) subject_token = credentials.retrieve_subject_token(None) assert subject_token == self.make_serialized_aws_signed_request( {"access_key_id": ACCESS_KEY_ID, "secret_access_key": SECRET_ACCESS_KEY} ) @mock.patch("google.auth._helpers.utcnow") def test_retrieve_subject_token_success_environment_vars_except_region( self, utcnow, monkeypatch ): monkeypatch.setenv(environment_vars.AWS_ACCESS_KEY_ID, ACCESS_KEY_ID) monkeypatch.setenv(environment_vars.AWS_SECRET_ACCESS_KEY, SECRET_ACCESS_KEY) monkeypatch.setenv(environment_vars.AWS_SESSION_TOKEN, TOKEN) utcnow.return_value = datetime.datetime.strptime( self.AWS_SIGNATURE_TIME, "%Y-%m-%dT%H:%M:%SZ" ) # Region will be queried since it is not found in envvars. request = self.make_mock_request( region_status=http_client.OK, region_name=self.AWS_REGION ) credentials = self.make_credentials(credential_source=self.CREDENTIAL_SOURCE) subject_token = credentials.retrieve_subject_token(request) assert subject_token == self.make_serialized_aws_signed_request( { "access_key_id": ACCESS_KEY_ID, "secret_access_key": SECRET_ACCESS_KEY, "security_token": TOKEN, } ) def test_retrieve_subject_token_error_determining_aws_region(self): # Simulate error in retrieving the AWS region. request = self.make_mock_request(region_status=http_client.BAD_REQUEST) credentials = self.make_credentials(credential_source=self.CREDENTIAL_SOURCE) with pytest.raises(exceptions.RefreshError) as excinfo: credentials.retrieve_subject_token(request) assert excinfo.match(r"Unable to retrieve AWS region") def test_retrieve_subject_token_error_determining_aws_role(self): # Simulate error in retrieving the AWS role name. request = self.make_mock_request( region_status=http_client.OK, region_name=self.AWS_REGION, role_status=http_client.BAD_REQUEST, ) credentials = self.make_credentials(credential_source=self.CREDENTIAL_SOURCE) with pytest.raises(exceptions.RefreshError) as excinfo: credentials.retrieve_subject_token(request) assert excinfo.match(r"Unable to retrieve AWS role name") def test_retrieve_subject_token_error_determining_security_creds_url(self): # Simulate the security-credentials url is missing. This is needed for # determining the AWS security credentials when not found in envvars. credential_source = self.CREDENTIAL_SOURCE.copy() credential_source.pop("url") request = self.make_mock_request( region_status=http_client.OK, region_name=self.AWS_REGION ) credentials = self.make_credentials(credential_source=credential_source) with pytest.raises(exceptions.RefreshError) as excinfo: credentials.retrieve_subject_token(request) assert excinfo.match( r"Unable to determine the AWS metadata server security credentials endpoint" ) def test_retrieve_subject_token_error_determining_aws_security_creds(self): # Simulate error in retrieving the AWS security credentials. request = self.make_mock_request( region_status=http_client.OK, region_name=self.AWS_REGION, role_status=http_client.OK, role_name=self.AWS_ROLE, security_credentials_status=http_client.BAD_REQUEST, ) credentials = self.make_credentials(credential_source=self.CREDENTIAL_SOURCE) with pytest.raises(exceptions.RefreshError) as excinfo: credentials.retrieve_subject_token(request) assert excinfo.match(r"Unable to retrieve AWS security credentials") @mock.patch( "google.auth.metrics.python_and_auth_lib_version", return_value=LANG_LIBRARY_METRICS_HEADER_VALUE, ) @mock.patch("google.auth._helpers.utcnow") def test_refresh_success_without_impersonation_ignore_default_scopes( self, utcnow, mock_auth_lib_value ): utcnow.return_value = datetime.datetime.strptime( self.AWS_SIGNATURE_TIME, "%Y-%m-%dT%H:%M:%SZ" ) expected_subject_token = self.make_serialized_aws_signed_request( { "access_key_id": ACCESS_KEY_ID, "secret_access_key": SECRET_ACCESS_KEY, "security_token": TOKEN, } ) token_headers = { "Content-Type": "application/x-www-form-urlencoded", "Authorization": "Basic " + BASIC_AUTH_ENCODING, "x-goog-api-client": "gl-python/3.7 auth/1.1 google-byoid-sdk sa-impersonation/false config-lifetime/false source/aws", } token_request_data = { "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange", "audience": AUDIENCE, "requested_token_type": "urn:ietf:params:oauth:token-type:access_token", "scope": " ".join(SCOPES), "subject_token": expected_subject_token, "subject_token_type": SUBJECT_TOKEN_TYPE, } request = self.make_mock_request( region_status=http_client.OK, region_name=self.AWS_REGION, role_status=http_client.OK, role_name=self.AWS_ROLE, security_credentials_status=http_client.OK, security_credentials_data=self.AWS_SECURITY_CREDENTIALS_RESPONSE, token_status=http_client.OK, token_data=self.SUCCESS_RESPONSE, ) credentials = self.make_credentials( client_id=CLIENT_ID, client_secret=CLIENT_SECRET, credential_source=self.CREDENTIAL_SOURCE, quota_project_id=QUOTA_PROJECT_ID, scopes=SCOPES, # Default scopes should be ignored. default_scopes=["ignored"], ) credentials.refresh(request) assert len(request.call_args_list) == 4 # Fourth request should be sent to GCP STS endpoint. self.assert_token_request_kwargs( request.call_args_list[3][1], token_headers, token_request_data ) assert credentials.token == self.SUCCESS_RESPONSE["access_token"] assert credentials.quota_project_id == QUOTA_PROJECT_ID assert credentials.scopes == SCOPES assert credentials.default_scopes == ["ignored"] @mock.patch( "google.auth.metrics.python_and_auth_lib_version", return_value=LANG_LIBRARY_METRICS_HEADER_VALUE, ) @mock.patch("google.auth._helpers.utcnow") def test_refresh_success_without_impersonation_use_default_scopes( self, utcnow, mock_auth_lib_value ): utcnow.return_value = datetime.datetime.strptime( self.AWS_SIGNATURE_TIME, "%Y-%m-%dT%H:%M:%SZ" ) expected_subject_token = self.make_serialized_aws_signed_request( { "access_key_id": ACCESS_KEY_ID, "secret_access_key": SECRET_ACCESS_KEY, "security_token": TOKEN, } ) token_headers = { "Content-Type": "application/x-www-form-urlencoded", "Authorization": "Basic " + BASIC_AUTH_ENCODING, "x-goog-api-client": "gl-python/3.7 auth/1.1 google-byoid-sdk sa-impersonation/false config-lifetime/false source/aws", } token_request_data = { "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange", "audience": AUDIENCE, "requested_token_type": "urn:ietf:params:oauth:token-type:access_token", "scope": " ".join(SCOPES), "subject_token": expected_subject_token, "subject_token_type": SUBJECT_TOKEN_TYPE, } request = self.make_mock_request( region_status=http_client.OK, region_name=self.AWS_REGION, role_status=http_client.OK, role_name=self.AWS_ROLE, security_credentials_status=http_client.OK, security_credentials_data=self.AWS_SECURITY_CREDENTIALS_RESPONSE, token_status=http_client.OK, token_data=self.SUCCESS_RESPONSE, ) credentials = self.make_credentials( client_id=CLIENT_ID, client_secret=CLIENT_SECRET, credential_source=self.CREDENTIAL_SOURCE, quota_project_id=QUOTA_PROJECT_ID, scopes=None, # Default scopes should be used since user specified scopes are none. default_scopes=SCOPES, ) credentials.refresh(request) assert len(request.call_args_list) == 4 # Fourth request should be sent to GCP STS endpoint. self.assert_token_request_kwargs( request.call_args_list[3][1], token_headers, token_request_data ) assert credentials.token == self.SUCCESS_RESPONSE["access_token"] assert credentials.quota_project_id == QUOTA_PROJECT_ID assert credentials.scopes is None assert credentials.default_scopes == SCOPES @mock.patch( "google.auth.metrics.token_request_access_token_impersonate", return_value=IMPERSONATE_ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE, ) @mock.patch( "google.auth.metrics.python_and_auth_lib_version", return_value=LANG_LIBRARY_METRICS_HEADER_VALUE, ) @mock.patch("google.auth._helpers.utcnow") def test_refresh_success_with_impersonation_ignore_default_scopes( self, utcnow, mock_metrics_header_value, mock_auth_lib_value ): utcnow.return_value = datetime.datetime.strptime( self.AWS_SIGNATURE_TIME, "%Y-%m-%dT%H:%M:%SZ" ) expire_time = ( _helpers.utcnow().replace(microsecond=0) + datetime.timedelta(seconds=3600) ).isoformat("T") + "Z" expected_subject_token = self.make_serialized_aws_signed_request( { "access_key_id": ACCESS_KEY_ID, "secret_access_key": SECRET_ACCESS_KEY, "security_token": TOKEN, } ) token_headers = { "Content-Type": "application/x-www-form-urlencoded", "Authorization": "Basic " + BASIC_AUTH_ENCODING, "x-goog-api-client": "gl-python/3.7 auth/1.1 google-byoid-sdk sa-impersonation/true config-lifetime/false source/aws", } token_request_data = { "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange", "audience": AUDIENCE, "requested_token_type": "urn:ietf:params:oauth:token-type:access_token", "scope": "https://www.googleapis.com/auth/iam", "subject_token": expected_subject_token, "subject_token_type": SUBJECT_TOKEN_TYPE, } # Service account impersonation request/response. impersonation_response = { "accessToken": "SA_ACCESS_TOKEN", "expireTime": expire_time, } impersonation_headers = { "Content-Type": "application/json", "authorization": "Bearer {}".format(self.SUCCESS_RESPONSE["access_token"]), "x-goog-user-project": QUOTA_PROJECT_ID, "x-goog-api-client": IMPERSONATE_ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE, "x-allowed-locations": "0x0", } impersonation_request_data = { "delegates": None, "scope": SCOPES, "lifetime": "3600s", } request = self.make_mock_request( region_status=http_client.OK, region_name=self.AWS_REGION, role_status=http_client.OK, role_name=self.AWS_ROLE, security_credentials_status=http_client.OK, security_credentials_data=self.AWS_SECURITY_CREDENTIALS_RESPONSE, token_status=http_client.OK, token_data=self.SUCCESS_RESPONSE, impersonation_status=http_client.OK, impersonation_data=impersonation_response, ) credentials = self.make_credentials( client_id=CLIENT_ID, client_secret=CLIENT_SECRET, credential_source=self.CREDENTIAL_SOURCE, service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL, quota_project_id=QUOTA_PROJECT_ID, scopes=SCOPES, # Default scopes should be ignored. default_scopes=["ignored"], ) credentials.refresh(request) assert len(request.call_args_list) == 5 # Fourth request should be sent to GCP STS endpoint. self.assert_token_request_kwargs( request.call_args_list[3][1], token_headers, token_request_data ) # Fifth request should be sent to iamcredentials endpoint for service # account impersonation. self.assert_impersonation_request_kwargs( request.call_args_list[4][1], impersonation_headers, impersonation_request_data, ) assert credentials.token == impersonation_response["accessToken"] assert credentials.quota_project_id == QUOTA_PROJECT_ID assert credentials.scopes == SCOPES assert credentials.default_scopes == ["ignored"] @mock.patch( "google.auth.metrics.token_request_access_token_impersonate", return_value=IMPERSONATE_ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE, ) @mock.patch( "google.auth.metrics.python_and_auth_lib_version", return_value=LANG_LIBRARY_METRICS_HEADER_VALUE, ) @mock.patch("google.auth._helpers.utcnow") def test_refresh_success_with_impersonation_use_default_scopes( self, utcnow, mock_metrics_header_value, mock_auth_lib_value ): utcnow.return_value = datetime.datetime.strptime( self.AWS_SIGNATURE_TIME, "%Y-%m-%dT%H:%M:%SZ" ) expire_time = ( _helpers.utcnow().replace(microsecond=0) + datetime.timedelta(seconds=3600) ).isoformat("T") + "Z" expected_subject_token = self.make_serialized_aws_signed_request( { "access_key_id": ACCESS_KEY_ID, "secret_access_key": SECRET_ACCESS_KEY, "security_token": TOKEN, } ) token_headers = { "Content-Type": "application/x-www-form-urlencoded", "Authorization": "Basic " + BASIC_AUTH_ENCODING, "x-goog-api-client": "gl-python/3.7 auth/1.1 google-byoid-sdk sa-impersonation/true config-lifetime/false source/aws", } token_request_data = { "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange", "audience": AUDIENCE, "requested_token_type": "urn:ietf:params:oauth:token-type:access_token", "scope": "https://www.googleapis.com/auth/iam", "subject_token": expected_subject_token, "subject_token_type": SUBJECT_TOKEN_TYPE, } # Service account impersonation request/response. impersonation_response = { "accessToken": "SA_ACCESS_TOKEN", "expireTime": expire_time, } impersonation_headers = { "Content-Type": "application/json", "authorization": "Bearer {}".format(self.SUCCESS_RESPONSE["access_token"]), "x-goog-user-project": QUOTA_PROJECT_ID, "x-goog-api-client": IMPERSONATE_ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE, "x-allowed-locations": "0x0", } impersonation_request_data = { "delegates": None, "scope": SCOPES, "lifetime": "3600s", } request = self.make_mock_request( region_status=http_client.OK, region_name=self.AWS_REGION, role_status=http_client.OK, role_name=self.AWS_ROLE, security_credentials_status=http_client.OK, security_credentials_data=self.AWS_SECURITY_CREDENTIALS_RESPONSE, token_status=http_client.OK, token_data=self.SUCCESS_RESPONSE, impersonation_status=http_client.OK, impersonation_data=impersonation_response, ) credentials = self.make_credentials( client_id=CLIENT_ID, client_secret=CLIENT_SECRET, credential_source=self.CREDENTIAL_SOURCE, service_account_impersonation_url=SERVICE_ACCOUNT_IMPERSONATION_URL, quota_project_id=QUOTA_PROJECT_ID, scopes=None, # Default scopes should be used since user specified scopes are none. default_scopes=SCOPES, ) credentials.refresh(request) assert len(request.call_args_list) == 5 # Fourth request should be sent to GCP STS endpoint. self.assert_token_request_kwargs( request.call_args_list[3][1], token_headers, token_request_data ) # Fifth request should be sent to iamcredentials endpoint for service # account impersonation. self.assert_impersonation_request_kwargs( request.call_args_list[4][1], impersonation_headers, impersonation_request_data, ) assert credentials.token == impersonation_response["accessToken"] assert credentials.quota_project_id == QUOTA_PROJECT_ID assert credentials.scopes is None assert credentials.default_scopes == SCOPES def test_refresh_with_retrieve_subject_token_error(self): request = self.make_mock_request(region_status=http_client.BAD_REQUEST) credentials = self.make_credentials(credential_source=self.CREDENTIAL_SOURCE) with pytest.raises(exceptions.RefreshError) as excinfo: credentials.refresh(request) assert excinfo.match(r"Unable to retrieve AWS region") test__oauth2client.py 0000644 00000013134 15025175543 0010726 0 ustar 00 # Copyright 2016 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import datetime import importlib import os import sys import mock import pytest # type: ignore try: import oauth2client.client # type: ignore import oauth2client.contrib.gce # type: ignore import oauth2client.service_account # type: ignore except ImportError: # pragma: NO COVER pytest.skip( "Skipping oauth2client tests since oauth2client is not installed.", allow_module_level=True, ) from google.auth import _oauth2client DATA_DIR = os.path.join(os.path.dirname(__file__), "data") SERVICE_ACCOUNT_JSON_FILE = os.path.join(DATA_DIR, "service_account.json") def test__convert_oauth2_credentials(): old_credentials = oauth2client.client.OAuth2Credentials( "access_token", "client_id", "client_secret", "refresh_token", datetime.datetime.min, "token_uri", "user_agent", scopes="one two", ) new_credentials = _oauth2client._convert_oauth2_credentials(old_credentials) assert new_credentials.token == old_credentials.access_token assert new_credentials._refresh_token == old_credentials.refresh_token assert new_credentials._client_id == old_credentials.client_id assert new_credentials._client_secret == old_credentials.client_secret assert new_credentials._token_uri == old_credentials.token_uri assert new_credentials.scopes == old_credentials.scopes def test__convert_service_account_credentials(): old_class = oauth2client.service_account.ServiceAccountCredentials old_credentials = old_class.from_json_keyfile_name(SERVICE_ACCOUNT_JSON_FILE) new_credentials = _oauth2client._convert_service_account_credentials( old_credentials ) assert ( new_credentials.service_account_email == old_credentials.service_account_email ) assert new_credentials._signer.key_id == old_credentials._private_key_id assert new_credentials._token_uri == old_credentials.token_uri def test__convert_service_account_credentials_with_jwt(): old_class = oauth2client.service_account._JWTAccessCredentials old_credentials = old_class.from_json_keyfile_name(SERVICE_ACCOUNT_JSON_FILE) new_credentials = _oauth2client._convert_service_account_credentials( old_credentials ) assert ( new_credentials.service_account_email == old_credentials.service_account_email ) assert new_credentials._signer.key_id == old_credentials._private_key_id assert new_credentials._token_uri == old_credentials.token_uri def test__convert_gce_app_assertion_credentials(): old_credentials = oauth2client.contrib.gce.AppAssertionCredentials( email="some_email" ) new_credentials = _oauth2client._convert_gce_app_assertion_credentials( old_credentials ) assert ( new_credentials.service_account_email == old_credentials.service_account_email ) @pytest.fixture def mock_oauth2client_gae_imports(mock_non_existent_module): mock_non_existent_module("google.appengine.api.app_identity") mock_non_existent_module("google.appengine.ext.ndb") mock_non_existent_module("google.appengine.ext.webapp.util") mock_non_existent_module("webapp2") @mock.patch("google.auth.app_engine.app_identity") def test__convert_appengine_app_assertion_credentials( app_identity, mock_oauth2client_gae_imports ): import oauth2client.contrib.appengine # type: ignore service_account_id = "service_account_id" old_credentials = oauth2client.contrib.appengine.AppAssertionCredentials( scope="one two", service_account_id=service_account_id ) new_credentials = _oauth2client._convert_appengine_app_assertion_credentials( old_credentials ) assert new_credentials.scopes == ["one", "two"] assert new_credentials._service_account_id == old_credentials.service_account_id class FakeCredentials(object): pass def test_convert_success(): convert_function = mock.Mock(spec=["__call__"]) conversion_map_patch = mock.patch.object( _oauth2client, "_CLASS_CONVERSION_MAP", {FakeCredentials: convert_function} ) credentials = FakeCredentials() with conversion_map_patch: result = _oauth2client.convert(credentials) convert_function.assert_called_once_with(credentials) assert result == convert_function.return_value def test_convert_not_found(): with pytest.raises(ValueError) as excinfo: _oauth2client.convert("a string is not a real credentials class") assert excinfo.match("Unable to convert") @pytest.fixture def reset__oauth2client_module(): """Reloads the _oauth2client module after a test.""" importlib.reload(_oauth2client) def test_import_has_app_engine( mock_oauth2client_gae_imports, reset__oauth2client_module ): importlib.reload(_oauth2client) assert _oauth2client._HAS_APPENGINE def test_import_without_oauth2client(monkeypatch, reset__oauth2client_module): monkeypatch.setitem(sys.modules, "oauth2client", None) with pytest.raises(ImportError) as excinfo: importlib.reload(_oauth2client) assert excinfo.match("oauth2client") test_packaging.py 0000644 00000001777 15025175543 0010124 0 ustar 00 # Copyright 2022 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import os import subprocess import sys def test_namespace_package_compat(tmp_path): """ The ``google`` namespace package should not be masked by the presence of ``google-auth``. """ google = tmp_path / "google" google.mkdir() google.joinpath("othermod.py").write_text("") env = dict(os.environ, PYTHONPATH=str(tmp_path)) cmd = [sys.executable, "-m", "google.othermod"] subprocess.check_call(cmd, env=env) test__helpers.py 0000644 00000013117 15025175543 0007770 0 ustar 00 # Copyright 2016 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import datetime import urllib import pytest # type: ignore from google.auth import _helpers class SourceClass(object): def func(self): # pragma: NO COVER """example docstring""" def test_copy_docstring_success(): def func(): # pragma: NO COVER pass _helpers.copy_docstring(SourceClass)(func) assert func.__doc__ == SourceClass.func.__doc__ def test_copy_docstring_conflict(): def func(): # pragma: NO COVER """existing docstring""" pass with pytest.raises(ValueError): _helpers.copy_docstring(SourceClass)(func) def test_copy_docstring_non_existing(): def func2(): # pragma: NO COVER pass with pytest.raises(AttributeError): _helpers.copy_docstring(SourceClass)(func2) def test_parse_content_type_plain(): assert _helpers.parse_content_type("text/html") == "text/html" assert _helpers.parse_content_type("application/xml") == "application/xml" assert _helpers.parse_content_type("application/json") == "application/json" def test_parse_content_type_with_parameters(): content_type_html = "text/html; charset=UTF-8" content_type_xml = "application/xml; charset=UTF-16; version=1.0" content_type_json = "application/json; charset=UTF-8; indent=2" assert _helpers.parse_content_type(content_type_html) == "text/html" assert _helpers.parse_content_type(content_type_xml) == "application/xml" assert _helpers.parse_content_type(content_type_json) == "application/json" def test_parse_content_type_missing_or_broken(): content_type_foo = None content_type_bar = "" content_type_baz = "1234" content_type_qux = " ; charset=UTF-8" assert _helpers.parse_content_type(content_type_foo) == "text/plain" assert _helpers.parse_content_type(content_type_bar) == "text/plain" assert _helpers.parse_content_type(content_type_baz) == "text/plain" assert _helpers.parse_content_type(content_type_qux) == "text/plain" def test_utcnow(): assert isinstance(_helpers.utcnow(), datetime.datetime) def test_datetime_to_secs(): assert _helpers.datetime_to_secs(datetime.datetime(1970, 1, 1)) == 0 assert _helpers.datetime_to_secs(datetime.datetime(1990, 5, 29)) == 643939200 def test_to_bytes_with_bytes(): value = b"bytes-val" assert _helpers.to_bytes(value) == value def test_to_bytes_with_unicode(): value = u"string-val" encoded_value = b"string-val" assert _helpers.to_bytes(value) == encoded_value def test_to_bytes_with_nonstring_type(): with pytest.raises(ValueError): _helpers.to_bytes(object()) def test_from_bytes_with_unicode(): value = u"bytes-val" assert _helpers.from_bytes(value) == value def test_from_bytes_with_bytes(): value = b"string-val" decoded_value = u"string-val" assert _helpers.from_bytes(value) == decoded_value def test_from_bytes_with_nonstring_type(): with pytest.raises(ValueError): _helpers.from_bytes(object()) def _assert_query(url, expected): parts = urllib.parse.urlsplit(url) query = urllib.parse.parse_qs(parts.query) assert query == expected def test_update_query_params_no_params(): uri = "http://www.google.com" updated = _helpers.update_query(uri, {"a": "b"}) assert updated == uri + "?a=b" def test_update_query_existing_params(): uri = "http://www.google.com?x=y" updated = _helpers.update_query(uri, {"a": "b", "c": "d&"}) _assert_query(updated, {"x": ["y"], "a": ["b"], "c": ["d&"]}) def test_update_query_replace_param(): base_uri = "http://www.google.com" uri = base_uri + "?x=a" updated = _helpers.update_query(uri, {"x": "b", "y": "c"}) _assert_query(updated, {"x": ["b"], "y": ["c"]}) def test_update_query_remove_param(): base_uri = "http://www.google.com" uri = base_uri + "?x=a" updated = _helpers.update_query(uri, {"y": "c"}, remove=["x"]) _assert_query(updated, {"y": ["c"]}) def test_scopes_to_string(): cases = [ ("", ()), ("", []), ("", ("",)), ("", [""]), ("a", ("a",)), ("b", ["b"]), ("a b", ["a", "b"]), ("a b", ("a", "b")), ("a b", (s for s in ["a", "b"])), ] for expected, case in cases: assert _helpers.scopes_to_string(case) == expected def test_string_to_scopes(): cases = [("", []), ("a", ["a"]), ("a b c d e f", ["a", "b", "c", "d", "e", "f"])] for case, expected in cases: assert _helpers.string_to_scopes(case) == expected def test_padded_urlsafe_b64decode(): cases = [ ("YQ==", b"a"), ("YQ", b"a"), ("YWE=", b"aa"), ("YWE", b"aa"), ("YWFhYQ==", b"aaaa"), ("YWFhYQ", b"aaaa"), ("YWFhYWE=", b"aaaaa"), ("YWFhYWE", b"aaaaa"), ] for case, expected in cases: assert _helpers.padded_urlsafe_b64decode(case) == expected def test_unpadded_urlsafe_b64encode(): cases = [(b"", b""), (b"a", b"YQ"), (b"aa", b"YWE"), (b"aaa", b"YWFh")] for case, expected in cases: assert _helpers.unpadded_urlsafe_b64encode(case) == expected test_impersonated_credentials.py 0000644 00000056166 15025175543 0013251 0 ustar 00 # Copyright 2018 Google Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import datetime import http.client as http_client import json import os import mock import pytest # type: ignore from google.auth import _helpers from google.auth import crypt from google.auth import exceptions from google.auth import impersonated_credentials from google.auth import transport from google.auth.impersonated_credentials import Credentials from google.oauth2 import credentials from google.oauth2 import service_account DATA_DIR = os.path.join(os.path.dirname(__file__), "", "data") with open(os.path.join(DATA_DIR, "privatekey.pem"), "rb") as fh: PRIVATE_KEY_BYTES = fh.read() SERVICE_ACCOUNT_JSON_FILE = os.path.join(DATA_DIR, "service_account.json") ID_TOKEN_DATA = ( "eyJhbGciOiJSUzI1NiIsImtpZCI6ImRmMzc1ODkwOGI3OTIyOTNhZDk3N2Ew" "Yjk5MWQ5OGE3N2Y0ZWVlY2QiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJodHRwc" "zovL2Zvby5iYXIiLCJhenAiOiIxMDIxMDE1NTA4MzQyMDA3MDg1NjgiLCJle" "HAiOjE1NjQ0NzUwNTEsImlhdCI6MTU2NDQ3MTQ1MSwiaXNzIjoiaHR0cHM6L" "y9hY2NvdW50cy5nb29nbGUuY29tIiwic3ViIjoiMTAyMTAxNTUwODM0MjAwN" "zA4NTY4In0.redacted" ) ID_TOKEN_EXPIRY = 1564475051 with open(SERVICE_ACCOUNT_JSON_FILE, "rb") as fh: SERVICE_ACCOUNT_INFO = json.load(fh) SIGNER = crypt.RSASigner.from_string(PRIVATE_KEY_BYTES, "1") TOKEN_URI = "https://example.com/oauth2/token" ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE = ( "gl-python/3.7 auth/1.1 auth-request-type/at cred-type/imp" ) ID_TOKEN_REQUEST_METRICS_HEADER_VALUE = ( "gl-python/3.7 auth/1.1 auth-request-type/it cred-type/imp" ) @pytest.fixture def mock_donor_credentials(): with mock.patch("google.oauth2._client.jwt_grant", autospec=True) as grant: grant.return_value = ( "source token", _helpers.utcnow() + datetime.timedelta(seconds=500), {}, ) yield grant class MockResponse: def __init__(self, json_data, status_code): self.json_data = json_data self.status_code = status_code def json(self): return self.json_data @pytest.fixture def mock_authorizedsession_sign(): with mock.patch( "google.auth.transport.requests.AuthorizedSession.request", autospec=True ) as auth_session: data = {"keyId": "1", "signedBlob": "c2lnbmF0dXJl"} auth_session.return_value = MockResponse(data, http_client.OK) yield auth_session @pytest.fixture def mock_authorizedsession_idtoken(): with mock.patch( "google.auth.transport.requests.AuthorizedSession.request", autospec=True ) as auth_session: data = {"token": ID_TOKEN_DATA} auth_session.return_value = MockResponse(data, http_client.OK) yield auth_session class TestImpersonatedCredentials(object): SERVICE_ACCOUNT_EMAIL = "service-account@example.com" TARGET_PRINCIPAL = "impersonated@project.iam.gserviceaccount.com" TARGET_SCOPES = ["https://www.googleapis.com/auth/devstorage.read_only"] # DELEGATES: List[str] = [] # Because Python 2.7: DELEGATES = [] # type: ignore LIFETIME = 3600 SOURCE_CREDENTIALS = service_account.Credentials( SIGNER, SERVICE_ACCOUNT_EMAIL, TOKEN_URI ) USER_SOURCE_CREDENTIALS = credentials.Credentials(token="ABCDE") IAM_ENDPOINT_OVERRIDE = ( "https://us-east1-iamcredentials.googleapis.com/v1/projects/-" + "/serviceAccounts/{}:generateAccessToken".format(SERVICE_ACCOUNT_EMAIL) ) def make_credentials( self, source_credentials=SOURCE_CREDENTIALS, lifetime=LIFETIME, target_principal=TARGET_PRINCIPAL, iam_endpoint_override=None, ): return Credentials( source_credentials=source_credentials, target_principal=target_principal, target_scopes=self.TARGET_SCOPES, delegates=self.DELEGATES, lifetime=lifetime, iam_endpoint_override=iam_endpoint_override, ) def test_make_from_user_credentials(self): credentials = self.make_credentials( source_credentials=self.USER_SOURCE_CREDENTIALS ) assert not credentials.valid assert credentials.expired def test_default_state(self): credentials = self.make_credentials() assert not credentials.valid assert credentials.expired def test_make_from_service_account_self_signed_jwt(self): source_credentials = service_account.Credentials( SIGNER, self.SERVICE_ACCOUNT_EMAIL, TOKEN_URI, always_use_jwt_access=True ) credentials = self.make_credentials(source_credentials=source_credentials) # test the source credential don't lose self signed jwt setting assert credentials._source_credentials._always_use_jwt_access assert credentials._source_credentials._jwt_credentials def make_request( self, data, status=http_client.OK, headers=None, side_effect=None, use_data_bytes=True, ): response = mock.create_autospec(transport.Response, instance=False) response.status = status response.data = _helpers.to_bytes(data) if use_data_bytes else data response.headers = headers or {} request = mock.create_autospec(transport.Request, instance=False) request.side_effect = side_effect request.return_value = response return request def test_token_usage_metrics(self): credentials = self.make_credentials() credentials.token = "token" credentials.expiry = None headers = {} credentials.before_request(mock.Mock(), None, None, headers) assert headers["authorization"] == "Bearer token" assert headers["x-goog-api-client"] == "cred-type/imp" @pytest.mark.parametrize("use_data_bytes", [True, False]) def test_refresh_success(self, use_data_bytes, mock_donor_credentials): credentials = self.make_credentials(lifetime=None) token = "token" 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, use_data_bytes=use_data_bytes, ) with mock.patch( "google.auth.metrics.token_request_access_token_impersonate", return_value=ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE, ): credentials.refresh(request) assert credentials.valid assert not credentials.expired assert ( request.call_args.kwargs["headers"]["x-goog-api-client"] == ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE ) @pytest.mark.parametrize("use_data_bytes", [True, False]) def test_refresh_success_iam_endpoint_override( self, use_data_bytes, mock_donor_credentials ): credentials = self.make_credentials( lifetime=None, iam_endpoint_override=self.IAM_ENDPOINT_OVERRIDE ) token = "token" 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, use_data_bytes=use_data_bytes, ) credentials.refresh(request) assert credentials.valid assert not credentials.expired # Confirm override endpoint used. request_kwargs = request.call_args[1] assert request_kwargs["url"] == self.IAM_ENDPOINT_OVERRIDE @pytest.mark.parametrize("time_skew", [100, -100]) def test_refresh_source_credentials(self, time_skew): credentials = self.make_credentials(lifetime=None) # Source credentials is refreshed only if it is expired within # _helpers.REFRESH_THRESHOLD from now. We add a time_skew to the expiry, so # source credentials is refreshed only if time_skew <= 0. credentials._source_credentials.expiry = ( _helpers.utcnow() + _helpers.REFRESH_THRESHOLD + datetime.timedelta(seconds=time_skew) ) credentials._source_credentials.token = "Token" with mock.patch( "google.oauth2.service_account.Credentials.refresh", autospec=True ) as source_cred_refresh: 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 # Source credentials is refreshed only if it is expired within # _helpers.REFRESH_THRESHOLD if time_skew > 0: source_cred_refresh.assert_not_called() else: source_cred_refresh.assert_called_once() def test_refresh_failure_malformed_expire_time(self, mock_donor_credentials): credentials = self.make_credentials(lifetime=None) token = "token" expire_time = (_helpers.utcnow() + datetime.timedelta(seconds=500)).isoformat( "T" ) response_body = {"accessToken": token, "expireTime": expire_time} request = self.make_request( data=json.dumps(response_body), status=http_client.OK ) with pytest.raises(exceptions.RefreshError) as excinfo: credentials.refresh(request) assert excinfo.match(impersonated_credentials._REFRESH_ERROR) assert not credentials.valid assert credentials.expired def test_refresh_failure_unauthorzed(self, mock_donor_credentials): credentials = self.make_credentials(lifetime=None) response_body = { "error": { "code": 403, "message": "The caller does not have permission", "status": "PERMISSION_DENIED", } } request = self.make_request( data=json.dumps(response_body), status=http_client.UNAUTHORIZED ) with pytest.raises(exceptions.RefreshError) as excinfo: credentials.refresh(request) assert excinfo.match(impersonated_credentials._REFRESH_ERROR) assert not credentials.valid assert credentials.expired def test_refresh_failure(self): credentials = self.make_credentials(lifetime=None) credentials.expiry = None credentials.token = "token" id_creds = impersonated_credentials.IDTokenCredentials( credentials, target_audience="audience" ) response = mock.create_autospec(transport.Response, instance=False) response.status_code = http_client.UNAUTHORIZED response.json = mock.Mock(return_value="failed to get ID token") with mock.patch( "google.auth.transport.requests.AuthorizedSession.post", return_value=response, ): with pytest.raises(exceptions.RefreshError) as excinfo: id_creds.refresh(None) assert excinfo.match("Error getting ID token") def test_refresh_failure_http_error(self, mock_donor_credentials): credentials = self.make_credentials(lifetime=None) response_body = {} request = self.make_request( data=json.dumps(response_body), status=http_client.HTTPException ) with pytest.raises(exceptions.RefreshError) as excinfo: credentials.refresh(request) assert excinfo.match(impersonated_credentials._REFRESH_ERROR) assert not credentials.valid assert credentials.expired def test_expired(self): credentials = self.make_credentials(lifetime=None) assert credentials.expired def test_signer(self): credentials = self.make_credentials() assert isinstance(credentials.signer, impersonated_credentials.Credentials) def test_signer_email(self): credentials = self.make_credentials(target_principal=self.TARGET_PRINCIPAL) assert credentials.signer_email == self.TARGET_PRINCIPAL def test_service_account_email(self): credentials = self.make_credentials(target_principal=self.TARGET_PRINCIPAL) assert credentials.service_account_email == self.TARGET_PRINCIPAL def test_sign_bytes(self, mock_donor_credentials, mock_authorizedsession_sign): credentials = self.make_credentials(lifetime=None) token = "token" expire_time = ( _helpers.utcnow().replace(microsecond=0) + datetime.timedelta(seconds=500) ).isoformat("T") + "Z" token_response_body = {"accessToken": token, "expireTime": expire_time} response = mock.create_autospec(transport.Response, instance=False) response.status = http_client.OK response.data = _helpers.to_bytes(json.dumps(token_response_body)) request = mock.create_autospec(transport.Request, instance=False) request.return_value = response credentials.refresh(request) assert credentials.valid assert not credentials.expired signature = credentials.sign_bytes(b"signed bytes") assert signature == b"signature" def test_sign_bytes_failure(self): credentials = self.make_credentials(lifetime=None) with mock.patch( "google.auth.transport.requests.AuthorizedSession.request", autospec=True ) as auth_session: data = {"error": {"code": 403, "message": "unauthorized"}} auth_session.return_value = MockResponse(data, http_client.FORBIDDEN) with pytest.raises(exceptions.TransportError) as excinfo: credentials.sign_bytes(b"foo") assert excinfo.match("'code': 403") 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" @pytest.mark.parametrize("use_data_bytes", [True, False]) def test_with_quota_project_iam_endpoint_override( self, use_data_bytes, mock_donor_credentials ): credentials = self.make_credentials( lifetime=None, iam_endpoint_override=self.IAM_ENDPOINT_OVERRIDE ) token = "token" # iam_endpoint_override should be copied to created credentials. quota_project_creds = credentials.with_quota_project("project-foo") 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, use_data_bytes=use_data_bytes, ) quota_project_creds.refresh(request) assert quota_project_creds.valid assert not quota_project_creds.expired # Confirm override endpoint used. request_kwargs = request.call_args[1] assert request_kwargs["url"] == self.IAM_ENDPOINT_OVERRIDE def test_with_scopes(self): credentials = self.make_credentials() credentials._target_scopes = [] assert credentials.requires_scopes is True credentials = credentials.with_scopes(["fake_scope1", "fake_scope2"]) assert credentials.requires_scopes is False assert credentials._target_scopes == ["fake_scope1", "fake_scope2"] def test_with_scopes_provide_default_scopes(self): credentials = self.make_credentials() credentials._target_scopes = [] credentials = credentials.with_scopes( ["fake_scope1"], default_scopes=["fake_scope2"] ) assert credentials._target_scopes == ["fake_scope1"] def test_id_token_success( 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.refresh(request) assert id_creds.token == ID_TOKEN_DATA assert id_creds.expiry == datetime.datetime.utcfromtimestamp(ID_TOKEN_EXPIRY) def test_id_token_metrics(self, mock_donor_credentials): credentials = self.make_credentials(lifetime=None) credentials.token = "token" credentials.expiry = None target_audience = "https://foo.bar" id_creds = impersonated_credentials.IDTokenCredentials( credentials, target_audience=target_audience ) with mock.patch( "google.auth.metrics.token_request_id_token_impersonate", return_value=ID_TOKEN_REQUEST_METRICS_HEADER_VALUE, ): with mock.patch( "google.auth.transport.requests.AuthorizedSession.post", autospec=True ) as mock_post: data = {"token": ID_TOKEN_DATA} mock_post.return_value = MockResponse(data, http_client.OK) id_creds.refresh(None) assert id_creds.token == ID_TOKEN_DATA assert id_creds.expiry == datetime.datetime.utcfromtimestamp( ID_TOKEN_EXPIRY ) assert ( mock_post.call_args.kwargs["headers"]["x-goog-api-client"] == ID_TOKEN_REQUEST_METRICS_HEADER_VALUE ) def test_id_token_from_credential( 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 new_credentials = self.make_credentials(lifetime=None) id_creds = impersonated_credentials.IDTokenCredentials( credentials, target_audience=target_audience, include_email=True ) id_creds = id_creds.from_credentials(target_credentials=new_credentials) id_creds.refresh(request) assert id_creds.token == ID_TOKEN_DATA assert id_creds._include_email is True assert id_creds._target_credentials is new_credentials def test_id_token_with_target_audience( 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, include_email=True ) id_creds = id_creds.with_target_audience(target_audience=target_audience) id_creds.refresh(request) assert id_creds.token == ID_TOKEN_DATA assert id_creds.expiry == datetime.datetime.utcfromtimestamp(ID_TOKEN_EXPIRY) assert id_creds._include_email is True def test_id_token_invalid_cred( self, mock_donor_credentials, mock_authorizedsession_idtoken ): credentials = None with pytest.raises(exceptions.GoogleAuthError) as excinfo: impersonated_credentials.IDTokenCredentials(credentials) assert excinfo.match("Provided Credential must be" " impersonated_credentials") def test_id_token_with_include_email( 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_include_email(True) 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" data/public_cert.pem 0000644 00000002173 15025175543 0010465 0 ustar 00 -----BEGIN CERTIFICATE----- MIIDIzCCAgugAwIBAgIJAMfISuBQ5m+5MA0GCSqGSIb3DQEBBQUAMBUxEzARBgNV BAMTCnVuaXQtdGVzdHMwHhcNMTExMjA2MTYyNjAyWhcNMjExMjAzMTYyNjAyWjAV MRMwEQYDVQQDEwp1bml0LXRlc3RzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB CgKCAQEA4ej0p7bQ7L/r4rVGUz9RN4VQWoej1Bg1mYWIDYslvKrk1gpj7wZgkdmM 7oVK2OfgrSj/FCTkInKPqaCR0gD7K80q+mLBrN3PUkDrJQZpvRZIff3/xmVU1Wer uQLFJjnFb2dqu0s/FY/2kWiJtBCakXvXEOb7zfbINuayL+MSsCGSdVYsSliS5qQp gyDap+8b5fpXZVJkq92hrcNtbkg7hCYUJczt8n9hcCTJCfUpApvaFQ18pe+zpyl4 +WzkP66I28hniMQyUlA1hBiskT7qiouq0m8IOodhv2fagSZKjOTTU2xkSBc//fy3 ZpsL7WqgsZS7Q+0VRK8gKfqkxg5OYQIDAQABo3YwdDAdBgNVHQ4EFgQU2RQ8yO+O gN8oVW2SW7RLrfYd9jEwRQYDVR0jBD4wPIAU2RQ8yO+OgN8oVW2SW7RLrfYd9jGh GaQXMBUxEzARBgNVBAMTCnVuaXQtdGVzdHOCCQDHyErgUOZvuTAMBgNVHRMEBTAD AQH/MA0GCSqGSIb3DQEBBQUAA4IBAQBRv+M/6+FiVu7KXNjFI5pSN17OcW5QUtPr odJMlWrJBtynn/TA1oJlYu3yV5clc/71Vr/AxuX5xGP+IXL32YDF9lTUJXG/uUGk +JETpKmQviPbRsvzYhz4pf6ZIOZMc3/GIcNq92ECbseGO+yAgyWUVKMmZM0HqXC9 ovNslqe0M8C1sLm1zAR5z/h/litE7/8O2ietija3Q/qtl2TOXJdCA6sgjJX2WUql ybrC55ct18NKf3qhpcEkGQvFU40rVYApJpi98DiZPYFdx1oBDp/f4uZ3ojpxRVFT cDwcJLfNRCPUhormsY7fDS9xSyThiHsW9mjJYdcaKQkwYZ0F11yB -----END CERTIFICATE----- data/client_secrets.json 0000644 00000000672 15025175543 0011372 0 ustar 00 { "web": { "client_id": "example.apps.googleusercontent.com", "project_id": "example", "auth_uri": "https://accounts.google.com/o/oauth2/auth", "token_uri": "https://accounts.google.com/o/oauth2/token", "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", "client_secret": "itsasecrettoeveryone", "redirect_uris": [ "urn:ietf:wg:oauth:2.0:oob", "http://localhost" ] } } data/enterprise_cert_valid.json 0000644 00000000163 15025175543 0012733 0 ustar 00 { "libs": { "ecp_client": "/path/to/signer/lib", "tls_offload": "/path/to/offload/lib" } } data/es256_service_account.json 0000644 00000001027 15025175543 0012457 0 ustar 00 { "type": "service_account", "project_id": "example-project", "private_key_id": "1", "private_key": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIAIC57aTx5ev4T2HBMQk4fXV09AzLDQ3Ju1uNoEB0LngoAoGCCqGSM49\nAwEHoUQDQgAEsACsrmP6Bp216OCFm73C8W/VRHZWcO8yU/bMwx96f05BkTII3KeJ\nz2O0IRAnXfso8K6YsjMuUDGCfj+b1IDIoA==\n-----END EC PRIVATE KEY-----", "client_email": "service-account@example.com", "client_id": "1234", "auth_uri": "https://accounts.google.com/o/oauth2/auth", "token_uri": "https://accounts.google.com/o/oauth2/token" } data/service_account.json 0000644 00000003733 15025175543 0011541 0 ustar 00 { "type": "service_account", "project_id": "example-project", "private_key_id": "1", "private_key": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA4ej0p7bQ7L/r4rVGUz9RN4VQWoej1Bg1mYWIDYslvKrk1gpj\n7wZgkdmM7oVK2OfgrSj/FCTkInKPqaCR0gD7K80q+mLBrN3PUkDrJQZpvRZIff3/\nxmVU1WeruQLFJjnFb2dqu0s/FY/2kWiJtBCakXvXEOb7zfbINuayL+MSsCGSdVYs\nSliS5qQpgyDap+8b5fpXZVJkq92hrcNtbkg7hCYUJczt8n9hcCTJCfUpApvaFQ18\npe+zpyl4+WzkP66I28hniMQyUlA1hBiskT7qiouq0m8IOodhv2fagSZKjOTTU2xk\nSBc//fy3ZpsL7WqgsZS7Q+0VRK8gKfqkxg5OYQIDAQABAoIBAQDGGHzQxGKX+ANk\nnQi53v/c6632dJKYXVJC+PDAz4+bzU800Y+n/bOYsWf/kCp94XcG4Lgsdd0Gx+Zq\nHD9CI1IcqqBRR2AFscsmmX6YzPLTuEKBGMW8twaYy3utlFxElMwoUEsrSWRcCA1y\nnHSDzTt871c7nxCXHxuZ6Nm/XCL7Bg8uidRTSC1sQrQyKgTPhtQdYrPQ4WZ1A4J9\nIisyDYmZodSNZe5P+LTJ6M1SCgH8KH9ZGIxv3diMwzNNpk3kxJc9yCnja4mjiGE2\nYCNusSycU5IhZwVeCTlhQGcNeV/skfg64xkiJE34c2y2ttFbdwBTPixStGaF09nU\nZ422D40BAoGBAPvVyRRsC3BF+qZdaSMFwI1yiXY7vQw5+JZh01tD28NuYdRFzjcJ\nvzT2n8LFpj5ZfZFvSMLMVEFVMgQvWnN0O6xdXvGov6qlRUSGaH9u+TCPNnIldjMP\nB8+xTwFMqI7uQr54wBB+Poq7dVRP+0oHb0NYAwUBXoEuvYo3c/nDoRcZAoGBAOWl\naLHjMv4CJbArzT8sPfic/8waSiLV9Ixs3Re5YREUTtnLq7LoymqB57UXJB3BNz/2\neCueuW71avlWlRtE/wXASj5jx6y5mIrlV4nZbVuyYff0QlcG+fgb6pcJQuO9DxMI\naqFGrWP3zye+LK87a6iR76dS9vRU+bHZpSVvGMKJAoGAFGt3TIKeQtJJyqeUWNSk\nklORNdcOMymYMIlqG+JatXQD1rR6ThgqOt8sgRyJqFCVT++YFMOAqXOBBLnaObZZ\nCFbh1fJ66BlSjoXff0W+SuOx5HuJJAa5+WtFHrPajwxeuRcNa8jwxUsB7n41wADu\nUqWWSRedVBg4Ijbw3nWwYDECgYB0pLew4z4bVuvdt+HgnJA9n0EuYowVdadpTEJg\nsoBjNHV4msLzdNqbjrAqgz6M/n8Ztg8D2PNHMNDNJPVHjJwcR7duSTA6w2p/4k28\nbvvk/45Ta3XmzlxZcZSOct3O31Cw0i2XDVc018IY5be8qendDYM08icNo7vQYkRH\n504kQQKBgQDjx60zpz8ozvm1XAj0wVhi7GwXe+5lTxiLi9Fxq721WDxPMiHDW2XL\nYXfFVy/9/GIMvEiGYdmarK1NW+VhWl1DC5xhDg0kvMfxplt4tynoq1uTsQTY31Mx\nBeF5CT/JuNYk3bEBF0H/Q3VGO1/ggVS+YezdFbLWIRoMnLj6XCFEGg==\n-----END RSA PRIVATE KEY-----\n", "client_email": "service-account@example.com", "client_id": "1234", "auth_uri": "https://accounts.google.com/o/oauth2/auth", "token_uri": "https://accounts.google.com/o/oauth2/token" } data/authorized_user_with_rapt_token.json 0000644 00000000240 15025175543 0015050 0 ustar 00 { "client_id": "123", "client_secret": "secret", "refresh_token": "alabalaportocala", "type": "authorized_user", "rapt_token": "rapt" } data/es256_public_cert.pem 0000644 00000000670 15025175543 0011411 0 ustar 00 -----BEGIN CERTIFICATE----- MIIBGDCBwAIJAPUA0H4EQWsdMAoGCCqGSM49BAMCMBUxEzARBgNVBAMMCnVuaXQt dGVzdHMwHhcNMTkwNTA5MDI1MDExWhcNMTkwNjA4MDI1MDExWjAVMRMwEQYDVQQD DAp1bml0LXRlc3RzMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEsACsrmP6Bp21 6OCFm73C8W/VRHZWcO8yU/bMwx96f05BkTII3KeJz2O0IRAnXfso8K6YsjMuUDGC fj+b1IDIoDAKBggqhkjOPQQDAgNHADBEAh8PcDTMyWk8SHqV/v8FLuMbDxdtAsq2 dwCpuHQwqCcmAiEAnwtkiyieN+8zozaf1P4QKp2mAqNGqua50y3ua5uVotc= -----END CERTIFICATE----- data/service_account_non_gdu.json 0000644 00000004317 15025175543 0013251 0 ustar 00 { "type": "service_account", "universe_domain": "universe.foo", "project_id": "example_project", "private_key_id": "1", "private_key": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA4ej0p7bQ7L/r4rVGUz9RN4VQWoej1Bg1mYWIDYslvKrk1gpj\n7wZgkdmM7oVK2OfgrSj/FCTkInKPqaCR0gD7K80q+mLBrN3PUkDrJQZpvRZIff3/\nxmVU1WeruQLFJjnFb2dqu0s/FY/2kWiJtBCakXvXEOb7zfbINuayL+MSsCGSdVYs\nSliS5qQpgyDap+8b5fpXZVJkq92hrcNtbkg7hCYUJczt8n9hcCTJCfUpApvaFQ18\npe+zpyl4+WzkP66I28hniMQyUlA1hBiskT7qiouq0m8IOodhv2fagSZKjOTTU2xk\nSBc//fy3ZpsL7WqgsZS7Q+0VRK8gKfqkxg5OYQIDAQABAoIBAQDGGHzQxGKX+ANk\nnQi53v/c6632dJKYXVJC+PDAz4+bzU800Y+n/bOYsWf/kCp94XcG4Lgsdd0Gx+Zq\nHD9CI1IcqqBRR2AFscsmmX6YzPLTuEKBGMW8twaYy3utlFxElMwoUEsrSWRcCA1y\nnHSDzTt871c7nxCXHxuZ6Nm/XCL7Bg8uidRTSC1sQrQyKgTPhtQdYrPQ4WZ1A4J9\nIisyDYmZodSNZe5P+LTJ6M1SCgH8KH9ZGIxv3diMwzNNpk3kxJc9yCnja4mjiGE2\nYCNusSycU5IhZwVeCTlhQGcNeV/skfg64xkiJE34c2y2ttFbdwBTPixStGaF09nU\nZ422D40BAoGBAPvVyRRsC3BF+qZdaSMFwI1yiXY7vQw5+JZh01tD28NuYdRFzjcJ\nvzT2n8LFpj5ZfZFvSMLMVEFVMgQvWnN0O6xdXvGov6qlRUSGaH9u+TCPNnIldjMP\nB8+xTwFMqI7uQr54wBB+Poq7dVRP+0oHb0NYAwUBXoEuvYo3c/nDoRcZAoGBAOWl\naLHjMv4CJbArzT8sPfic/8waSiLV9Ixs3Re5YREUTtnLq7LoymqB57UXJB3BNz/2\neCueuW71avlWlRtE/wXASj5jx6y5mIrlV4nZbVuyYff0QlcG+fgb6pcJQuO9DxMI\naqFGrWP3zye+LK87a6iR76dS9vRU+bHZpSVvGMKJAoGAFGt3TIKeQtJJyqeUWNSk\nklORNdcOMymYMIlqG+JatXQD1rR6ThgqOt8sgRyJqFCVT++YFMOAqXOBBLnaObZZ\nCFbh1fJ66BlSjoXff0W+SuOx5HuJJAa5+WtFHrPajwxeuRcNa8jwxUsB7n41wADu\nUqWWSRedVBg4Ijbw3nWwYDECgYB0pLew4z4bVuvdt+HgnJA9n0EuYowVdadpTEJg\nsoBjNHV4msLzdNqbjrAqgz6M/n8Ztg8D2PNHMNDNJPVHjJwcR7duSTA6w2p/4k28\nbvvk/45Ta3XmzlxZcZSOct3O31Cw0i2XDVc018IY5be8qendDYM08icNo7vQYkRH\n504kQQKBgQDjx60zpz8ozvm1XAj0wVhi7GwXe+5lTxiLi9Fxq721WDxPMiHDW2XL\nYXfFVy/9/GIMvEiGYdmarK1NW+VhWl1DC5xhDg0kvMfxplt4tynoq1uTsQTY31Mx\nBeF5CT/JuNYk3bEBF0H/Q3VGO1/ggVS+YezdFbLWIRoMnLj6XCFEGg==\n-----END RSA PRIVATE KEY-----\n", "client_email": "testsa@foo.iam.gserviceaccount.com", "client_id": "1234", "auth_uri": "https://accounts.google.com/o/oauth2/auth", "token_uri": "https://oauth2.universe.foo/token", "auth_provider_x509_cert_url": "https://www.universe.foo/oauth2/v1/certs", "client_x509_cert_url": "https://www.universe.foo/robot/v1/metadata/x509/foo.iam.gserviceaccount.com" } data/authorized_user_cloud_sdk.json 0000644 00000000277 15025175543 0013630 0 ustar 00 { "client_id": "764086051850-6qr4p6gpi6hn506pt8ejuq83di341hur.apps.googleusercontent.com", "client_secret": "secret", "refresh_token": "alabalaportocala", "type": "authorized_user" } data/enterprise_cert_invalid.json 0000644 00000000022 15025175543 0013254 0 ustar 00 { "libs": {} } data/old_oauth_credentials_py3.pickle 0000644 00000000433 15025175543 0014003 0 ustar 00 �cgoogle.oauth2.credentials Credentials q )�q}q(X tokenqX 123qX expiryqNX _scopesqNX _refresh_tokenqX 321qX _id_tokenq NX _token_uriq X https://example.com/oauth2/tokenqX _client_idqX client_idq X _client_secretqX client_secretqub. data/privatekey.pub 0000644 00000000652 15025175543 0010362 0 ustar 00 -----BEGIN RSA PUBLIC KEY----- MIIBCgKCAQEA4ej0p7bQ7L/r4rVGUz9RN4VQWoej1Bg1mYWIDYslvKrk1gpj7wZg kdmM7oVK2OfgrSj/FCTkInKPqaCR0gD7K80q+mLBrN3PUkDrJQZpvRZIff3/xmVU 1WeruQLFJjnFb2dqu0s/FY/2kWiJtBCakXvXEOb7zfbINuayL+MSsCGSdVYsSliS 5qQpgyDap+8b5fpXZVJkq92hrcNtbkg7hCYUJczt8n9hcCTJCfUpApvaFQ18pe+z pyl4+WzkP66I28hniMQyUlA1hBiskT7qiouq0m8IOodhv2fagSZKjOTTU2xkSBc/ /fy3ZpsL7WqgsZS7Q+0VRK8gKfqkxg5OYQIDAQAB -----END RSA PUBLIC KEY----- data/pem_from_pkcs12.pem 0000644 00000003472 15025175543 0011164 0 ustar 00 Bag Attributes friendlyName: key localKeyID: 22 7E 04 FC 64 48 20 83 1E C1 BD E3 F5 2F 44 7D EA 99 A5 BC Key Attributes: <No Attributes> -----BEGIN PRIVATE KEY----- MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDh6PSnttDsv+vi tUZTP1E3hVBah6PUGDWZhYgNiyW8quTWCmPvBmCR2YzuhUrY5+CtKP8UJOQico+p oJHSAPsrzSr6YsGs3c9SQOslBmm9Fkh9/f/GZVTVZ6u5AsUmOcVvZ2q7Sz8Vj/aR aIm0EJqRe9cQ5vvN9sg25rIv4xKwIZJ1VixKWJLmpCmDINqn7xvl+ldlUmSr3aGt w21uSDuEJhQlzO3yf2FwJMkJ9SkCm9oVDXyl77OnKXj5bOQ/rojbyGeIxDJSUDWE GKyRPuqKi6rSbwg6h2G/Z9qBJkqM5NNTbGRIFz/9/LdmmwvtaqCxlLtD7RVEryAp +qTGDk5hAgMBAAECggEBAMYYfNDEYpf4A2SdCLne/9zrrfZ0kphdUkL48MDPj5vN TzTRj6f9s5ixZ/+QKn3hdwbguCx13QbH5mocP0IjUhyqoFFHYAWxyyaZfpjM8tO4 QoEYxby3BpjLe62UXESUzChQSytJZFwIDXKcdIPNO3zvVzufEJcfG5no2b9cIvsG Dy6J1FNILWxCtDIqBM+G1B1is9DhZnUDgn0iKzINiZmh1I1l7k/4tMnozVIKAfwo f1kYjG/d2IzDM02mTeTElz3IKeNriaOIYTZgI26xLJxTkiFnBV4JOWFAZw15X+yR +DrjGSIkTfhzbLa20Vt3AFM+LFK0ZoXT2dRnjbYPjQECgYEA+9XJFGwLcEX6pl1p IwXAjXKJdju9DDn4lmHTW0Pbw25h1EXONwm/NPafwsWmPll9kW9IwsxUQVUyBC9a c3Q7rF1e8ai/qqVFRIZof275MI82ciV2Mw8Hz7FPAUyoju5CvnjAEH4+irt1VE/7 SgdvQ1gDBQFegS69ijdz+cOhFxkCgYEA5aVoseMy/gIlsCvNPyw9+Jz/zBpKItX0 jGzdF7lhERRO2cursujKaoHntRckHcE3P/Z4K565bvVq+VaVG0T/BcBKPmPHrLmY iuVXidltW7Jh9/RCVwb5+BvqlwlC470PEwhqoUatY/fPJ74srztrqJHvp1L29FT5 sdmlJW8YwokCgYAUa3dMgp5C0knKp5RY1KSSU5E11w4zKZgwiWob4lq1dAPWtHpO GCo63yyBHImoUJVP75gUw4Cpc4EEudo5tlkIVuHV8nroGVKOhd9/Rb5K47Hke4kk Brn5a0Ues9qPDF65Fw1ryPDFSwHufjXAAO5SpZZJF51UGDgiNvDedbBgMQKBgHSk t7DjPhtW69234eCckD2fQS5ijBV1p2lMQmCygGM0dXiawvN02puOsCqDPoz+fxm2 DwPY80cw0M0k9UeMnBxHt25JMDrDan/iTbxu++T/jlNrdebOXFlxlI5y3c7fULDS LZcNVzTXwhjlt7yp6d0NgzTyJw2ju9BiREfnTiRBAoGBAOPHrTOnPyjO+bVcCPTB WGLsbBd77mVPGIuL0XGrvbVYPE8yIcNbZcthd8VXL/38Ygy8SIZh2ZqsrU1b5WFa XUMLnGEODSS8x/GmW3i3KeirW5OxBNjfUzEF4XkJP8m41iTdsQEXQf9DdUY7X+CB VL5h7N0VstYhGgycuPpcIUQa -----END PRIVATE KEY----- data/context_aware_metadata.json 0000644 00000000254 15025175543 0013063 0 ustar 00 { "cert_provider_command":[ "/opt/google/endpoint-verification/bin/SecureConnectHelper", "--print_certificate"], "device_resource_ids":["11111111-1111-1111"] } data/privatekey.pem 0000644 00000003217 15025175543 0010355 0 ustar 00 -----BEGIN RSA PRIVATE KEY----- MIIEpAIBAAKCAQEA4ej0p7bQ7L/r4rVGUz9RN4VQWoej1Bg1mYWIDYslvKrk1gpj 7wZgkdmM7oVK2OfgrSj/FCTkInKPqaCR0gD7K80q+mLBrN3PUkDrJQZpvRZIff3/ xmVU1WeruQLFJjnFb2dqu0s/FY/2kWiJtBCakXvXEOb7zfbINuayL+MSsCGSdVYs SliS5qQpgyDap+8b5fpXZVJkq92hrcNtbkg7hCYUJczt8n9hcCTJCfUpApvaFQ18 pe+zpyl4+WzkP66I28hniMQyUlA1hBiskT7qiouq0m8IOodhv2fagSZKjOTTU2xk SBc//fy3ZpsL7WqgsZS7Q+0VRK8gKfqkxg5OYQIDAQABAoIBAQDGGHzQxGKX+ANk nQi53v/c6632dJKYXVJC+PDAz4+bzU800Y+n/bOYsWf/kCp94XcG4Lgsdd0Gx+Zq HD9CI1IcqqBRR2AFscsmmX6YzPLTuEKBGMW8twaYy3utlFxElMwoUEsrSWRcCA1y nHSDzTt871c7nxCXHxuZ6Nm/XCL7Bg8uidRTSC1sQrQyKgTPhtQdYrPQ4WZ1A4J9 IisyDYmZodSNZe5P+LTJ6M1SCgH8KH9ZGIxv3diMwzNNpk3kxJc9yCnja4mjiGE2 YCNusSycU5IhZwVeCTlhQGcNeV/skfg64xkiJE34c2y2ttFbdwBTPixStGaF09nU Z422D40BAoGBAPvVyRRsC3BF+qZdaSMFwI1yiXY7vQw5+JZh01tD28NuYdRFzjcJ vzT2n8LFpj5ZfZFvSMLMVEFVMgQvWnN0O6xdXvGov6qlRUSGaH9u+TCPNnIldjMP B8+xTwFMqI7uQr54wBB+Poq7dVRP+0oHb0NYAwUBXoEuvYo3c/nDoRcZAoGBAOWl aLHjMv4CJbArzT8sPfic/8waSiLV9Ixs3Re5YREUTtnLq7LoymqB57UXJB3BNz/2 eCueuW71avlWlRtE/wXASj5jx6y5mIrlV4nZbVuyYff0QlcG+fgb6pcJQuO9DxMI aqFGrWP3zye+LK87a6iR76dS9vRU+bHZpSVvGMKJAoGAFGt3TIKeQtJJyqeUWNSk klORNdcOMymYMIlqG+JatXQD1rR6ThgqOt8sgRyJqFCVT++YFMOAqXOBBLnaObZZ CFbh1fJ66BlSjoXff0W+SuOx5HuJJAa5+WtFHrPajwxeuRcNa8jwxUsB7n41wADu UqWWSRedVBg4Ijbw3nWwYDECgYB0pLew4z4bVuvdt+HgnJA9n0EuYowVdadpTEJg soBjNHV4msLzdNqbjrAqgz6M/n8Ztg8D2PNHMNDNJPVHjJwcR7duSTA6w2p/4k28 bvvk/45Ta3XmzlxZcZSOct3O31Cw0i2XDVc018IY5be8qendDYM08icNo7vQYkRH 504kQQKBgQDjx60zpz8ozvm1XAj0wVhi7GwXe+5lTxiLi9Fxq721WDxPMiHDW2XL YXfFVy/9/GIMvEiGYdmarK1NW+VhWl1DC5xhDg0kvMfxplt4tynoq1uTsQTY31Mx BeF5CT/JuNYk3bEBF0H/Q3VGO1/ggVS+YezdFbLWIRoMnLj6XCFEGg== -----END RSA PRIVATE KEY----- data/impersonated_service_account_with_quota_project.json 0000644 00000000770 15025175543 0020303 0 ustar 00 { "delegates": [ "service-account-delegate@example.com" ], "quota_project_id": "quota_project", "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/service-account-target@example.com:generateAccessToken", "source_credentials": { "client_id": "123", "client_secret": "secret", "refresh_token": "alabalaportocala", "type": "authorized_user" }, "type": "impersonated_service_account" } data/external_subject_token.json 0000644 00000000076 15025175543 0013123 0 ustar 00 { "access_token": "HEADER.SIMULATED_JWT_PAYLOAD.SIGNATURE" } data/privatekey.p12 0000644 00000004624 15025175543 0010201 0 ustar 00 0� �0� V *�H�� �� G� C0� ?0�� *�H�� ���0�� 0�� *�H�� 0 *�H�� 0'�dX}�1 ���F�+BNq� �bұF�W_ ,K���4��a9�v�t�G���9N���/�*�^�i̬#����|�e�Aށz~~h2�aC��xњL��ɨ�13l"�(Ex�)w�_$��Gu�t2�ycxg����ԝ�1&:�� V��3f�~���6ٯ�^��iDw�m&��w�j-~τ���쌒����|�z��)S���O3 �w(���g�X�V�Qڵ/���rl�8�wxᦕ'��� �n��2�� ���f��Y>�:����a�w�x�L #��$ہѦv��5A8�_x���C���ݮ�vr�o�g�>H�B;"�3O ��x]�fX���)@kMI�(�u,H[�j� �������6�9�9����I\�I39M}h�܋�}�,�hNz�h!���/�P��OQ�}�Gѩ�[�`:i�s���B#l���v*���(���1�xX<,�Q�;u��F���2��.5��ݞ/��AQ�+T^Ӄu��q������,�=b7�a� ��&��gR���/�F"�����]�"-�#����s �Hko+�ےB)Z���ٖf�Iܱ����9�����"="�v�"#Ɖ���q����i�m(���@Wi���'�Ah#9:�|d_�6����ɨjoD�ڞ���!'�HHsOᗃ������Ɇ��P�xº �e&&���§@�A�����&G���ABI0�r�y��u>�v�2���� ۲�PT/��� �kr77? A������\�3�_fu��U�?Ԝ�wxC-uHOJ�>ʋn�4,�f>Q��k�|+>��Iݱ�<A�;'��>�XD7=ܽK)��X�Ʀ�I����L0���}��.�J��5{ �v� k�ӈ�����C=Z F̊.B �Q�o^c�1����0�X *�H�� ��I�E0�A0�=*�H�� ���0��0 *�H�� 0!�P;7�]� �Ȏ=��MV��dAv����"�"����3�?�|�����c�j5FY��������7e�O�X��驓jpPa<�(��2����4�w�R��z���/ر��Y[«��W�|��Խ�И�'~N�x�=����S�qF��ﳍ6(�b /?x?r?�sܟ|�?�Ġt��݄����T�v]5w���-,6�txmA����Kȓ��y�HN�ܸ��O���2���<����k��Rx]����\�\wr$$0����T�[2a��e�*V��4֯�Ѓ�iB�7���͜���K~%�^Z��,Kw���*�ܖ��w����.���ݛ�{y[�M��OV�sK�⸛: Ů8��pЀ�j�ާn��9h.�s+O!�+ɖ�s���j�/��xa/_D�bg�UQ�"P鞳NpZ��f<'H����42 � 6�����Ȕhp8�i A�ir�g̞��~:N�����lAX˴x)8d�8�<5� �iǎ�W�ʑ�G�h���*���m!F,�aP�nBm#OƄ�-"�L� �&�~g�aFi�&�C*ڪ�e�u�#��)����ȣ=j�u2�:�p>��Կ�=k3d�gN�(� v��ӽ>9e�_�?�g�K�{�q�%�J�"%�����6h���*�_T�l� ��>�i]`�кlC�*�+�\ �w�2��� ��!�u�c���e'�D��L��zY��SzIi�W��w��s�N� �q"����N�.[��",I.h��]#E���M�<