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

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

195 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 is not installed 

208 google.auth.exceptions.MutualTLSChannelError: If the cert or key is invalid. 

209 """ 

210 

211 def __init__(self, cert, key): 

212 import certifi 

213 import ssl 

214 

215 ctx_poolmanager = create_urllib3_context() 

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

217 

218 ctx_proxymanager = create_urllib3_context() 

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

220 

221 try: 

222 with _mtls_helper.secure_cert_key_paths(cert, key) as ( 

223 cert_path, 

224 key_path, 

225 passphrase, 

226 ): 

227 password = passphrase 

228 ctx_poolmanager.load_cert_chain( 

229 certfile=cert_path, 

230 keyfile=key_path, 

231 password=password, 

232 ) 

233 ctx_proxymanager.load_cert_chain( 

234 certfile=cert_path, 

235 keyfile=key_path, 

236 password=password, 

237 ) 

238 except ( 

239 ssl.SSLError, 

240 OSError, 

241 IOError, 

242 ValueError, 

243 RuntimeError, 

244 TypeError, 

245 ) as exc: 

246 raise exceptions.MutualTLSChannelError( 

247 "Failed to configure client certificate and key for mTLS." 

248 ) from exc 

249 

250 self._ctx_poolmanager = ctx_poolmanager 

251 self._ctx_proxymanager = ctx_proxymanager 

252 

253 super(_MutualTlsAdapter, self).__init__() 

254 

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

256 kwargs["ssl_context"] = self._ctx_poolmanager 

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

258 

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

260 kwargs["ssl_context"] = self._ctx_proxymanager 

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

262 

263 

264class _MutualTlsOffloadAdapter(requests.adapters.HTTPAdapter): 

265 """ 

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

267 signing operation to the signing library. 

268 

269 Args: 

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

271 file. The file should contain the following field: 

272 

273 { 

274 "libs": { 

275 "signer_library": "...", 

276 "offload_library": "..." 

277 } 

278 } 

279 

280 Raises: 

281 ImportError: if certifi is not installed 

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

283 creation failed for any reason. 

284 """ 

285 

286 def __init__(self, enterprise_cert_file_path): 

287 import certifi 

288 from google.auth.transport import _custom_tls_signer 

289 

290 self.signer = _custom_tls_signer.CustomTlsSigner(enterprise_cert_file_path) 

291 self.signer.load_libraries() 

292 

293 poolmanager = create_urllib3_context() 

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

295 self.signer.attach_to_ssl_context(poolmanager) 

296 self._ctx_poolmanager = poolmanager 

297 

298 proxymanager = create_urllib3_context() 

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

300 self.signer.attach_to_ssl_context(proxymanager) 

301 self._ctx_proxymanager = proxymanager 

302 

303 super(_MutualTlsOffloadAdapter, self).__init__() 

304 

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

306 kwargs["ssl_context"] = self._ctx_poolmanager 

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

308 

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

310 kwargs["ssl_context"] = self._ctx_proxymanager 

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

312 

313 

314class AuthorizedSession(requests.Session): 

315 """A Requests Session class with credentials. 

316 

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

318 authorization:: 

319 

320 from google.auth.transport.requests import AuthorizedSession 

321 

322 authed_session = AuthorizedSession(credentials) 

323 

324 response = authed_session.request( 

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

326 

327 

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

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

330 

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

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

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

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

335 following manner: 

336 

337 If client_cert_callback is provided, client certificate and private 

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

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

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

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

342 

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

344 instance and specify the endpoints:: 

345 

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

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

348 

349 authed_session = AuthorizedSession(credentials) 

350 

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

352 

353 def my_cert_callback(): 

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

355 # PEM format. 

356 some_code_to_load_client_cert_and_key() 

357 if loaded: 

358 return cert, key 

359 raise MyClientCertFailureException() 

360 

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

362 try: 

363 authed_session.configure_mtls_channel(my_cert_callback) 

364 except: 

365 # handle exceptions. 

366 

367 if authed_session.is_mtls: 

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

369 else: 

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

371 

372 

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

374 

375 try: 

376 authed_session.configure_mtls_channel() 

377 except: 

378 # handle exceptions. 

379 

380 Args: 

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

382 add to the request. 

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

384 that credentials should be refreshed and the request should be 

385 retried. 

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

387 refresh the credentials and retry the request. 

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

389 credential refresh HTTP requests. 

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

391 (Optional) An instance of 

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

393 refreshing credentials. If not passed, 

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

395 is created. 

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

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

398 account credentials. 

399 """ 

400 

401 def __init__( 

402 self, 

403 credentials, 

404 refresh_status_codes=transport.DEFAULT_REFRESH_STATUS_CODES, 

405 max_refresh_attempts=transport.DEFAULT_MAX_REFRESH_ATTEMPTS, 

406 refresh_timeout=None, 

407 auth_request=None, 

408 default_host=None, 

409 ): 

410 super(AuthorizedSession, self).__init__() 

411 self.credentials = credentials 

412 self._refresh_status_codes = refresh_status_codes 

413 self._max_refresh_attempts = max_refresh_attempts 

414 self._refresh_timeout = refresh_timeout 

415 self._is_mtls = False 

416 self._default_host = default_host 

417 

418 if auth_request is None: 

419 self._auth_request_session = requests.Session() 

420 

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

422 # This adapter retrys HTTP requests when network errors occur 

423 # and the requests seems safely retryable. 

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

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

426 

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

428 # infinite recursion. 

429 auth_request = Request(self._auth_request_session) 

430 else: 

431 self._auth_request_session = None 

432 

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

434 # credentials.refresh). 

435 self._auth_request = auth_request 

436 

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

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

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

440 self.credentials._create_self_signed_jwt( 

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

442 ) 

443 

444 def configure_mtls_channel(self, client_cert_callback=None): 

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

446 

447 This method configures mTLS if client certificates are explicitly enabled 

448 (via GOOGLE_API_USE_CLIENT_CERTIFICATE=true) or auto-enabled (when the env 

449 variable is unset and workload certificates are discovered). In these cases, 

450 if the client certificate and key are successfully obtained, a 

451 :class:`_MutualTlsAdapter` instance will be mounted to the "https://" prefix. 

452 

453 Args: 

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

455 The optional callback returns the client certificate and private 

456 key bytes both in PEM format. 

457 If the callback is None, application default SSL credentials 

458 will be used. 

459 

460 Raises: 

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

462 creation failed for any reason. The existing session state (such 

463 as adapter mounts) remains unmodified if this error is raised. 

464 """ 

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

466 if not use_client_cert: 

467 return 

468 

469 try: 

470 ( 

471 is_mtls, 

472 cert, 

473 key, 

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

475 client_cert_callback 

476 ) 

477 

478 if is_mtls: 

479 new_adapter = _MutualTlsAdapter(cert, key) 

480 else: 

481 new_adapter = requests.adapters.HTTPAdapter() 

482 except ( 

483 exceptions.ClientCertError, 

484 ImportError, 

485 OSError, 

486 ValueError, 

487 ) as caught_exc: 

488 new_exc = exceptions.MutualTLSChannelError(caught_exc) 

489 raise new_exc from caught_exc 

490 

491 self.mount("https://", new_adapter) 

492 self._is_mtls = is_mtls 

493 if is_mtls: 

494 self._cached_cert = cert 

495 else: 

496 if hasattr(self, "_cached_cert"): 

497 del self._cached_cert 

498 

499 def request( 

500 self, 

501 method, 

502 url, 

503 data=None, 

504 headers=None, 

505 max_allowed_time=None, 

506 timeout=_DEFAULT_TIMEOUT, 

507 **kwargs 

508 ): 

509 """Implementation of Requests' request. 

510 

511 Args: 

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

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

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

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

516 documentation for details. 

517 max_allowed_time (Optional[float]): 

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

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

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

521 multiple requests are made under the hood. 

522 

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

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

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

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

527 transmitted. The timeout error will be raised after such 

528 request completes. 

529 Raises: 

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

531 channel creation fails for any reason. 

532 ValueError: If the client certificate is invalid. 

533 """ 

534 # pylint: disable=arguments-differ 

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

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

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

538 

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

540 # thread-safety. 

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

542 

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

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

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

546 

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

548 # _auth_request's default timeout. 

549 auth_request = ( 

550 self._auth_request 

551 if timeout is None 

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

553 ) 

554 

555 remaining_time = max_allowed_time 

556 

557 with TimeoutGuard(remaining_time) as guard: 

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

559 remaining_time = guard.remaining_timeout 

560 

561 with TimeoutGuard(remaining_time) as guard: 

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

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

564 method, 

565 url, 

566 data=data, 

567 headers=request_headers, 

568 timeout=timeout, 

569 **kwargs 

570 ) 

571 remaining_time = guard.remaining_timeout 

572 

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

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

575 # request. 

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

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

578 if ( 

579 response.status_code in self._refresh_status_codes 

580 and _credential_refresh_attempt < self._max_refresh_attempts 

581 ): 

582 # Handle unauthorized permission error(401 status code) 

583 if response.status_code == http_client.UNAUTHORIZED: 

584 if self.is_mtls: 

585 ( 

586 call_cert_bytes, 

587 call_key_bytes, 

588 cached_fingerprint, 

589 current_cert_fingerprint, 

590 ) = _mtls_helper.check_parameters_for_unauthorized_response( 

591 self._cached_cert 

592 ) 

593 if cached_fingerprint != current_cert_fingerprint: 

594 try: 

595 _LOGGER.info( 

596 "Client certificate has changed, reconfiguring mTLS " 

597 "channel." 

598 ) 

599 self.configure_mtls_channel( 

600 lambda: (call_cert_bytes, call_key_bytes) 

601 ) 

602 except Exception as e: 

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

604 raise exceptions.MutualTLSChannelError( 

605 "Failed to reconfigure mTLS channel" 

606 ) from e 

607 else: 

608 _LOGGER.info( 

609 "Skipping reconfiguration of mTLS channel because the client" 

610 " certificate has not changed." 

611 ) 

612 _LOGGER.info( 

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

614 response.status_code, 

615 _credential_refresh_attempt + 1, 

616 self._max_refresh_attempts, 

617 ) 

618 

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

620 # _auth_request's default timeout. 

621 auth_request = ( 

622 self._auth_request 

623 if timeout is None 

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

625 ) 

626 

627 with TimeoutGuard(remaining_time) as guard: 

628 self.credentials.refresh(auth_request) 

629 remaining_time = guard.remaining_timeout 

630 

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

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

633 return self.request( 

634 method, 

635 url, 

636 data=data, 

637 headers=headers, 

638 max_allowed_time=remaining_time, 

639 timeout=timeout, 

640 _credential_refresh_attempt=_credential_refresh_attempt + 1, 

641 **kwargs 

642 ) 

643 

644 return response 

645 

646 @property 

647 def is_mtls(self): 

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

649 return self._is_mtls 

650 

651 def close(self): 

652 if self._auth_request_session is not None: 

653 self._auth_request_session.close() 

654 super(AuthorizedSession, self).close()