eodag 2.12.0__py3-none-any.whl → 3.0.0__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.
- eodag/__init__.py +6 -8
- eodag/api/core.py +654 -538
- eodag/api/product/__init__.py +12 -2
- eodag/api/product/_assets.py +59 -16
- eodag/api/product/_product.py +100 -93
- eodag/api/product/drivers/__init__.py +7 -2
- eodag/api/product/drivers/base.py +0 -3
- eodag/api/product/metadata_mapping.py +192 -96
- eodag/api/search_result.py +69 -10
- eodag/cli.py +55 -25
- eodag/config.py +391 -116
- eodag/plugins/apis/base.py +11 -165
- eodag/plugins/apis/ecmwf.py +36 -25
- eodag/plugins/apis/usgs.py +80 -35
- eodag/plugins/authentication/aws_auth.py +13 -4
- eodag/plugins/authentication/base.py +10 -1
- eodag/plugins/authentication/generic.py +2 -2
- eodag/plugins/authentication/header.py +31 -6
- eodag/plugins/authentication/keycloak.py +17 -84
- eodag/plugins/authentication/oauth.py +3 -3
- eodag/plugins/authentication/openid_connect.py +268 -49
- eodag/plugins/authentication/qsauth.py +4 -1
- eodag/plugins/authentication/sas_auth.py +9 -2
- eodag/plugins/authentication/token.py +98 -47
- eodag/plugins/authentication/token_exchange.py +122 -0
- eodag/plugins/crunch/base.py +3 -1
- eodag/plugins/crunch/filter_date.py +3 -9
- eodag/plugins/crunch/filter_latest_intersect.py +0 -3
- eodag/plugins/crunch/filter_latest_tpl_name.py +1 -4
- eodag/plugins/crunch/filter_overlap.py +4 -8
- eodag/plugins/crunch/filter_property.py +5 -11
- eodag/plugins/download/aws.py +149 -185
- eodag/plugins/download/base.py +88 -97
- eodag/plugins/download/creodias_s3.py +1 -1
- eodag/plugins/download/http.py +638 -310
- eodag/plugins/download/s3rest.py +47 -45
- eodag/plugins/manager.py +228 -88
- eodag/plugins/search/__init__.py +36 -0
- eodag/plugins/search/base.py +239 -30
- eodag/plugins/search/build_search_result.py +382 -37
- eodag/plugins/search/cop_marine.py +441 -0
- eodag/plugins/search/creodias_s3.py +25 -20
- eodag/plugins/search/csw.py +5 -7
- eodag/plugins/search/data_request_search.py +61 -30
- eodag/plugins/search/qssearch.py +713 -255
- eodag/plugins/search/static_stac_search.py +106 -40
- eodag/resources/ext_product_types.json +1 -1
- eodag/resources/product_types.yml +1921 -34
- eodag/resources/providers.yml +4091 -3655
- eodag/resources/stac.yml +50 -216
- eodag/resources/stac_api.yml +71 -25
- eodag/resources/stac_provider.yml +5 -0
- eodag/resources/user_conf_template.yml +89 -32
- eodag/rest/__init__.py +6 -0
- eodag/rest/cache.py +70 -0
- eodag/rest/config.py +68 -0
- eodag/rest/constants.py +26 -0
- eodag/rest/core.py +735 -0
- eodag/rest/errors.py +178 -0
- eodag/rest/server.py +264 -431
- eodag/rest/stac.py +442 -836
- eodag/rest/types/collections_search.py +44 -0
- eodag/rest/types/eodag_search.py +238 -47
- eodag/rest/types/queryables.py +164 -0
- eodag/rest/types/stac_search.py +273 -0
- eodag/rest/utils/__init__.py +216 -0
- eodag/rest/utils/cql_evaluate.py +119 -0
- eodag/rest/utils/rfc3339.py +64 -0
- eodag/types/__init__.py +106 -10
- eodag/types/bbox.py +15 -14
- eodag/types/download_args.py +40 -0
- eodag/types/search_args.py +57 -7
- eodag/types/whoosh.py +79 -0
- eodag/utils/__init__.py +110 -91
- eodag/utils/constraints.py +37 -45
- eodag/utils/exceptions.py +39 -22
- eodag/utils/import_system.py +0 -4
- eodag/utils/logging.py +37 -80
- eodag/utils/notebook.py +4 -4
- eodag/utils/repr.py +113 -0
- eodag/utils/requests.py +128 -0
- eodag/utils/rest.py +100 -0
- eodag/utils/stac_reader.py +93 -21
- {eodag-2.12.0.dist-info → eodag-3.0.0.dist-info}/METADATA +88 -53
- eodag-3.0.0.dist-info/RECORD +109 -0
- {eodag-2.12.0.dist-info → eodag-3.0.0.dist-info}/WHEEL +1 -1
- {eodag-2.12.0.dist-info → eodag-3.0.0.dist-info}/entry_points.txt +7 -5
- eodag/plugins/apis/cds.py +0 -540
- eodag/rest/types/stac_queryables.py +0 -134
- eodag/rest/utils.py +0 -1133
- eodag-2.12.0.dist-info/RECORD +0 -94
- {eodag-2.12.0.dist-info → eodag-3.0.0.dist-info}/LICENSE +0 -0
- {eodag-2.12.0.dist-info → eodag-3.0.0.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
|
|
@@ -36,7 +38,133 @@ if TYPE_CHECKING:
|
|
|
36
38
|
from eodag.config import PluginConfig
|
|
37
39
|
|
|
38
40
|
|
|
39
|
-
|
|
41
|
+
logger = logging.getLogger("eodag.auth.openid_connect")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class OIDCRefreshTokenBase(Authentication):
|
|
45
|
+
"""OIDC refresh token base class, to be used through specific OIDC flows plugins.
|
|
46
|
+
|
|
47
|
+
Common mechanism to handle refresh token from all OIDC auth plugins.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
class TokenInfo(TypedDict, total=False):
|
|
51
|
+
"""Token infos"""
|
|
52
|
+
|
|
53
|
+
refresh_token: str
|
|
54
|
+
refresh_time: datetime
|
|
55
|
+
token_time: datetime
|
|
56
|
+
access_token: str
|
|
57
|
+
access_token_expiration: float
|
|
58
|
+
refresh_token_expiration: float
|
|
59
|
+
|
|
60
|
+
def __init__(self, provider: str, config: PluginConfig) -> None:
|
|
61
|
+
super(OIDCRefreshTokenBase, self).__init__(provider, config)
|
|
62
|
+
self.session = requests.Session()
|
|
63
|
+
# already retrieved token info store
|
|
64
|
+
self.token_info: OIDCRefreshTokenBase.TokenInfo = {}
|
|
65
|
+
|
|
66
|
+
def _get_access_token(self) -> str:
|
|
67
|
+
current_time = datetime.now()
|
|
68
|
+
if (
|
|
69
|
+
# No info: first time
|
|
70
|
+
not self.token_info
|
|
71
|
+
# Refresh token available but expired
|
|
72
|
+
or (
|
|
73
|
+
"refresh_token" in self.token_info
|
|
74
|
+
and self.token_info["refresh_token_expiration"] > 0
|
|
75
|
+
and (current_time - self.token_info["token_time"]).seconds
|
|
76
|
+
>= self.token_info["refresh_token_expiration"]
|
|
77
|
+
)
|
|
78
|
+
# Refresh token *not* available and access token expired
|
|
79
|
+
or (
|
|
80
|
+
"refresh_token" not in self.token_info
|
|
81
|
+
and (current_time - self.token_info["token_time"]).seconds
|
|
82
|
+
>= self.token_info["access_token_expiration"]
|
|
83
|
+
)
|
|
84
|
+
):
|
|
85
|
+
# Request *new* token on first attempt or if token expired
|
|
86
|
+
res = self._request_new_token()
|
|
87
|
+
self.token_info["token_time"] = current_time
|
|
88
|
+
self.token_info["access_token_expiration"] = float(res["expires_in"])
|
|
89
|
+
if "refresh_token" in res:
|
|
90
|
+
self.token_info["refresh_time"] = current_time
|
|
91
|
+
self.token_info["refresh_token_expiration"] = float(
|
|
92
|
+
res["refresh_expires_in"]
|
|
93
|
+
)
|
|
94
|
+
self.token_info["refresh_token"] = str(res["refresh_token"])
|
|
95
|
+
return str(res["access_token"])
|
|
96
|
+
|
|
97
|
+
elif (
|
|
98
|
+
# Refresh token available and access token expired
|
|
99
|
+
"refresh_token" in self.token_info
|
|
100
|
+
and (current_time - self.token_info["refresh_time"]).seconds
|
|
101
|
+
>= self.token_info["access_token_expiration"]
|
|
102
|
+
):
|
|
103
|
+
# Use refresh token
|
|
104
|
+
res = self._get_token_with_refresh_token()
|
|
105
|
+
self.token_info["refresh_token"] = res["refresh_token"]
|
|
106
|
+
self.token_info["refresh_time"] = current_time
|
|
107
|
+
return res["access_token"]
|
|
108
|
+
|
|
109
|
+
logger.debug("Using already retrieved access token")
|
|
110
|
+
return self.token_info["access_token"]
|
|
111
|
+
|
|
112
|
+
def _request_new_token(self) -> Dict[str, str]:
|
|
113
|
+
"""Fetch the access token with a new authentcation"""
|
|
114
|
+
raise NotImplementedError(
|
|
115
|
+
"Incomplete OIDC refresh token retrieval mechanism implementation"
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
def _request_new_token_error(self, e: requests.RequestException) -> Dict[str, str]:
|
|
119
|
+
"""Handle RequestException raised by `self._request_new_token()`"""
|
|
120
|
+
if self.token_info.get("access_token"):
|
|
121
|
+
# try using already retrieved token if authenticate() fails (OTP use-case)
|
|
122
|
+
if "access_token_expiration" in self.token_info:
|
|
123
|
+
return {
|
|
124
|
+
"access_token": self.token_info["access_token"],
|
|
125
|
+
"expires_in": str(self.token_info["access_token_expiration"]),
|
|
126
|
+
}
|
|
127
|
+
else:
|
|
128
|
+
return {
|
|
129
|
+
"access_token": self.token_info["access_token"],
|
|
130
|
+
"expires_in": "0",
|
|
131
|
+
}
|
|
132
|
+
response_text = getattr(e.response, "text", "").strip()
|
|
133
|
+
# check if error is identified as auth_error in provider conf
|
|
134
|
+
auth_errors = getattr(self.config, "auth_error_code", [None])
|
|
135
|
+
if not isinstance(auth_errors, list):
|
|
136
|
+
auth_errors = [auth_errors]
|
|
137
|
+
if (
|
|
138
|
+
e.response
|
|
139
|
+
and hasattr(e.response, "status_code")
|
|
140
|
+
and e.response.status_code in auth_errors
|
|
141
|
+
):
|
|
142
|
+
raise AuthenticationError(
|
|
143
|
+
f"Please check your credentials for {self.provider}.",
|
|
144
|
+
f"HTTP Error {e.response.status_code} returned.",
|
|
145
|
+
response_text,
|
|
146
|
+
)
|
|
147
|
+
# other error
|
|
148
|
+
else:
|
|
149
|
+
import traceback as tb
|
|
150
|
+
|
|
151
|
+
logger.error(
|
|
152
|
+
f"Provider {self.provider} returned {getattr(e.response, 'status_code', '')}: {response_text}"
|
|
153
|
+
)
|
|
154
|
+
raise AuthenticationError(
|
|
155
|
+
"Something went wrong while trying to get access token:\n{}".format(
|
|
156
|
+
tb.format_exc()
|
|
157
|
+
)
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
def _get_token_with_refresh_token(self) -> Dict[str, str]:
|
|
161
|
+
"""Fetch the access token with the refresh token"""
|
|
162
|
+
raise NotImplementedError(
|
|
163
|
+
"Incomplete OIDC refresh token retrieval mechanism implementation"
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class OIDCAuthorizationCodeFlowAuth(OIDCRefreshTokenBase):
|
|
40
168
|
"""Implement the authorization code flow of the OpenIDConnect authorization specification.
|
|
41
169
|
|
|
42
170
|
The `OpenID Connect <http://openid.net/specs/openid-connect-core-1_0.html>`_ specification
|
|
@@ -109,6 +237,10 @@ class OIDCAuthorizationCodeFlowAuth(Authentication):
|
|
|
109
237
|
# same rules as with user_consent_form_data
|
|
110
238
|
additional_login_form_data:
|
|
111
239
|
|
|
240
|
+
# (optional) Key/value pairs of patterns/messages. If exchange_url contains the given pattern, the associated
|
|
241
|
+
message will be sent in an AuthenticationError
|
|
242
|
+
exchange_url_error_pattern:
|
|
243
|
+
|
|
112
244
|
# (optional) The OIDC provider's client secret of the eodag provider
|
|
113
245
|
client_secret:
|
|
114
246
|
|
|
@@ -123,6 +255,8 @@ class OIDCAuthorizationCodeFlowAuth(Authentication):
|
|
|
123
255
|
# used in the query request
|
|
124
256
|
token_qs_key:
|
|
125
257
|
|
|
258
|
+
# (optional) The key pointing to the refresh_token in the json response to the POST request to the token server
|
|
259
|
+
refresh_token_key:
|
|
126
260
|
"""
|
|
127
261
|
|
|
128
262
|
SCOPE = "openid"
|
|
@@ -131,6 +265,10 @@ class OIDCAuthorizationCodeFlowAuth(Authentication):
|
|
|
131
265
|
|
|
132
266
|
def __init__(self, provider: str, config: PluginConfig) -> None:
|
|
133
267
|
super(OIDCAuthorizationCodeFlowAuth, self).__init__(provider, config)
|
|
268
|
+
|
|
269
|
+
def validate_config_credentials(self) -> None:
|
|
270
|
+
"""Validate configured credentials"""
|
|
271
|
+
super(OIDCAuthorizationCodeFlowAuth, self).validate_config_credentials()
|
|
134
272
|
if getattr(self.config, "token_provision", None) not in ("qs", "header"):
|
|
135
273
|
raise MisconfiguredError(
|
|
136
274
|
'Provider config parameter "token_provision" must be one of "qs" or "header"'
|
|
@@ -142,33 +280,76 @@ class OIDCAuthorizationCodeFlowAuth(Authentication):
|
|
|
142
280
|
'Provider config parameter "token_provision" with value "qs" must have '
|
|
143
281
|
'"token_qs_key" config parameter as well'
|
|
144
282
|
)
|
|
145
|
-
self.session = requests.Session()
|
|
146
283
|
|
|
147
|
-
def authenticate(self) ->
|
|
284
|
+
def authenticate(self) -> CodeAuthorizedAuth:
|
|
148
285
|
"""Authenticate"""
|
|
286
|
+
self.token_info["access_token"] = self._get_access_token()
|
|
287
|
+
|
|
288
|
+
return CodeAuthorizedAuth(
|
|
289
|
+
self.token_info["access_token"],
|
|
290
|
+
self.config.token_provision,
|
|
291
|
+
key=getattr(self.config, "token_qs_key", None),
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
def _request_new_token(self) -> Dict[str, str]:
|
|
295
|
+
"""Fetch the access token with a new authentcation"""
|
|
296
|
+
logger.debug("Fetching access token from %s", self.config.token_uri)
|
|
149
297
|
state = self.compute_state()
|
|
150
298
|
authentication_response = self.authenticate_user(state)
|
|
151
299
|
exchange_url = authentication_response.url
|
|
300
|
+
for err_pattern, err_message in getattr(
|
|
301
|
+
self.config, "exchange_url_error_pattern", {}
|
|
302
|
+
).items():
|
|
303
|
+
if err_pattern in exchange_url:
|
|
304
|
+
raise AuthenticationError(err_message)
|
|
305
|
+
if not exchange_url.startswith(self.config.redirect_uri):
|
|
306
|
+
raise AuthenticationError(
|
|
307
|
+
f"Could not authenticate user with provider {self.provider}.",
|
|
308
|
+
"Please verify your credentials",
|
|
309
|
+
)
|
|
152
310
|
if self.config.user_consent_needed:
|
|
153
311
|
user_consent_response = self.grant_user_consent(authentication_response)
|
|
154
312
|
exchange_url = user_consent_response.url
|
|
155
313
|
try:
|
|
156
|
-
|
|
314
|
+
token_response = self.exchange_code_for_token(exchange_url, state)
|
|
315
|
+
token_response.raise_for_status()
|
|
157
316
|
except requests.exceptions.Timeout as exc:
|
|
158
317
|
raise TimeOutError(exc, timeout=HTTP_REQ_TIMEOUT) from exc
|
|
159
|
-
except
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
return CodeAuthorizedAuth(
|
|
168
|
-
token,
|
|
169
|
-
self.config.token_provision,
|
|
170
|
-
key=getattr(self.config, "token_qs_key", None),
|
|
318
|
+
except requests.RequestException as e:
|
|
319
|
+
return self._request_new_token_error(e)
|
|
320
|
+
return token_response.json()
|
|
321
|
+
|
|
322
|
+
def _get_token_with_refresh_token(self) -> Dict[str, str]:
|
|
323
|
+
"""Fetch the access token with the refresh token"""
|
|
324
|
+
logger.debug(
|
|
325
|
+
"Fetching access token with refresh token from %s", self.config.token_uri
|
|
171
326
|
)
|
|
327
|
+
token_data: Dict[str, Any] = {
|
|
328
|
+
"refresh_token": self.token_info["refresh_token"],
|
|
329
|
+
"grant_type": "refresh_token",
|
|
330
|
+
}
|
|
331
|
+
token_data = self._prepare_token_post_data(token_data)
|
|
332
|
+
post_request_kwargs: Any = {
|
|
333
|
+
self.config.token_exchange_post_data_method: token_data
|
|
334
|
+
}
|
|
335
|
+
ssl_verify = getattr(self.config, "ssl_verify", True)
|
|
336
|
+
try:
|
|
337
|
+
token_response = self.session.post(
|
|
338
|
+
self.config.token_uri,
|
|
339
|
+
timeout=HTTP_REQ_TIMEOUT,
|
|
340
|
+
verify=ssl_verify,
|
|
341
|
+
**post_request_kwargs,
|
|
342
|
+
)
|
|
343
|
+
token_response.raise_for_status()
|
|
344
|
+
except requests.exceptions.Timeout as exc:
|
|
345
|
+
raise TimeOutError(exc, timeout=HTTP_REQ_TIMEOUT) from exc
|
|
346
|
+
except requests.RequestException as exc:
|
|
347
|
+
logger.error(
|
|
348
|
+
"Could not fetch access token with refresh token, executing new token request, error: %s",
|
|
349
|
+
getattr(exc.response, "text", ""),
|
|
350
|
+
)
|
|
351
|
+
return self._request_new_token()
|
|
352
|
+
return token_response.json()
|
|
172
353
|
|
|
173
354
|
def authenticate_user(self, state: str) -> Response:
|
|
174
355
|
"""Authenticate user"""
|
|
@@ -180,15 +361,19 @@ class OIDCAuthorizationCodeFlowAuth(Authentication):
|
|
|
180
361
|
"state": state,
|
|
181
362
|
"redirect_uri": self.config.redirect_uri,
|
|
182
363
|
}
|
|
364
|
+
ssl_verify = getattr(self.config, "ssl_verify", True)
|
|
183
365
|
authorization_response = self.session.get(
|
|
184
366
|
self.config.authorization_uri,
|
|
185
367
|
params=params,
|
|
186
368
|
headers=USER_AGENT,
|
|
187
369
|
timeout=HTTP_REQ_TIMEOUT,
|
|
370
|
+
verify=ssl_verify,
|
|
188
371
|
)
|
|
189
372
|
|
|
190
373
|
login_document = etree.HTML(authorization_response.text)
|
|
191
|
-
|
|
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,16 +383,29 @@ class OIDCAuthorizationCodeFlowAuth(Authentication):
|
|
|
198
383
|
}
|
|
199
384
|
# Add the credentials
|
|
200
385
|
login_data.update(self.config.credentials)
|
|
201
|
-
|
|
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
|
-
)
|
|
393
|
+
)
|
|
394
|
+
if not auth_uri or not auth_uri[0]:
|
|
395
|
+
raise MisconfiguredError(
|
|
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
|
-
auth_uri,
|
|
404
|
+
auth_uri,
|
|
405
|
+
data=login_data,
|
|
406
|
+
headers=USER_AGENT,
|
|
407
|
+
timeout=HTTP_REQ_TIMEOUT,
|
|
408
|
+
verify=ssl_verify,
|
|
211
409
|
)
|
|
212
410
|
|
|
213
411
|
def grant_user_consent(self, authentication_response: Response) -> Response:
|
|
@@ -221,56 +419,69 @@ class OIDCAuthorizationCodeFlowAuth(Authentication):
|
|
|
221
419
|
key: self._constant_or_xpath_extracted(value, user_consent_form)
|
|
222
420
|
for key, value in self.config.user_consent_form_data.items()
|
|
223
421
|
}
|
|
422
|
+
ssl_verify = getattr(self.config, "ssl_verify", True)
|
|
224
423
|
return self.session.post(
|
|
225
424
|
self.config.authorization_uri,
|
|
226
425
|
data=user_consent_data,
|
|
227
426
|
headers=USER_AGENT,
|
|
228
427
|
timeout=HTTP_REQ_TIMEOUT,
|
|
428
|
+
verify=ssl_verify,
|
|
229
429
|
)
|
|
230
430
|
|
|
231
|
-
def
|
|
232
|
-
"""
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
"
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
}
|
|
431
|
+
def _prepare_token_post_data(self, token_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
432
|
+
"""Prepare the common data to post to the token URI"""
|
|
433
|
+
token_data.update(
|
|
434
|
+
{
|
|
435
|
+
"redirect_uri": self.config.redirect_uri,
|
|
436
|
+
"client_id": self.config.client_id,
|
|
437
|
+
}
|
|
438
|
+
)
|
|
245
439
|
# If necessary, change the keys of the form data that will be passed to the token exchange POST request
|
|
246
440
|
custom_token_exchange_params = getattr(self.config, "token_exchange_params", {})
|
|
247
441
|
if custom_token_exchange_params:
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
442
|
+
token_data[custom_token_exchange_params["redirect_uri"]] = token_data.pop(
|
|
443
|
+
"redirect_uri"
|
|
444
|
+
)
|
|
445
|
+
token_data[custom_token_exchange_params["client_id"]] = token_data.pop(
|
|
446
|
+
"client_id"
|
|
447
|
+
)
|
|
254
448
|
# If the client_secret is known, the token exchange request must be authenticated with a BASIC Auth, using the
|
|
255
449
|
# client_id and client_secret as username and password respectively
|
|
256
450
|
if getattr(self.config, "client_secret", None):
|
|
257
|
-
|
|
451
|
+
token_data.update(
|
|
258
452
|
{
|
|
259
453
|
"auth": (self.config.client_id, self.config.client_secret),
|
|
260
|
-
"grant_type": "authorization_code",
|
|
261
454
|
"client_secret": self.config.client_secret,
|
|
262
455
|
}
|
|
263
456
|
)
|
|
264
|
-
|
|
457
|
+
return token_data
|
|
458
|
+
|
|
459
|
+
def exchange_code_for_token(self, authorized_url: str, state: str) -> Response:
|
|
460
|
+
"""Get exchange code for token"""
|
|
461
|
+
qs = parse_qs(urlparse(authorized_url).query)
|
|
462
|
+
if qs["state"][0] != state:
|
|
463
|
+
raise AuthenticationError(
|
|
464
|
+
"The state received in the authorized url does not match initially computed state"
|
|
465
|
+
)
|
|
466
|
+
code = qs["code"][0]
|
|
467
|
+
token_exchange_data: Dict[str, Any] = {
|
|
468
|
+
"code": code,
|
|
469
|
+
"state": state,
|
|
470
|
+
"grant_type": "authorization_code",
|
|
471
|
+
}
|
|
472
|
+
token_exchange_data = self._prepare_token_post_data(token_exchange_data)
|
|
473
|
+
post_request_kwargs: Any = {
|
|
265
474
|
self.config.token_exchange_post_data_method: token_exchange_data
|
|
266
475
|
}
|
|
476
|
+
ssl_verify = getattr(self.config, "ssl_verify", True)
|
|
267
477
|
r = self.session.post(
|
|
268
478
|
self.config.token_uri,
|
|
269
479
|
headers=USER_AGENT,
|
|
270
480
|
timeout=HTTP_REQ_TIMEOUT,
|
|
481
|
+
verify=ssl_verify,
|
|
271
482
|
**post_request_kwargs,
|
|
272
483
|
)
|
|
273
|
-
return r
|
|
484
|
+
return r
|
|
274
485
|
|
|
275
486
|
def _constant_or_xpath_extracted(
|
|
276
487
|
self, value: str, form_element: Any
|
|
@@ -279,7 +490,7 @@ class OIDCAuthorizationCodeFlowAuth(Authentication):
|
|
|
279
490
|
if not match:
|
|
280
491
|
return value
|
|
281
492
|
value_from_xpath = form_element.xpath(
|
|
282
|
-
|
|
493
|
+
match.groupdict("xpath_value")["xpath_value"]
|
|
283
494
|
)
|
|
284
495
|
if len(value_from_xpath) == 1:
|
|
285
496
|
return value_from_xpath[0]
|
|
@@ -309,13 +520,21 @@ class CodeAuthorizedAuth(AuthBase):
|
|
|
309
520
|
def __call__(self, request: PreparedRequest) -> PreparedRequest:
|
|
310
521
|
"""Perform the actual authentication"""
|
|
311
522
|
if self.where == "qs":
|
|
312
|
-
parts = urlparse(request.url)
|
|
523
|
+
parts = urlparse(str(request.url))
|
|
313
524
|
query_dict = parse_qs(parts.query)
|
|
314
|
-
|
|
315
|
-
|
|
525
|
+
if self.key is not None:
|
|
526
|
+
query_dict.update({self.key: [self.token]})
|
|
527
|
+
url_without_args = parts._replace(query="").geturl()
|
|
316
528
|
|
|
317
529
|
request.prepare_url(url_without_args, query_dict)
|
|
318
530
|
|
|
319
531
|
elif self.where == "header":
|
|
320
532
|
request.headers["Authorization"] = "Bearer {}".format(self.token)
|
|
533
|
+
logger.debug(
|
|
534
|
+
re.sub(
|
|
535
|
+
r"'Bearer [^']+'",
|
|
536
|
+
r"'Bearer ***'",
|
|
537
|
+
f"PreparedRequest: {request.__dict__}",
|
|
538
|
+
)
|
|
539
|
+
)
|
|
321
540
|
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,12 +76,13 @@ 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:
|
|
80
83
|
raise TimeOutError(exc, timeout=HTTP_REQ_TIMEOUT) from exc
|
|
81
84
|
except RequestException as e:
|
|
82
|
-
raise AuthenticationError(
|
|
85
|
+
raise AuthenticationError("Could no authenticate", str(e)) from e
|
|
83
86
|
|
|
84
87
|
return auth
|
|
85
88
|
|
|
@@ -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,14 +65,17 @@ 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,
|
|
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)
|
|
70
75
|
except requests.exceptions.Timeout as exc:
|
|
71
76
|
raise TimeOutError(exc, timeout=HTTP_REQ_TIMEOUT) from exc
|
|
72
77
|
except (requests.RequestException, JSONDecodeError, KeyError) as e:
|
|
73
|
-
raise AuthenticationError(
|
|
78
|
+
raise AuthenticationError("Could no get signed url", str(e)) from e
|
|
74
79
|
else:
|
|
75
80
|
self.signed_urls[req_signed_url] = signed_url
|
|
76
81
|
|
|
@@ -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
|
)
|