miso-client 0.1.0__py3-none-any.whl → 3.7.2__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.
- miso_client/__init__.py +523 -130
- miso_client/api/__init__.py +35 -0
- miso_client/api/auth_api.py +367 -0
- miso_client/api/logs_api.py +91 -0
- miso_client/api/permissions_api.py +88 -0
- miso_client/api/roles_api.py +88 -0
- miso_client/api/types/__init__.py +75 -0
- miso_client/api/types/auth_types.py +183 -0
- miso_client/api/types/logs_types.py +71 -0
- miso_client/api/types/permissions_types.py +31 -0
- miso_client/api/types/roles_types.py +31 -0
- miso_client/errors.py +30 -4
- miso_client/models/__init__.py +4 -0
- miso_client/models/config.py +275 -72
- miso_client/models/error_response.py +39 -0
- miso_client/models/filter.py +255 -0
- miso_client/models/pagination.py +44 -0
- miso_client/models/sort.py +25 -0
- miso_client/services/__init__.py +6 -5
- miso_client/services/auth.py +496 -87
- miso_client/services/cache.py +42 -41
- miso_client/services/encryption.py +18 -17
- miso_client/services/logger.py +467 -328
- miso_client/services/logger_chain.py +288 -0
- miso_client/services/permission.py +130 -67
- miso_client/services/redis.py +28 -23
- miso_client/services/role.py +145 -62
- miso_client/utils/__init__.py +3 -3
- miso_client/utils/audit_log_queue.py +222 -0
- miso_client/utils/auth_strategy.py +88 -0
- miso_client/utils/auth_utils.py +65 -0
- miso_client/utils/circuit_breaker.py +125 -0
- miso_client/utils/client_token_manager.py +244 -0
- miso_client/utils/config_loader.py +88 -17
- miso_client/utils/controller_url_resolver.py +80 -0
- miso_client/utils/data_masker.py +104 -33
- miso_client/utils/environment_token.py +126 -0
- miso_client/utils/error_utils.py +216 -0
- miso_client/utils/fastapi_endpoints.py +166 -0
- miso_client/utils/filter.py +364 -0
- miso_client/utils/filter_applier.py +143 -0
- miso_client/utils/filter_parser.py +110 -0
- miso_client/utils/flask_endpoints.py +169 -0
- miso_client/utils/http_client.py +494 -262
- miso_client/utils/http_client_logging.py +352 -0
- miso_client/utils/http_client_logging_helpers.py +197 -0
- miso_client/utils/http_client_query_helpers.py +138 -0
- miso_client/utils/http_error_handler.py +92 -0
- miso_client/utils/http_log_formatter.py +115 -0
- miso_client/utils/http_log_masker.py +203 -0
- miso_client/utils/internal_http_client.py +435 -0
- miso_client/utils/jwt_tools.py +125 -16
- miso_client/utils/logger_helpers.py +206 -0
- miso_client/utils/logging_helpers.py +70 -0
- miso_client/utils/origin_validator.py +128 -0
- miso_client/utils/pagination.py +275 -0
- miso_client/utils/request_context.py +285 -0
- miso_client/utils/sensitive_fields_loader.py +116 -0
- miso_client/utils/sort.py +116 -0
- miso_client/utils/token_utils.py +114 -0
- miso_client/utils/url_validator.py +66 -0
- miso_client/utils/user_token_refresh.py +245 -0
- miso_client-3.7.2.dist-info/METADATA +1021 -0
- miso_client-3.7.2.dist-info/RECORD +68 -0
- miso_client-0.1.0.dist-info/METADATA +0 -551
- miso_client-0.1.0.dist-info/RECORD +0 -23
- {miso_client-0.1.0.dist-info → miso_client-3.7.2.dist-info}/WHEEL +0 -0
- {miso_client-0.1.0.dist-info → miso_client-3.7.2.dist-info}/licenses/LICENSE +0 -0
- {miso_client-0.1.0.dist-info → miso_client-3.7.2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"""User token refresh manager for automatic token refresh."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
from datetime import datetime, timedelta
|
|
6
|
+
from typing import Any, Callable, Dict, Optional
|
|
7
|
+
|
|
8
|
+
from .jwt_tools import decode_token, extract_user_id
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class UserTokenRefreshManager:
|
|
14
|
+
"""
|
|
15
|
+
Manages user token refresh with proactive refresh and 401 retry.
|
|
16
|
+
|
|
17
|
+
Similar to client token refresh but for user Bearer tokens.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self):
|
|
21
|
+
"""Initialize user token refresh manager."""
|
|
22
|
+
# Store refresh callbacks per user: {user_id: callback}
|
|
23
|
+
self._refresh_callbacks: Dict[str, Callable[[str], Any]] = {}
|
|
24
|
+
# Store refresh tokens per user: {user_id: refresh_token}
|
|
25
|
+
self._refresh_tokens: Dict[str, str] = {}
|
|
26
|
+
# Track token expiration: {token: expiration_datetime}
|
|
27
|
+
self._token_expirations: Dict[str, datetime] = {}
|
|
28
|
+
# Locks per user to prevent concurrent refreshes: {user_id: Lock}
|
|
29
|
+
self._refresh_locks: Dict[str, asyncio.Lock] = {}
|
|
30
|
+
# Cache refreshed tokens: {old_token: new_token}
|
|
31
|
+
self._refreshed_tokens: Dict[str, str] = {}
|
|
32
|
+
# AuthService instance for refresh endpoint calls
|
|
33
|
+
self._auth_service: Optional[Any] = None
|
|
34
|
+
|
|
35
|
+
def register_refresh_callback(self, user_id: str, callback: Callable[[str], Any]) -> None:
|
|
36
|
+
"""
|
|
37
|
+
Register refresh callback for a user.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
user_id: User ID
|
|
41
|
+
callback: Async function that takes old token and returns new token
|
|
42
|
+
"""
|
|
43
|
+
self._refresh_callbacks[user_id] = callback
|
|
44
|
+
|
|
45
|
+
def register_refresh_token(self, user_id: str, refresh_token: str) -> None:
|
|
46
|
+
"""
|
|
47
|
+
Register refresh token for a user.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
user_id: User ID
|
|
51
|
+
refresh_token: Refresh token string
|
|
52
|
+
"""
|
|
53
|
+
self._refresh_tokens[user_id] = refresh_token
|
|
54
|
+
|
|
55
|
+
def set_auth_service(self, auth_service: Any) -> None:
|
|
56
|
+
"""
|
|
57
|
+
Set AuthService instance for refresh endpoint calls.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
auth_service: AuthService instance
|
|
61
|
+
"""
|
|
62
|
+
self._auth_service = auth_service
|
|
63
|
+
|
|
64
|
+
def _get_user_id(self, token: str) -> Optional[str]:
|
|
65
|
+
"""Extract user ID from token."""
|
|
66
|
+
return extract_user_id(token)
|
|
67
|
+
|
|
68
|
+
def _is_token_expired(self, token: str, buffer_seconds: int = 60) -> bool:
|
|
69
|
+
"""
|
|
70
|
+
Check if token is expired or will expire soon.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
token: JWT token string
|
|
74
|
+
buffer_seconds: Buffer time before expiration (default: 60 seconds)
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
True if token is expired or will expire within buffer time
|
|
78
|
+
"""
|
|
79
|
+
# Check cached expiration first
|
|
80
|
+
if token in self._token_expirations:
|
|
81
|
+
expires_at = self._token_expirations[token]
|
|
82
|
+
return datetime.now() + timedelta(seconds=buffer_seconds) >= expires_at
|
|
83
|
+
|
|
84
|
+
# Decode token to check expiration
|
|
85
|
+
decoded = decode_token(token)
|
|
86
|
+
if not decoded:
|
|
87
|
+
return True # Invalid token, consider expired
|
|
88
|
+
|
|
89
|
+
# Check exp claim
|
|
90
|
+
if "exp" in decoded and isinstance(decoded["exp"], (int, float)):
|
|
91
|
+
token_exp = datetime.fromtimestamp(decoded["exp"])
|
|
92
|
+
buffer_time = datetime.now() + timedelta(seconds=buffer_seconds)
|
|
93
|
+
is_expired = buffer_time >= token_exp
|
|
94
|
+
# Cache expiration for future checks
|
|
95
|
+
self._token_expirations[token] = token_exp
|
|
96
|
+
return is_expired
|
|
97
|
+
|
|
98
|
+
# No expiration claim - assume not expired
|
|
99
|
+
return False
|
|
100
|
+
|
|
101
|
+
def _get_refresh_token_from_jwt(self, token: str) -> Optional[str]:
|
|
102
|
+
"""
|
|
103
|
+
Extract refresh token from JWT claims.
|
|
104
|
+
|
|
105
|
+
Checks common refresh token claim names: refreshToken, refresh_token, rt
|
|
106
|
+
"""
|
|
107
|
+
decoded = decode_token(token)
|
|
108
|
+
if not decoded:
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
# Try common refresh token claim names
|
|
112
|
+
refresh_token = (
|
|
113
|
+
decoded.get("refreshToken") or decoded.get("refresh_token") or decoded.get("rt")
|
|
114
|
+
)
|
|
115
|
+
return str(refresh_token) if refresh_token else None
|
|
116
|
+
|
|
117
|
+
async def _refresh_token(self, token: str, user_id: Optional[str] = None) -> Optional[str]:
|
|
118
|
+
"""
|
|
119
|
+
Refresh user token using available refresh mechanism.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
token: Current user token
|
|
123
|
+
user_id: Optional user ID (extracted from token if not provided)
|
|
124
|
+
auth_service: Optional AuthService instance for refresh endpoint calls
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
New token if refresh successful, None otherwise
|
|
128
|
+
"""
|
|
129
|
+
if not user_id:
|
|
130
|
+
user_id = self._get_user_id(token)
|
|
131
|
+
if not user_id:
|
|
132
|
+
logger.warning("Cannot refresh token: user ID not found")
|
|
133
|
+
return None
|
|
134
|
+
|
|
135
|
+
# Get or create lock for this user
|
|
136
|
+
if user_id not in self._refresh_locks:
|
|
137
|
+
self._refresh_locks[user_id] = asyncio.Lock()
|
|
138
|
+
|
|
139
|
+
async with self._refresh_locks[user_id]:
|
|
140
|
+
# Check if token was already refreshed (by another concurrent request)
|
|
141
|
+
if token in self._refreshed_tokens:
|
|
142
|
+
return self._refreshed_tokens[token]
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
# Try refresh callback first
|
|
146
|
+
if user_id in self._refresh_callbacks:
|
|
147
|
+
callback = self._refresh_callbacks[user_id]
|
|
148
|
+
new_token = await callback(token)
|
|
149
|
+
if new_token:
|
|
150
|
+
token_str = str(new_token) if not isinstance(new_token, str) else new_token
|
|
151
|
+
self._refreshed_tokens[token] = token_str
|
|
152
|
+
logger.info(f"Token refreshed successfully for user {user_id} via callback")
|
|
153
|
+
return token_str
|
|
154
|
+
|
|
155
|
+
# Try stored refresh token
|
|
156
|
+
if user_id in self._refresh_tokens and self._auth_service:
|
|
157
|
+
refresh_token = self._refresh_tokens[user_id]
|
|
158
|
+
refresh_response = await self._auth_service.refresh_user_token(refresh_token)
|
|
159
|
+
if refresh_response and refresh_response.get("token"):
|
|
160
|
+
new_token = refresh_response["token"]
|
|
161
|
+
token_str = str(new_token) if not isinstance(new_token, str) else new_token
|
|
162
|
+
self._refreshed_tokens[token] = token_str
|
|
163
|
+
# Update refresh token if new one provided
|
|
164
|
+
if refresh_response.get("refreshToken"):
|
|
165
|
+
self._refresh_tokens[user_id] = refresh_response["refreshToken"]
|
|
166
|
+
logger.info(
|
|
167
|
+
f"Token refreshed successfully for user {user_id} via refresh token"
|
|
168
|
+
)
|
|
169
|
+
return token_str
|
|
170
|
+
|
|
171
|
+
# Try refresh token from JWT claims
|
|
172
|
+
jwt_refresh_token = self._get_refresh_token_from_jwt(token)
|
|
173
|
+
if jwt_refresh_token and self._auth_service:
|
|
174
|
+
refresh_response = await self._auth_service.refresh_user_token(
|
|
175
|
+
jwt_refresh_token
|
|
176
|
+
)
|
|
177
|
+
if refresh_response and refresh_response.get("token"):
|
|
178
|
+
new_token = refresh_response["token"]
|
|
179
|
+
token_str = str(new_token) if not isinstance(new_token, str) else new_token
|
|
180
|
+
self._refreshed_tokens[token] = token_str
|
|
181
|
+
# Update refresh token if new one provided
|
|
182
|
+
if refresh_response.get("refreshToken"):
|
|
183
|
+
self._refresh_tokens[user_id] = refresh_response["refreshToken"]
|
|
184
|
+
logger.info(
|
|
185
|
+
f"Token refreshed successfully for user {user_id} via JWT refresh token"
|
|
186
|
+
)
|
|
187
|
+
return token_str
|
|
188
|
+
|
|
189
|
+
logger.warning(f"No refresh mechanism available for user {user_id}")
|
|
190
|
+
return None
|
|
191
|
+
|
|
192
|
+
except Exception as error:
|
|
193
|
+
logger.error(f"Token refresh failed for user {user_id}", exc_info=error)
|
|
194
|
+
return None
|
|
195
|
+
|
|
196
|
+
async def get_valid_token(self, token: str, refresh_if_needed: bool = True) -> Optional[str]:
|
|
197
|
+
"""
|
|
198
|
+
Get valid token, refreshing if expired.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
token: Current user token
|
|
202
|
+
refresh_if_needed: Whether to refresh if token is expired
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
Valid token (original or refreshed), None if refresh failed
|
|
206
|
+
"""
|
|
207
|
+
# Check if token is expired
|
|
208
|
+
if refresh_if_needed and self._is_token_expired(token):
|
|
209
|
+
user_id = self._get_user_id(token)
|
|
210
|
+
refreshed = await self._refresh_token(token, user_id)
|
|
211
|
+
if refreshed:
|
|
212
|
+
return refreshed
|
|
213
|
+
# Refresh failed, return original token (let request fail naturally)
|
|
214
|
+
|
|
215
|
+
return token
|
|
216
|
+
|
|
217
|
+
def clear_user_tokens(self, user_id: str) -> None:
|
|
218
|
+
"""
|
|
219
|
+
Clear all tokens and refresh data for a user.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
user_id: User ID
|
|
223
|
+
"""
|
|
224
|
+
# Clear refresh callback
|
|
225
|
+
self._refresh_callbacks.pop(user_id, None)
|
|
226
|
+
# Clear refresh token
|
|
227
|
+
self._refresh_tokens.pop(user_id, None)
|
|
228
|
+
# Clear refresh lock
|
|
229
|
+
self._refresh_locks.pop(user_id, None)
|
|
230
|
+
# Clear cached refreshed tokens (find by user_id in old tokens)
|
|
231
|
+
tokens_to_remove = [
|
|
232
|
+
old_token
|
|
233
|
+
for old_token in self._refreshed_tokens.keys()
|
|
234
|
+
if self._get_user_id(old_token) == user_id
|
|
235
|
+
]
|
|
236
|
+
for old_token in tokens_to_remove:
|
|
237
|
+
self._refreshed_tokens.pop(old_token, None)
|
|
238
|
+
# Clear token expirations
|
|
239
|
+
expirations_to_remove = [
|
|
240
|
+
old_token
|
|
241
|
+
for old_token in self._token_expirations.keys()
|
|
242
|
+
if self._get_user_id(old_token) == user_id
|
|
243
|
+
]
|
|
244
|
+
for old_token in expirations_to_remove:
|
|
245
|
+
self._token_expirations.pop(old_token, None)
|