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.
Files changed (69) hide show
  1. miso_client/__init__.py +523 -130
  2. miso_client/api/__init__.py +35 -0
  3. miso_client/api/auth_api.py +367 -0
  4. miso_client/api/logs_api.py +91 -0
  5. miso_client/api/permissions_api.py +88 -0
  6. miso_client/api/roles_api.py +88 -0
  7. miso_client/api/types/__init__.py +75 -0
  8. miso_client/api/types/auth_types.py +183 -0
  9. miso_client/api/types/logs_types.py +71 -0
  10. miso_client/api/types/permissions_types.py +31 -0
  11. miso_client/api/types/roles_types.py +31 -0
  12. miso_client/errors.py +30 -4
  13. miso_client/models/__init__.py +4 -0
  14. miso_client/models/config.py +275 -72
  15. miso_client/models/error_response.py +39 -0
  16. miso_client/models/filter.py +255 -0
  17. miso_client/models/pagination.py +44 -0
  18. miso_client/models/sort.py +25 -0
  19. miso_client/services/__init__.py +6 -5
  20. miso_client/services/auth.py +496 -87
  21. miso_client/services/cache.py +42 -41
  22. miso_client/services/encryption.py +18 -17
  23. miso_client/services/logger.py +467 -328
  24. miso_client/services/logger_chain.py +288 -0
  25. miso_client/services/permission.py +130 -67
  26. miso_client/services/redis.py +28 -23
  27. miso_client/services/role.py +145 -62
  28. miso_client/utils/__init__.py +3 -3
  29. miso_client/utils/audit_log_queue.py +222 -0
  30. miso_client/utils/auth_strategy.py +88 -0
  31. miso_client/utils/auth_utils.py +65 -0
  32. miso_client/utils/circuit_breaker.py +125 -0
  33. miso_client/utils/client_token_manager.py +244 -0
  34. miso_client/utils/config_loader.py +88 -17
  35. miso_client/utils/controller_url_resolver.py +80 -0
  36. miso_client/utils/data_masker.py +104 -33
  37. miso_client/utils/environment_token.py +126 -0
  38. miso_client/utils/error_utils.py +216 -0
  39. miso_client/utils/fastapi_endpoints.py +166 -0
  40. miso_client/utils/filter.py +364 -0
  41. miso_client/utils/filter_applier.py +143 -0
  42. miso_client/utils/filter_parser.py +110 -0
  43. miso_client/utils/flask_endpoints.py +169 -0
  44. miso_client/utils/http_client.py +494 -262
  45. miso_client/utils/http_client_logging.py +352 -0
  46. miso_client/utils/http_client_logging_helpers.py +197 -0
  47. miso_client/utils/http_client_query_helpers.py +138 -0
  48. miso_client/utils/http_error_handler.py +92 -0
  49. miso_client/utils/http_log_formatter.py +115 -0
  50. miso_client/utils/http_log_masker.py +203 -0
  51. miso_client/utils/internal_http_client.py +435 -0
  52. miso_client/utils/jwt_tools.py +125 -16
  53. miso_client/utils/logger_helpers.py +206 -0
  54. miso_client/utils/logging_helpers.py +70 -0
  55. miso_client/utils/origin_validator.py +128 -0
  56. miso_client/utils/pagination.py +275 -0
  57. miso_client/utils/request_context.py +285 -0
  58. miso_client/utils/sensitive_fields_loader.py +116 -0
  59. miso_client/utils/sort.py +116 -0
  60. miso_client/utils/token_utils.py +114 -0
  61. miso_client/utils/url_validator.py +66 -0
  62. miso_client/utils/user_token_refresh.py +245 -0
  63. miso_client-3.7.2.dist-info/METADATA +1021 -0
  64. miso_client-3.7.2.dist-info/RECORD +68 -0
  65. miso_client-0.1.0.dist-info/METADATA +0 -551
  66. miso_client-0.1.0.dist-info/RECORD +0 -23
  67. {miso_client-0.1.0.dist-info → miso_client-3.7.2.dist-info}/WHEEL +0 -0
  68. {miso_client-0.1.0.dist-info → miso_client-3.7.2.dist-info}/licenses/LICENSE +0 -0
  69. {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)