eodag 2.12.1__py3-none-any.whl → 3.0.0b1__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/api/core.py +434 -319
- eodag/api/product/__init__.py +5 -1
- eodag/api/product/_assets.py +7 -2
- eodag/api/product/_product.py +46 -68
- eodag/api/product/metadata_mapping.py +181 -66
- eodag/api/search_result.py +21 -1
- eodag/cli.py +20 -6
- eodag/config.py +95 -6
- eodag/plugins/apis/base.py +8 -165
- eodag/plugins/apis/ecmwf.py +36 -24
- eodag/plugins/apis/usgs.py +40 -24
- eodag/plugins/authentication/aws_auth.py +2 -2
- eodag/plugins/authentication/header.py +31 -6
- eodag/plugins/authentication/keycloak.py +13 -84
- eodag/plugins/authentication/oauth.py +3 -3
- eodag/plugins/authentication/openid_connect.py +256 -46
- eodag/plugins/authentication/qsauth.py +3 -0
- eodag/plugins/authentication/sas_auth.py +8 -1
- eodag/plugins/authentication/token.py +92 -46
- eodag/plugins/authentication/token_exchange.py +120 -0
- eodag/plugins/download/aws.py +86 -91
- eodag/plugins/download/base.py +72 -40
- eodag/plugins/download/http.py +607 -264
- eodag/plugins/download/s3rest.py +28 -15
- eodag/plugins/manager.py +73 -57
- eodag/plugins/search/__init__.py +36 -0
- eodag/plugins/search/base.py +225 -18
- eodag/plugins/search/build_search_result.py +389 -32
- eodag/plugins/search/cop_marine.py +378 -0
- eodag/plugins/search/creodias_s3.py +15 -14
- eodag/plugins/search/csw.py +5 -7
- eodag/plugins/search/data_request_search.py +44 -20
- eodag/plugins/search/qssearch.py +508 -203
- eodag/plugins/search/static_stac_search.py +99 -36
- eodag/resources/constraints/climate-dt.json +13 -0
- eodag/resources/constraints/extremes-dt.json +8 -0
- eodag/resources/ext_product_types.json +1 -1
- eodag/resources/product_types.yml +1897 -34
- eodag/resources/providers.yml +3539 -3277
- eodag/resources/stac.yml +48 -54
- eodag/resources/stac_api.yml +71 -25
- eodag/resources/stac_provider.yml +5 -0
- eodag/resources/user_conf_template.yml +51 -3
- eodag/rest/__init__.py +6 -0
- eodag/rest/cache.py +70 -0
- eodag/rest/config.py +68 -0
- eodag/rest/constants.py +27 -0
- eodag/rest/core.py +757 -0
- eodag/rest/server.py +397 -258
- eodag/rest/stac.py +438 -307
- eodag/rest/types/collections_search.py +44 -0
- eodag/rest/types/eodag_search.py +232 -43
- eodag/rest/types/{stac_queryables.py → queryables.py} +81 -43
- eodag/rest/types/stac_search.py +277 -0
- eodag/rest/utils/__init__.py +216 -0
- eodag/rest/utils/cql_evaluate.py +119 -0
- eodag/rest/utils/rfc3339.py +65 -0
- eodag/types/__init__.py +99 -9
- eodag/types/bbox.py +15 -14
- eodag/types/download_args.py +31 -0
- eodag/types/search_args.py +58 -7
- eodag/types/whoosh.py +81 -0
- eodag/utils/__init__.py +72 -9
- eodag/utils/constraints.py +37 -37
- eodag/utils/exceptions.py +23 -17
- eodag/utils/requests.py +138 -0
- eodag/utils/rest.py +104 -0
- eodag/utils/stac_reader.py +100 -16
- {eodag-2.12.1.dist-info → eodag-3.0.0b1.dist-info}/METADATA +64 -44
- eodag-3.0.0b1.dist-info/RECORD +109 -0
- {eodag-2.12.1.dist-info → eodag-3.0.0b1.dist-info}/WHEEL +1 -1
- {eodag-2.12.1.dist-info → eodag-3.0.0b1.dist-info}/entry_points.txt +6 -5
- eodag/plugins/apis/cds.py +0 -540
- eodag/rest/utils.py +0 -1133
- eodag-2.12.1.dist-info/RECORD +0 -94
- {eodag-2.12.1.dist-info → eodag-3.0.0b1.dist-info}/LICENSE +0 -0
- {eodag-2.12.1.dist-info → eodag-3.0.0b1.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
|
|
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
|
-
|
|
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) ->
|
|
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
|
-
|
|
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
|
|
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),
|
|
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
|
-
|
|
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
|
-
|
|
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 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
|
|
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
|
-
}
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
106
|
-
|
|
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(
|
|
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
|
-
|
|
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"] =
|
|
212
|
+
request.headers["Authorization"] = request.headers.get(
|
|
213
|
+
"Authorization", "Bearer {token}"
|
|
214
|
+
).format(token=self.token)
|
|
169
215
|
return request
|