eodag 2.12.1__py3-none-any.whl → 3.0.0b2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. eodag/api/core.py +440 -321
  2. eodag/api/product/__init__.py +5 -1
  3. eodag/api/product/_assets.py +57 -2
  4. eodag/api/product/_product.py +89 -68
  5. eodag/api/product/metadata_mapping.py +181 -66
  6. eodag/api/search_result.py +48 -1
  7. eodag/cli.py +20 -6
  8. eodag/config.py +95 -6
  9. eodag/plugins/apis/base.py +8 -165
  10. eodag/plugins/apis/ecmwf.py +36 -24
  11. eodag/plugins/apis/usgs.py +40 -24
  12. eodag/plugins/authentication/aws_auth.py +2 -2
  13. eodag/plugins/authentication/header.py +31 -6
  14. eodag/plugins/authentication/keycloak.py +13 -84
  15. eodag/plugins/authentication/oauth.py +3 -3
  16. eodag/plugins/authentication/openid_connect.py +256 -46
  17. eodag/plugins/authentication/qsauth.py +3 -0
  18. eodag/plugins/authentication/sas_auth.py +8 -1
  19. eodag/plugins/authentication/token.py +92 -46
  20. eodag/plugins/authentication/token_exchange.py +120 -0
  21. eodag/plugins/download/aws.py +86 -91
  22. eodag/plugins/download/base.py +72 -40
  23. eodag/plugins/download/http.py +607 -264
  24. eodag/plugins/download/s3rest.py +28 -15
  25. eodag/plugins/manager.py +74 -57
  26. eodag/plugins/search/__init__.py +36 -0
  27. eodag/plugins/search/base.py +225 -18
  28. eodag/plugins/search/build_search_result.py +389 -32
  29. eodag/plugins/search/cop_marine.py +378 -0
  30. eodag/plugins/search/creodias_s3.py +15 -14
  31. eodag/plugins/search/csw.py +5 -7
  32. eodag/plugins/search/data_request_search.py +44 -20
  33. eodag/plugins/search/qssearch.py +508 -203
  34. eodag/plugins/search/static_stac_search.py +99 -36
  35. eodag/resources/constraints/climate-dt.json +13 -0
  36. eodag/resources/constraints/extremes-dt.json +8 -0
  37. eodag/resources/ext_product_types.json +1 -1
  38. eodag/resources/product_types.yml +1897 -34
  39. eodag/resources/providers.yml +3539 -3277
  40. eodag/resources/stac.yml +48 -54
  41. eodag/resources/stac_api.yml +71 -25
  42. eodag/resources/stac_provider.yml +5 -0
  43. eodag/resources/user_conf_template.yml +51 -3
  44. eodag/rest/__init__.py +6 -0
  45. eodag/rest/cache.py +70 -0
  46. eodag/rest/config.py +68 -0
  47. eodag/rest/constants.py +27 -0
  48. eodag/rest/core.py +757 -0
  49. eodag/rest/server.py +397 -258
  50. eodag/rest/stac.py +438 -307
  51. eodag/rest/types/collections_search.py +44 -0
  52. eodag/rest/types/eodag_search.py +232 -43
  53. eodag/rest/types/{stac_queryables.py → queryables.py} +81 -43
  54. eodag/rest/types/stac_search.py +277 -0
  55. eodag/rest/utils/__init__.py +216 -0
  56. eodag/rest/utils/cql_evaluate.py +119 -0
  57. eodag/rest/utils/rfc3339.py +65 -0
  58. eodag/types/__init__.py +99 -9
  59. eodag/types/bbox.py +15 -14
  60. eodag/types/download_args.py +31 -0
  61. eodag/types/search_args.py +58 -7
  62. eodag/types/whoosh.py +81 -0
  63. eodag/utils/__init__.py +72 -9
  64. eodag/utils/constraints.py +37 -37
  65. eodag/utils/exceptions.py +23 -17
  66. eodag/utils/repr.py +113 -0
  67. eodag/utils/requests.py +138 -0
  68. eodag/utils/rest.py +104 -0
  69. eodag/utils/stac_reader.py +100 -16
  70. {eodag-2.12.1.dist-info → eodag-3.0.0b2.dist-info}/METADATA +65 -44
  71. eodag-3.0.0b2.dist-info/RECORD +110 -0
  72. {eodag-2.12.1.dist-info → eodag-3.0.0b2.dist-info}/WHEEL +1 -1
  73. {eodag-2.12.1.dist-info → eodag-3.0.0b2.dist-info}/entry_points.txt +6 -5
  74. eodag/plugins/apis/cds.py +0 -540
  75. eodag/rest/utils.py +0 -1133
  76. eodag-2.12.1.dist-info/RECORD +0 -94
  77. {eodag-2.12.1.dist-info → eodag-3.0.0b2.dist-info}/LICENSE +0 -0
  78. {eodag-2.12.1.dist-info → eodag-3.0.0b2.dist-info}/top_level.txt +0 -0
@@ -17,10 +17,12 @@
17
17
  # limitations under the License.
18
18
  from __future__ import annotations
19
19
 
20
+ import logging
20
21
  import re
21
22
  import string
23
+ from datetime import datetime
22
24
  from random import SystemRandom
23
- from typing import TYPE_CHECKING, Any, Dict, Optional
25
+ from typing import TYPE_CHECKING, Any, Dict, Optional, TypedDict
24
26
 
25
27
  import requests
26
28
  from lxml import etree
@@ -28,7 +30,12 @@ from requests.auth import AuthBase
28
30
 
29
31
  from eodag.plugins.authentication import Authentication
30
32
  from eodag.utils import HTTP_REQ_TIMEOUT, USER_AGENT, parse_qs, repeatfunc, urlparse
31
- from eodag.utils.exceptions import AuthenticationError, MisconfiguredError, TimeOutError
33
+ from eodag.utils.exceptions import (
34
+ AuthenticationError,
35
+ MisconfiguredError,
36
+ RequestError,
37
+ TimeOutError,
38
+ )
32
39
 
33
40
  if TYPE_CHECKING:
34
41
  from requests import PreparedRequest, Response
@@ -36,7 +43,132 @@ if TYPE_CHECKING:
36
43
  from eodag.config import PluginConfig
37
44
 
38
45
 
39
- class OIDCAuthorizationCodeFlowAuth(Authentication):
46
+ logger = logging.getLogger("eodag.auth.openid_connect")
47
+
48
+
49
+ class OIDCRefreshTokenBase(Authentication):
50
+ """OIDC refresh token base class, to be used through specific OIDC flows plugins.
51
+
52
+ Common mechanism to handle refresh token from all OIDC auth plugins.
53
+ """
54
+
55
+ class TokenInfo(TypedDict, total=False):
56
+ """Token infos"""
57
+
58
+ refresh_token: str
59
+ refresh_time: datetime
60
+ token_time: datetime
61
+ access_token: str
62
+ access_token_expiration: float
63
+ refresh_token_expiration: float
64
+
65
+ def __init__(self, provider: str, config: PluginConfig) -> None:
66
+ super(OIDCRefreshTokenBase, self).__init__(provider, config)
67
+ self.session = requests.Session()
68
+ # already retrieved token info store
69
+ self.token_info: OIDCRefreshTokenBase.TokenInfo = {}
70
+
71
+ def _get_access_token(self) -> str:
72
+ current_time = datetime.now()
73
+ if (
74
+ # No info: first time
75
+ not self.token_info
76
+ # Refresh token available but expired
77
+ or (
78
+ "refresh_token" in self.token_info
79
+ and self.token_info["refresh_token_expiration"] > 0
80
+ and (current_time - self.token_info["token_time"]).seconds
81
+ >= self.token_info["refresh_token_expiration"]
82
+ )
83
+ # Refresh token *not* available and access token expired
84
+ or (
85
+ "refresh_token" not in self.token_info
86
+ and (current_time - self.token_info["token_time"]).seconds
87
+ >= self.token_info["access_token_expiration"]
88
+ )
89
+ ):
90
+ # Request *new* token on first attempt or if token expired
91
+ res = self._request_new_token()
92
+ self.token_info["token_time"] = current_time
93
+ self.token_info["access_token_expiration"] = float(res["expires_in"])
94
+ if "refresh_token" in res:
95
+ self.token_info["refresh_time"] = current_time
96
+ self.token_info["refresh_token_expiration"] = float(
97
+ res["refresh_expires_in"]
98
+ )
99
+ self.token_info["refresh_token"] = str(res["refresh_token"])
100
+ return str(res["access_token"])
101
+
102
+ elif (
103
+ # Refresh token available and access token expired
104
+ "refresh_token" in self.token_info
105
+ and (current_time - self.token_info["refresh_time"]).seconds
106
+ >= self.token_info["access_token_expiration"]
107
+ ):
108
+ # Use refresh token
109
+ res = self._get_token_with_refresh_token()
110
+ self.token_info["refresh_token"] = res["refresh_token"]
111
+ self.token_info["refresh_time"] = current_time
112
+ return res["access_token"]
113
+
114
+ logger.debug("Using already retrieved access token")
115
+ return self.token_info["access_token"]
116
+
117
+ def _request_new_token(self) -> Dict[str, str]:
118
+ """Fetch the access token with a new authentcation"""
119
+ raise NotImplementedError(
120
+ "Incomplete OIDC refresh token retrieval mechanism implementation"
121
+ )
122
+
123
+ def _request_new_token_error(self, e: requests.RequestException) -> Dict[str, str]:
124
+ """Handle RequestException raised by `self._request_new_token()`"""
125
+ if self.token_info.get("access_token"):
126
+ # try using already retrieved token if authenticate() fails (OTP use-case)
127
+ if "access_token_expiration" in self.token_info:
128
+ return {
129
+ "access_token": self.token_info["access_token"],
130
+ "expires_in": str(self.token_info["access_token_expiration"]),
131
+ }
132
+ else:
133
+ return {
134
+ "access_token": self.token_info["access_token"],
135
+ "expires_in": "0",
136
+ }
137
+ response_text = getattr(e.response, "text", "").strip()
138
+ # check if error is identified as auth_error in provider conf
139
+ auth_errors = getattr(self.config, "auth_error_code", [None])
140
+ if not isinstance(auth_errors, list):
141
+ auth_errors = [auth_errors]
142
+ if (
143
+ e.response
144
+ and hasattr(e.response, "status_code")
145
+ and e.response.status_code in auth_errors
146
+ ):
147
+ raise AuthenticationError(
148
+ "HTTP Error %s returned, %s\nPlease check your credentials for %s"
149
+ % (e.response.status_code, response_text, self.provider)
150
+ )
151
+ # other error
152
+ else:
153
+ import traceback as tb
154
+
155
+ logger.error(
156
+ f"Provider {self.provider} returned {getattr(e.response, 'status_code', '')}: {response_text}"
157
+ )
158
+ raise AuthenticationError(
159
+ "Something went wrong while trying to get access token:\n{}".format(
160
+ tb.format_exc()
161
+ )
162
+ )
163
+
164
+ def _get_token_with_refresh_token(self) -> Dict[str, str]:
165
+ """Fetch the access token with the refresh token"""
166
+ raise NotImplementedError(
167
+ "Incomplete OIDC refresh token retrieval mechanism implementation"
168
+ )
169
+
170
+
171
+ class OIDCAuthorizationCodeFlowAuth(OIDCRefreshTokenBase):
40
172
  """Implement the authorization code flow of the OpenIDConnect authorization specification.
41
173
 
42
174
  The `OpenID Connect <http://openid.net/specs/openid-connect-core-1_0.html>`_ specification
@@ -109,6 +241,10 @@ class OIDCAuthorizationCodeFlowAuth(Authentication):
109
241
  # same rules as with user_consent_form_data
110
242
  additional_login_form_data:
111
243
 
244
+ # (optional) Key/value pairs of patterns/messages. If exchange_url contains the given pattern, the associated
245
+ message will be sent in an AuthenticationError
246
+ exchange_url_error_pattern:
247
+
112
248
  # (optional) The OIDC provider's client secret of the eodag provider
113
249
  client_secret:
114
250
 
@@ -123,6 +259,8 @@ class OIDCAuthorizationCodeFlowAuth(Authentication):
123
259
  # used in the query request
124
260
  token_qs_key:
125
261
 
262
+ # (optional) The key pointing to the refresh_token in the json response to the POST request to the token server
263
+ refresh_token_key:
126
264
  """
127
265
 
128
266
  SCOPE = "openid"
@@ -131,6 +269,10 @@ class OIDCAuthorizationCodeFlowAuth(Authentication):
131
269
 
132
270
  def __init__(self, provider: str, config: PluginConfig) -> None:
133
271
  super(OIDCAuthorizationCodeFlowAuth, self).__init__(provider, config)
272
+
273
+ def validate_config_credentials(self) -> None:
274
+ """Validate configured credentials"""
275
+ super(OIDCAuthorizationCodeFlowAuth, self).validate_config_credentials()
134
276
  if getattr(self.config, "token_provision", None) not in ("qs", "header"):
135
277
  raise MisconfiguredError(
136
278
  'Provider config parameter "token_provision" must be one of "qs" or "header"'
@@ -142,33 +284,74 @@ class OIDCAuthorizationCodeFlowAuth(Authentication):
142
284
  'Provider config parameter "token_provision" with value "qs" must have '
143
285
  '"token_qs_key" config parameter as well'
144
286
  )
145
- self.session = requests.Session()
146
287
 
147
- def authenticate(self) -> AuthBase:
288
+ def authenticate(self) -> CodeAuthorizedAuth:
148
289
  """Authenticate"""
290
+ self.token_info["access_token"] = self._get_access_token()
291
+
292
+ return CodeAuthorizedAuth(
293
+ self.token_info["access_token"],
294
+ self.config.token_provision,
295
+ key=getattr(self.config, "token_qs_key", None),
296
+ )
297
+
298
+ def _request_new_token(self) -> Dict[str, str]:
299
+ """Fetch the access token with a new authentcation"""
300
+ logger.debug("Fetching access token from %s", self.config.token_uri)
149
301
  state = self.compute_state()
150
302
  authentication_response = self.authenticate_user(state)
151
303
  exchange_url = authentication_response.url
304
+ for err_pattern, err_message in getattr(
305
+ self.config, "exchange_url_error_pattern", {}
306
+ ).items():
307
+ if err_pattern in exchange_url:
308
+ raise AuthenticationError(err_message)
309
+ if not exchange_url.startswith(self.config.redirect_uri):
310
+ raise AuthenticationError(
311
+ f"Could not authenticate user with provider {self.provider}.",
312
+ "Please verify your credentials",
313
+ )
152
314
  if self.config.user_consent_needed:
153
315
  user_consent_response = self.grant_user_consent(authentication_response)
154
316
  exchange_url = user_consent_response.url
155
317
  try:
156
- token = self.exchange_code_for_token(exchange_url, state)
318
+ token_response = self.exchange_code_for_token(exchange_url, state)
319
+ token_response.raise_for_status()
157
320
  except requests.exceptions.Timeout as exc:
158
321
  raise TimeOutError(exc, timeout=HTTP_REQ_TIMEOUT) from exc
159
- except Exception:
160
- import traceback as tb
161
-
162
- raise AuthenticationError(
163
- "Something went wrong while trying to get authorization token:\n{}".format(
164
- tb.format_exc()
165
- )
166
- )
167
- return CodeAuthorizedAuth(
168
- token,
169
- self.config.token_provision,
170
- key=getattr(self.config, "token_qs_key", None),
322
+ except requests.RequestException as e:
323
+ return self._request_new_token_error(e)
324
+ return token_response.json()
325
+
326
+ def _get_token_with_refresh_token(self) -> Dict[str, str]:
327
+ """Fetch the access token with the refresh token"""
328
+ logger.debug(
329
+ "Fetching access token with refresh token from %s", self.config.token_uri
171
330
  )
331
+ token_data: Dict[str, Any] = {
332
+ "refresh_token": self.token_info["refresh_token"],
333
+ "grant_type": "refresh_token",
334
+ }
335
+ token_data = self._prepare_token_post_data(token_data)
336
+ post_request_kwargs: Any = {
337
+ self.config.token_exchange_post_data_method: token_data
338
+ }
339
+ try:
340
+ token_response = self.session.post(
341
+ self.config.token_uri,
342
+ timeout=HTTP_REQ_TIMEOUT,
343
+ **post_request_kwargs,
344
+ )
345
+ token_response.raise_for_status()
346
+ except requests.exceptions.Timeout as exc:
347
+ raise TimeOutError(exc, timeout=HTTP_REQ_TIMEOUT) from exc
348
+ except requests.RequestException as exc:
349
+ logger.error(
350
+ "Could not fetch access token with refresh token, executing new token request, error: %s",
351
+ getattr(exc.response, "text", ""),
352
+ )
353
+ return self._request_new_token()
354
+ return token_response.json()
172
355
 
173
356
  def authenticate_user(self, state: str) -> Response:
174
357
  """Authenticate user"""
@@ -188,7 +371,9 @@ class OIDCAuthorizationCodeFlowAuth(Authentication):
188
371
  )
189
372
 
190
373
  login_document = etree.HTML(authorization_response.text)
191
- login_form = login_document.xpath(self.config.login_form_xpath)[0]
374
+ login_forms = login_document.xpath(self.config.login_form_xpath)
375
+ login_form = login_forms[0]
376
+
192
377
  # Get the form data to pass to the login form from config or from the login form
193
378
  login_data = {
194
379
  key: self._constant_or_xpath_extracted(value, login_form)
@@ -198,14 +383,23 @@ class OIDCAuthorizationCodeFlowAuth(Authentication):
198
383
  }
199
384
  # Add the credentials
200
385
  login_data.update(self.config.credentials)
201
- auth_uri = getattr(self.config, "authentication_uri", None)
386
+
202
387
  # Retrieve the authentication_uri from the login form if so configured
203
388
  if self.config.authentication_uri_source == "login-form":
204
389
  # Given that the login_form_xpath resolves to an HTML element, if suffices to add '/@action' to get
205
390
  # the value of its action attribute to this xpath
206
391
  auth_uri = login_form.xpath(
207
392
  self.config.login_form_xpath.rstrip("/") + "/@action"
208
- )[0]
393
+ )
394
+ if not auth_uri or not auth_uri[0]:
395
+ raise RequestError(
396
+ f"Could not get auth_uri from {self.config.login_form_xpath}"
397
+ )
398
+ auth_uri = auth_uri[0]
399
+ else:
400
+ auth_uri = getattr(self.config, "authentication_uri", None)
401
+ if not auth_uri:
402
+ raise MisconfiguredError("authentication_uri is missing")
209
403
  return self.session.post(
210
404
  auth_uri, data=login_data, headers=USER_AGENT, timeout=HTTP_REQ_TIMEOUT
211
405
  )
@@ -228,40 +422,49 @@ class OIDCAuthorizationCodeFlowAuth(Authentication):
228
422
  timeout=HTTP_REQ_TIMEOUT,
229
423
  )
230
424
 
231
- def exchange_code_for_token(self, authorized_url: str, state: str) -> str:
232
- """Get exchange code for token"""
233
- qs = parse_qs(urlparse(authorized_url).query)
234
- if qs["state"][0] != state:
235
- raise AuthenticationError(
236
- "The state received in the authorized url does not match initially computed state"
237
- )
238
- code = qs["code"][0]
239
- token_exchange_data: Dict[str, Any] = {
240
- "redirect_uri": self.config.redirect_uri,
241
- "client_id": self.config.client_id,
242
- "code": code,
243
- "state": state,
244
- }
425
+ def _prepare_token_post_data(self, token_data: Dict[str, Any]) -> Dict[str, Any]:
426
+ """Prepare the common data to post to the token URI"""
427
+ token_data.update(
428
+ {
429
+ "redirect_uri": self.config.redirect_uri,
430
+ "client_id": self.config.client_id,
431
+ }
432
+ )
245
433
  # If necessary, change the keys of the form data that will be passed to the token exchange POST request
246
434
  custom_token_exchange_params = getattr(self.config, "token_exchange_params", {})
247
435
  if custom_token_exchange_params:
248
- token_exchange_data[
249
- custom_token_exchange_params["redirect_uri"]
250
- ] = token_exchange_data.pop("redirect_uri")
251
- token_exchange_data[
252
- custom_token_exchange_params["client_id"]
253
- ] = token_exchange_data.pop("client_id")
436
+ token_data[custom_token_exchange_params["redirect_uri"]] = token_data.pop(
437
+ "redirect_uri"
438
+ )
439
+ token_data[custom_token_exchange_params["client_id"]] = token_data.pop(
440
+ "client_id"
441
+ )
254
442
  # If the client_secret is known, the token exchange request must be authenticated with a BASIC Auth, using the
255
443
  # client_id and client_secret as username and password respectively
256
444
  if getattr(self.config, "client_secret", None):
257
- token_exchange_data.update(
445
+ token_data.update(
258
446
  {
259
447
  "auth": (self.config.client_id, self.config.client_secret),
260
- "grant_type": "authorization_code",
261
448
  "client_secret": self.config.client_secret,
262
449
  }
263
450
  )
264
- post_request_kwargs = {
451
+ return token_data
452
+
453
+ def exchange_code_for_token(self, authorized_url: str, state: str) -> Response:
454
+ """Get exchange code for token"""
455
+ qs = parse_qs(urlparse(authorized_url).query)
456
+ if qs["state"][0] != state:
457
+ raise AuthenticationError(
458
+ "The state received in the authorized url does not match initially computed state"
459
+ )
460
+ code = qs["code"][0]
461
+ token_exchange_data: Dict[str, Any] = {
462
+ "code": code,
463
+ "state": state,
464
+ "grant_type": "authorization_code",
465
+ }
466
+ token_exchange_data = self._prepare_token_post_data(token_exchange_data)
467
+ post_request_kwargs: Any = {
265
468
  self.config.token_exchange_post_data_method: token_exchange_data
266
469
  }
267
470
  r = self.session.post(
@@ -270,7 +473,7 @@ class OIDCAuthorizationCodeFlowAuth(Authentication):
270
473
  timeout=HTTP_REQ_TIMEOUT,
271
474
  **post_request_kwargs,
272
475
  )
273
- return r.json()[self.config.token_key]
476
+ return r
274
477
 
275
478
  def _constant_or_xpath_extracted(
276
479
  self, value: str, form_element: Any
@@ -279,7 +482,7 @@ class OIDCAuthorizationCodeFlowAuth(Authentication):
279
482
  if not match:
280
483
  return value
281
484
  value_from_xpath = form_element.xpath(
282
- self.CONFIG_XPATH_REGEX.match(value).groupdict("xpath_value")
485
+ self.CONFIG_XPATH_REGEX.match(value).groupdict("xpath_value")["xpath_value"]
283
486
  )
284
487
  if len(value_from_xpath) == 1:
285
488
  return value_from_xpath[0]
@@ -318,4 +521,11 @@ class CodeAuthorizedAuth(AuthBase):
318
521
 
319
522
  elif self.where == "header":
320
523
  request.headers["Authorization"] = "Bearer {}".format(self.token)
524
+ logger.debug(
525
+ re.sub(
526
+ r"'Bearer [^']+'",
527
+ r"'Bearer ***'",
528
+ f"PreparedRequest: {request.__dict__}",
529
+ )
530
+ )
321
531
  return request
@@ -67,6 +67,8 @@ class HttpQueryStringAuth(Authentication):
67
67
  auth = QueryStringAuth(**self.config.credentials)
68
68
 
69
69
  auth_uri = getattr(self.config, "auth_uri", None)
70
+ ssl_verify = getattr(self.config, "ssl_verify", True)
71
+
70
72
  if auth_uri:
71
73
  try:
72
74
  response = requests.get(
@@ -74,6 +76,7 @@ class HttpQueryStringAuth(Authentication):
74
76
  timeout=HTTP_REQ_TIMEOUT,
75
77
  headers=USER_AGENT,
76
78
  auth=auth,
79
+ verify=ssl_verify,
77
80
  )
78
81
  response.raise_for_status()
79
82
  except requests.exceptions.Timeout as exc:
@@ -43,11 +43,13 @@ class RequestsSASAuth(AuthBase):
43
43
  auth_uri: str,
44
44
  signed_url_key: str,
45
45
  headers: Optional[Dict[str, str]] = None,
46
+ ssl_verify: bool = True,
46
47
  ) -> None:
47
48
  self.auth_uri = auth_uri
48
49
  self.signed_url_key = signed_url_key
49
50
  self.headers = headers
50
51
  self.signed_urls: Dict[str, str] = {}
52
+ self.ssl_verify = ssl_verify
51
53
 
52
54
  def __call__(self, request: PreparedRequest) -> PreparedRequest:
53
55
  """Perform the actual authentication"""
@@ -63,7 +65,10 @@ class RequestsSASAuth(AuthBase):
63
65
  logger.debug(f"Signed URL request: {req_signed_url}")
64
66
  try:
65
67
  response = requests.get(
66
- req_signed_url, headers=self.headers, timeout=HTTP_REQ_TIMEOUT
68
+ req_signed_url,
69
+ headers=self.headers,
70
+ timeout=HTTP_REQ_TIMEOUT,
71
+ verify=self.ssl_verify,
67
72
  )
68
73
  response.raise_for_status()
69
74
  signed_url = response.json().get(self.signed_url_key)
@@ -95,6 +100,7 @@ class SASAuth(Authentication):
95
100
 
96
101
  # update headers with subscription key if exists
97
102
  apikey = getattr(self.config, "credentials", {}).get("apikey", None)
103
+ ssl_verify = getattr(self.config, "ssl_verify", True)
98
104
  if apikey:
99
105
  headers_update = format_dict_items(self.config.headers, apikey=apikey)
100
106
  headers.update(headers_update)
@@ -103,4 +109,5 @@ class SASAuth(Authentication):
103
109
  auth_uri=self.config.auth_uri,
104
110
  signed_url_key=self.config.signed_url_key,
105
111
  headers=headers,
112
+ ssl_verify=ssl_verify,
106
113
  )
@@ -46,6 +46,7 @@ class TokenAuth(Authentication):
46
46
  def __init__(self, provider: str, config: PluginConfig) -> None:
47
47
  super(TokenAuth, self).__init__(provider, config)
48
48
  self.token = ""
49
+ self.refresh_token = ""
49
50
 
50
51
  def validate_config_credentials(self) -> None:
51
52
  """Validate configured credentials"""
@@ -55,9 +56,9 @@ class TokenAuth(Authentication):
55
56
  self.config.auth_uri = self.config.auth_uri.format(
56
57
  **self.config.credentials
57
58
  )
58
- # format headers if needed
59
+ # format headers if needed (and accepts {token} to be formatted later)
59
60
  self.config.headers = {
60
- header: value.format(**self.config.credentials)
61
+ header: value.format(**{"token": "{token}", **self.config.credentials})
61
62
  for header, value in getattr(self.config, "headers", {}).items()
62
63
  }
63
64
  except KeyError as e:
@@ -69,65 +70,108 @@ class TokenAuth(Authentication):
69
70
  """Authenticate"""
70
71
  self.validate_config_credentials()
71
72
 
72
- # append headers to req if some are specified in config
73
- req_kwargs: Dict[str, Any] = (
74
- {"headers": dict(self.config.headers, **USER_AGENT)}
75
- if hasattr(self.config, "headers")
76
- else {"headers": USER_AGENT}
77
- )
78
73
  s = requests.Session()
79
- retries = Retry(
80
- total=3, backoff_factor=2, status_forcelist=[401, 429, 500, 502, 503, 504]
81
- )
82
- s.mount(self.config.auth_uri, HTTPAdapter(max_retries=retries))
83
74
  try:
84
75
  # First get the token
85
- if getattr(self.config, "request_method", "POST") == "POST":
86
- response = s.post(
87
- self.config.auth_uri,
88
- data=self.config.credentials,
89
- timeout=HTTP_REQ_TIMEOUT,
90
- **req_kwargs,
91
- )
92
- else:
93
- cred = self.config.credentials
94
- response = s.get(
95
- self.config.auth_uri,
96
- auth=(cred["username"], cred["password"]),
97
- timeout=HTTP_REQ_TIMEOUT,
98
- **req_kwargs,
99
- )
76
+ response = self._token_request(session=s)
100
77
  response.raise_for_status()
101
78
  except requests.exceptions.Timeout as exc:
102
79
  raise TimeOutError(exc, timeout=HTTP_REQ_TIMEOUT) from exc
103
80
  except RequestException as e:
104
81
  response_text = getattr(e.response, "text", "").strip()
105
- raise AuthenticationError(
106
- f"Could no get authentication token: {str(e)}, {response_text}"
107
- )
82
+ # check if error is identified as auth_error in provider conf
83
+ auth_errors = getattr(self.config, "auth_error_code", [None])
84
+ if not isinstance(auth_errors, list):
85
+ auth_errors = [auth_errors]
86
+ if (
87
+ e.response is not None
88
+ and getattr(e.response, "status_code", None)
89
+ and e.response.status_code in auth_errors
90
+ ):
91
+ raise AuthenticationError(
92
+ f"HTTP Error {e.response.status_code} returned, {response_text}\n"
93
+ f"Please check your credentials for {self.provider}"
94
+ )
95
+ # other error
96
+ else:
97
+ raise AuthenticationError(
98
+ f"Could no get authentication token: {str(e)}, {response_text}"
99
+ )
108
100
  else:
109
101
  if getattr(self.config, "token_type", "text") == "json":
110
102
  token = response.json()[self.config.token_key]
111
103
  else:
112
104
  token = response.text
113
- headers = self._get_headers(token)
114
105
  self.token = token
106
+ if getattr(self.config, "refresh_token_key", None):
107
+ self.refresh_token = response.json()[self.config.refresh_token_key]
108
+ if not hasattr(self.config, "headers"):
109
+ raise MisconfiguredError(f"Missing headers configuration for {self}")
115
110
  # Return auth class set with obtained token
116
- return RequestsTokenAuth(token, "header", headers=headers)
117
-
118
- def _get_headers(self, token: str) -> Dict[str, str]:
119
- headers = self.config.headers
120
- if "Authorization" in headers and "$" in headers["Authorization"]:
121
- headers["Authorization"] = headers["Authorization"].replace("$token", token)
122
- if (
123
- self.token
124
- and token != self.token
125
- and self.token in headers["Authorization"]
126
- ):
127
- headers["Authorization"] = headers["Authorization"].replace(
128
- self.token, token
111
+ return RequestsTokenAuth(
112
+ token, "header", headers=getattr(self.config, "headers", {})
129
113
  )
130
- return headers
114
+
115
+ def _token_request(
116
+ self,
117
+ session: requests.Session,
118
+ ) -> requests.Response:
119
+ retries = Retry(
120
+ total=3,
121
+ backoff_factor=2,
122
+ status_forcelist=[401, 429, 500, 502, 503, 504],
123
+ )
124
+
125
+ # append headers to req if some are specified in config
126
+ req_kwargs: Dict[str, Any] = {
127
+ "headers": dict(self.config.headers, **USER_AGENT)
128
+ }
129
+
130
+ if self.refresh_token:
131
+ logger.debug("fetching access token with refresh token")
132
+ session.mount(self.config.refresh_uri, HTTPAdapter(max_retries=retries))
133
+ try:
134
+ response = session.post(
135
+ self.config.refresh_uri,
136
+ data={"refresh_token": self.refresh_token},
137
+ timeout=HTTP_REQ_TIMEOUT,
138
+ **req_kwargs,
139
+ )
140
+ response.raise_for_status()
141
+ return response
142
+ except requests.exceptions.HTTPError as e:
143
+ logger.debug(getattr(e.response, "text", "").strip())
144
+
145
+ logger.debug("fetching access token from %s", self.config.auth_uri)
146
+ # append headers to req if some are specified in config
147
+ session.mount(self.config.auth_uri, HTTPAdapter(max_retries=retries))
148
+ method = getattr(self.config, "request_method", "POST")
149
+
150
+ # send credentials also as data in POST requests
151
+ if method == "POST":
152
+ # append req_data to credentials if specified in config
153
+ req_kwargs["data"] = dict(
154
+ getattr(self.config, "req_data", {}), **self.config.credentials
155
+ )
156
+
157
+ # credentials as auth tuple if possible
158
+ req_kwargs["auth"] = (
159
+ (
160
+ self.config.credentials["username"],
161
+ self.config.credentials["password"],
162
+ )
163
+ if all(
164
+ k in self.config.credentials.keys() for k in ["username", "password"]
165
+ )
166
+ else None
167
+ )
168
+
169
+ return session.request(
170
+ method=method,
171
+ url=self.config.auth_uri,
172
+ timeout=HTTP_REQ_TIMEOUT,
173
+ **req_kwargs,
174
+ )
131
175
 
132
176
 
133
177
  class RequestsTokenAuth(AuthBase):
@@ -165,5 +209,7 @@ class RequestsTokenAuth(AuthBase):
165
209
  )
166
210
  )
167
211
  elif self.where == "header":
168
- request.headers["Authorization"] = "Bearer {}".format(self.token)
212
+ request.headers["Authorization"] = request.headers.get(
213
+ "Authorization", "Bearer {token}"
214
+ ).format(token=self.token)
169
215
  return request