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

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

186 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 logging 

21import numbers 

22import os 

23import time 

24 

25try: 

26 import requests 

27except ImportError as caught_exc: # pragma: NO COVER 

28 raise ImportError( 

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

30 ) from caught_exc 

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

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

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

34 create_urllib3_context, 

35) # pylint: disable=ungrouped-imports 

36 

37from google.auth import _helpers 

38from google.auth import environment_vars 

39from google.auth import exceptions 

40from google.auth import transport 

41import google.auth.transport._mtls_helper 

42from google.oauth2 import service_account 

43 

44_LOGGER = logging.getLogger(__name__) 

45 

46_DEFAULT_TIMEOUT = 120 # in seconds 

47 

48 

49class _Response(transport.Response): 

50 """Requests transport response adapter. 

51 

52 Args: 

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

54 """ 

55 

56 def __init__(self, response): 

57 self._response = response 

58 

59 @property 

60 def status(self): 

61 return self._response.status_code 

62 

63 @property 

64 def headers(self): 

65 return self._response.headers 

66 

67 @property 

68 def data(self): 

69 return self._response.content 

70 

71 

72class TimeoutGuard(object): 

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

74 

75 Args: 

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

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

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

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

80 timeout error is never raised. 

81 timeout_error_type (Optional[Exception]): 

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

83 :class:`requests.exceptions.Timeout`. 

84 """ 

85 

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

87 self._timeout = timeout 

88 self.remaining_timeout = timeout 

89 self._timeout_error_type = timeout_error_type 

90 

91 def __enter__(self): 

92 self._start = time.time() 

93 return self 

94 

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

96 if exc_value: 

97 return # let the error bubble up automatically 

98 

99 if self._timeout is None: 

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

101 

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

103 deadline_hit = False 

104 

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

106 self.remaining_timeout = self._timeout - elapsed 

107 deadline_hit = self.remaining_timeout <= 0 

108 else: 

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

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

111 

112 if deadline_hit: 

113 raise self._timeout_error_type() 

114 

115 

116class Request(transport.Request): 

117 """Requests request adapter. 

118 

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

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

121 to construct or use this class directly. 

122 

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

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

125 

126 import google.auth.transport.requests 

127 import requests 

128 

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

130 

131 credentials.refresh(request) 

132 

133 Args: 

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

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

136 

137 .. automethod:: __call__ 

138 """ 

139 

140 def __init__(self, session=None): 

141 if not session: 

142 session = requests.Session() 

143 

144 self.session = session 

145 

146 def __del__(self): 

147 try: 

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

149 self.session.close() 

150 except TypeError: 

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

152 # might not be considered a normal Python exception causing 

153 # TypeError. 

154 pass 

155 

156 def __call__( 

157 self, 

158 url, 

159 method="GET", 

160 body=None, 

161 headers=None, 

162 timeout=_DEFAULT_TIMEOUT, 

163 **kwargs 

164 ): 

165 """Make an HTTP request using requests. 

166 

167 Args: 

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

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

170 to 'GET'. 

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

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

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

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

175 requests default timeout will be used. 

176 kwargs: Additional arguments passed through to the underlying 

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

178 

179 Returns: 

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

181 

182 Raises: 

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

184 """ 

185 try: 

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

187 response = self.session.request( 

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

189 ) 

190 _helpers.response_log(_LOGGER, response) 

191 return _Response(response) 

192 except requests.exceptions.RequestException as caught_exc: 

193 new_exc = exceptions.TransportError(caught_exc) 

194 raise new_exc from caught_exc 

195 

196 

197class _MutualTlsAdapter(requests.adapters.HTTPAdapter): 

198 """ 

199 A TransportAdapter that enables mutual TLS. 

200 

201 Args: 

202 cert (bytes): client certificate in PEM format 

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

204 

205 Raises: 

206 ImportError: if certifi or pyOpenSSL is not installed 

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

208 """ 

209 

210 def __init__(self, cert, key): 

211 import certifi 

212 from OpenSSL import crypto 

213 import urllib3.contrib.pyopenssl # type: ignore 

214 

215 urllib3.contrib.pyopenssl.inject_into_urllib3() 

216 

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

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

219 

220 ctx_poolmanager = create_urllib3_context() 

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

222 ctx_poolmanager._ctx.use_certificate(x509) 

223 ctx_poolmanager._ctx.use_privatekey(pkey) 

224 self._ctx_poolmanager = ctx_poolmanager 

225 

226 ctx_proxymanager = create_urllib3_context() 

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

228 ctx_proxymanager._ctx.use_certificate(x509) 

229 ctx_proxymanager._ctx.use_privatekey(pkey) 

230 self._ctx_proxymanager = ctx_proxymanager 

231 

232 super(_MutualTlsAdapter, self).__init__() 

233 

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

235 kwargs["ssl_context"] = self._ctx_poolmanager 

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

237 

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

239 kwargs["ssl_context"] = self._ctx_proxymanager 

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

241 

242 

243class _MutualTlsOffloadAdapter(requests.adapters.HTTPAdapter): 

244 """ 

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

246 signing operation to the signing library. 

247 

248 Args: 

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

250 file. The file should contain the following field: 

251 

252 { 

253 "libs": { 

254 "signer_library": "...", 

255 "offload_library": "..." 

256 } 

257 } 

258 

259 Raises: 

260 ImportError: if certifi or pyOpenSSL is not installed 

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

262 creation failed for any reason. 

263 """ 

264 

265 def __init__(self, enterprise_cert_file_path): 

266 import certifi 

267 from google.auth.transport import _custom_tls_signer 

268 

269 self.signer = _custom_tls_signer.CustomTlsSigner(enterprise_cert_file_path) 

270 self.signer.load_libraries() 

271 

272 import urllib3.contrib.pyopenssl 

273 

274 urllib3.contrib.pyopenssl.inject_into_urllib3() 

275 

276 poolmanager = create_urllib3_context() 

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

278 self.signer.attach_to_ssl_context(poolmanager) 

279 self._ctx_poolmanager = poolmanager 

280 

281 proxymanager = create_urllib3_context() 

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

283 self.signer.attach_to_ssl_context(proxymanager) 

284 self._ctx_proxymanager = proxymanager 

285 

286 super(_MutualTlsOffloadAdapter, self).__init__() 

287 

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

289 kwargs["ssl_context"] = self._ctx_poolmanager 

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

291 

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

293 kwargs["ssl_context"] = self._ctx_proxymanager 

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

295 

296 

297class AuthorizedSession(requests.Session): 

298 """A Requests Session class with credentials. 

299 

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

301 authorization:: 

302 

303 from google.auth.transport.requests import AuthorizedSession 

304 

305 authed_session = AuthorizedSession(credentials) 

306 

307 response = authed_session.request( 

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

309 

310 

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

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

313 

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

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

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

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

318 following manner: 

319 

320 If client_cert_callback is provided, client certificate and private 

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

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

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

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

325 

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

327 instance and specify the endpoints:: 

328 

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

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

331 

332 authed_session = AuthorizedSession(credentials) 

333 

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

335 

336 def my_cert_callback(): 

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

338 # PEM format. 

339 some_code_to_load_client_cert_and_key() 

340 if loaded: 

341 return cert, key 

342 raise MyClientCertFailureException() 

343 

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

345 try: 

346 authed_session.configure_mtls_channel(my_cert_callback) 

347 except: 

348 # handle exceptions. 

349 

350 if authed_session.is_mtls: 

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

352 else: 

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

354 

355 

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

357 

358 try: 

359 authed_session.configure_mtls_channel() 

360 except: 

361 # handle exceptions. 

362 

363 Args: 

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

365 add to the request. 

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

367 that credentials should be refreshed and the request should be 

368 retried. 

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

370 refresh the credentials and retry the request. 

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

372 credential refresh HTTP requests. 

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

374 (Optional) An instance of 

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

376 refreshing credentials. If not passed, 

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

378 is created. 

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

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

381 account credentials. 

382 """ 

383 

384 def __init__( 

385 self, 

386 credentials, 

387 refresh_status_codes=transport.DEFAULT_REFRESH_STATUS_CODES, 

388 max_refresh_attempts=transport.DEFAULT_MAX_REFRESH_ATTEMPTS, 

389 refresh_timeout=None, 

390 auth_request=None, 

391 default_host=None, 

392 ): 

393 super(AuthorizedSession, self).__init__() 

394 self.credentials = credentials 

395 self._refresh_status_codes = refresh_status_codes 

396 self._max_refresh_attempts = max_refresh_attempts 

397 self._refresh_timeout = refresh_timeout 

398 self._is_mtls = False 

399 self._default_host = default_host 

400 

401 if auth_request is None: 

402 self._auth_request_session = requests.Session() 

403 

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

405 # This adapter retrys HTTP requests when network errors occur 

406 # and the requests seems safely retryable. 

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

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

409 

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

411 # infinite recursion. 

412 auth_request = Request(self._auth_request_session) 

413 else: 

414 self._auth_request_session = None 

415 

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

417 # credentials.refresh). 

418 self._auth_request = auth_request 

419 

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

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

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

423 self.credentials._create_self_signed_jwt( 

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

425 ) 

426 

427 def configure_mtls_channel(self, client_cert_callback=None): 

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

429 

430 The function does nothing unless `GOOGLE_API_USE_CLIENT_CERTIFICATE` is 

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

432 successfully obtained (from the given client_cert_callback or from application 

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

434 to "https://" prefix. 

435 

436 Args: 

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

438 The optional callback returns the client certificate and private 

439 key bytes both in PEM format. 

440 If the callback is None, application default SSL credentials 

441 will be used. 

442 

443 Raises: 

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

445 creation failed for any reason. 

446 """ 

447 use_client_cert = os.getenv( 

448 environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE, "false" 

449 ) 

450 if use_client_cert != "true": 

451 self._is_mtls = False 

452 return 

453 

454 try: 

455 import OpenSSL 

456 except ImportError as caught_exc: 

457 new_exc = exceptions.MutualTLSChannelError(caught_exc) 

458 raise new_exc from caught_exc 

459 

460 try: 

461 ( 

462 self._is_mtls, 

463 cert, 

464 key, 

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

466 client_cert_callback 

467 ) 

468 

469 if self._is_mtls: 

470 mtls_adapter = _MutualTlsAdapter(cert, key) 

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

472 except ( 

473 exceptions.ClientCertError, 

474 ImportError, 

475 OpenSSL.crypto.Error, 

476 ) as caught_exc: 

477 new_exc = exceptions.MutualTLSChannelError(caught_exc) 

478 raise new_exc from caught_exc 

479 

480 def request( 

481 self, 

482 method, 

483 url, 

484 data=None, 

485 headers=None, 

486 max_allowed_time=None, 

487 timeout=_DEFAULT_TIMEOUT, 

488 **kwargs 

489 ): 

490 """Implementation of Requests' request. 

491 

492 Args: 

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

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

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

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

497 documentation for details. 

498 max_allowed_time (Optional[float]): 

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

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

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

502 multiple requests are made under the hood. 

503 

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

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

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

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

508 transmitted. The timout error will be raised after such 

509 request completes. 

510 """ 

511 # pylint: disable=arguments-differ 

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

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

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

515 

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

517 # thread-safety. 

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

519 

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

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

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

523 

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

525 # _auth_request's default timeout. 

526 auth_request = ( 

527 self._auth_request 

528 if timeout is None 

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

530 ) 

531 

532 remaining_time = max_allowed_time 

533 

534 with TimeoutGuard(remaining_time) as guard: 

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

536 remaining_time = guard.remaining_timeout 

537 

538 with TimeoutGuard(remaining_time) as guard: 

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

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

541 method, 

542 url, 

543 data=data, 

544 headers=request_headers, 

545 timeout=timeout, 

546 **kwargs 

547 ) 

548 remaining_time = guard.remaining_timeout 

549 

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

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

552 # request. 

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

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

555 if ( 

556 response.status_code in self._refresh_status_codes 

557 and _credential_refresh_attempt < self._max_refresh_attempts 

558 ): 

559 

560 _LOGGER.info( 

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

562 response.status_code, 

563 _credential_refresh_attempt + 1, 

564 self._max_refresh_attempts, 

565 ) 

566 

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

568 # _auth_request's default timeout. 

569 auth_request = ( 

570 self._auth_request 

571 if timeout is None 

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

573 ) 

574 

575 with TimeoutGuard(remaining_time) as guard: 

576 self.credentials.refresh(auth_request) 

577 remaining_time = guard.remaining_timeout 

578 

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

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

581 return self.request( 

582 method, 

583 url, 

584 data=data, 

585 headers=headers, 

586 max_allowed_time=remaining_time, 

587 timeout=timeout, 

588 _credential_refresh_attempt=_credential_refresh_attempt + 1, 

589 **kwargs 

590 ) 

591 

592 return response 

593 

594 @property 

595 def is_mtls(self): 

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

597 return self._is_mtls 

598 

599 def close(self): 

600 if self._auth_request_session is not None: 

601 self._auth_request_session.close() 

602 super(AuthorizedSession, self).close()