digitalhub 0.13.0b2__py3-none-any.whl → 0.13.0b3__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.
- digitalhub/__init__.py +1 -1
- digitalhub/context/api.py +5 -5
- digitalhub/context/builder.py +3 -5
- digitalhub/context/context.py +9 -1
- digitalhub/entities/_base/material/entity.py +3 -3
- digitalhub/entities/dataitem/crud.py +10 -2
- digitalhub/entities/dataitem/table/entity.py +3 -3
- digitalhub/entities/dataitem/utils.py +1 -2
- digitalhub/entities/task/_base/models.py +12 -3
- digitalhub/factory/factory.py +25 -3
- digitalhub/factory/utils.py +11 -3
- digitalhub/runtimes/_base.py +1 -1
- digitalhub/runtimes/builder.py +18 -1
- digitalhub/stores/client/__init__.py +12 -0
- digitalhub/stores/client/_base/api_builder.py +14 -0
- digitalhub/stores/client/_base/client.py +93 -0
- digitalhub/stores/client/_base/key_builder.py +28 -0
- digitalhub/stores/client/_base/params_builder.py +14 -0
- digitalhub/stores/client/api.py +10 -5
- digitalhub/stores/client/builder.py +3 -1
- digitalhub/stores/client/dhcore/api_builder.py +17 -0
- digitalhub/stores/client/dhcore/client.py +276 -58
- digitalhub/stores/client/dhcore/configurator.py +336 -141
- digitalhub/stores/client/dhcore/error_parser.py +35 -1
- digitalhub/stores/client/dhcore/params_builder.py +113 -17
- digitalhub/stores/client/dhcore/utils.py +32 -14
- digitalhub/stores/client/local/api_builder.py +17 -0
- digitalhub/stores/client/local/client.py +6 -8
- digitalhub/stores/credentials/api.py +8 -8
- digitalhub/stores/credentials/configurator.py +176 -3
- digitalhub/stores/credentials/enums.py +16 -3
- digitalhub/stores/credentials/handler.py +73 -45
- digitalhub/stores/credentials/ini_module.py +59 -27
- digitalhub/stores/credentials/store.py +33 -1
- digitalhub/stores/data/_base/store.py +8 -3
- digitalhub/stores/data/api.py +20 -16
- digitalhub/stores/data/builder.py +3 -9
- digitalhub/stores/data/s3/configurator.py +64 -23
- digitalhub/stores/data/s3/store.py +30 -27
- digitalhub/stores/data/s3/utils.py +9 -9
- digitalhub/stores/data/sql/configurator.py +23 -22
- digitalhub/stores/data/sql/store.py +14 -16
- digitalhub/utils/exceptions.py +6 -0
- digitalhub/utils/file_utils.py +53 -30
- digitalhub/utils/generic_utils.py +41 -33
- digitalhub/utils/git_utils.py +24 -14
- digitalhub/utils/io_utils.py +19 -18
- digitalhub/utils/uri_utils.py +31 -31
- {digitalhub-0.13.0b2.dist-info → digitalhub-0.13.0b3.dist-info}/METADATA +1 -1
- {digitalhub-0.13.0b2.dist-info → digitalhub-0.13.0b3.dist-info}/RECORD +53 -53
- {digitalhub-0.13.0b2.dist-info → digitalhub-0.13.0b3.dist-info}/WHEEL +0 -0
- {digitalhub-0.13.0b2.dist-info → digitalhub-0.13.0b3.dist-info}/licenses/AUTHORS +0 -0
- {digitalhub-0.13.0b2.dist-info → digitalhub-0.13.0b3.dist-info}/licenses/LICENSE +0 -0
|
@@ -10,7 +10,7 @@ from requests import request
|
|
|
10
10
|
|
|
11
11
|
from digitalhub.stores.client.dhcore.enums import AuthType
|
|
12
12
|
from digitalhub.stores.credentials.configurator import Configurator
|
|
13
|
-
from digitalhub.stores.credentials.enums import CredsEnvVar
|
|
13
|
+
from digitalhub.stores.credentials.enums import CredsEnvVar
|
|
14
14
|
from digitalhub.stores.credentials.handler import creds_handler
|
|
15
15
|
from digitalhub.utils.exceptions import ClientError
|
|
16
16
|
from digitalhub.utils.generic_utils import list_enum
|
|
@@ -61,10 +61,17 @@ class ClientDHCoreConfigurator(Configurator):
|
|
|
61
61
|
]
|
|
62
62
|
|
|
63
63
|
def __init__(self) -> None:
|
|
64
|
+
"""
|
|
65
|
+
Initialize the DHCore configurator.
|
|
66
|
+
|
|
67
|
+
Sets up the configurator by calling the parent constructor and
|
|
68
|
+
initializing the authentication type evaluation process.
|
|
69
|
+
|
|
70
|
+
Returns
|
|
71
|
+
-------
|
|
72
|
+
None
|
|
73
|
+
"""
|
|
64
74
|
super().__init__()
|
|
65
|
-
self.load_configs()
|
|
66
|
-
self._origin = self.set_origin()
|
|
67
|
-
self._current_profile = creds_handler.get_current_env()
|
|
68
75
|
self._auth_type: str | None = None
|
|
69
76
|
self.set_auth_type()
|
|
70
77
|
|
|
@@ -72,34 +79,53 @@ class ClientDHCoreConfigurator(Configurator):
|
|
|
72
79
|
# Credentials methods
|
|
73
80
|
##############################
|
|
74
81
|
|
|
75
|
-
def load_configs(self) -> str:
|
|
76
|
-
"""
|
|
77
|
-
Load the configuration from the environment and from the file.
|
|
78
|
-
"""
|
|
79
|
-
self.load_env_vars()
|
|
80
|
-
self.load_file_vars()
|
|
81
|
-
|
|
82
82
|
def load_env_vars(self) -> None:
|
|
83
83
|
"""
|
|
84
|
-
Load
|
|
84
|
+
Load credentials from environment variables.
|
|
85
|
+
|
|
86
|
+
Retrieves DHCore credentials from environment variables, sanitizes
|
|
87
|
+
them (particularly endpoint URLs), and stores them in the credentials
|
|
88
|
+
handler for the environment origin.
|
|
89
|
+
|
|
90
|
+
Returns
|
|
91
|
+
-------
|
|
92
|
+
None
|
|
93
|
+
|
|
94
|
+
Notes
|
|
95
|
+
-----
|
|
96
|
+
This method sanitizes endpoint and issuer URLs to ensure they have
|
|
97
|
+
proper schemes and removes trailing slashes.
|
|
85
98
|
"""
|
|
86
|
-
env_creds =
|
|
99
|
+
env_creds = self._creds_handler.load_from_env(self.keys)
|
|
87
100
|
env_creds = self._sanitize_env_vars(env_creds)
|
|
88
101
|
self._creds_handler.set_credentials(self._env, env_creds)
|
|
89
102
|
|
|
90
103
|
def _sanitize_env_vars(self, creds: dict) -> dict:
|
|
91
104
|
"""
|
|
92
|
-
Sanitize
|
|
93
|
-
|
|
105
|
+
Sanitize credentials loaded from environment variables.
|
|
106
|
+
|
|
107
|
+
Validates and normalizes endpoint and issuer URLs from environment
|
|
108
|
+
variables. Ensures URLs have proper schemes and removes trailing slashes.
|
|
94
109
|
|
|
95
110
|
Parameters
|
|
96
111
|
----------
|
|
97
112
|
creds : dict
|
|
98
|
-
|
|
113
|
+
Raw credentials dictionary loaded from environment variables.
|
|
99
114
|
|
|
100
115
|
Returns
|
|
101
116
|
-------
|
|
102
117
|
dict
|
|
118
|
+
Sanitized credentials dictionary with normalized URLs.
|
|
119
|
+
|
|
120
|
+
Raises
|
|
121
|
+
------
|
|
122
|
+
ClientError
|
|
123
|
+
If endpoint or issuer URLs have invalid schemes.
|
|
124
|
+
|
|
125
|
+
Notes
|
|
126
|
+
-----
|
|
127
|
+
Environment variables are expected to have the full "DHCORE_" prefix
|
|
128
|
+
for issuer endpoints.
|
|
103
129
|
"""
|
|
104
130
|
creds[CredsEnvVar.DHCORE_ENDPOINT.value] = self._sanitize_endpoint(creds[CredsEnvVar.DHCORE_ENDPOINT.value])
|
|
105
131
|
creds[CredsEnvVar.DHCORE_ISSUER.value] = self._sanitize_endpoint(creds[CredsEnvVar.DHCORE_ISSUER.value])
|
|
@@ -107,20 +133,35 @@ class ClientDHCoreConfigurator(Configurator):
|
|
|
107
133
|
|
|
108
134
|
def load_file_vars(self) -> None:
|
|
109
135
|
"""
|
|
110
|
-
Load
|
|
136
|
+
Load credentials from configuration file.
|
|
137
|
+
|
|
138
|
+
Retrieves DHCore credentials from the .dhcore.ini file, handles
|
|
139
|
+
compatibility with CLI format (keys without DHCORE_ prefix), and
|
|
140
|
+
falls back to environment variables for missing endpoint and
|
|
141
|
+
personal access token values.
|
|
142
|
+
|
|
143
|
+
Returns
|
|
144
|
+
-------
|
|
145
|
+
None
|
|
146
|
+
|
|
147
|
+
Notes
|
|
148
|
+
-----
|
|
149
|
+
This method handles the case where:
|
|
150
|
+
- Endpoint might not be present in file response, falls back to env
|
|
151
|
+
- Personal access token might not be present, falls back to env
|
|
152
|
+
- File format uses keys without "DHCORE_" prefix for compatibility
|
|
111
153
|
"""
|
|
112
154
|
keys = [*self._remove_prefix_dhcore()]
|
|
113
|
-
file_creds =
|
|
155
|
+
file_creds = self._creds_handler.load_from_file(keys)
|
|
156
|
+
env_creds = self._creds_handler.load_from_env(self.keys)
|
|
114
157
|
|
|
115
158
|
# Because in the response there is no endpoint
|
|
116
159
|
if file_creds[CredsEnvVar.DHCORE_ENDPOINT.value] is None:
|
|
117
|
-
file_creds[CredsEnvVar.DHCORE_ENDPOINT.value] =
|
|
118
|
-
CredsEnvVar.DHCORE_ENDPOINT.value
|
|
119
|
-
)
|
|
160
|
+
file_creds[CredsEnvVar.DHCORE_ENDPOINT.value] = env_creds.get(CredsEnvVar.DHCORE_ENDPOINT.value)
|
|
120
161
|
|
|
121
162
|
# Because in the response there is no personal access token
|
|
122
163
|
if file_creds[CredsEnvVar.DHCORE_PERSONAL_ACCESS_TOKEN.value] is None:
|
|
123
|
-
file_creds[CredsEnvVar.DHCORE_PERSONAL_ACCESS_TOKEN.value] =
|
|
164
|
+
file_creds[CredsEnvVar.DHCORE_PERSONAL_ACCESS_TOKEN.value] = env_creds.get(
|
|
124
165
|
CredsEnvVar.DHCORE_PERSONAL_ACCESS_TOKEN.value
|
|
125
166
|
)
|
|
126
167
|
|
|
@@ -129,17 +170,33 @@ class ClientDHCoreConfigurator(Configurator):
|
|
|
129
170
|
|
|
130
171
|
def _sanitize_file_vars(self, creds: dict) -> dict:
|
|
131
172
|
"""
|
|
132
|
-
Sanitize
|
|
133
|
-
|
|
173
|
+
Sanitize credentials loaded from configuration file.
|
|
174
|
+
|
|
175
|
+
Handles the different key formats used in configuration files compared
|
|
176
|
+
to environment variables. File format omits "DHCORE_" prefix for
|
|
177
|
+
certain keys for CLI compatibility.
|
|
134
178
|
|
|
135
179
|
Parameters
|
|
136
180
|
----------
|
|
137
181
|
creds : dict
|
|
138
|
-
|
|
182
|
+
Raw credentials dictionary loaded from configuration file.
|
|
139
183
|
|
|
140
184
|
Returns
|
|
141
185
|
-------
|
|
142
186
|
dict
|
|
187
|
+
Sanitized credentials dictionary with standardized key names
|
|
188
|
+
and normalized URLs, filtered to include only valid keys.
|
|
189
|
+
|
|
190
|
+
Raises
|
|
191
|
+
------
|
|
192
|
+
ClientError
|
|
193
|
+
If endpoint or issuer URLs have invalid schemes.
|
|
194
|
+
|
|
195
|
+
Notes
|
|
196
|
+
-----
|
|
197
|
+
File format expects these keys without "DHCORE_" prefix:
|
|
198
|
+
- issuer, client_id, access_token, refresh_token
|
|
199
|
+
But uses full names for: endpoint, user, password, personal_access_token
|
|
143
200
|
"""
|
|
144
201
|
creds[CredsEnvVar.DHCORE_ENDPOINT.value] = self._sanitize_endpoint(creds[CredsEnvVar.DHCORE_ENDPOINT.value])
|
|
145
202
|
creds[CredsEnvVar.DHCORE_ISSUER.value] = self._sanitize_endpoint(
|
|
@@ -157,12 +214,31 @@ class ClientDHCoreConfigurator(Configurator):
|
|
|
157
214
|
@staticmethod
|
|
158
215
|
def _sanitize_endpoint(endpoint: str | None = None) -> str | None:
|
|
159
216
|
"""
|
|
160
|
-
Sanitize
|
|
217
|
+
Sanitize and validate endpoint URL.
|
|
218
|
+
|
|
219
|
+
Validates that the endpoint URL has a proper HTTP/HTTPS scheme,
|
|
220
|
+
trims whitespace, and removes trailing slashes for consistency.
|
|
221
|
+
|
|
222
|
+
Parameters
|
|
223
|
+
----------
|
|
224
|
+
endpoint : str, optional
|
|
225
|
+
The endpoint URL to sanitize. If None, returns None.
|
|
161
226
|
|
|
162
227
|
Returns
|
|
163
228
|
-------
|
|
164
|
-
str
|
|
165
|
-
The sanitized endpoint
|
|
229
|
+
str or None
|
|
230
|
+
The sanitized endpoint URL with trailing slash removed,
|
|
231
|
+
or None if input was None.
|
|
232
|
+
|
|
233
|
+
Raises
|
|
234
|
+
------
|
|
235
|
+
ClientError
|
|
236
|
+
If the endpoint does not start with http:// or https://.
|
|
237
|
+
|
|
238
|
+
Notes
|
|
239
|
+
-----
|
|
240
|
+
This method ensures endpoint URLs are properly formatted for
|
|
241
|
+
HTTP requests and prevents common URL formatting issues.
|
|
166
242
|
"""
|
|
167
243
|
if endpoint is None:
|
|
168
244
|
return
|
|
@@ -172,31 +248,27 @@ class ClientDHCoreConfigurator(Configurator):
|
|
|
172
248
|
endpoint = endpoint.strip()
|
|
173
249
|
return endpoint.removesuffix("/")
|
|
174
250
|
|
|
175
|
-
def
|
|
251
|
+
def get_endpoint(self) -> str:
|
|
176
252
|
"""
|
|
177
|
-
|
|
253
|
+
Get the configured DHCore backend endpoint.
|
|
178
254
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
config : dict
|
|
182
|
-
Configuration dictionary.
|
|
255
|
+
Retrieves the DHCore endpoint URL from the current credential source
|
|
256
|
+
(environment or file based on current origin).
|
|
183
257
|
|
|
184
258
|
Returns
|
|
185
259
|
-------
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
if (current := creds_handler.get_current_env()) != self._current_profile:
|
|
189
|
-
self.load_file_vars()
|
|
190
|
-
self._current_profile = current
|
|
260
|
+
str
|
|
261
|
+
The DHCore backend endpoint URL.
|
|
191
262
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
263
|
+
Raises
|
|
264
|
+
------
|
|
265
|
+
KeyError
|
|
266
|
+
If the endpoint is not configured in the current credential source.
|
|
195
267
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
268
|
+
Notes
|
|
269
|
+
-----
|
|
270
|
+
The endpoint returned is already sanitized and validated during
|
|
271
|
+
the credential loading process.
|
|
200
272
|
"""
|
|
201
273
|
creds = self._creds_handler.get_credentials(self._origin)
|
|
202
274
|
return creds[CredsEnvVar.DHCORE_ENDPOINT.value]
|
|
@@ -205,70 +277,59 @@ class ClientDHCoreConfigurator(Configurator):
|
|
|
205
277
|
# Origin methods
|
|
206
278
|
##############################
|
|
207
279
|
|
|
208
|
-
def
|
|
280
|
+
def change_origin(self) -> None:
|
|
209
281
|
"""
|
|
210
|
-
|
|
282
|
+
Change the credentials origin and re-evaluate authentication type.
|
|
283
|
+
|
|
284
|
+
Switches the credential source (between environment and file) and
|
|
285
|
+
re-evaluates the authentication type based on the new credential set.
|
|
286
|
+
This is typically called when the current credential source fails
|
|
287
|
+
or when switching contexts.
|
|
211
288
|
|
|
212
289
|
Returns
|
|
213
290
|
-------
|
|
214
|
-
|
|
215
|
-
The origin.
|
|
216
|
-
"""
|
|
217
|
-
origin = CredsOrigin.ENV.value
|
|
218
|
-
|
|
219
|
-
env_creds = self._creds_handler.get_credentials(self._env)
|
|
220
|
-
missing_env = self._check_credentials(env_creds)
|
|
221
|
-
|
|
222
|
-
file_creds = self._creds_handler.get_credentials(self._file)
|
|
223
|
-
missing_file = self._check_credentials(file_creds)
|
|
224
|
-
|
|
225
|
-
msg = ""
|
|
226
|
-
if missing_env:
|
|
227
|
-
msg = f"Missing required vars in env: {', '.join(missing_env)}"
|
|
228
|
-
origin = CredsOrigin.FILE.value
|
|
229
|
-
elif missing_file:
|
|
230
|
-
msg += f"Missing required vars in .dhcore.ini file: {', '.join(missing_file)}"
|
|
231
|
-
|
|
232
|
-
if missing_env and missing_file:
|
|
233
|
-
raise ClientError(msg)
|
|
234
|
-
|
|
235
|
-
return origin
|
|
291
|
+
None
|
|
236
292
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
293
|
+
Notes
|
|
294
|
+
-----
|
|
295
|
+
This method extends the parent class behavior by also re-evaluating
|
|
296
|
+
the authentication type, which may change based on different
|
|
297
|
+
credentials available in the new source.
|
|
240
298
|
"""
|
|
241
|
-
|
|
242
|
-
self.change_to_file()
|
|
243
|
-
else:
|
|
244
|
-
self.change_to_env()
|
|
299
|
+
super().change_origin()
|
|
245
300
|
|
|
246
301
|
# Re-evaluate the auth type
|
|
247
302
|
self.set_auth_type()
|
|
248
303
|
|
|
249
|
-
def change_to_file(self) -> None:
|
|
250
|
-
"""
|
|
251
|
-
Change the origin to file. Re-evaluate the auth type.
|
|
252
|
-
"""
|
|
253
|
-
self._origin = CredsOrigin.FILE.value
|
|
254
|
-
|
|
255
|
-
def change_to_env(self) -> None:
|
|
256
|
-
"""
|
|
257
|
-
Change the origin to env. Re-evaluate the auth type.
|
|
258
|
-
"""
|
|
259
|
-
self._origin = CredsOrigin.ENV.value
|
|
260
|
-
|
|
261
304
|
##############################
|
|
262
305
|
# Auth methods
|
|
263
306
|
##############################
|
|
264
307
|
|
|
265
308
|
def set_auth_type(self) -> None:
|
|
266
309
|
"""
|
|
267
|
-
Evaluate the
|
|
310
|
+
Evaluate and set the authentication type from available credentials.
|
|
311
|
+
|
|
312
|
+
Analyzes the available credentials and determines the appropriate
|
|
313
|
+
authentication method based on the following priority:
|
|
314
|
+
1. EXCHANGE - Personal access token available
|
|
315
|
+
2. OAUTH2 - Access token and refresh token available
|
|
316
|
+
3. ACCESS_TOKEN - Only access token available
|
|
317
|
+
4. BASIC - Username and password available
|
|
318
|
+
5. None - No valid credentials found
|
|
319
|
+
|
|
320
|
+
For EXCHANGE authentication, automatically performs token exchange
|
|
321
|
+
and switches to file-based credential storage.
|
|
268
322
|
|
|
269
323
|
Returns
|
|
270
324
|
-------
|
|
271
325
|
None
|
|
326
|
+
|
|
327
|
+
Notes
|
|
328
|
+
-----
|
|
329
|
+
When EXCHANGE authentication is detected, this method automatically:
|
|
330
|
+
- Performs credential refresh to exchange the personal access token
|
|
331
|
+
- Changes origin to file-based storage for the new tokens
|
|
332
|
+
- Updates the authentication type accordingly
|
|
272
333
|
"""
|
|
273
334
|
creds = creds_handler.get_credentials(self._origin)
|
|
274
335
|
self._auth_type = self._eval_auth_type(creds)
|
|
@@ -276,39 +337,68 @@ class ClientDHCoreConfigurator(Configurator):
|
|
|
276
337
|
# Therefore, we change the origin to file, where the refresh token is written.
|
|
277
338
|
# We also try to fetch the PAT from both env and file
|
|
278
339
|
if self._auth_type == AuthType.EXCHANGE.value:
|
|
279
|
-
self.
|
|
340
|
+
self.refresh_credentials(change_origin=True)
|
|
280
341
|
# Just to ensure we get the right source from file
|
|
281
342
|
self.change_to_file()
|
|
282
343
|
|
|
283
344
|
def refreshable_auth_types(self) -> bool:
|
|
284
345
|
"""
|
|
285
|
-
Check if the
|
|
346
|
+
Check if the current authentication type supports token refresh.
|
|
347
|
+
|
|
348
|
+
Determines whether the current authentication method supports
|
|
349
|
+
automatic token refresh capabilities.
|
|
286
350
|
|
|
287
351
|
Returns
|
|
288
352
|
-------
|
|
289
353
|
bool
|
|
290
|
-
True if the
|
|
354
|
+
True if the authentication type supports refresh (OAUTH2 or EXCHANGE),
|
|
355
|
+
False otherwise (BASIC or ACCESS_TOKEN).
|
|
356
|
+
|
|
357
|
+
Notes
|
|
358
|
+
-----
|
|
359
|
+
Only OAUTH2 and EXCHANGE authentication types support refresh:
|
|
360
|
+
- OAUTH2: Uses refresh token to get new access tokens
|
|
361
|
+
- EXCHANGE: Uses personal access token for token exchange
|
|
362
|
+
- BASIC and ACCESS_TOKEN do not support refresh
|
|
291
363
|
"""
|
|
292
364
|
return self._auth_type in [AuthType.OAUTH2.value, AuthType.EXCHANGE.value]
|
|
293
365
|
|
|
294
366
|
def get_auth_parameters(self, kwargs: dict) -> dict:
|
|
295
367
|
"""
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
368
|
+
Add authentication parameters to HTTP request arguments.
|
|
369
|
+
|
|
370
|
+
Modifies the provided kwargs dictionary to include the appropriate
|
|
371
|
+
authentication headers or parameters based on the current authentication
|
|
372
|
+
type and available credentials.
|
|
299
373
|
|
|
300
374
|
Parameters
|
|
301
375
|
----------
|
|
302
376
|
kwargs : dict
|
|
303
|
-
|
|
377
|
+
HTTP request keyword arguments to be modified with authentication.
|
|
304
378
|
|
|
305
379
|
Returns
|
|
306
380
|
-------
|
|
307
381
|
dict
|
|
308
|
-
|
|
382
|
+
The modified kwargs dictionary with authentication parameters added.
|
|
383
|
+
|
|
384
|
+
Notes
|
|
385
|
+
-----
|
|
386
|
+
Authentication is added based on auth type:
|
|
387
|
+
- OAUTH2/EXCHANGE/ACCESS_TOKEN: Adds Authorization Bearer header
|
|
388
|
+
- BASIC: Adds auth tuple with username/password
|
|
389
|
+
- None: No authentication added
|
|
390
|
+
|
|
391
|
+
The method assumes that:
|
|
392
|
+
- Authentication type has been properly set
|
|
393
|
+
- For EXCHANGE type, refresh token has been obtained
|
|
394
|
+
- Required credentials are available for the current auth type
|
|
309
395
|
"""
|
|
310
396
|
creds = creds_handler.get_credentials(self._origin)
|
|
311
|
-
if self._auth_type in (
|
|
397
|
+
if self._auth_type in (
|
|
398
|
+
AuthType.EXCHANGE.value,
|
|
399
|
+
AuthType.OAUTH2.value,
|
|
400
|
+
AuthType.ACCESS_TOKEN.value,
|
|
401
|
+
):
|
|
312
402
|
access_token = creds[CredsEnvVar.DHCORE_ACCESS_TOKEN.value]
|
|
313
403
|
if "headers" not in kwargs:
|
|
314
404
|
kwargs["headers"] = {}
|
|
@@ -319,18 +409,43 @@ class ClientDHCoreConfigurator(Configurator):
|
|
|
319
409
|
kwargs["auth"] = (user, password)
|
|
320
410
|
return kwargs
|
|
321
411
|
|
|
322
|
-
def
|
|
412
|
+
def refresh_credentials(self, change_origin: bool = False) -> None:
|
|
323
413
|
"""
|
|
324
|
-
|
|
414
|
+
Refresh authentication credentials by obtaining new access tokens.
|
|
415
|
+
|
|
416
|
+
Performs credential refresh using either OAuth2 refresh token flow
|
|
417
|
+
or personal access token exchange, depending on the current
|
|
418
|
+
authentication type. Updates stored credentials with new tokens.
|
|
325
419
|
|
|
326
420
|
Parameters
|
|
327
421
|
----------
|
|
328
|
-
change_origin : bool,
|
|
329
|
-
Whether to
|
|
422
|
+
change_origin : bool, default False
|
|
423
|
+
Whether to allow changing credential source if refresh fails.
|
|
424
|
+
If True and refresh fails, attempts to switch credential sources
|
|
425
|
+
and retry once.
|
|
330
426
|
|
|
331
427
|
Returns
|
|
332
428
|
-------
|
|
333
429
|
None
|
|
430
|
+
|
|
431
|
+
Raises
|
|
432
|
+
------
|
|
433
|
+
ClientError
|
|
434
|
+
If the authentication type doesn't support refresh, if required
|
|
435
|
+
credentials are missing, or if refresh fails and change_origin
|
|
436
|
+
is False.
|
|
437
|
+
|
|
438
|
+
Notes
|
|
439
|
+
-----
|
|
440
|
+
Refresh behavior by authentication type:
|
|
441
|
+
- OAUTH2: Uses refresh_token grant to get new access/refresh tokens
|
|
442
|
+
- EXCHANGE: Uses token exchange with personal access token
|
|
443
|
+
|
|
444
|
+
If refresh fails with 400/401/403 status and change_origin=True,
|
|
445
|
+
attempts to switch credential sources and retry once.
|
|
446
|
+
|
|
447
|
+
New credentials are automatically saved to the configuration file
|
|
448
|
+
and the origin is switched to file-based storage.
|
|
334
449
|
"""
|
|
335
450
|
if not self.refreshable_auth_types():
|
|
336
451
|
raise ClientError(f"Auth type {self._auth_type} does not support refresh.")
|
|
@@ -347,7 +462,7 @@ class ClientDHCoreConfigurator(Configurator):
|
|
|
347
462
|
|
|
348
463
|
# Handling of token exchange or refresh
|
|
349
464
|
if self._auth_type == AuthType.OAUTH2.value:
|
|
350
|
-
response = self.
|
|
465
|
+
response = self._call_refresh_endpoint(
|
|
351
466
|
url,
|
|
352
467
|
client_id=client_id,
|
|
353
468
|
refresh_token=creds.get(CredsEnvVar.DHCORE_REFRESH_TOKEN.value),
|
|
@@ -355,7 +470,7 @@ class ClientDHCoreConfigurator(Configurator):
|
|
|
355
470
|
scope="credentials",
|
|
356
471
|
)
|
|
357
472
|
elif self._auth_type == AuthType.EXCHANGE.value:
|
|
358
|
-
response = self.
|
|
473
|
+
response = self._call_refresh_endpoint(
|
|
359
474
|
url,
|
|
360
475
|
client_id=client_id,
|
|
361
476
|
subject_token=creds.get(CredsEnvVar.DHCORE_PERSONAL_ACCESS_TOKEN.value),
|
|
@@ -367,47 +482,37 @@ class ClientDHCoreConfigurator(Configurator):
|
|
|
367
482
|
# Change origin of creds if needed
|
|
368
483
|
if response.status_code in (400, 401, 403):
|
|
369
484
|
if not change_origin:
|
|
370
|
-
raise ClientError("Unable to refresh
|
|
371
|
-
self.
|
|
372
|
-
self.
|
|
485
|
+
raise ClientError("Unable to refresh credentials. Please check your credentials.")
|
|
486
|
+
self.eval_change_origin()
|
|
487
|
+
self.refresh_credentials(change_origin=False)
|
|
373
488
|
|
|
374
489
|
response.raise_for_status()
|
|
375
490
|
|
|
376
491
|
# Read new credentials and propagate to config file
|
|
377
492
|
self._export_new_creds(response.json())
|
|
378
493
|
|
|
379
|
-
def _export_new_creds(self, response: dict) -> None:
|
|
380
|
-
"""
|
|
381
|
-
Set new credentials.
|
|
382
|
-
|
|
383
|
-
Parameters
|
|
384
|
-
----------
|
|
385
|
-
response : dict
|
|
386
|
-
Response from refresh token endpoint.
|
|
387
|
-
|
|
388
|
-
Returns
|
|
389
|
-
-------
|
|
390
|
-
None
|
|
391
|
-
"""
|
|
392
|
-
creds_handler.write_env(response)
|
|
393
|
-
self.load_file_vars()
|
|
394
|
-
|
|
395
|
-
# Change current origin to file because of refresh
|
|
396
|
-
self._origin = CredsOrigin.FILE.value
|
|
397
|
-
|
|
398
494
|
def _remove_prefix_dhcore(self) -> list[str]:
|
|
399
495
|
"""
|
|
400
|
-
Remove prefix from selected keys
|
|
496
|
+
Remove DHCORE_ prefix from selected credential keys for CLI compatibility.
|
|
401
497
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
List of keys.
|
|
498
|
+
Creates a list of credential key names with "DHCORE_" prefix removed
|
|
499
|
+
from specific keys that are stored without the prefix in configuration
|
|
500
|
+
files for compatibility with CLI tools.
|
|
406
501
|
|
|
407
502
|
Returns
|
|
408
503
|
-------
|
|
409
504
|
list[str]
|
|
410
|
-
List of keys
|
|
505
|
+
List of credential keys with selective prefix removal applied.
|
|
506
|
+
|
|
507
|
+
Notes
|
|
508
|
+
-----
|
|
509
|
+
Keys that have prefix removed (defined in keys_to_unprefix):
|
|
510
|
+
- DHCORE_REFRESH_TOKEN -> refresh_token
|
|
511
|
+
- DHCORE_ACCESS_TOKEN -> access_token
|
|
512
|
+
- DHCORE_ISSUER -> issuer
|
|
513
|
+
- DHCORE_CLIENT_ID -> client_id
|
|
514
|
+
|
|
515
|
+
Other keys retain their full names for consistency.
|
|
411
516
|
"""
|
|
412
517
|
new_list = []
|
|
413
518
|
for key in self.keys:
|
|
@@ -419,12 +524,31 @@ class ClientDHCoreConfigurator(Configurator):
|
|
|
419
524
|
|
|
420
525
|
def _get_refresh_endpoint(self) -> str:
|
|
421
526
|
"""
|
|
422
|
-
|
|
527
|
+
Discover the OAuth2 token refresh endpoint from the issuer.
|
|
528
|
+
|
|
529
|
+
Queries the OAuth2 issuer's well-known configuration endpoint to
|
|
530
|
+
discover the token endpoint used for credential refresh operations.
|
|
423
531
|
|
|
424
532
|
Returns
|
|
425
533
|
-------
|
|
426
534
|
str
|
|
427
|
-
|
|
535
|
+
The token endpoint URL for credential refresh.
|
|
536
|
+
|
|
537
|
+
Raises
|
|
538
|
+
------
|
|
539
|
+
ClientError
|
|
540
|
+
If the issuer endpoint is not configured.
|
|
541
|
+
HTTPError
|
|
542
|
+
If the well-known configuration endpoint is not accessible.
|
|
543
|
+
KeyError
|
|
544
|
+
If the token_endpoint is not found in the issuer configuration.
|
|
545
|
+
|
|
546
|
+
Notes
|
|
547
|
+
-----
|
|
548
|
+
This method follows the OAuth2/OpenID Connect discovery standard by:
|
|
549
|
+
1. Accessing the issuer's /.well-known/openid-configuration endpoint
|
|
550
|
+
2. Extracting the token_endpoint from the configuration
|
|
551
|
+
3. Using this endpoint for subsequent token refresh operations
|
|
428
552
|
"""
|
|
429
553
|
# Get issuer endpoint
|
|
430
554
|
creds = self._creds_handler.get_credentials(self._origin)
|
|
@@ -440,25 +564,36 @@ class ClientDHCoreConfigurator(Configurator):
|
|
|
440
564
|
r.raise_for_status()
|
|
441
565
|
return r.json().get("token_endpoint")
|
|
442
566
|
|
|
443
|
-
def
|
|
567
|
+
def _call_refresh_endpoint(
|
|
444
568
|
self,
|
|
445
569
|
url: str,
|
|
446
570
|
**kwargs,
|
|
447
571
|
) -> Response:
|
|
448
572
|
"""
|
|
449
|
-
|
|
573
|
+
Make HTTP request to OAuth2 token refresh endpoint.
|
|
574
|
+
|
|
575
|
+
Performs a POST request to the OAuth2 token endpoint with the
|
|
576
|
+
appropriate form-encoded payload for token refresh or exchange.
|
|
450
577
|
|
|
451
578
|
Parameters
|
|
452
579
|
----------
|
|
453
580
|
url : str
|
|
454
|
-
|
|
455
|
-
kwargs : dict
|
|
456
|
-
|
|
581
|
+
The token endpoint URL to call.
|
|
582
|
+
**kwargs : dict
|
|
583
|
+
Token request parameters such as grant_type, client_id,
|
|
584
|
+
refresh_token, subject_token, etc.
|
|
457
585
|
|
|
458
586
|
Returns
|
|
459
587
|
-------
|
|
460
588
|
Response
|
|
461
|
-
|
|
589
|
+
The HTTP response object from the token endpoint.
|
|
590
|
+
|
|
591
|
+
Notes
|
|
592
|
+
-----
|
|
593
|
+
This method:
|
|
594
|
+
- Uses application/x-www-form-urlencoded content type as required by OAuth2
|
|
595
|
+
- Sets a 60-second timeout for the request
|
|
596
|
+
- Returns the raw response for caller to handle status and parsing
|
|
462
597
|
"""
|
|
463
598
|
# Send request to get new access token
|
|
464
599
|
payload = {**kwargs}
|
|
@@ -466,6 +601,32 @@ class ClientDHCoreConfigurator(Configurator):
|
|
|
466
601
|
return request("POST", url, data=payload, headers=headers, timeout=60)
|
|
467
602
|
|
|
468
603
|
def _eval_auth_type(self, creds: dict) -> str | None:
|
|
604
|
+
"""
|
|
605
|
+
Evaluate authentication type based on available credentials.
|
|
606
|
+
|
|
607
|
+
Analyzes the provided credentials and determines the most appropriate
|
|
608
|
+
authentication method based on which credential types are available.
|
|
609
|
+
|
|
610
|
+
Parameters
|
|
611
|
+
----------
|
|
612
|
+
creds : dict
|
|
613
|
+
Dictionary containing credential values.
|
|
614
|
+
|
|
615
|
+
Returns
|
|
616
|
+
-------
|
|
617
|
+
str or None
|
|
618
|
+
The determined authentication type from AuthType enum, or None
|
|
619
|
+
if no valid authentication method can be determined.
|
|
620
|
+
|
|
621
|
+
Notes
|
|
622
|
+
-----
|
|
623
|
+
Authentication type priority (checked in order):
|
|
624
|
+
1. EXCHANGE - Personal access token is available
|
|
625
|
+
2. OAUTH2 - Both access token and refresh token are available
|
|
626
|
+
3. ACCESS_TOKEN - Only access token is available
|
|
627
|
+
4. BASIC - Both username and password are available
|
|
628
|
+
5. None - No valid credential combination found
|
|
629
|
+
"""
|
|
469
630
|
if creds[CredsEnvVar.DHCORE_PERSONAL_ACCESS_TOKEN.value] is not None:
|
|
470
631
|
return AuthType.EXCHANGE.value
|
|
471
632
|
if (
|
|
@@ -478,3 +639,37 @@ class ClientDHCoreConfigurator(Configurator):
|
|
|
478
639
|
if creds[CredsEnvVar.DHCORE_USER.value] is not None and creds[CredsEnvVar.DHCORE_PASSWORD.value] is not None:
|
|
479
640
|
return AuthType.BASIC.value
|
|
480
641
|
return None
|
|
642
|
+
|
|
643
|
+
def _export_new_creds(self, response: dict) -> None:
|
|
644
|
+
"""
|
|
645
|
+
Save new credentials from token refresh response.
|
|
646
|
+
|
|
647
|
+
Takes the response from a successful token refresh operation and
|
|
648
|
+
persists the new credentials to the configuration file, then
|
|
649
|
+
reloads file-based credentials and switches to file origin.
|
|
650
|
+
|
|
651
|
+
Parameters
|
|
652
|
+
----------
|
|
653
|
+
response : dict
|
|
654
|
+
Token response containing new access_token, refresh_token,
|
|
655
|
+
and other credential information.
|
|
656
|
+
|
|
657
|
+
Returns
|
|
658
|
+
-------
|
|
659
|
+
None
|
|
660
|
+
|
|
661
|
+
Notes
|
|
662
|
+
-----
|
|
663
|
+
This method:
|
|
664
|
+
1. Writes new credentials to the configuration file
|
|
665
|
+
2. Reloads file-based credentials to ensure consistency
|
|
666
|
+
3. Changes current origin to file since new tokens are file-based
|
|
667
|
+
|
|
668
|
+
The response typically contains access_token, refresh_token,
|
|
669
|
+
token_type, expires_in, and other OAuth2 standard fields.
|
|
670
|
+
"""
|
|
671
|
+
creds_handler.write_env(response)
|
|
672
|
+
self.load_file_vars()
|
|
673
|
+
|
|
674
|
+
# Change current origin to file because of refresh
|
|
675
|
+
self.change_to_file()
|