test_requests.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506
  1. # Copyright 2016 Google LLC
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. import datetime
  15. import functools
  16. import os
  17. import sys
  18. import freezegun
  19. import mock
  20. import OpenSSL
  21. import pytest
  22. import requests
  23. import requests.adapters
  24. from six.moves import http_client
  25. from google.auth import environment_vars
  26. from google.auth import exceptions
  27. import google.auth.credentials
  28. import google.auth.transport._mtls_helper
  29. import google.auth.transport.requests
  30. from google.oauth2 import service_account
  31. from tests.transport import compliance
  32. @pytest.fixture
  33. def frozen_time():
  34. with freezegun.freeze_time("1970-01-01 00:00:00", tick=False) as frozen:
  35. yield frozen
  36. class TestRequestResponse(compliance.RequestResponseTests):
  37. def make_request(self):
  38. return google.auth.transport.requests.Request()
  39. def test_timeout(self):
  40. http = mock.create_autospec(requests.Session, instance=True)
  41. request = google.auth.transport.requests.Request(http)
  42. request(url="http://example.com", method="GET", timeout=5)
  43. assert http.request.call_args[1]["timeout"] == 5
  44. class TestTimeoutGuard(object):
  45. def make_guard(self, *args, **kwargs):
  46. return google.auth.transport.requests.TimeoutGuard(*args, **kwargs)
  47. def test_tracks_elapsed_time_w_numeric_timeout(self, frozen_time):
  48. with self.make_guard(timeout=10) as guard:
  49. frozen_time.tick(delta=datetime.timedelta(seconds=3.8))
  50. assert guard.remaining_timeout == 6.2
  51. def test_tracks_elapsed_time_w_tuple_timeout(self, frozen_time):
  52. with self.make_guard(timeout=(16, 19)) as guard:
  53. frozen_time.tick(delta=datetime.timedelta(seconds=3.8))
  54. assert guard.remaining_timeout == (12.2, 15.2)
  55. def test_noop_if_no_timeout(self, frozen_time):
  56. with self.make_guard(timeout=None) as guard:
  57. frozen_time.tick(delta=datetime.timedelta(days=3650))
  58. # NOTE: no timeout error raised, despite years have passed
  59. assert guard.remaining_timeout is None
  60. def test_timeout_error_w_numeric_timeout(self, frozen_time):
  61. with pytest.raises(requests.exceptions.Timeout):
  62. with self.make_guard(timeout=10) as guard:
  63. frozen_time.tick(delta=datetime.timedelta(seconds=10.001))
  64. assert guard.remaining_timeout == pytest.approx(-0.001)
  65. def test_timeout_error_w_tuple_timeout(self, frozen_time):
  66. with pytest.raises(requests.exceptions.Timeout):
  67. with self.make_guard(timeout=(11, 10)) as guard:
  68. frozen_time.tick(delta=datetime.timedelta(seconds=10.001))
  69. assert guard.remaining_timeout == pytest.approx((0.999, -0.001))
  70. def test_custom_timeout_error_type(self, frozen_time):
  71. class FooError(Exception):
  72. pass
  73. with pytest.raises(FooError):
  74. with self.make_guard(timeout=1, timeout_error_type=FooError):
  75. frozen_time.tick(delta=datetime.timedelta(seconds=2))
  76. def test_lets_suite_errors_bubble_up(self, frozen_time):
  77. with pytest.raises(IndexError):
  78. with self.make_guard(timeout=1):
  79. [1, 2, 3][3]
  80. class CredentialsStub(google.auth.credentials.Credentials):
  81. def __init__(self, token="token"):
  82. super(CredentialsStub, self).__init__()
  83. self.token = token
  84. def apply(self, headers, token=None):
  85. headers["authorization"] = self.token
  86. def before_request(self, request, method, url, headers):
  87. self.apply(headers)
  88. def refresh(self, request):
  89. self.token += "1"
  90. def with_quota_project(self, quota_project_id):
  91. raise NotImplementedError()
  92. class TimeTickCredentialsStub(CredentialsStub):
  93. """Credentials that spend some (mocked) time when refreshing a token."""
  94. def __init__(self, time_tick, token="token"):
  95. self._time_tick = time_tick
  96. super(TimeTickCredentialsStub, self).__init__(token=token)
  97. def refresh(self, request):
  98. self._time_tick()
  99. super(TimeTickCredentialsStub, self).refresh(requests)
  100. class AdapterStub(requests.adapters.BaseAdapter):
  101. def __init__(self, responses, headers=None):
  102. super(AdapterStub, self).__init__()
  103. self.responses = responses
  104. self.requests = []
  105. self.headers = headers or {}
  106. def send(self, request, **kwargs):
  107. # pylint: disable=arguments-differ
  108. # request is the only required argument here and the only argument
  109. # we care about.
  110. self.requests.append(request)
  111. return self.responses.pop(0)
  112. def close(self): # pragma: NO COVER
  113. # pylint wants this to be here because it's abstract in the base
  114. # class, but requests never actually calls it.
  115. return
  116. class TimeTickAdapterStub(AdapterStub):
  117. """Adapter that spends some (mocked) time when making a request."""
  118. def __init__(self, time_tick, responses, headers=None):
  119. self._time_tick = time_tick
  120. super(TimeTickAdapterStub, self).__init__(responses, headers=headers)
  121. def send(self, request, **kwargs):
  122. self._time_tick()
  123. return super(TimeTickAdapterStub, self).send(request, **kwargs)
  124. class TestMutualTlsAdapter(object):
  125. @mock.patch.object(requests.adapters.HTTPAdapter, "init_poolmanager")
  126. @mock.patch.object(requests.adapters.HTTPAdapter, "proxy_manager_for")
  127. def test_success(self, mock_proxy_manager_for, mock_init_poolmanager):
  128. adapter = google.auth.transport.requests._MutualTlsAdapter(
  129. pytest.public_cert_bytes, pytest.private_key_bytes
  130. )
  131. adapter.init_poolmanager()
  132. mock_init_poolmanager.assert_called_with(ssl_context=adapter._ctx_poolmanager)
  133. adapter.proxy_manager_for()
  134. mock_proxy_manager_for.assert_called_with(ssl_context=adapter._ctx_proxymanager)
  135. def test_invalid_cert_or_key(self):
  136. with pytest.raises(OpenSSL.crypto.Error):
  137. google.auth.transport.requests._MutualTlsAdapter(
  138. b"invalid cert", b"invalid key"
  139. )
  140. @mock.patch.dict("sys.modules", {"OpenSSL.crypto": None})
  141. def test_import_error(self):
  142. with pytest.raises(ImportError):
  143. google.auth.transport.requests._MutualTlsAdapter(
  144. pytest.public_cert_bytes, pytest.private_key_bytes
  145. )
  146. def make_response(status=http_client.OK, data=None):
  147. response = requests.Response()
  148. response.status_code = status
  149. response._content = data
  150. return response
  151. class TestAuthorizedSession(object):
  152. TEST_URL = "http://example.com/"
  153. def test_constructor(self):
  154. authed_session = google.auth.transport.requests.AuthorizedSession(
  155. mock.sentinel.credentials
  156. )
  157. assert authed_session.credentials == mock.sentinel.credentials
  158. def test_constructor_with_auth_request(self):
  159. http = mock.create_autospec(requests.Session)
  160. auth_request = google.auth.transport.requests.Request(http)
  161. authed_session = google.auth.transport.requests.AuthorizedSession(
  162. mock.sentinel.credentials, auth_request=auth_request
  163. )
  164. assert authed_session._auth_request == auth_request
  165. def test_request_default_timeout(self):
  166. credentials = mock.Mock(wraps=CredentialsStub())
  167. response = make_response()
  168. adapter = AdapterStub([response])
  169. authed_session = google.auth.transport.requests.AuthorizedSession(credentials)
  170. authed_session.mount(self.TEST_URL, adapter)
  171. patcher = mock.patch("google.auth.transport.requests.requests.Session.request")
  172. with patcher as patched_request:
  173. authed_session.request("GET", self.TEST_URL)
  174. expected_timeout = google.auth.transport.requests._DEFAULT_TIMEOUT
  175. assert patched_request.call_args[1]["timeout"] == expected_timeout
  176. def test_request_no_refresh(self):
  177. credentials = mock.Mock(wraps=CredentialsStub())
  178. response = make_response()
  179. adapter = AdapterStub([response])
  180. authed_session = google.auth.transport.requests.AuthorizedSession(credentials)
  181. authed_session.mount(self.TEST_URL, adapter)
  182. result = authed_session.request("GET", self.TEST_URL)
  183. assert response == result
  184. assert credentials.before_request.called
  185. assert not credentials.refresh.called
  186. assert len(adapter.requests) == 1
  187. assert adapter.requests[0].url == self.TEST_URL
  188. assert adapter.requests[0].headers["authorization"] == "token"
  189. def test_request_refresh(self):
  190. credentials = mock.Mock(wraps=CredentialsStub())
  191. final_response = make_response(status=http_client.OK)
  192. # First request will 401, second request will succeed.
  193. adapter = AdapterStub(
  194. [make_response(status=http_client.UNAUTHORIZED), final_response]
  195. )
  196. authed_session = google.auth.transport.requests.AuthorizedSession(
  197. credentials, refresh_timeout=60
  198. )
  199. authed_session.mount(self.TEST_URL, adapter)
  200. result = authed_session.request("GET", self.TEST_URL)
  201. assert result == final_response
  202. assert credentials.before_request.call_count == 2
  203. assert credentials.refresh.called
  204. assert len(adapter.requests) == 2
  205. assert adapter.requests[0].url == self.TEST_URL
  206. assert adapter.requests[0].headers["authorization"] == "token"
  207. assert adapter.requests[1].url == self.TEST_URL
  208. assert adapter.requests[1].headers["authorization"] == "token1"
  209. def test_request_max_allowed_time_timeout_error(self, frozen_time):
  210. tick_one_second = functools.partial(
  211. frozen_time.tick, delta=datetime.timedelta(seconds=1.0)
  212. )
  213. credentials = mock.Mock(
  214. wraps=TimeTickCredentialsStub(time_tick=tick_one_second)
  215. )
  216. adapter = TimeTickAdapterStub(
  217. time_tick=tick_one_second, responses=[make_response(status=http_client.OK)]
  218. )
  219. authed_session = google.auth.transport.requests.AuthorizedSession(credentials)
  220. authed_session.mount(self.TEST_URL, adapter)
  221. # Because a request takes a full mocked second, max_allowed_time shorter
  222. # than that will cause a timeout error.
  223. with pytest.raises(requests.exceptions.Timeout):
  224. authed_session.request("GET", self.TEST_URL, max_allowed_time=0.9)
  225. def test_request_max_allowed_time_w_transport_timeout_no_error(self, frozen_time):
  226. tick_one_second = functools.partial(
  227. frozen_time.tick, delta=datetime.timedelta(seconds=1.0)
  228. )
  229. credentials = mock.Mock(
  230. wraps=TimeTickCredentialsStub(time_tick=tick_one_second)
  231. )
  232. adapter = TimeTickAdapterStub(
  233. time_tick=tick_one_second,
  234. responses=[
  235. make_response(status=http_client.UNAUTHORIZED),
  236. make_response(status=http_client.OK),
  237. ],
  238. )
  239. authed_session = google.auth.transport.requests.AuthorizedSession(credentials)
  240. authed_session.mount(self.TEST_URL, adapter)
  241. # A short configured transport timeout does not affect max_allowed_time.
  242. # The latter is not adjusted to it and is only concerned with the actual
  243. # execution time. The call below should thus not raise a timeout error.
  244. authed_session.request("GET", self.TEST_URL, timeout=0.5, max_allowed_time=3.1)
  245. def test_request_max_allowed_time_w_refresh_timeout_no_error(self, frozen_time):
  246. tick_one_second = functools.partial(
  247. frozen_time.tick, delta=datetime.timedelta(seconds=1.0)
  248. )
  249. credentials = mock.Mock(
  250. wraps=TimeTickCredentialsStub(time_tick=tick_one_second)
  251. )
  252. adapter = TimeTickAdapterStub(
  253. time_tick=tick_one_second,
  254. responses=[
  255. make_response(status=http_client.UNAUTHORIZED),
  256. make_response(status=http_client.OK),
  257. ],
  258. )
  259. authed_session = google.auth.transport.requests.AuthorizedSession(
  260. credentials, refresh_timeout=1.1
  261. )
  262. authed_session.mount(self.TEST_URL, adapter)
  263. # A short configured refresh timeout does not affect max_allowed_time.
  264. # The latter is not adjusted to it and is only concerned with the actual
  265. # execution time. The call below should thus not raise a timeout error
  266. # (and `timeout` does not come into play either, as it's very long).
  267. authed_session.request("GET", self.TEST_URL, timeout=60, max_allowed_time=3.1)
  268. def test_request_timeout_w_refresh_timeout_timeout_error(self, frozen_time):
  269. tick_one_second = functools.partial(
  270. frozen_time.tick, delta=datetime.timedelta(seconds=1.0)
  271. )
  272. credentials = mock.Mock(
  273. wraps=TimeTickCredentialsStub(time_tick=tick_one_second)
  274. )
  275. adapter = TimeTickAdapterStub(
  276. time_tick=tick_one_second,
  277. responses=[
  278. make_response(status=http_client.UNAUTHORIZED),
  279. make_response(status=http_client.OK),
  280. ],
  281. )
  282. authed_session = google.auth.transport.requests.AuthorizedSession(
  283. credentials, refresh_timeout=100
  284. )
  285. authed_session.mount(self.TEST_URL, adapter)
  286. # An UNAUTHORIZED response triggers a refresh (an extra request), thus
  287. # the final request that otherwise succeeds results in a timeout error
  288. # (all three requests together last 3 mocked seconds).
  289. with pytest.raises(requests.exceptions.Timeout):
  290. authed_session.request(
  291. "GET", self.TEST_URL, timeout=60, max_allowed_time=2.9
  292. )
  293. def test_authorized_session_without_default_host(self):
  294. credentials = mock.create_autospec(service_account.Credentials)
  295. authed_session = google.auth.transport.requests.AuthorizedSession(credentials)
  296. authed_session.credentials._create_self_signed_jwt.assert_not_called()
  297. def test_authorized_session_with_default_host(self):
  298. default_host = "pubsub.googleapis.com"
  299. credentials = mock.create_autospec(service_account.Credentials)
  300. authed_session = google.auth.transport.requests.AuthorizedSession(
  301. credentials, default_host=default_host
  302. )
  303. authed_session.credentials._create_self_signed_jwt.assert_called_once_with(
  304. "https://{}/".format(default_host)
  305. )
  306. def test_configure_mtls_channel_with_callback(self):
  307. mock_callback = mock.Mock()
  308. mock_callback.return_value = (
  309. pytest.public_cert_bytes,
  310. pytest.private_key_bytes,
  311. )
  312. auth_session = google.auth.transport.requests.AuthorizedSession(
  313. credentials=mock.Mock()
  314. )
  315. with mock.patch.dict(
  316. os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"}
  317. ):
  318. auth_session.configure_mtls_channel(mock_callback)
  319. assert auth_session.is_mtls
  320. assert isinstance(
  321. auth_session.adapters["https://"],
  322. google.auth.transport.requests._MutualTlsAdapter,
  323. )
  324. @mock.patch(
  325. "google.auth.transport._mtls_helper.get_client_cert_and_key", autospec=True
  326. )
  327. def test_configure_mtls_channel_with_metadata(self, mock_get_client_cert_and_key):
  328. mock_get_client_cert_and_key.return_value = (
  329. True,
  330. pytest.public_cert_bytes,
  331. pytest.private_key_bytes,
  332. )
  333. auth_session = google.auth.transport.requests.AuthorizedSession(
  334. credentials=mock.Mock()
  335. )
  336. with mock.patch.dict(
  337. os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"}
  338. ):
  339. auth_session.configure_mtls_channel()
  340. assert auth_session.is_mtls
  341. assert isinstance(
  342. auth_session.adapters["https://"],
  343. google.auth.transport.requests._MutualTlsAdapter,
  344. )
  345. @mock.patch.object(google.auth.transport.requests._MutualTlsAdapter, "__init__")
  346. @mock.patch(
  347. "google.auth.transport._mtls_helper.get_client_cert_and_key", autospec=True
  348. )
  349. def test_configure_mtls_channel_non_mtls(
  350. self, mock_get_client_cert_and_key, mock_adapter_ctor
  351. ):
  352. mock_get_client_cert_and_key.return_value = (False, None, None)
  353. auth_session = google.auth.transport.requests.AuthorizedSession(
  354. credentials=mock.Mock()
  355. )
  356. with mock.patch.dict(
  357. os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"}
  358. ):
  359. auth_session.configure_mtls_channel()
  360. assert not auth_session.is_mtls
  361. # Assert _MutualTlsAdapter constructor is not called.
  362. mock_adapter_ctor.assert_not_called()
  363. @mock.patch(
  364. "google.auth.transport._mtls_helper.get_client_cert_and_key", autospec=True
  365. )
  366. def test_configure_mtls_channel_exceptions(self, mock_get_client_cert_and_key):
  367. mock_get_client_cert_and_key.side_effect = exceptions.ClientCertError()
  368. auth_session = google.auth.transport.requests.AuthorizedSession(
  369. credentials=mock.Mock()
  370. )
  371. with pytest.raises(exceptions.MutualTLSChannelError):
  372. with mock.patch.dict(
  373. os.environ, {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"}
  374. ):
  375. auth_session.configure_mtls_channel()
  376. mock_get_client_cert_and_key.return_value = (False, None, None)
  377. with mock.patch.dict("sys.modules"):
  378. sys.modules["OpenSSL"] = None
  379. with pytest.raises(exceptions.MutualTLSChannelError):
  380. with mock.patch.dict(
  381. os.environ,
  382. {environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE: "true"},
  383. ):
  384. auth_session.configure_mtls_channel()
  385. @mock.patch(
  386. "google.auth.transport._mtls_helper.get_client_cert_and_key", autospec=True
  387. )
  388. def test_configure_mtls_channel_without_client_cert_env(
  389. self, get_client_cert_and_key
  390. ):
  391. # Test client cert won't be used if GOOGLE_API_USE_CLIENT_CERTIFICATE
  392. # environment variable is not set.
  393. auth_session = google.auth.transport.requests.AuthorizedSession(
  394. credentials=mock.Mock()
  395. )
  396. auth_session.configure_mtls_channel()
  397. assert not auth_session.is_mtls
  398. get_client_cert_and_key.assert_not_called()
  399. mock_callback = mock.Mock()
  400. auth_session.configure_mtls_channel(mock_callback)
  401. assert not auth_session.is_mtls
  402. mock_callback.assert_not_called()