Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/google/auth/transport/requests.py: 26%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

199 statements  

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 

15"""Transport adapter for Requests.""" 

16 

17from __future__ import absolute_import 

18 

19import functools 

20import http.client as http_client 

21import logging 

22import numbers 

23import time 

24from typing import Optional 

25 

26try: 

27 import requests 

28except ImportError as caught_exc: # pragma: NO COVER 

29 raise ImportError( 

30 "The requests library is not installed from please install the requests package to use the requests transport." 

31 ) from caught_exc 

32import requests.adapters # pylint: disable=ungrouped-imports 

33import requests.exceptions # pylint: disable=ungrouped-imports 

34from requests.packages.urllib3.util.ssl_ import ( # type: ignore 

35 create_urllib3_context, 

36) # pylint: disable=ungrouped-imports 

37 

38from google.auth import _helpers 

39from google.auth import exceptions 

40from google.auth import transport 

41from google.auth.transport import _mtls_helper 

42import google.auth.transport._mtls_helper 

43from google.oauth2 import service_account 

44 

45_LOGGER = logging.getLogger(__name__) 

46 

47_DEFAULT_TIMEOUT = 120 # in seconds 

48 

49 

50class _Response(transport.Response): 

51 """Requests transport response adapter. 

52 

53 Args: 

54 response (requests.Response): The raw Requests response. 

55 """ 

56 

57 def __init__(self, response): 

58 self._response = response 

59 

60 @property 

61 def status(self): 

62 return self._response.status_code 

63 

64 @property 

65 def headers(self): 

66 return self._response.headers 

67 

68 @property 

69 def data(self): 

70 return self._response.content 

71 

72 

73class TimeoutGuard(object): 

74 """A context manager raising an error if the suite execution took too long. 

75 

76 Args: 

77 timeout (Union[None, Union[float, Tuple[float, float]]]): 

78 The maximum number of seconds a suite can run without the context 

79 manager raising a timeout exception on exit. If passed as a tuple, 

80 the smaller of the values is taken as a timeout. If ``None``, a 

81 timeout error is never raised. 

82 timeout_error_type (Optional[Exception]): 

83 The type of the error to raise on timeout. Defaults to 

84 :class:`requests.exceptions.Timeout`. 

85 """ 

86 

87 def __init__(self, timeout, timeout_error_type=requests.exceptions.Timeout): 

88 self._timeout = timeout 

89 self.remaining_timeout = timeout 

90 self._timeout_error_type = timeout_error_type 

91 

92 def __enter__(self): 

93 self._start = time.time() 

94 return self 

95 

96 def __exit__(self, exc_type, exc_value, traceback): 

97 if exc_value: 

98 return # let the error bubble up automatically 

99 

100 if self._timeout is None: 

101 return # nothing to do, the timeout was not specified 

102 

103 elapsed = time.time() - self._start 

104 deadline_hit = False 

105 

106 if isinstance(self._timeout, numbers.Number): 

107 self.remaining_timeout = self._timeout - elapsed 

108 deadline_hit = self.remaining_timeout <= 0 

109 else: 

110 self.remaining_timeout = tuple(x - elapsed for x in self._timeout) 

111 deadline_hit = min(self.remaining_timeout) <= 0 

112 

113 if deadline_hit: 

114 raise self._timeout_error_type() 

115 

116 

117class Request(transport.Request): 

118 """Requests request adapter. 

119 

120 This class is used internally for making requests using various transports 

121 in a consistent way. If you use :class:`AuthorizedSession` you do not need 

122 to construct or use this class directly. 

123 

124 This class can be useful if you want to manually refresh a 

125 :class:`~google.auth.credentials.Credentials` instance:: 

126 

127 import google.auth.transport.requests 

128 import requests 

129 

130 request = google.auth.transport.requests.Request() 

131 

132 credentials.refresh(request) 

133 

134 Args: 

135 session (requests.Session): An instance :class:`requests.Session` used 

136 to make HTTP requests. If not specified, a session will be created. 

137 

138 .. automethod:: __call__ 

139 """ 

140 

141 def __init__(self, session: Optional[requests.Session] = None) -> None: 

142 if not session: 

143 session = requests.Session() 

144 

145 self.session = session 

146 

147 def __del__(self): 

148 try: 

149 if hasattr(self, "session") and self.session is not None: 

150 self.session.close() 

151 except TypeError: 

152 # NOTE: For certain Python binary built, the queue.Empty exception 

153 # might not be considered a normal Python exception causing 

154 # TypeError. 

155 pass 

156 

157 def __call__( 

158 self, 

159 url, 

160 method="GET", 

161 body=None, 

162 headers=None, 

163 timeout=_DEFAULT_TIMEOUT, 

164 **kwargs 

165 ): 

166 """Make an HTTP request using requests. 

167 

168 Args: 

169 url (str): The URI to be requested. 

170 method (str): The HTTP method to use for the request. Defaults 

171 to 'GET'. 

172 body (bytes): The payload or body in HTTP request. 

173 headers (Mapping[str, str]): Request headers. 

174 timeout (Optional[int]): The number of seconds to wait for a 

175 response from the server. If not specified or if None, the 

176 requests default timeout will be used. 

177 kwargs: Additional arguments passed through to the underlying 

178 requests :meth:`~requests.Session.request` method. 

179 

180 Returns: 

181 google.auth.transport.Response: The HTTP response. 

182 

183 Raises: 

184 google.auth.exceptions.TransportError: If any exception occurred. 

185 """ 

186 try: 

187 _helpers.request_log(_LOGGER, method, url, body, headers) 

188 response = self.session.request( 

189 method, url, data=body, headers=headers, timeout=timeout, **kwargs 

190 ) 

191 _helpers.response_log(_LOGGER, response) 

192 return _Response(response) 

193 except requests.exceptions.RequestException as caught_exc: 

194 new_exc = exceptions.TransportError(caught_exc) 

195 raise new_exc from caught_exc 

196 

197 

198class _MutualTlsAdapter(requests.adapters.HTTPAdapter): 

199 """ 

200 A TransportAdapter that enables mutual TLS. 

201 

202 Args: 

203 cert (bytes): client certificate in PEM format 

204 key (bytes): client private key in PEM format 

205 

206 Raises: 

207 ImportError: if certifi or pyOpenSSL is not installed 

208 OpenSSL.crypto.Error: if client cert or key is invalid 

209 """ 

210 

211 def __init__(self, cert, key): 

212 import certifi 

213 from OpenSSL import crypto 

214 import urllib3.contrib.pyopenssl # type: ignore 

215 

216 urllib3.contrib.pyopenssl.inject_into_urllib3() 

217 

218 pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, key) 

219 x509 = crypto.load_certificate(crypto.FILETYPE_PEM, cert) 

220 

221 ctx_poolmanager = create_urllib3_context() 

222 ctx_poolmanager.load_verify_locations(cafile=certifi.where()) 

223 ctx_poolmanager._ctx.use_certificate(x509) 

224 ctx_poolmanager._ctx.use_privatekey(pkey) 

225 self._ctx_poolmanager = ctx_poolmanager 

226 

227 ctx_proxymanager = create_urllib3_context() 

228 ctx_proxymanager.load_verify_locations(cafile=certifi.where()) 

229 ctx_proxymanager._ctx.use_certificate(x509) 

230 ctx_proxymanager._ctx.use_privatekey(pkey) 

231 self._ctx_proxymanager = ctx_proxymanager 

232 

233 super(_MutualTlsAdapter, self).__init__() 

234 

235 def init_poolmanager(self, *args, **kwargs): 

236 kwargs["ssl_context"] = self._ctx_poolmanager 

237 super(_MutualTlsAdapter, self).init_poolmanager(*args, **kwargs) 

238 

239 def proxy_manager_for(self, *args, **kwargs): 

240 kwargs["ssl_context"] = self._ctx_proxymanager 

241 return super(_MutualTlsAdapter, self).proxy_manager_for(*args, **kwargs) 

242 

243 

244class _MutualTlsOffloadAdapter(requests.adapters.HTTPAdapter): 

245 """ 

246 A TransportAdapter that enables mutual TLS and offloads the client side 

247 signing operation to the signing library. 

248 

249 Args: 

250 enterprise_cert_file_path (str): the path to a enterprise cert JSON 

251 file. The file should contain the following field: 

252 

253 { 

254 "libs": { 

255 "signer_library": "...", 

256 "offload_library": "..." 

257 } 

258 } 

259 

260 Raises: 

261 ImportError: if certifi or pyOpenSSL is not installed 

262 google.auth.exceptions.MutualTLSChannelError: If mutual TLS channel 

263 creation failed for any reason. 

264 """ 

265 

266 def __init__(self, enterprise_cert_file_path): 

267 import certifi 

268 from google.auth.transport import _custom_tls_signer 

269 

270 self.signer = _custom_tls_signer.CustomTlsSigner(enterprise_cert_file_path) 

271 self.signer.load_libraries() 

272 

273 import urllib3.contrib.pyopenssl 

274 

275 urllib3.contrib.pyopenssl.inject_into_urllib3() 

276 

277 poolmanager = create_urllib3_context() 

278 poolmanager.load_verify_locations(cafile=certifi.where()) 

279 self.signer.attach_to_ssl_context(poolmanager) 

280 self._ctx_poolmanager = poolmanager 

281 

282 proxymanager = create_urllib3_context() 

283 proxymanager.load_verify_locations(cafile=certifi.where()) 

284 self.signer.attach_to_ssl_context(proxymanager) 

285 self._ctx_proxymanager = proxymanager 

286 

287 super(_MutualTlsOffloadAdapter, self).__init__() 

288 

289 def init_poolmanager(self, *args, **kwargs): 

290 kwargs["ssl_context"] = self._ctx_poolmanager 

291 super(_MutualTlsOffloadAdapter, self).init_poolmanager(*args, **kwargs) 

292 

293 def proxy_manager_for(self, *args, **kwargs): 

294 kwargs["ssl_context"] = self._ctx_proxymanager 

295 return super(_MutualTlsOffloadAdapter, self).proxy_manager_for(*args, **kwargs) 

296 

297 

298class AuthorizedSession(requests.Session): 

299 """A Requests Session class with credentials. 

300 

301 This class is used to perform requests to API endpoints that require 

302 authorization:: 

303 

304 from google.auth.transport.requests import AuthorizedSession 

305 

306 authed_session = AuthorizedSession(credentials) 

307 

308 response = authed_session.request( 

309 'GET', 'https://www.googleapis.com/storage/v1/b') 

310 

311 

312 The underlying :meth:`request` implementation handles adding the 

313 credentials' headers to the request and refreshing credentials as needed. 

314 

315 This class also supports mutual TLS via :meth:`configure_mtls_channel` 

316 method. In order to use this method, the `GOOGLE_API_USE_CLIENT_CERTIFICATE` 

317 environment variable must be explicitly set to ``true``, otherwise it does 

318 nothing. Assume the environment is set to ``true``, the method behaves in the 

319 following manner: 

320 

321 If client_cert_callback is provided, client certificate and private 

322 key are loaded using the callback; if client_cert_callback is None, 

323 application default SSL credentials will be used. Exceptions are raised if 

324 there are problems with the certificate, private key, or the loading process, 

325 so it should be called within a try/except block. 

326 

327 First we set the environment variable to ``true``, then create an :class:`AuthorizedSession` 

328 instance and specify the endpoints:: 

329 

330 regular_endpoint = 'https://pubsub.googleapis.com/v1/projects/{my_project_id}/topics' 

331 mtls_endpoint = 'https://pubsub.mtls.googleapis.com/v1/projects/{my_project_id}/topics' 

332 

333 authed_session = AuthorizedSession(credentials) 

334 

335 Now we can pass a callback to :meth:`configure_mtls_channel`:: 

336 

337 def my_cert_callback(): 

338 # some code to load client cert bytes and private key bytes, both in 

339 # PEM format. 

340 some_code_to_load_client_cert_and_key() 

341 if loaded: 

342 return cert, key 

343 raise MyClientCertFailureException() 

344 

345 # Always call configure_mtls_channel within a try/except block. 

346 try: 

347 authed_session.configure_mtls_channel(my_cert_callback) 

348 except: 

349 # handle exceptions. 

350 

351 if authed_session.is_mtls: 

352 response = authed_session.request('GET', mtls_endpoint) 

353 else: 

354 response = authed_session.request('GET', regular_endpoint) 

355 

356 

357 You can alternatively use application default SSL credentials like this:: 

358 

359 try: 

360 authed_session.configure_mtls_channel() 

361 except: 

362 # handle exceptions. 

363 

364 Args: 

365 credentials (google.auth.credentials.Credentials): The credentials to 

366 add to the request. 

367 refresh_status_codes (Sequence[int]): Which HTTP status codes indicate 

368 that credentials should be refreshed and the request should be 

369 retried. 

370 max_refresh_attempts (int): The maximum number of times to attempt to 

371 refresh the credentials and retry the request. 

372 refresh_timeout (Optional[int]): The timeout value in seconds for 

373 credential refresh HTTP requests. 

374 auth_request (google.auth.transport.requests.Request): 

375 (Optional) An instance of 

376 :class:`~google.auth.transport.requests.Request` used when 

377 refreshing credentials. If not passed, 

378 an instance of :class:`~google.auth.transport.requests.Request` 

379 is created. 

380 default_host (Optional[str]): A host like "pubsub.googleapis.com". 

381 This is used when a self-signed JWT is created from service 

382 account credentials. 

383 """ 

384 

385 def __init__( 

386 self, 

387 credentials, 

388 refresh_status_codes=transport.DEFAULT_REFRESH_STATUS_CODES, 

389 max_refresh_attempts=transport.DEFAULT_MAX_REFRESH_ATTEMPTS, 

390 refresh_timeout=None, 

391 auth_request=None, 

392 default_host=None, 

393 ): 

394 super(AuthorizedSession, self).__init__() 

395 self.credentials = credentials 

396 self._refresh_status_codes = refresh_status_codes 

397 self._max_refresh_attempts = max_refresh_attempts 

398 self._refresh_timeout = refresh_timeout 

399 self._is_mtls = False 

400 self._default_host = default_host 

401 

402 if auth_request is None: 

403 self._auth_request_session = requests.Session() 

404 

405 # Using an adapter to make HTTP requests robust to network errors. 

406 # This adapter retrys HTTP requests when network errors occur 

407 # and the requests seems safely retryable. 

408 retry_adapter = requests.adapters.HTTPAdapter(max_retries=3) 

409 self._auth_request_session.mount("https://", retry_adapter) 

410 

411 # Do not pass `self` as the session here, as it can lead to 

412 # infinite recursion. 

413 auth_request = Request(self._auth_request_session) 

414 else: 

415 self._auth_request_session = None 

416 

417 # Request instance used by internal methods (for example, 

418 # credentials.refresh). 

419 self._auth_request = auth_request 

420 

421 # https://google.aip.dev/auth/4111 

422 # Attempt to use self-signed JWTs when a service account is used. 

423 if isinstance(self.credentials, service_account.Credentials): 

424 self.credentials._create_self_signed_jwt( 

425 "https://{}/".format(self._default_host) if self._default_host else None 

426 ) 

427 

428 def configure_mtls_channel(self, client_cert_callback=None): 

429 """Configure the client certificate and key for SSL connection. 

430 

431 The function does nothing unless `GOOGLE_API_USE_CLIENT_CERTIFICATE` is 

432 explicitly set to `true`. In this case if client certificate and key are 

433 successfully obtained (from the given client_cert_callback or from application 

434 default SSL credentials), a :class:`_MutualTlsAdapter` instance will be mounted 

435 to "https://" prefix. 

436 

437 Args: 

438 client_cert_callback (Optional[Callable[[], (bytes, bytes)]]): 

439 The optional callback returns the client certificate and private 

440 key bytes both in PEM format. 

441 If the callback is None, application default SSL credentials 

442 will be used. 

443 

444 Raises: 

445 google.auth.exceptions.MutualTLSChannelError: If mutual TLS channel 

446 creation failed for any reason. 

447 """ 

448 use_client_cert = google.auth.transport._mtls_helper.check_use_client_cert() 

449 if not use_client_cert: 

450 self._is_mtls = False 

451 return 

452 try: 

453 import OpenSSL 

454 except ImportError as caught_exc: 

455 new_exc = exceptions.MutualTLSChannelError(caught_exc) 

456 raise new_exc from caught_exc 

457 

458 try: 

459 ( 

460 self._is_mtls, 

461 cert, 

462 key, 

463 ) = google.auth.transport._mtls_helper.get_client_cert_and_key( 

464 client_cert_callback 

465 ) 

466 

467 if self._is_mtls: 

468 mtls_adapter = _MutualTlsAdapter(cert, key) 

469 self._cached_cert = cert 

470 self.mount("https://", mtls_adapter) 

471 except ( 

472 exceptions.ClientCertError, 

473 ImportError, 

474 OpenSSL.crypto.Error, 

475 ) as caught_exc: 

476 new_exc = exceptions.MutualTLSChannelError(caught_exc) 

477 raise new_exc from caught_exc 

478 

479 def request( 

480 self, 

481 method, 

482 url, 

483 data=None, 

484 headers=None, 

485 max_allowed_time=None, 

486 timeout=_DEFAULT_TIMEOUT, 

487 **kwargs 

488 ): 

489 """Implementation of Requests' request. 

490 

491 Args: 

492 timeout (Optional[Union[float, Tuple[float, float]]]): 

493 The amount of time in seconds to wait for the server response 

494 with each individual request. Can also be passed as a tuple 

495 ``(connect_timeout, read_timeout)``. See :meth:`requests.Session.request` 

496 documentation for details. 

497 max_allowed_time (Optional[float]): 

498 If the method runs longer than this, a ``Timeout`` exception is 

499 automatically raised. Unlike the ``timeout`` parameter, this 

500 value applies to the total method execution time, even if 

501 multiple requests are made under the hood. 

502 

503 Mind that it is not guaranteed that the timeout error is raised 

504 at ``max_allowed_time``. It might take longer, for example, if 

505 an underlying request takes a lot of time, but the request 

506 itself does not timeout, e.g. if a large file is being 

507 transmitted. The timeout error will be raised after such 

508 request completes. 

509 Raises: 

510 google.auth.exceptions.MutualTLSChannelError: If mutual TLS 

511 channel creation fails for any reason. 

512 ValueError: If the client certificate is invalid. 

513 """ 

514 # pylint: disable=arguments-differ 

515 # Requests has a ton of arguments to request, but only two 

516 # (method, url) are required. We pass through all of the other 

517 # arguments to super, so no need to exhaustively list them here. 

518 

519 # Use a kwarg for this instead of an attribute to maintain 

520 # thread-safety. 

521 _credential_refresh_attempt = kwargs.pop("_credential_refresh_attempt", 0) 

522 

523 # Make a copy of the headers. They will be modified by the credentials 

524 # and we want to pass the original headers if we recurse. 

525 request_headers = headers.copy() if headers is not None else {} 

526 

527 # Do not apply the timeout unconditionally in order to not override the 

528 # _auth_request's default timeout. 

529 auth_request = ( 

530 self._auth_request 

531 if timeout is None 

532 else functools.partial(self._auth_request, timeout=timeout) 

533 ) 

534 

535 remaining_time = max_allowed_time 

536 

537 with TimeoutGuard(remaining_time) as guard: 

538 self.credentials.before_request(auth_request, method, url, request_headers) 

539 remaining_time = guard.remaining_timeout 

540 

541 with TimeoutGuard(remaining_time) as guard: 

542 _helpers.request_log(_LOGGER, method, url, data, headers) 

543 response = super(AuthorizedSession, self).request( 

544 method, 

545 url, 

546 data=data, 

547 headers=request_headers, 

548 timeout=timeout, 

549 **kwargs 

550 ) 

551 remaining_time = guard.remaining_timeout 

552 

553 # If the response indicated that the credentials needed to be 

554 # refreshed, then refresh the credentials and re-attempt the 

555 # request. 

556 # A stored token may expire between the time it is retrieved and 

557 # the time the request is made, so we may need to try twice. 

558 if ( 

559 response.status_code in self._refresh_status_codes 

560 and _credential_refresh_attempt < self._max_refresh_attempts 

561 ): 

562 # Handle unauthorized permission error(401 status code) 

563 if response.status_code == http_client.UNAUTHORIZED: 

564 if self.is_mtls: 

565 ( 

566 call_cert_bytes, 

567 call_key_bytes, 

568 cached_fingerprint, 

569 current_cert_fingerprint, 

570 ) = _mtls_helper.check_parameters_for_unauthorized_response( 

571 self._cached_cert 

572 ) 

573 if cached_fingerprint != current_cert_fingerprint: 

574 try: 

575 _LOGGER.info( 

576 "Client certificate has changed, reconfiguring mTLS " 

577 "channel." 

578 ) 

579 self.configure_mtls_channel( 

580 lambda: (call_cert_bytes, call_key_bytes) 

581 ) 

582 except Exception as e: 

583 _LOGGER.error("Failed to reconfigure mTLS channel: %s", e) 

584 raise exceptions.MutualTLSChannelError( 

585 "Failed to reconfigure mTLS channel" 

586 ) from e 

587 else: 

588 _LOGGER.info( 

589 "Skipping reconfiguration of mTLS channel because the client" 

590 " certificate has not changed." 

591 ) 

592 _LOGGER.info( 

593 "Refreshing credentials due to a %s response. Attempt %s/%s.", 

594 response.status_code, 

595 _credential_refresh_attempt + 1, 

596 self._max_refresh_attempts, 

597 ) 

598 

599 # Do not apply the timeout unconditionally in order to not override the 

600 # _auth_request's default timeout. 

601 auth_request = ( 

602 self._auth_request 

603 if timeout is None 

604 else functools.partial(self._auth_request, timeout=timeout) 

605 ) 

606 

607 with TimeoutGuard(remaining_time) as guard: 

608 self.credentials.refresh(auth_request) 

609 remaining_time = guard.remaining_timeout 

610 

611 # Recurse. Pass in the original headers, not our modified set, but 

612 # do pass the adjusted max allowed time (i.e. the remaining total time). 

613 return self.request( 

614 method, 

615 url, 

616 data=data, 

617 headers=headers, 

618 max_allowed_time=remaining_time, 

619 timeout=timeout, 

620 _credential_refresh_attempt=_credential_refresh_attempt + 1, 

621 **kwargs 

622 ) 

623 

624 return response 

625 

626 @property 

627 def is_mtls(self): 

628 """Indicates if the created SSL channel is mutual TLS.""" 

629 return self._is_mtls 

630 

631 def close(self): 

632 if self._auth_request_session is not None: 

633 self._auth_request_session.close() 

634 super(AuthorizedSession, self).close()