dnastack-client-library 3.1.144__py3-none-any.whl → 3.1.145__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.
- dnastack/__main__.py +20 -2
- dnastack/cli/commands/auth/commands.py +5 -3
- dnastack/cli/commands/config/contexts.py +2 -2
- dnastack/client/service_registry/helper.py +2 -0
- dnastack/constants.py +1 -1
- dnastack/context/manager.py +44 -1
- dnastack/context/models.py +4 -1
- dnastack/http/authenticators/abstract.py +1 -1
- dnastack/http/authenticators/oauth2.py +80 -38
- dnastack/http/authenticators/oauth2_adapter/client_credential.py +7 -0
- dnastack/http/authenticators/oauth2_adapter/cloud_providers.py +122 -0
- dnastack/http/authenticators/oauth2_adapter/device_code_flow.py +6 -0
- dnastack/http/authenticators/oauth2_adapter/factory.py +2 -0
- dnastack/http/authenticators/oauth2_adapter/models.py +9 -1
- dnastack/http/authenticators/oauth2_adapter/token_exchange.py +142 -0
- dnastack/http/session_info.py +4 -0
- {dnastack_client_library-3.1.144.dist-info → dnastack_client_library-3.1.145.dist-info}/METADATA +1 -1
- {dnastack_client_library-3.1.144.dist-info → dnastack_client_library-3.1.145.dist-info}/RECORD +22 -20
- {dnastack_client_library-3.1.144.dist-info → dnastack_client_library-3.1.145.dist-info}/WHEEL +0 -0
- {dnastack_client_library-3.1.144.dist-info → dnastack_client_library-3.1.145.dist-info}/entry_points.txt +0 -0
- {dnastack_client_library-3.1.144.dist-info → dnastack_client_library-3.1.145.dist-info}/licenses/LICENSE +0 -0
- {dnastack_client_library-3.1.144.dist-info → dnastack_client_library-3.1.145.dist-info}/top_level.txt +0 -0
dnastack/__main__.py
CHANGED
|
@@ -71,12 +71,30 @@ def version():
|
|
|
71
71
|
type=bool,
|
|
72
72
|
required=False,
|
|
73
73
|
hidden=True,
|
|
74
|
+
),
|
|
75
|
+
ArgumentSpec(
|
|
76
|
+
name='platform_credentials',
|
|
77
|
+
arg_names=['--platform-credentials'],
|
|
78
|
+
help='Use platform-specific credentials (IMDS) for authentication',
|
|
79
|
+
type=bool,
|
|
80
|
+
required=False,
|
|
81
|
+
hidden=True,
|
|
82
|
+
),
|
|
83
|
+
ArgumentSpec(
|
|
84
|
+
name='subject_token',
|
|
85
|
+
arg_names=['--subject-token'],
|
|
86
|
+
help='Subject token for token exchange authentication',
|
|
87
|
+
type=str,
|
|
88
|
+
required=False,
|
|
89
|
+
hidden=True,
|
|
74
90
|
)
|
|
75
91
|
]
|
|
76
92
|
)
|
|
77
93
|
def use(registry_hostname_or_url: str,
|
|
78
94
|
context_name: Optional[str] = None,
|
|
79
|
-
no_auth: bool = False
|
|
95
|
+
no_auth: bool = False,
|
|
96
|
+
platform_credentials: bool = False,
|
|
97
|
+
subject_token: Optional[str] = None):
|
|
80
98
|
"""
|
|
81
99
|
Import a configuration from host's service registry (if available) or the corresponding public configuration from
|
|
82
100
|
cloud storage. If "--no-auth" is not defined, it will automatically initiate all authentication.
|
|
@@ -85,7 +103,7 @@ def use(registry_hostname_or_url: str,
|
|
|
85
103
|
|
|
86
104
|
This is a shortcut to dnastack config contexts use".
|
|
87
105
|
"""
|
|
88
|
-
_context_command_handler.use(registry_hostname_or_url, context_name=context_name, no_auth=no_auth)
|
|
106
|
+
_context_command_handler.use(registry_hostname_or_url, context_name=context_name, no_auth=no_auth, platform_credentials=platform_credentials, subject_token=subject_token)
|
|
89
107
|
|
|
90
108
|
|
|
91
109
|
# noinspection PyTypeChecker
|
|
@@ -50,8 +50,7 @@ def init_auth_commands(group: Group):
|
|
|
50
50
|
handler.initiate_authentications(endpoint_ids=[endpoint_id] if endpoint_id else [],
|
|
51
51
|
force_refresh=force_refresh,
|
|
52
52
|
revoke_existing=revoke_existing)
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
|
|
55
54
|
@formatted_command(
|
|
56
55
|
group=group,
|
|
57
56
|
name='status',
|
|
@@ -94,6 +93,8 @@ def init_auth_commands(group: Group):
|
|
|
94
93
|
handler.revoke([endpoint_id] if endpoint_id else [], force)
|
|
95
94
|
|
|
96
95
|
|
|
96
|
+
|
|
97
|
+
|
|
97
98
|
class AuthCommandHandler:
|
|
98
99
|
def __init__(self, context_name: Optional[str] = None):
|
|
99
100
|
self._logger = get_logger(type(self).__name__)
|
|
@@ -126,7 +127,7 @@ class AuthCommandHandler:
|
|
|
126
127
|
echo_header('Summary')
|
|
127
128
|
|
|
128
129
|
if affected_endpoint_ids:
|
|
129
|
-
echo_list('The client is no longer authenticated to the
|
|
130
|
+
echo_list('The client is no longer authenticated to the following endpoints:',
|
|
130
131
|
affected_endpoint_ids)
|
|
131
132
|
else:
|
|
132
133
|
click.echo('No changes')
|
|
@@ -158,3 +159,4 @@ class AuthCommandHandler:
|
|
|
158
159
|
auth_manager.events.on('refresh-skipped', handle_refresh_skipped)
|
|
159
160
|
|
|
160
161
|
auth_manager.initiate_authentications(endpoint_ids, force_refresh, revoke_existing)
|
|
162
|
+
|
|
@@ -170,9 +170,9 @@ class ContextCommandHandler:
|
|
|
170
170
|
def manager(self):
|
|
171
171
|
return self._context_manager
|
|
172
172
|
|
|
173
|
-
def use(self, registry_hostname_or_url: str, context_name: Optional[str] = None, no_auth: bool = False):
|
|
173
|
+
def use(self, registry_hostname_or_url: str, context_name: Optional[str] = None, no_auth: bool = False, platform_credentials: bool = False, subject_token: Optional[str] = None):
|
|
174
174
|
echo_result('Context', 'blue', 'syncing', registry_hostname_or_url)
|
|
175
|
-
self._context_manager.use(registry_hostname_or_url, context_name=context_name, no_auth=no_auth)
|
|
175
|
+
self._context_manager.use(registry_hostname_or_url, context_name=context_name, no_auth=no_auth, platform_credentials=platform_credentials, subject_token=subject_token)
|
|
176
176
|
echo_result('Context', 'green', 'use', registry_hostname_or_url)
|
|
177
177
|
|
|
178
178
|
def __handle_sync_event(self, event: Event):
|
|
@@ -2,6 +2,7 @@ from typing import Any, Dict, Optional
|
|
|
2
2
|
|
|
3
3
|
from dnastack.client.models import ServiceEndpoint
|
|
4
4
|
from dnastack.client.service_registry.models import Service
|
|
5
|
+
from dnastack.http.authenticators.oauth2_adapter.models import GRANT_TYPE_TOKEN_EXCHANGE
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
def parse_ga4gh_service_info(service: Service, alternate_service_id: Optional[str] = None) -> ServiceEndpoint:
|
|
@@ -36,4 +37,5 @@ def _parse_authentication_info(auth_info: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
36
37
|
resource_url=auth_info.get('resource'),
|
|
37
38
|
scope=auth_info.get('scope'),
|
|
38
39
|
token_endpoint=auth_info.get('accessTokenUrl'),
|
|
40
|
+
platform_credentials=auth_info.get('grantType') == GRANT_TYPE_TOKEN_EXCHANGE,
|
|
39
41
|
)
|
dnastack/constants.py
CHANGED
dnastack/context/manager.py
CHANGED
|
@@ -17,6 +17,7 @@ from dnastack.common.events import EventSource, Event
|
|
|
17
17
|
from dnastack.common.logger import get_logger
|
|
18
18
|
from dnastack.configuration.manager import ConfigurationManager
|
|
19
19
|
from dnastack.context.models import Context
|
|
20
|
+
from dnastack.http.authenticators.oauth2_adapter.models import GRANT_TYPE_TOKEN_EXCHANGE
|
|
20
21
|
from dnastack.http.client_factory import HttpClientFactory
|
|
21
22
|
|
|
22
23
|
|
|
@@ -252,7 +253,9 @@ class BaseContextManager:
|
|
|
252
253
|
def use(self,
|
|
253
254
|
registry_hostname_or_url: str,
|
|
254
255
|
context_name: Optional[str] = None,
|
|
255
|
-
no_auth: Optional[bool] = False
|
|
256
|
+
no_auth: Optional[bool] = False,
|
|
257
|
+
platform_credentials: Optional[bool] = False,
|
|
258
|
+
subject_token: Optional[str] = None) -> EndpointRepository:
|
|
256
259
|
target_hostname = self._get_hostname(registry_hostname_or_url)
|
|
257
260
|
context_name = context_name or target_hostname
|
|
258
261
|
|
|
@@ -312,6 +315,11 @@ class BaseContextManager:
|
|
|
312
315
|
self._logger.debug(f'Syncing: {reg_endpoint.url}')
|
|
313
316
|
reg_manager.synchronize_endpoints(reg_endpoint.id)
|
|
314
317
|
|
|
318
|
+
if platform_credentials:
|
|
319
|
+
self._filter_endpoints_for_token_exchange(context)
|
|
320
|
+
if subject_token:
|
|
321
|
+
context.platform_subject_token = subject_token
|
|
322
|
+
|
|
315
323
|
# Set the current context.
|
|
316
324
|
self._contexts.set_current_context_name(context_name)
|
|
317
325
|
self._contexts.set(context_name, context)
|
|
@@ -404,6 +412,41 @@ class BaseContextManager:
|
|
|
404
412
|
return None
|
|
405
413
|
# end: if
|
|
406
414
|
|
|
415
|
+
def _filter_endpoints_for_token_exchange(self, context: Context):
|
|
416
|
+
"""Filter endpoint authentication methods to only include token-exchange grant types
|
|
417
|
+
Raises error if no endpoints support token exchange authentication"""
|
|
418
|
+
has_any_token_exchange = False
|
|
419
|
+
|
|
420
|
+
for endpoint in context.endpoints:
|
|
421
|
+
if endpoint.type == STANDARD_SERVICE_REGISTRY_TYPE_V1_0:
|
|
422
|
+
continue
|
|
423
|
+
|
|
424
|
+
all_auths = []
|
|
425
|
+
if endpoint.authentication:
|
|
426
|
+
all_auths.append(endpoint.authentication)
|
|
427
|
+
if endpoint.fallback_authentications:
|
|
428
|
+
all_auths.extend(endpoint.fallback_authentications)
|
|
429
|
+
|
|
430
|
+
token_exchange_auths = [
|
|
431
|
+
auth for auth in all_auths
|
|
432
|
+
if auth.get('grant_type') == GRANT_TYPE_TOKEN_EXCHANGE
|
|
433
|
+
]
|
|
434
|
+
|
|
435
|
+
if token_exchange_auths:
|
|
436
|
+
endpoint.authentication = token_exchange_auths[0]
|
|
437
|
+
endpoint.fallback_authentications = token_exchange_auths[1:] or None
|
|
438
|
+
has_any_token_exchange = True
|
|
439
|
+
else:
|
|
440
|
+
endpoint.authentication = None
|
|
441
|
+
endpoint.fallback_authentications = None
|
|
442
|
+
|
|
443
|
+
if not has_any_token_exchange:
|
|
444
|
+
raise InvalidServiceRegistryError(
|
|
445
|
+
"Platform credentials (--platform-credentials) requested, but no endpoints "
|
|
446
|
+
"support token exchange authentication. Either remove --platform-credentials "
|
|
447
|
+
"or ensure the service registry includes token exchange authentication methods."
|
|
448
|
+
)
|
|
449
|
+
|
|
407
450
|
|
|
408
451
|
@service.registered()
|
|
409
452
|
class InMemoryContextManager(BaseContextManager):
|
dnastack/context/models.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import Dict, List
|
|
1
|
+
from typing import Dict, List, Optional
|
|
2
2
|
from uuid import uuid4
|
|
3
3
|
|
|
4
4
|
from pydantic import BaseModel, Field
|
|
@@ -16,3 +16,6 @@ class Context(BaseModel):
|
|
|
16
16
|
defaults: Dict[str, str] = Field(default_factory=lambda: dict())
|
|
17
17
|
|
|
18
18
|
endpoints: List[Endpoint] = Field(default_factory=lambda: list())
|
|
19
|
+
|
|
20
|
+
# Store subject token for token exchange authentication if provided
|
|
21
|
+
platform_subject_token: Optional[str] = Field(default=None)
|
|
@@ -147,7 +147,7 @@ class Authenticator(AuthBase, ABC):
|
|
|
147
147
|
logger.debug('initialize: Restoring the session...')
|
|
148
148
|
info = self.restore_session()
|
|
149
149
|
if not info:
|
|
150
|
-
logger.debug('initialize: Session is
|
|
150
|
+
logger.debug('initialize: Session is UNAVAILABLE.')
|
|
151
151
|
raise AuthenticationRequired('Session is not available')
|
|
152
152
|
elif not info.is_valid():
|
|
153
153
|
logger.debug(f'initialize: Session is INVALID ({"expired" if info.access_token else "token revoked"}).')
|
|
@@ -17,7 +17,8 @@ from dnastack.http.authenticators.abstract import Authenticator, AuthenticationR
|
|
|
17
17
|
AuthStateStatus
|
|
18
18
|
from dnastack.http.authenticators.constants import get_authenticator_log_level
|
|
19
19
|
from dnastack.http.authenticators.oauth2_adapter.factory import OAuth2AdapterFactory
|
|
20
|
-
from dnastack.http.authenticators.oauth2_adapter.
|
|
20
|
+
from dnastack.http.authenticators.oauth2_adapter.token_exchange import TokenExchangeAdapter
|
|
21
|
+
from dnastack.http.authenticators.oauth2_adapter.models import OAuth2Authentication, GRANT_TYPE_TOKEN_EXCHANGE
|
|
21
22
|
from dnastack.http.client_factory import HttpClientFactory
|
|
22
23
|
from dnastack.http.session_info import SessionInfo, SessionManager, SessionInfoHandler
|
|
23
24
|
|
|
@@ -26,9 +27,18 @@ class OAuth2MisconfigurationError(RuntimeError):
|
|
|
26
27
|
pass
|
|
27
28
|
|
|
28
29
|
|
|
30
|
+
def _is_token_exchange_session(session_info: SessionInfo) -> bool:
|
|
31
|
+
"""Check if this session was created via token exchange"""
|
|
32
|
+
if not session_info.handler or not session_info.handler.auth_info:
|
|
33
|
+
return False
|
|
34
|
+
|
|
35
|
+
grant_type = session_info.handler.auth_info.get('grant_type')
|
|
36
|
+
return grant_type == GRANT_TYPE_TOKEN_EXCHANGE
|
|
37
|
+
|
|
38
|
+
|
|
29
39
|
class OAuth2Authenticator(Authenticator):
|
|
30
40
|
def __init__(self,
|
|
31
|
-
endpoint: ServiceEndpoint,
|
|
41
|
+
endpoint: Optional[ServiceEndpoint],
|
|
32
42
|
auth_info: Dict[str, Any],
|
|
33
43
|
session_manager: Optional[SessionManager] = None,
|
|
34
44
|
adapter_factory: Optional[OAuth2AdapterFactory] = None,
|
|
@@ -105,6 +115,12 @@ class OAuth2Authenticator(Authenticator):
|
|
|
105
115
|
self.events.dispatch('authentication-before', event_details)
|
|
106
116
|
|
|
107
117
|
auth_info = OAuth2Authentication(**self._auth_info)
|
|
118
|
+
|
|
119
|
+
use_platform_credentials = self._auth_info.get('platform_credentials', False) is True
|
|
120
|
+
|
|
121
|
+
if auth_info.grant_type == GRANT_TYPE_TOKEN_EXCHANGE or use_platform_credentials:
|
|
122
|
+
return self._authenticate_token_exchange(auth_info, trace_context)
|
|
123
|
+
|
|
108
124
|
adapter = self._adapter_factory.get_from(auth_info)
|
|
109
125
|
|
|
110
126
|
if adapter:
|
|
@@ -131,7 +147,7 @@ class OAuth2Authenticator(Authenticator):
|
|
|
131
147
|
return self._session_info
|
|
132
148
|
|
|
133
149
|
def refresh(self, trace_context: Optional[Span] = None) -> SessionInfo:
|
|
134
|
-
""" Refresh the session using a refresh token. """
|
|
150
|
+
""" Refresh the session using a refresh token or re-authenticate for token exchange. """
|
|
135
151
|
trace_context = trace_context or Span(origin='OAuth2Authenticator.refresh')
|
|
136
152
|
logger = trace_context.create_span_logger(self._logger)
|
|
137
153
|
|
|
@@ -158,6 +174,15 @@ class OAuth2Authenticator(Authenticator):
|
|
|
158
174
|
raise ReauthenticationRequired(f'The stored session information does not provide enough information to '
|
|
159
175
|
f'refresh token. (given: {session_info})')
|
|
160
176
|
|
|
177
|
+
if _is_token_exchange_session(session_info):
|
|
178
|
+
try:
|
|
179
|
+
return self._reauthenticate_token_exchange(session_info, trace_context)
|
|
180
|
+
except Exception as e:
|
|
181
|
+
logger.error(f'refresh: Token exchange re-authentication failed: {e}')
|
|
182
|
+
event_details['reason'] = f'Token exchange re-authentication failed: {e}'
|
|
183
|
+
self.events.dispatch('refresh-failure', event_details)
|
|
184
|
+
raise ReauthenticationRequired(f'Token exchange re-authentication failed: {e}')
|
|
185
|
+
|
|
161
186
|
if not session_info.refresh_token:
|
|
162
187
|
logger.debug('refresh: Cannot refresh the tokens as the refresh token is not provided.')
|
|
163
188
|
|
|
@@ -209,7 +234,7 @@ class OAuth2Authenticator(Authenticator):
|
|
|
209
234
|
|
|
210
235
|
if updated_session_info.scope:
|
|
211
236
|
session_info.scope = updated_session_info.scope
|
|
212
|
-
|
|
237
|
+
|
|
213
238
|
if updated_session_info.refresh_token:
|
|
214
239
|
# NOTE: The refresh token may not be available in the response.
|
|
215
240
|
session_info.refresh_token = updated_session_info.refresh_token
|
|
@@ -276,9 +301,7 @@ class OAuth2Authenticator(Authenticator):
|
|
|
276
301
|
# Clear the local cache
|
|
277
302
|
self._session_info = None
|
|
278
303
|
self._session_manager.delete(session_id)
|
|
279
|
-
|
|
280
304
|
self._logger.debug(f'Revoked Session {session_id}')
|
|
281
|
-
|
|
282
305
|
self.events.dispatch('session-revoked', dict(session_id=session_id))
|
|
283
306
|
|
|
284
307
|
def clear_access_token(self):
|
|
@@ -296,13 +319,11 @@ class OAuth2Authenticator(Authenticator):
|
|
|
296
319
|
|
|
297
320
|
def restore_session(self) -> Optional[SessionInfo]:
|
|
298
321
|
logger = self._logger
|
|
299
|
-
|
|
300
322
|
session_id = self.session_id
|
|
301
|
-
event_details = dict(cached=self._session_info is not None,
|
|
302
|
-
cache_hash=session_id)
|
|
303
|
-
|
|
304
|
-
session: SessionInfo = self._session_info
|
|
323
|
+
event_details = dict(cached=self._session_info is not None, cache_hash=session_id)
|
|
305
324
|
|
|
325
|
+
# Try to get session (in-memory, then stored)
|
|
326
|
+
session = self._session_info
|
|
306
327
|
if session:
|
|
307
328
|
logger.debug(f'In-memory Session Info: {session}')
|
|
308
329
|
else:
|
|
@@ -312,46 +333,37 @@ class OAuth2Authenticator(Authenticator):
|
|
|
312
333
|
if not session:
|
|
313
334
|
event_details['reason'] = 'No session available'
|
|
314
335
|
self.events.dispatch('session-not-restored', event_details)
|
|
315
|
-
|
|
316
336
|
logger.debug(f'Require RE-AUTH -- event details = {event_details}')
|
|
317
|
-
|
|
318
337
|
raise AuthenticationRequired('No session available')
|
|
319
|
-
elif session.is_valid():
|
|
320
|
-
logger.debug('The session is valid.')
|
|
321
|
-
|
|
322
|
-
current_auth_info = OAuth2Authentication(**self._auth_info)
|
|
323
|
-
current_config_hash = current_auth_info.get_content_hash()
|
|
324
|
-
stored_config_hash = session.config_hash
|
|
325
338
|
|
|
326
|
-
|
|
327
|
-
return session
|
|
328
|
-
else:
|
|
329
|
-
event_details['reason'] = 'Authentication information has changed and the session is invalidated.'
|
|
330
|
-
self.events.dispatch('session-not-restored', event_details)
|
|
331
|
-
|
|
332
|
-
logger.debug(f'Require RE-AUTH -- event details = {event_details}')
|
|
333
|
-
|
|
334
|
-
raise ReauthenticationRequiredDueToConfigChange(
|
|
335
|
-
'The session is invalidated as the endpoint configuration has changed.'
|
|
336
|
-
)
|
|
337
|
-
else:
|
|
339
|
+
if not session.is_valid():
|
|
338
340
|
logger.debug(f'The session is INVALID ({"expired" if session.access_token else "token revoked"}).')
|
|
339
341
|
|
|
340
342
|
if session.refresh_token:
|
|
341
343
|
event_details['reason'] = 'The session is invalid but it can be refreshed.'
|
|
342
344
|
self.events.dispatch('session-not-restored', event_details)
|
|
343
|
-
|
|
344
345
|
logger.debug(f'Require REFRESH -- event details = {event_details}')
|
|
345
|
-
|
|
346
346
|
raise RefreshRequired(session)
|
|
347
347
|
else:
|
|
348
348
|
event_details['reason'] = 'The session is invalid. Require re-authentication.'
|
|
349
349
|
self.events.dispatch('session-not-restored', event_details)
|
|
350
|
-
|
|
351
350
|
logger.debug(f'Require RE-AUTH -- event details = {event_details}')
|
|
352
|
-
|
|
353
351
|
raise ReauthenticationRequired('The session is invalid and refreshing tokens is not possible.')
|
|
354
352
|
|
|
353
|
+
current_auth_info = OAuth2Authentication(**self._auth_info)
|
|
354
|
+
current_config_hash = current_auth_info.get_content_hash()
|
|
355
|
+
stored_config_hash = session.config_hash
|
|
356
|
+
|
|
357
|
+
if current_config_hash == stored_config_hash:
|
|
358
|
+
return session
|
|
359
|
+
else:
|
|
360
|
+
event_details['reason'] = 'Authentication information has changed and the session is invalidated.'
|
|
361
|
+
self.events.dispatch('session-not-restored', event_details)
|
|
362
|
+
logger.debug(f'Require RE-AUTH -- event details = {event_details}')
|
|
363
|
+
raise ReauthenticationRequiredDueToConfigChange(
|
|
364
|
+
'The session is invalidated as the endpoint configuration has changed.'
|
|
365
|
+
)
|
|
366
|
+
|
|
355
367
|
def _convert_token_response_to_session(self,
|
|
356
368
|
authentication: Dict[str, Any],
|
|
357
369
|
response: Dict[str, Any]):
|
|
@@ -359,8 +371,8 @@ class OAuth2Authenticator(Authenticator):
|
|
|
359
371
|
|
|
360
372
|
created_time = time()
|
|
361
373
|
expiry_time = created_time + response['expires_in']
|
|
362
|
-
|
|
363
374
|
current_auth_info = OAuth2Authentication(**self._auth_info)
|
|
375
|
+
self._logger.debug(f'Creating session: created_time={created_time}, expires_in={response["expires_in"]}, expiry_time={expiry_time}')
|
|
364
376
|
|
|
365
377
|
return SessionInfo(
|
|
366
378
|
model_version=4,
|
|
@@ -369,8 +381,8 @@ class OAuth2Authenticator(Authenticator):
|
|
|
369
381
|
refresh_token=response.get('refresh_token'),
|
|
370
382
|
scope=response.get('scope'),
|
|
371
383
|
token_type=response['token_type'],
|
|
372
|
-
issued_at=created_time,
|
|
373
|
-
valid_until=expiry_time,
|
|
384
|
+
issued_at=int(created_time),
|
|
385
|
+
valid_until=int(expiry_time),
|
|
374
386
|
handler=SessionInfoHandler(auth_info=authentication)
|
|
375
387
|
)
|
|
376
388
|
|
|
@@ -385,3 +397,33 @@ class OAuth2Authenticator(Authenticator):
|
|
|
385
397
|
@classmethod
|
|
386
398
|
def make(cls, endpoint: ServiceEndpoint, auth_info: Dict[str, Any]):
|
|
387
399
|
return cls(endpoint, auth_info)
|
|
400
|
+
|
|
401
|
+
def _reauthenticate_token_exchange(self, session_info: SessionInfo, trace_context: Span) -> SessionInfo:
|
|
402
|
+
"""
|
|
403
|
+
Re-authenticate a token exchange session by performing the exchange again.
|
|
404
|
+
Assumes we're in a cloud environment and attempts to fetch new identity token.
|
|
405
|
+
"""
|
|
406
|
+
if not session_info.handler or not session_info.handler.auth_info:
|
|
407
|
+
raise ReauthenticationRequired('Cannot re-authenticate: missing authentication configuration')
|
|
408
|
+
auth_info_dict = session_info.handler.auth_info.copy()
|
|
409
|
+
auth_info_dict['subject_token'] = None # Force cloud metadata fetch
|
|
410
|
+
|
|
411
|
+
auth_info = OAuth2Authentication(**auth_info_dict)
|
|
412
|
+
return self._authenticate_token_exchange(auth_info, trace_context)
|
|
413
|
+
|
|
414
|
+
def _authenticate_token_exchange(self, auth_info: OAuth2Authentication, trace_context: Span) -> SessionInfo:
|
|
415
|
+
"""Handle token exchange authentication flow"""
|
|
416
|
+
session_id = self.session_id
|
|
417
|
+
event_details = dict(session_id=session_id, auth_info=self._auth_info)
|
|
418
|
+
adapter = TokenExchangeAdapter(auth_info)
|
|
419
|
+
for auth_event_type in ['blocking-response-required', 'blocking-response-ok', 'blocking-response-failed']:
|
|
420
|
+
self.events.relay_from(adapter.events, auth_event_type)
|
|
421
|
+
|
|
422
|
+
token_response = adapter.exchange_tokens(trace_context)
|
|
423
|
+
self._session_info = self._convert_token_response_to_session(auth_info.dict(), token_response)
|
|
424
|
+
self._session_manager.save(session_id, self._session_info)
|
|
425
|
+
|
|
426
|
+
event_details['session_info'] = self._session_info
|
|
427
|
+
self.events.dispatch('authentication-ok', event_details)
|
|
428
|
+
|
|
429
|
+
return self._session_info
|
|
@@ -2,12 +2,19 @@ from typing import Dict, Any, List
|
|
|
2
2
|
|
|
3
3
|
from dnastack.common.tracing import Span
|
|
4
4
|
from dnastack.http.authenticators.oauth2_adapter.abstract import OAuth2Adapter, AuthException
|
|
5
|
+
from dnastack.http.authenticators.oauth2_adapter.models import OAuth2Authentication
|
|
5
6
|
from dnastack.http.client_factory import HttpClientFactory
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
class ClientCredentialAdapter(OAuth2Adapter):
|
|
9
10
|
__grant_type = 'client_credentials'
|
|
10
11
|
|
|
12
|
+
@classmethod
|
|
13
|
+
def is_compatible_with(cls, auth_info: OAuth2Authentication) -> bool:
|
|
14
|
+
if auth_info.grant_type != cls.__grant_type:
|
|
15
|
+
return False
|
|
16
|
+
return super().is_compatible_with(auth_info)
|
|
17
|
+
|
|
11
18
|
@staticmethod
|
|
12
19
|
def get_expected_auth_info_fields() -> List[str]:
|
|
13
20
|
return [
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Optional
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from pydantic import BaseModel, Field
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
from dnastack.common.tracing import Span
|
|
8
|
+
from dnastack.http.client_factory import HttpClientFactory
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CloudProvider(str, Enum):
|
|
12
|
+
GCP = "gcp"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CloudMetadataProvider(ABC):
|
|
16
|
+
"""Abstract base class for cloud metadata providers."""
|
|
17
|
+
|
|
18
|
+
timeout: int
|
|
19
|
+
_logger: logging.Logger
|
|
20
|
+
|
|
21
|
+
def __init__(self, timeout: int = 5):
|
|
22
|
+
self.timeout = timeout
|
|
23
|
+
self._logger = logging.getLogger(type(self).__name__)
|
|
24
|
+
|
|
25
|
+
@abstractmethod
|
|
26
|
+
def is_available(self) -> bool:
|
|
27
|
+
"""Check if this cloud provider's metadata service is available."""
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
@abstractmethod
|
|
31
|
+
def get_identity_token(self, audience: str, trace_context: Span) -> Optional[str]:
|
|
32
|
+
"""Fetch an identity token from the cloud metadata service."""
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
@abstractmethod
|
|
37
|
+
def name(self) -> str:
|
|
38
|
+
"""Return the name of this cloud provider."""
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class GCPMetadataProvider(CloudMetadataProvider):
|
|
43
|
+
"""Google Cloud Platform metadata provider."""
|
|
44
|
+
|
|
45
|
+
_METADATA_BASE_URL = 'http://metadata.google.internal/computeMetadata/v1'
|
|
46
|
+
_IDENTITY_ENDPOINT = '/instance/service-accounts/default/identity'
|
|
47
|
+
_METADATA_FLAVOR = 'Google'
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def name(self) -> str:
|
|
51
|
+
return CloudProvider.GCP.value
|
|
52
|
+
|
|
53
|
+
def is_available(self) -> bool:
|
|
54
|
+
"""Check if GCP metadata service is available."""
|
|
55
|
+
try:
|
|
56
|
+
with HttpClientFactory.make() as http_session:
|
|
57
|
+
response = http_session.get(
|
|
58
|
+
f'{self._METADATA_BASE_URL}/project/project-id',
|
|
59
|
+
headers={'Metadata-Flavor': self._METADATA_FLAVOR},
|
|
60
|
+
timeout=1
|
|
61
|
+
)
|
|
62
|
+
return response.ok
|
|
63
|
+
except Exception:
|
|
64
|
+
return False
|
|
65
|
+
|
|
66
|
+
def get_identity_token(self, audience: str, trace_context: Span) -> Optional[str]:
|
|
67
|
+
"""Fetch GCP identity token from metadata service.
|
|
68
|
+
'&format=full' ensures we get email in response"""
|
|
69
|
+
url = f'{self._METADATA_BASE_URL}{self._IDENTITY_ENDPOINT}?audience={audience}&format=full'
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
with HttpClientFactory.make() as http_session:
|
|
73
|
+
response = http_session.get(
|
|
74
|
+
url,
|
|
75
|
+
headers={'Metadata-Flavor': self._METADATA_FLAVOR},
|
|
76
|
+
timeout=self.timeout
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
if response.ok:
|
|
80
|
+
token = response.text.strip()
|
|
81
|
+
self._logger.debug(f'Successfully fetched GCP identity token for audience: {audience}')
|
|
82
|
+
return token
|
|
83
|
+
else:
|
|
84
|
+
self._logger.warning(f'GCP metadata service returned {response.status_code}: {response.text}')
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
except Exception as e:
|
|
88
|
+
self._logger.warning(f'Failed to fetch GCP identity token: {e}')
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class CloudMetadataConfig(BaseModel):
|
|
93
|
+
"""Configuration model for cloud metadata provider."""
|
|
94
|
+
timeout: int = Field(5, ge=1, le=30, description="Timeout for metadata service request (1-30 seconds).")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class CloudProviderFactory:
|
|
98
|
+
"""Factory for creating cloud metadata providers."""
|
|
99
|
+
_providers = {
|
|
100
|
+
CloudProvider.GCP: GCPMetadataProvider,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
@classmethod
|
|
104
|
+
def create(cls, provider: CloudProvider, config: CloudMetadataConfig) -> CloudMetadataProvider:
|
|
105
|
+
"""Create a cloud metadata provider instance."""
|
|
106
|
+
provider_class = cls._providers.get(provider)
|
|
107
|
+
if not provider_class:
|
|
108
|
+
raise ValueError(f'Unsupported cloud provider: {provider}')
|
|
109
|
+
return provider_class(timeout=config.timeout)
|
|
110
|
+
|
|
111
|
+
@classmethod
|
|
112
|
+
def detect_provider(cls, config: CloudMetadataConfig) -> Optional[CloudMetadataProvider]:
|
|
113
|
+
"""Auto-detect the current cloud provider by checking all available providers."""
|
|
114
|
+
for provider_type in cls._providers.keys():
|
|
115
|
+
try:
|
|
116
|
+
provider = cls.create(provider_type, config)
|
|
117
|
+
if provider.is_available():
|
|
118
|
+
return provider
|
|
119
|
+
except Exception:
|
|
120
|
+
# Skip providers that fail to initialize or check availability
|
|
121
|
+
continue
|
|
122
|
+
return None
|
|
@@ -18,6 +18,12 @@ class DeviceCodeFlowAdapter(OAuth2Adapter):
|
|
|
18
18
|
super(DeviceCodeFlowAdapter, self).__init__(auth_info)
|
|
19
19
|
self.__console: Console = container.get(Console)
|
|
20
20
|
|
|
21
|
+
@classmethod
|
|
22
|
+
def is_compatible_with(cls, auth_info: OAuth2Authentication) -> bool:
|
|
23
|
+
if auth_info.grant_type != cls.__grant_type:
|
|
24
|
+
return False
|
|
25
|
+
return super().is_compatible_with(auth_info)
|
|
26
|
+
|
|
21
27
|
@staticmethod
|
|
22
28
|
def get_expected_auth_info_fields() -> List[str]:
|
|
23
29
|
return [
|
|
@@ -5,6 +5,7 @@ from imagination.decorator import service
|
|
|
5
5
|
from dnastack.http.authenticators.oauth2_adapter.abstract import OAuth2Adapter
|
|
6
6
|
from dnastack.http.authenticators.oauth2_adapter.client_credential import ClientCredentialAdapter
|
|
7
7
|
from dnastack.http.authenticators.oauth2_adapter.device_code_flow import DeviceCodeFlowAdapter
|
|
8
|
+
from dnastack.http.authenticators.oauth2_adapter.token_exchange import TokenExchangeAdapter
|
|
8
9
|
from dnastack.http.authenticators.oauth2_adapter.models import OAuth2Authentication
|
|
9
10
|
|
|
10
11
|
|
|
@@ -14,6 +15,7 @@ class OAuth2AdapterFactory:
|
|
|
14
15
|
__supported_auth_adapter_classes = [
|
|
15
16
|
DeviceCodeFlowAdapter,
|
|
16
17
|
ClientCredentialAdapter,
|
|
18
|
+
TokenExchangeAdapter,
|
|
17
19
|
]
|
|
18
20
|
|
|
19
21
|
def get_from(self, auth_info: OAuth2Authentication) -> Optional[OAuth2Adapter]:
|
|
@@ -5,6 +5,9 @@ from pydantic import BaseModel
|
|
|
5
5
|
from dnastack.common.model_mixin import JsonModelMixin as HashableModel
|
|
6
6
|
|
|
7
7
|
|
|
8
|
+
GRANT_TYPE_TOKEN_EXCHANGE = 'urn:ietf:params:oauth:grant-type:token-exchange'
|
|
9
|
+
|
|
10
|
+
|
|
8
11
|
class OAuth2Authentication(BaseModel, HashableModel):
|
|
9
12
|
"""OAuth2 Authentication Information"""
|
|
10
13
|
authorization_endpoint: Optional[str]
|
|
@@ -19,4 +22,9 @@ class OAuth2Authentication(BaseModel, HashableModel):
|
|
|
19
22
|
resource_url: str
|
|
20
23
|
scope: Optional[str]
|
|
21
24
|
token_endpoint: Optional[str]
|
|
22
|
-
type: str = 'oauth2'
|
|
25
|
+
type: str = 'oauth2'
|
|
26
|
+
subject_token: Optional[str]
|
|
27
|
+
subject_token_type: Optional[str]
|
|
28
|
+
requested_token_type: Optional[str]
|
|
29
|
+
audience: Optional[str]
|
|
30
|
+
cloud_provider: Optional[str] # Currently supported: 'gcp'
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
from typing import Dict, Any, List, Optional
|
|
2
|
+
|
|
3
|
+
from imagination import container
|
|
4
|
+
|
|
5
|
+
from dnastack.common.tracing import Span
|
|
6
|
+
from dnastack.http.authenticators.oauth2_adapter.abstract import OAuth2Adapter, AuthException
|
|
7
|
+
from dnastack.http.authenticators.oauth2_adapter.models import OAuth2Authentication, GRANT_TYPE_TOKEN_EXCHANGE
|
|
8
|
+
from dnastack.http.authenticators.oauth2_adapter.cloud_providers import (
|
|
9
|
+
CloudProviderFactory, CloudMetadataProvider, CloudMetadataConfig
|
|
10
|
+
)
|
|
11
|
+
from dnastack.http.client_factory import HttpClientFactory
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TokenExchangeAdapter(OAuth2Adapter):
|
|
15
|
+
__grant_type = GRANT_TYPE_TOKEN_EXCHANGE
|
|
16
|
+
__subject_token_type = 'urn:ietf:params:oauth:token-type:jwt'
|
|
17
|
+
__METADATA_TIMEOUT = 10
|
|
18
|
+
|
|
19
|
+
def __init__(self, auth_info: OAuth2Authentication):
|
|
20
|
+
super().__init__(auth_info)
|
|
21
|
+
self._cloud_provider: Optional[CloudMetadataProvider] = None
|
|
22
|
+
|
|
23
|
+
@classmethod
|
|
24
|
+
def is_compatible_with(cls, auth_info: OAuth2Authentication) -> bool:
|
|
25
|
+
if auth_info.grant_type != cls.__grant_type:
|
|
26
|
+
return False
|
|
27
|
+
required_fields = ['token_endpoint', 'resource_url']
|
|
28
|
+
return all(getattr(auth_info, field, None) for field in required_fields)
|
|
29
|
+
|
|
30
|
+
@staticmethod
|
|
31
|
+
def get_expected_auth_info_fields() -> List[str]:
|
|
32
|
+
return [
|
|
33
|
+
'grant_type',
|
|
34
|
+
'resource_url',
|
|
35
|
+
'token_endpoint',
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
def _get_subject_token(self, trace_context: Span) -> str:
|
|
39
|
+
"""
|
|
40
|
+
Get ID token from cloud metadata service or use provided token.
|
|
41
|
+
For re-authentication, always tries cloud metadata fetch.
|
|
42
|
+
"""
|
|
43
|
+
if self._auth_info.subject_token:
|
|
44
|
+
return self._auth_info.subject_token
|
|
45
|
+
|
|
46
|
+
context_subject_token = self._get_and_clear_context_subject_token()
|
|
47
|
+
if context_subject_token:
|
|
48
|
+
return context_subject_token
|
|
49
|
+
|
|
50
|
+
audience = self._auth_info.audience or self._auth_info.resource_url
|
|
51
|
+
token = self._fetch_cloud_identity_token(audience, trace_context)
|
|
52
|
+
if token:
|
|
53
|
+
return token
|
|
54
|
+
|
|
55
|
+
raise AuthException(
|
|
56
|
+
'No subject token provided and unable to fetch from cloud. '
|
|
57
|
+
'Please provide a subject token or run from a supported cloud environment.'
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
def _fetch_cloud_identity_token(self, audience: str, trace_context: Span) -> Optional[str]:
|
|
61
|
+
"""Fetch identity token from cloud metadata service."""
|
|
62
|
+
logger = trace_context.create_span_logger(self._logger)
|
|
63
|
+
|
|
64
|
+
if self._cloud_provider:
|
|
65
|
+
logger.debug(f'Attempting to fetch identity token from {self._cloud_provider.name}')
|
|
66
|
+
token = self._cloud_provider.get_identity_token(audience, trace_context)
|
|
67
|
+
if token:
|
|
68
|
+
return token
|
|
69
|
+
logger.error(f'Failed to fetch token from configured provider: {self._cloud_provider.name}')
|
|
70
|
+
|
|
71
|
+
logger.info('Auto-detecting cloud provider...')
|
|
72
|
+
config = CloudMetadataConfig(timeout=self.__METADATA_TIMEOUT)
|
|
73
|
+
detected_provider = CloudProviderFactory.detect_provider(config)
|
|
74
|
+
if detected_provider:
|
|
75
|
+
logger.info(f'Detected cloud provider: {detected_provider.name}')
|
|
76
|
+
self._cloud_provider = detected_provider
|
|
77
|
+
token = detected_provider.get_identity_token(audience, trace_context)
|
|
78
|
+
if token:
|
|
79
|
+
return token
|
|
80
|
+
logger.error(f'Failed to fetch token from detected provider: {detected_provider.name}')
|
|
81
|
+
else:
|
|
82
|
+
logger.error('No cloud provider detected')
|
|
83
|
+
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
def _get_and_clear_context_subject_token(self) -> Optional[str]:
|
|
87
|
+
"""Get subject token from current context if available and clear it after use"""
|
|
88
|
+
from dnastack.context.manager import ContextManager
|
|
89
|
+
context_manager = container.get(ContextManager)
|
|
90
|
+
current_context = context_manager.contexts.current_context
|
|
91
|
+
if current_context and current_context.platform_subject_token:
|
|
92
|
+
token = current_context.platform_subject_token
|
|
93
|
+
current_context.platform_subject_token = None
|
|
94
|
+
context_manager.contexts.set(context_manager.contexts.current_context_name, current_context)
|
|
95
|
+
return token
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
def exchange_tokens(self, trace_context: Span) -> Dict[str, Any]:
|
|
99
|
+
logger = trace_context.create_span_logger(self._logger)
|
|
100
|
+
auth_info = self._auth_info
|
|
101
|
+
resource_urls = self._prepare_resource_urls_for_request(auth_info.resource_url)
|
|
102
|
+
subject_token = self._get_subject_token(trace_context)
|
|
103
|
+
client_id = auth_info.client_id
|
|
104
|
+
client_secret = auth_info.client_secret
|
|
105
|
+
|
|
106
|
+
trace_info = dict(
|
|
107
|
+
oauth='token-exchange',
|
|
108
|
+
token_url=auth_info.token_endpoint,
|
|
109
|
+
client_id=client_id,
|
|
110
|
+
grant_type=self.__grant_type,
|
|
111
|
+
resource_urls=resource_urls,
|
|
112
|
+
subject_token_type=self.__subject_token_type,
|
|
113
|
+
cloud_provider=self._cloud_provider.name if self._cloud_provider else 'none',
|
|
114
|
+
)
|
|
115
|
+
logger.debug(f'exchange_token: Authenticating with {trace_info}')
|
|
116
|
+
auth_params = {
|
|
117
|
+
'grant_type': self.__grant_type,
|
|
118
|
+
'subject_token_type': self.__subject_token_type,
|
|
119
|
+
'subject_token': subject_token,
|
|
120
|
+
'resource': resource_urls,
|
|
121
|
+
**({'requested_token_type': self._auth_info.requested_token_type} if self._auth_info.requested_token_type else {}),
|
|
122
|
+
**({'scope': auth_info.scope} if auth_info.scope else {})
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
with trace_context.new_span(metadata=trace_info) as sub_span:
|
|
126
|
+
with HttpClientFactory.make() as http_session:
|
|
127
|
+
span_headers = sub_span.create_http_headers()
|
|
128
|
+
response = http_session.post(
|
|
129
|
+
auth_info.token_endpoint,
|
|
130
|
+
data=auth_params,
|
|
131
|
+
headers=span_headers,
|
|
132
|
+
auth=(client_id, client_secret)
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
if not response.ok:
|
|
136
|
+
raise AuthException(
|
|
137
|
+
f'Failed to perform token exchange for {client_id} as the server responds with HTTP {response.status_code}:'
|
|
138
|
+
f'\n\n{response.text}\n',
|
|
139
|
+
resource_urls
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
return response.json()
|
dnastack/http/session_info.py
CHANGED
|
@@ -97,6 +97,7 @@ class BaseSessionStorage(ABC):
|
|
|
97
97
|
def __delitem__(self, id: str):
|
|
98
98
|
raise NotImplementedError()
|
|
99
99
|
|
|
100
|
+
|
|
100
101
|
def __str__(self):
|
|
101
102
|
return f'{type(self).__module__}.{type(self).__name__}'
|
|
102
103
|
|
|
@@ -125,6 +126,7 @@ class InMemorySessionStorage(BaseSessionStorage):
|
|
|
125
126
|
del self.__cache_map[id]
|
|
126
127
|
|
|
127
128
|
|
|
129
|
+
|
|
128
130
|
@service.registered(
|
|
129
131
|
params=[
|
|
130
132
|
EnvironmentVariable('DNASTACK_SESSION_DIR',
|
|
@@ -174,6 +176,7 @@ class FileSessionStorage(BaseSessionStorage):
|
|
|
174
176
|
final_file_path = self.__get_file_path(id)
|
|
175
177
|
os.unlink(final_file_path)
|
|
176
178
|
|
|
179
|
+
|
|
177
180
|
def __get_file_path(self, id: str) -> str:
|
|
178
181
|
path_blocks = []
|
|
179
182
|
|
|
@@ -262,6 +265,7 @@ class SessionManager:
|
|
|
262
265
|
finally:
|
|
263
266
|
del self.__change_locks[id]
|
|
264
267
|
|
|
268
|
+
|
|
265
269
|
def __lock(self, id) -> Lock:
|
|
266
270
|
if id not in self.__change_locks:
|
|
267
271
|
self.__change_locks[id] = Lock()
|
{dnastack_client_library-3.1.144.dist-info → dnastack_client_library-3.1.145.dist-info}/RECORD
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
dnastack/__init__.py,sha256=mslf7se8vBSK_HkqWTGPdibeVhT4xyKXgzQBV7dEK1M,333
|
|
2
|
-
dnastack/__main__.py,sha256=
|
|
3
|
-
dnastack/constants.py,sha256=
|
|
2
|
+
dnastack/__main__.py,sha256=EKmtIs4TBseQJi-OT_U6LqRyKLiyrGTBuTQg9zE-G2I,4376
|
|
3
|
+
dnastack/constants.py,sha256=9HWP6mq4zFzR5KcChCWx0_tG3OvN3GT_mjqi3CwAck4,114
|
|
4
4
|
dnastack/feature_flags.py,sha256=RK_V_Ovncoe6NeTheAA_frP-kYkZC1fDlTbbup2KYG4,1419
|
|
5
5
|
dnastack/json_path.py,sha256=TyghhDf7nGQmnsUWBhenU_fKsE_Ez-HLVER6HgH5-hU,2700
|
|
6
6
|
dnastack/omics_cli.py,sha256=ZppKZTHv_XjUUZyRIzSkx0Ug5ODAYrCOTsU0ezCOVrA,3694
|
|
@@ -31,7 +31,7 @@ dnastack/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
|
31
31
|
dnastack/cli/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
32
32
|
dnastack/cli/commands/utils.py,sha256=ZwKTyyvqBTrt27cF9wF6SglE01t7NkHWLMjXegi_6iA,574
|
|
33
33
|
dnastack/cli/commands/auth/__init__.py,sha256=SGnt7w8ccS1ED6EQzBq4PaH3kqoLuXC4wrFMa7P_Vg4,313
|
|
34
|
-
dnastack/cli/commands/auth/commands.py,sha256=
|
|
34
|
+
dnastack/cli/commands/auth/commands.py,sha256=RhLvCIwjg3fRbRKSVqYqEFlSaGwp2hJqYByZlMMBkJM,6470
|
|
35
35
|
dnastack/cli/commands/auth/event_handlers.py,sha256=g6FPChlBZZr7M6hejGWT6Z3THNZr7AtbV0S5PJEtU_o,3627
|
|
36
36
|
dnastack/cli/commands/collections/__init__.py,sha256=hrsjIkOvqOnYUN4xwoSB3iTCLrbX6qG-XeafVzoGleI,535
|
|
37
37
|
dnastack/cli/commands/collections/commands.py,sha256=yawGsTZfgkTYvyCIyRaup9mEQOrr_nfE7Cj6lPcEaIk,8978
|
|
@@ -39,7 +39,7 @@ dnastack/cli/commands/collections/tables.py,sha256=31D3QuI7VQp9V7M4EeoiTzwf3lg1d
|
|
|
39
39
|
dnastack/cli/commands/collections/utils.py,sha256=o59nT42PACZiGo-dgo5Qjg8UDWgvcjmgb4fDGWpjm7w,6225
|
|
40
40
|
dnastack/cli/commands/config/__init__.py,sha256=xBBLgV5tvRr45gWgZhGHLxB12fNMF8LDqeBvMXCt_sk,706
|
|
41
41
|
dnastack/cli/commands/config/commands.py,sha256=ANyepqMCCwL2qtbrlS4OxYyGzbMQLF_P7iiKnACIsj8,813
|
|
42
|
-
dnastack/cli/commands/config/contexts.py,sha256=
|
|
42
|
+
dnastack/cli/commands/config/contexts.py,sha256=Dhl3C90D1cuO_LgO0zZIwZtfX3yB3WqhIX3Nu2Bsr78,6240
|
|
43
43
|
dnastack/cli/commands/config/endpoints.py,sha256=2R6bw7cx32iJ_zzu4C7nUsQiDFsu0tvj_PplmpgsA0E,23906
|
|
44
44
|
dnastack/cli/commands/config/registries.py,sha256=PCSA05mLuG8_4XR6Gl6yjZy6wPXT4BLhTRMIjO6Zq0I,6422
|
|
45
45
|
dnastack/cli/commands/dataconnect/__init__.py,sha256=pmSnzfQQkhupJ-ORfTNVo3ev9ASpOGk5Cv4kMZA7zQ8,521
|
|
@@ -130,7 +130,7 @@ dnastack/client/explorer/models.py,sha256=vrltbcb4qAx6z1oGXG8ufw2kZ36dDiwU5QGqOo
|
|
|
130
130
|
dnastack/client/service_registry/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
131
131
|
dnastack/client/service_registry/client.py,sha256=r7D8CnPJLbNkc03g2PYHt880Ba1oPW2d8B0ShP0p4Eo,1131
|
|
132
132
|
dnastack/client/service_registry/factory.py,sha256=MKidmvDuIdnz9jsqm6yDjImldcNajKxopwmKFFCTN4U,10751
|
|
133
|
-
dnastack/client/service_registry/helper.py,sha256=
|
|
133
|
+
dnastack/client/service_registry/helper.py,sha256=9zJLsgEN1kwEZJs9MdXQbcqRikXHf-uXHHyKcYUxBwo,1501
|
|
134
134
|
dnastack/client/service_registry/manager.py,sha256=o_Eqg1GRj8x2q-duWozVge8JRFXUEGhV5-jbkcATFw8,9311
|
|
135
135
|
dnastack/client/service_registry/models.py,sha256=X0vf1sv1f5sZ_7wZMmynjPF57ZrKHZ8FDXV5iHxv7b4,1513
|
|
136
136
|
dnastack/client/workbench/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -175,26 +175,28 @@ dnastack/configuration/wrapper.py,sha256=qLmI8bgwTTflym_e0HC85IwdoCuLfazJt_O8mJJ
|
|
|
175
175
|
dnastack/context/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
176
176
|
dnastack/context/context_wraper.py,sha256=G88LciVBW5HJkvhi3oYS6RlrH_-l0H2CSYSU8nS33_0,315
|
|
177
177
|
dnastack/context/helper.py,sha256=GOlmi2ddkstSRBo3sG6yB8_2oMFqN1o6yI8Ny-gpkbQ,1475
|
|
178
|
-
dnastack/context/manager.py,sha256=
|
|
179
|
-
dnastack/context/models.py,sha256=
|
|
178
|
+
dnastack/context/manager.py,sha256=PMIGG34alXjFN6WJTqDpSEAO8jdRagzLs6iamcDlz7g,17354
|
|
179
|
+
dnastack/context/models.py,sha256=BXSJZSwiopNZ_l2nY8uzofoYebLYwu0BLbirSBIY110,671
|
|
180
180
|
dnastack/http/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
181
181
|
dnastack/http/client_factory.py,sha256=HdRZpTEnFQcHEdVbdYl7AbCcRUv3K3kVyznIxEWIqvA,650
|
|
182
182
|
dnastack/http/session.py,sha256=R-rsE_TTswlTTlqJW7a6_O-sVMmbrnEMnKBf7m_5I8U,15058
|
|
183
|
-
dnastack/http/session_info.py,sha256=
|
|
183
|
+
dnastack/http/session_info.py,sha256=I0m8Z17MCtsWgMCmlzhtOzsXZfUddBAXp0XSOLWfgrM,9366
|
|
184
184
|
dnastack/http/authenticators/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
185
|
-
dnastack/http/authenticators/abstract.py,sha256=
|
|
185
|
+
dnastack/http/authenticators/abstract.py,sha256=b5zXqSdCasiy6Bz5LYTr7e_p7l4__Mq5shgTV6bBGnk,9241
|
|
186
186
|
dnastack/http/authenticators/constants.py,sha256=mSpBnm5lMMmMJwr13KIfCoOXXJLP4qGDkFprYXALu8o,1278
|
|
187
187
|
dnastack/http/authenticators/factory.py,sha256=PZhE7c59rDtmtUx3PcX_NzarSeRpoPCD6oexDE6Z05I,2062
|
|
188
|
-
dnastack/http/authenticators/oauth2.py,sha256=
|
|
188
|
+
dnastack/http/authenticators/oauth2.py,sha256=wPTrDzR8Jzk6ugn9yLt9HBcFae9bH6tgSRhScVQcglU,20043
|
|
189
189
|
dnastack/http/authenticators/oauth2_adapter/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
190
190
|
dnastack/http/authenticators/oauth2_adapter/abstract.py,sha256=Tm4Nnroo5_vp0UgZHhcEDVRRbhIrvVdfPr8nTyihoH4,2832
|
|
191
|
-
dnastack/http/authenticators/oauth2_adapter/client_credential.py,sha256=
|
|
192
|
-
dnastack/http/authenticators/oauth2_adapter/
|
|
193
|
-
dnastack/http/authenticators/oauth2_adapter/
|
|
194
|
-
dnastack/http/authenticators/oauth2_adapter/
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
dnastack_client_library-3.1.
|
|
198
|
-
dnastack_client_library-3.1.
|
|
199
|
-
dnastack_client_library-3.1.
|
|
200
|
-
dnastack_client_library-3.1.
|
|
191
|
+
dnastack/http/authenticators/oauth2_adapter/client_credential.py,sha256=wWHk6-l-GIxG5_MtcjLvu6sU6wugAFudaBUH33BkXPQ,2823
|
|
192
|
+
dnastack/http/authenticators/oauth2_adapter/cloud_providers.py,sha256=UHQ-YHHr5ipqSQVzCfr95Uv3zkFcop_RCpK4q6a2yJg,4317
|
|
193
|
+
dnastack/http/authenticators/oauth2_adapter/device_code_flow.py,sha256=dXI5CyUcsqYg6gf5vDC_3eY6Cc-H1C8W7FeD_24j92A,6750
|
|
194
|
+
dnastack/http/authenticators/oauth2_adapter/factory.py,sha256=ZtNXOklWEim-26ooNoPp3ji_hRg1vf4fHHnY94F0wLI,1087
|
|
195
|
+
dnastack/http/authenticators/oauth2_adapter/models.py,sha256=iY7asrSElyjubInrGV5rJKKZAxJWeq7csnaj-EqMq00,943
|
|
196
|
+
dnastack/http/authenticators/oauth2_adapter/token_exchange.py,sha256=aLSs0P5lj5LgpqUTzAn0hdbZxseVhqX2qV8KdcjmY6k,6303
|
|
197
|
+
dnastack_client_library-3.1.145.dist-info/licenses/LICENSE,sha256=uwybO-wUbQhxkosgjhJlxmYATMy-AzoULFO9FUedE34,11580
|
|
198
|
+
dnastack_client_library-3.1.145.dist-info/METADATA,sha256=10kJ53EGKrSgDaOEIBGHvQARcwfp488x6WmLMQ_ur6c,1490
|
|
199
|
+
dnastack_client_library-3.1.145.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
200
|
+
dnastack_client_library-3.1.145.dist-info/entry_points.txt,sha256=Y6OeicsiyGn3-8D-SiV4NiKlJgXfkSqK88kFBR6R1rY,89
|
|
201
|
+
dnastack_client_library-3.1.145.dist-info/top_level.txt,sha256=P2RgRyqJ7hfNy1wLVRoVLJYEppUVkCX3syGK9zBqkt8,9
|
|
202
|
+
dnastack_client_library-3.1.145.dist-info/RECORD,,
|
{dnastack_client_library-3.1.144.dist-info → dnastack_client_library-3.1.145.dist-info}/WHEEL
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|