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

184 statements  

« prev     ^ index     » next       coverage.py v7.2.2, created at 2023-03-26 07:30 +0000

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 import six 

29 

30 six.raise_from( 

31 ImportError( 

32 "The requests library is not installed, please install the " 

33 "requests package to use the requests transport." 

34 ), 

35 caught_exc, 

36 ) 

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

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

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

40 create_urllib3_context, 

41) # pylint: disable=ungrouped-imports 

42import six # pylint: disable=ungrouped-imports 

43 

44from google.auth import environment_vars 

45from google.auth import exceptions 

46from google.auth import transport 

47import google.auth.transport._mtls_helper 

48from google.oauth2 import service_account 

49 

50_LOGGER = logging.getLogger(__name__) 

51 

52_DEFAULT_TIMEOUT = 120 # in seconds 

53 

54 

55class _Response(transport.Response): 

56 """Requests transport response adapter. 

57 

58 Args: 

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

60 """ 

61 

62 def __init__(self, response): 

63 self._response = response 

64 

65 @property 

66 def status(self): 

67 return self._response.status_code 

68 

69 @property 

70 def headers(self): 

71 return self._response.headers 

72 

73 @property 

74 def data(self): 

75 return self._response.content 

76 

77 

78class TimeoutGuard(object): 

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

80 

81 Args: 

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

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

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

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

86 timeout error is never raised. 

87 timeout_error_type (Optional[Exception]): 

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

89 :class:`requests.exceptions.Timeout`. 

90 """ 

91 

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

93 self._timeout = timeout 

94 self.remaining_timeout = timeout 

95 self._timeout_error_type = timeout_error_type 

96 

97 def __enter__(self): 

98 self._start = time.time() 

99 return self 

100 

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

102 if exc_value: 

103 return # let the error bubble up automatically 

104 

105 if self._timeout is None: 

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

107 

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

109 deadline_hit = False 

110 

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

112 self.remaining_timeout = self._timeout - elapsed 

113 deadline_hit = self.remaining_timeout <= 0 

114 else: 

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

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

117 

118 if deadline_hit: 

119 raise self._timeout_error_type() 

120 

121 

122class Request(transport.Request): 

123 """Requests request adapter. 

124 

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

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

127 to construct or use this class directly. 

128 

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

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

131 

132 import google.auth.transport.requests 

133 import requests 

134 

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

136 

137 credentials.refresh(request) 

138 

139 Args: 

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

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

142 

143 .. automethod:: __call__ 

144 """ 

145 

146 def __init__(self, session=None): 

147 if not session: 

148 session = requests.Session() 

149 

150 self.session = session 

151 

152 def __del__(self): 

153 try: 

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

155 self.session.close() 

156 except TypeError: 

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

158 # might not be considered a normal Python exception causing 

159 # TypeError. 

160 pass 

161 

162 def __call__( 

163 self, 

164 url, 

165 method="GET", 

166 body=None, 

167 headers=None, 

168 timeout=_DEFAULT_TIMEOUT, 

169 **kwargs 

170 ): 

171 """Make an HTTP request using requests. 

172 

173 Args: 

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

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

176 to 'GET'. 

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

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

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

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

181 requests default timeout will be used. 

182 kwargs: Additional arguments passed through to the underlying 

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

184 

185 Returns: 

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

187 

188 Raises: 

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

190 """ 

191 try: 

192 _LOGGER.debug("Making request: %s %s", method, url) 

193 response = self.session.request( 

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

195 ) 

196 return _Response(response) 

197 except requests.exceptions.RequestException as caught_exc: 

198 new_exc = exceptions.TransportError(caught_exc) 

199 six.raise_from(new_exc, caught_exc) 

200 

201 

202class _MutualTlsAdapter(requests.adapters.HTTPAdapter): 

203 """ 

204 A TransportAdapter that enables mutual TLS. 

205 

206 Args: 

207 cert (bytes): client certificate in PEM format 

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

209 

210 Raises: 

211 ImportError: if certifi or pyOpenSSL is not installed 

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

213 """ 

214 

215 def __init__(self, cert, key): 

216 import certifi 

217 from OpenSSL import crypto 

218 import urllib3.contrib.pyopenssl # type: ignore 

219 

220 urllib3.contrib.pyopenssl.inject_into_urllib3() 

221 

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

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

224 

225 ctx_poolmanager = create_urllib3_context() 

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

227 ctx_poolmanager._ctx.use_certificate(x509) 

228 ctx_poolmanager._ctx.use_privatekey(pkey) 

229 self._ctx_poolmanager = ctx_poolmanager 

230 

231 ctx_proxymanager = create_urllib3_context() 

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

233 ctx_proxymanager._ctx.use_certificate(x509) 

234 ctx_proxymanager._ctx.use_privatekey(pkey) 

235 self._ctx_proxymanager = ctx_proxymanager 

236 

237 super(_MutualTlsAdapter, self).__init__() 

238 

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

240 kwargs["ssl_context"] = self._ctx_poolmanager 

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

242 

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

244 kwargs["ssl_context"] = self._ctx_proxymanager 

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

246 

247 

248class _MutualTlsOffloadAdapter(requests.adapters.HTTPAdapter): 

249 """ 

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

251 signing operation to the signing library. 

252 

253 Args: 

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

255 file. The file should contain the following field: 

256 

257 { 

258 "libs": { 

259 "signer_library": "...", 

260 "offload_library": "..." 

261 } 

262 } 

263 

264 Raises: 

265 ImportError: if certifi or pyOpenSSL is not installed 

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

267 creation failed for any reason. 

268 """ 

269 

270 def __init__(self, enterprise_cert_file_path): 

271 import certifi 

272 import urllib3.contrib.pyopenssl 

273 

274 from google.auth.transport import _custom_tls_signer 

275 

276 # Call inject_into_urllib3 to activate certificate checking. See the 

277 # following links for more info: 

278 # (1) doc: https://github.com/urllib3/urllib3/blob/cb9ebf8aac5d75f64c8551820d760b72b619beff/src/urllib3/contrib/pyopenssl.py#L31-L32 

279 # (2) mTLS example: https://github.com/urllib3/urllib3/issues/474#issuecomment-253168415 

280 urllib3.contrib.pyopenssl.inject_into_urllib3() 

281 

282 self.signer = _custom_tls_signer.CustomTlsSigner(enterprise_cert_file_path) 

283 self.signer.load_libraries() 

284 self.signer.set_up_custom_key() 

285 

286 poolmanager = create_urllib3_context() 

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

288 self.signer.attach_to_ssl_context(poolmanager) 

289 self._ctx_poolmanager = poolmanager 

290 

291 proxymanager = create_urllib3_context() 

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

293 self.signer.attach_to_ssl_context(proxymanager) 

294 self._ctx_proxymanager = proxymanager 

295 

296 super(_MutualTlsOffloadAdapter, self).__init__() 

297 

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

299 kwargs["ssl_context"] = self._ctx_poolmanager 

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

301 

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

303 kwargs["ssl_context"] = self._ctx_proxymanager 

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

305 

306 

307class AuthorizedSession(requests.Session): 

308 """A Requests Session class with credentials. 

309 

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

311 authorization:: 

312 

313 from google.auth.transport.requests import AuthorizedSession 

314 

315 authed_session = AuthorizedSession(credentials) 

316 

317 response = authed_session.request( 

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

319 

320 

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

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

323 

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

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

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

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

328 following manner: 

329 

330 If client_cert_callback is provided, client certificate and private 

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

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

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

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

335 

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

337 instance and specify the endpoints:: 

338 

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

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

341 

342 authed_session = AuthorizedSession(credentials) 

343 

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

345 

346 def my_cert_callback(): 

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

348 # PEM format. 

349 some_code_to_load_client_cert_and_key() 

350 if loaded: 

351 return cert, key 

352 raise MyClientCertFailureException() 

353 

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

355 try: 

356 authed_session.configure_mtls_channel(my_cert_callback) 

357 except: 

358 # handle exceptions. 

359 

360 if authed_session.is_mtls: 

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

362 else: 

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

364 

365 

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

367 

368 try: 

369 authed_session.configure_mtls_channel() 

370 except: 

371 # handle exceptions. 

372 

373 Args: 

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

375 add to the request. 

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

377 that credentials should be refreshed and the request should be 

378 retried. 

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

380 refresh the credentials and retry the request. 

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

382 credential refresh HTTP requests. 

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

384 (Optional) An instance of 

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

386 refreshing credentials. If not passed, 

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

388 is created. 

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

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

391 account credentials. 

392 """ 

393 

394 def __init__( 

395 self, 

396 credentials, 

397 refresh_status_codes=transport.DEFAULT_REFRESH_STATUS_CODES, 

398 max_refresh_attempts=transport.DEFAULT_MAX_REFRESH_ATTEMPTS, 

399 refresh_timeout=None, 

400 auth_request=None, 

401 default_host=None, 

402 ): 

403 super(AuthorizedSession, self).__init__() 

404 self.credentials = credentials 

405 self._refresh_status_codes = refresh_status_codes 

406 self._max_refresh_attempts = max_refresh_attempts 

407 self._refresh_timeout = refresh_timeout 

408 self._is_mtls = False 

409 self._default_host = default_host 

410 

411 if auth_request is None: 

412 self._auth_request_session = requests.Session() 

413 

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

415 # This adapter retrys HTTP requests when network errors occur 

416 # and the requests seems safely retryable. 

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

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

419 

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

421 # infinite recursion. 

422 auth_request = Request(self._auth_request_session) 

423 else: 

424 self._auth_request_session = None 

425 

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

427 # credentials.refresh). 

428 self._auth_request = auth_request 

429 

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

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

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

433 self.credentials._create_self_signed_jwt( 

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

435 ) 

436 

437 def configure_mtls_channel(self, client_cert_callback=None): 

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

439 

440 The function does nothing unless `GOOGLE_API_USE_CLIENT_CERTIFICATE` is 

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

442 successfully obtained (from the given client_cert_callback or from application 

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

444 to "https://" prefix. 

445 

446 Args: 

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

448 The optional callback returns the client certificate and private 

449 key bytes both in PEM format. 

450 If the callback is None, application default SSL credentials 

451 will be used. 

452 

453 Raises: 

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

455 creation failed for any reason. 

456 """ 

457 use_client_cert = os.getenv( 

458 environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE, "false" 

459 ) 

460 if use_client_cert != "true": 

461 self._is_mtls = False 

462 return 

463 

464 try: 

465 import OpenSSL 

466 except ImportError as caught_exc: 

467 new_exc = exceptions.MutualTLSChannelError(caught_exc) 

468 six.raise_from(new_exc, caught_exc) 

469 

470 try: 

471 ( 

472 self._is_mtls, 

473 cert, 

474 key, 

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

476 client_cert_callback 

477 ) 

478 

479 if self._is_mtls: 

480 mtls_adapter = _MutualTlsAdapter(cert, key) 

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

482 except ( 

483 exceptions.ClientCertError, 

484 ImportError, 

485 OpenSSL.crypto.Error, 

486 ) as caught_exc: 

487 new_exc = exceptions.MutualTLSChannelError(caught_exc) 

488 six.raise_from(new_exc, caught_exc) 

489 

490 def request( 

491 self, 

492 method, 

493 url, 

494 data=None, 

495 headers=None, 

496 max_allowed_time=None, 

497 timeout=_DEFAULT_TIMEOUT, 

498 **kwargs 

499 ): 

500 """Implementation of Requests' request. 

501 

502 Args: 

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

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

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

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

507 documentation for details. 

508 max_allowed_time (Optional[float]): 

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

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

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

512 multiple requests are made under the hood. 

513 

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

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

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

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

518 transmitted. The timout error will be raised after such 

519 request completes. 

520 """ 

521 # pylint: disable=arguments-differ 

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

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

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

525 

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

527 # thread-safety. 

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

529 

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

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

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

533 

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

535 # _auth_request's default timeout. 

536 auth_request = ( 

537 self._auth_request 

538 if timeout is None 

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

540 ) 

541 

542 remaining_time = max_allowed_time 

543 

544 with TimeoutGuard(remaining_time) as guard: 

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

546 remaining_time = guard.remaining_timeout 

547 

548 with TimeoutGuard(remaining_time) as guard: 

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

550 method, 

551 url, 

552 data=data, 

553 headers=request_headers, 

554 timeout=timeout, 

555 **kwargs 

556 ) 

557 remaining_time = guard.remaining_timeout 

558 

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

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

561 # request. 

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

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

564 if ( 

565 response.status_code in self._refresh_status_codes 

566 and _credential_refresh_attempt < self._max_refresh_attempts 

567 ): 

568 

569 _LOGGER.info( 

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

571 response.status_code, 

572 _credential_refresh_attempt + 1, 

573 self._max_refresh_attempts, 

574 ) 

575 

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

577 # _auth_request's default timeout. 

578 auth_request = ( 

579 self._auth_request 

580 if timeout is None 

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

582 ) 

583 

584 with TimeoutGuard(remaining_time) as guard: 

585 self.credentials.refresh(auth_request) 

586 remaining_time = guard.remaining_timeout 

587 

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

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

590 return self.request( 

591 method, 

592 url, 

593 data=data, 

594 headers=headers, 

595 max_allowed_time=remaining_time, 

596 timeout=timeout, 

597 _credential_refresh_attempt=_credential_refresh_attempt + 1, 

598 **kwargs 

599 ) 

600 

601 return response 

602 

603 @property 

604 def is_mtls(self): 

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

606 return self._is_mtls 

607 

608 def close(self): 

609 if self._auth_request_session is not None: 

610 self._auth_request_session.close() 

611 super(AuthorizedSession, self).close()