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
miso_client/__init__.py CHANGED
@@ -5,38 +5,96 @@ This package provides a reusable client SDK for integrating with the Miso Contro
5
5
  for authentication, role-based access control, permission management, and logging.
6
6
  """
7
7
 
8
- from typing import Any, Optional
8
+ import asyncio
9
+ from typing import Any, Dict, List, Literal, Optional
9
10
 
11
+ from .errors import (
12
+ AuthenticationError,
13
+ AuthorizationError,
14
+ ConfigurationError,
15
+ ConnectionError,
16
+ MisoClientError,
17
+ )
10
18
  from .models.config import (
11
- RedisConfig,
12
- MisoClientConfig,
13
- UserInfo,
14
19
  AuthResult,
20
+ AuthStrategy,
21
+ CircuitBreakerConfig,
22
+ ClientLoggingOptions,
23
+ ClientTokenEndpointOptions,
24
+ ClientTokenEndpointResponse,
25
+ ClientTokenResponse,
26
+ DataClientConfigResponse,
15
27
  LogEntry,
16
- RoleResult,
28
+ MisoClientConfig,
17
29
  PermissionResult,
18
- ClientTokenResponse,
19
- PerformanceMetrics,
20
- ClientLoggingOptions,
30
+ RedisConfig,
31
+ RoleResult,
32
+ UserInfo,
21
33
  )
34
+ from .models.error_response import ErrorResponse
35
+ from .models.filter import (
36
+ FilterBuilder,
37
+ FilterGroup,
38
+ FilterOperator,
39
+ FilterOption,
40
+ FilterQuery,
41
+ JsonFilter,
42
+ )
43
+ from .models.pagination import Meta, PaginatedListResponse
44
+ from .models.sort import SortOption
22
45
  from .services.auth import AuthService
23
- from .services.role import RoleService
46
+ from .services.cache import CacheService
47
+ from .services.encryption import EncryptionService
48
+ from .services.logger import LoggerService
49
+ from .services.logger_chain import LoggerChain
24
50
  from .services.permission import PermissionService
25
- from .services.logger import LoggerService, LoggerChain
26
51
  from .services.redis import RedisService
27
- from .services.encryption import EncryptionService
28
- from .services.cache import CacheService
29
- from .utils.http_client import HttpClient
52
+ from .services.role import RoleService
53
+ from .utils.audit_log_queue import AuditLogQueue
30
54
  from .utils.config_loader import load_config
31
- from .errors import (
32
- MisoClientError,
33
- AuthenticationError,
34
- AuthorizationError,
35
- ConnectionError,
36
- ConfigurationError,
55
+ from .utils.controller_url_resolver import is_browser, resolve_controller_url
56
+ from .utils.environment_token import get_environment_token
57
+ from .utils.error_utils import (
58
+ ApiErrorException,
59
+ handle_api_error_snake_case,
60
+ handleApiError,
61
+ transform_error_to_snake_case,
62
+ transformError,
63
+ )
64
+ from .utils.fastapi_endpoints import create_fastapi_client_token_endpoint
65
+ from .utils.filter import (
66
+ apply_filters,
67
+ build_query_string,
68
+ filter_query_to_json,
69
+ json_filter_to_query_string,
70
+ json_to_filter_query,
71
+ parse_filter_params,
72
+ query_string_to_json_filter,
73
+ validate_filter_option,
74
+ validate_json_filter,
75
+ )
76
+ from .utils.flask_endpoints import create_flask_client_token_endpoint
77
+ from .utils.http_client import HttpClient
78
+ from .utils.internal_http_client import InternalHttpClient
79
+ from .utils.jwt_tools import extract_user_id
80
+ from .utils.logging_helpers import extract_logging_context
81
+ from .utils.origin_validator import validate_origin
82
+ from .utils.pagination import (
83
+ apply_pagination_to_array,
84
+ applyPaginationToArray,
85
+ create_meta_object,
86
+ create_paginated_list_response,
87
+ createMetaObject,
88
+ createPaginatedListResponse,
89
+ parse_pagination_params,
90
+ parsePaginationParams,
37
91
  )
92
+ from .utils.request_context import RequestContext, extract_request_context
93
+ from .utils.sort import build_sort_string, parse_sort_params
94
+ from .utils.token_utils import extract_client_token_info
95
+ from .utils.url_validator import validate_url
38
96
 
39
- __version__ = "0.1.0"
97
+ __version__ = "3.7.2"
40
98
  __author__ = "AI Fabrix Team"
41
99
  __license__ = "MIT"
42
100
 
@@ -44,38 +102,77 @@ __license__ = "MIT"
44
102
  class MisoClient:
45
103
  """
46
104
  Main MisoClient SDK class for authentication, authorization, and logging.
47
-
105
+
48
106
  This client provides a unified interface for:
49
107
  - Token validation and user management
50
108
  - Role-based access control
51
109
  - Permission management
52
110
  - Application logging with Redis caching
53
111
  """
54
-
112
+
55
113
  def __init__(self, config: MisoClientConfig):
56
114
  """
57
115
  Initialize MisoClient with configuration.
58
-
116
+
59
117
  Args:
60
118
  config: MisoClient configuration including controller URL, client credentials, etc.
61
119
  """
62
120
  self.config = config
63
- self.http_client = HttpClient(config)
121
+
122
+ # Create InternalHttpClient first (pure HTTP functionality, no logging)
123
+ self._internal_http_client = InternalHttpClient(config)
124
+
125
+ # Create Redis service
64
126
  self.redis = RedisService(config.redis)
127
+
128
+ # Create LoggerService with InternalHttpClient (to avoid circular dependency)
129
+ # LoggerService uses InternalHttpClient for sending logs to prevent audit loops
130
+ self.logger = LoggerService(self._internal_http_client, self.redis)
131
+
132
+ # Create public HttpClient wrapping InternalHttpClient with logger
133
+ # This HttpClient adds automatic ISO 27001 compliant audit and debug logging
134
+ self.http_client = HttpClient(config, self.logger)
135
+
136
+ # Create ApiClient for typed API calls (import here to avoid circular import)
137
+ from .api import ApiClient
138
+
139
+ self.api_client = ApiClient(self.http_client)
140
+
141
+ # Update LoggerService with http_client and api_client for audit log queue (if needed)
142
+ # This is safe because http_client is already created and logger is already set
143
+ if config.audit and (
144
+ config.audit.batchSize is not None or config.audit.batchInterval is not None
145
+ ):
146
+ self.logger.audit_log_queue = AuditLogQueue(self.http_client, self.redis, config)
147
+
148
+ # Update LoggerService with api_client (optional, for typed API calls)
149
+ # Note: LoggerService primarily uses InternalHttpClient to avoid circular dependency
150
+ # ApiClient is provided as optional fallback
151
+ self.logger.api_client = self.api_client
152
+
65
153
  # Cache service (uses Redis if available, falls back to in-memory)
66
154
  self.cache = CacheService(self.redis)
67
- self.auth = AuthService(self.http_client, self.redis)
68
- self.roles = RoleService(self.http_client, self.cache)
69
- self.permissions = PermissionService(self.http_client, self.cache)
70
- self.logger = LoggerService(self.http_client, self.redis)
71
- # Encryption service (reads ENCRYPTION_KEY from environment by default)
72
- self.encryption = EncryptionService()
155
+
156
+ # Services use ApiClient for typed API calls (with HttpClient fallback for backward compatibility)
157
+ self.auth = AuthService(self.http_client, self.redis, self.cache, self.api_client)
158
+ # Set auth_service on refresh manager for refresh endpoint calls
159
+ self.http_client.set_auth_service_for_refresh(self.auth)
160
+ self.roles = RoleService(self.http_client, self.cache, self.api_client)
161
+ self.permissions = PermissionService(self.http_client, self.cache, self.api_client)
162
+
163
+ # Encryption service (optional - only initialized if ENCRYPTION_KEY is configured)
164
+ self.encryption: Optional[EncryptionService]
165
+ try:
166
+ self.encryption = EncryptionService()
167
+ except ConfigurationError:
168
+ # ENCRYPTION_KEY not configured or invalid - encryption service unavailable
169
+ self.encryption = None
73
170
  self.initialized = False
74
171
 
75
172
  async def initialize(self) -> None:
76
173
  """
77
174
  Initialize the client (connect to Redis if configured).
78
-
175
+
79
176
  This method should be called before using the client. It will attempt
80
177
  to connect to Redis if configured, but will gracefully fall back to
81
178
  controller-only mode if Redis is unavailable.
@@ -105,16 +202,18 @@ class MisoClient:
105
202
  def get_token(self, req: dict) -> str | None:
106
203
  """
107
204
  Extract Bearer token from request headers.
108
-
205
+
109
206
  Supports common request object patterns (dict with headers).
110
-
207
+
111
208
  Args:
112
209
  req: Request object with headers dict containing 'authorization' key
113
-
210
+
114
211
  Returns:
115
212
  Bearer token string or None if not found
116
213
  """
117
- headers_obj = req.get("headers", {}) if isinstance(req, dict) else getattr(req, "headers", {})
214
+ headers_obj = (
215
+ req.get("headers", {}) if isinstance(req, dict) else getattr(req, "headers", {})
216
+ )
118
217
  headers: dict[str, Any] = headers_obj if isinstance(headers_obj, dict) else {}
119
218
  auth_value = headers.get("authorization") or headers.get("Authorization")
120
219
  if not isinstance(auth_value, str):
@@ -123,223 +222,375 @@ class MisoClient:
123
222
  # Support "Bearer <token>" format
124
223
  if auth_value.startswith("Bearer "):
125
224
  return auth_value[7:]
126
-
225
+
127
226
  # If no Bearer prefix, assume the whole header is the token
128
227
  return auth_value
129
228
 
130
229
  async def get_environment_token(self) -> str:
131
230
  """
132
231
  Get environment token using client credentials.
133
-
232
+
134
233
  This is called automatically by HttpClient but can be called manually.
135
-
234
+
136
235
  Returns:
137
236
  Client token string
138
237
  """
139
238
  return await self.auth.get_environment_token()
140
239
 
141
- def login(self, redirect_uri: str) -> str:
240
+ async def login(self, redirect: str, state: Optional[str] = None) -> Dict[str, Any]:
142
241
  """
143
- Initiate login flow by redirecting to controller.
144
-
145
- Returns the login URL for browser redirect or manual navigation.
146
-
242
+ Initiate login flow by calling the controller login endpoint.
243
+
244
+ This method calls GET /api/v1/auth/login with redirect and optional state parameters.
245
+ The controller returns a login URL that should be used to redirect the user to Keycloak.
246
+
147
247
  Args:
148
- redirect_uri: URI to redirect to after successful login
149
-
248
+ redirect: Callback URL where Keycloak redirects after authentication (required)
249
+ state: Optional CSRF protection token (auto-generated by backend if omitted)
250
+
150
251
  Returns:
151
- Login URL string
252
+ Dictionary containing:
253
+ - success: True if successful
254
+ - data: Dictionary with loginUrl and state
255
+ - timestamp: Response timestamp
256
+
257
+ Example:
258
+ >>> response = await client.login(
259
+ ... redirect="http://localhost:3000/auth/callback",
260
+ ... state="abc123"
261
+ ... )
262
+ >>> login_url = response["data"]["loginUrl"]
263
+ >>> state = response["data"]["state"]
152
264
  """
153
- return self.auth.login(redirect_uri)
265
+ return await self.auth.login(redirect, state)
154
266
 
155
- async def validate_token(self, token: str) -> bool:
267
+ async def validate_token(
268
+ self, token: str, auth_strategy: Optional[AuthStrategy] = None
269
+ ) -> bool:
156
270
  """
157
271
  Validate token with controller.
158
-
272
+
159
273
  Args:
160
274
  token: JWT token to validate
161
-
275
+ auth_strategy: Optional authentication strategy
276
+
162
277
  Returns:
163
278
  True if token is valid, False otherwise
164
279
  """
165
- return await self.auth.validate_token(token)
280
+ return await self.auth.validate_token(token, auth_strategy=auth_strategy)
166
281
 
167
- async def get_user(self, token: str) -> UserInfo | None:
282
+ async def get_user(
283
+ self, token: str, auth_strategy: Optional[AuthStrategy] = None
284
+ ) -> UserInfo | None:
168
285
  """
169
286
  Get user information from token.
170
-
287
+
171
288
  Args:
172
289
  token: JWT token
173
-
290
+ auth_strategy: Optional authentication strategy
291
+
174
292
  Returns:
175
293
  UserInfo if token is valid, None otherwise
176
294
  """
177
- return await self.auth.get_user(token)
295
+ return await self.auth.get_user(token, auth_strategy=auth_strategy)
178
296
 
179
- async def get_user_info(self, token: str) -> UserInfo | None:
297
+ async def get_user_info(
298
+ self, token: str, auth_strategy: Optional[AuthStrategy] = None
299
+ ) -> UserInfo | None:
180
300
  """
181
- Get user information from GET /api/auth/user endpoint.
182
-
301
+ Get user information from GET /api/v1/auth/user endpoint.
302
+
183
303
  Args:
184
304
  token: JWT token
185
-
305
+ auth_strategy: Optional authentication strategy
306
+
186
307
  Returns:
187
308
  UserInfo if token is valid, None otherwise
188
309
  """
189
- return await self.auth.get_user_info(token)
310
+ return await self.auth.get_user_info(token, auth_strategy=auth_strategy)
190
311
 
191
- async def is_authenticated(self, token: str) -> bool:
312
+ async def is_authenticated(
313
+ self, token: str, auth_strategy: Optional[AuthStrategy] = None
314
+ ) -> bool:
192
315
  """
193
316
  Check if user is authenticated.
194
-
317
+
195
318
  Args:
196
319
  token: JWT token
197
-
320
+ auth_strategy: Optional authentication strategy
321
+
198
322
  Returns:
199
323
  True if user is authenticated, False otherwise
200
324
  """
201
- return await self.auth.is_authenticated(token)
325
+ return await self.auth.is_authenticated(token, auth_strategy=auth_strategy)
326
+
327
+ async def logout(self, token: str) -> Dict[str, Any]:
328
+ """
329
+ Logout user by invalidating the access token.
330
+
331
+ This method calls POST /api/v1/auth/logout with the user's access token in the request body.
332
+ The token will be invalidated on the server side, and all local caches (roles, permissions, JWT)
333
+ will be cleared automatically. Refresh tokens and callbacks are also cleared.
334
+
335
+ Args:
336
+ token: Access token to invalidate (required)
337
+
338
+ Returns:
339
+ Dictionary containing:
340
+ - success: True if successful
341
+ - message: Success message
342
+ - timestamp: Response timestamp
343
+
344
+ Example:
345
+ >>> response = await client.logout(token="jwt-token-here")
346
+ >>> if response.get("success"):
347
+ ... print("Logout successful")
348
+ """
349
+ # Extract user ID before logout
350
+ user_id = extract_user_id(token)
351
+
352
+ # Call AuthService logout (invalidates token on server)
353
+ response = await self.auth.logout(token)
354
+
355
+ # Clear refresh data for user
356
+ if user_id:
357
+ self.clear_user_token_refresh(user_id)
358
+
359
+ # Clear all caches after logout (even if logout failed, clear caches for security)
360
+ # Use asyncio.gather() for concurrent cache clearing
361
+ await asyncio.gather(
362
+ self.roles.clear_roles_cache(token),
363
+ self.permissions.clear_permissions_cache(token),
364
+ return_exceptions=True, # Don't fail if any cache clear fails
365
+ )
366
+
367
+ return response
368
+
369
+ def register_user_token_refresh_callback(self, user_id: str, callback: Any) -> None:
370
+ """
371
+ Register refresh callback for a user.
372
+
373
+ The callback will be called when the user's token needs to be refreshed.
374
+ The callback should be an async function that takes the old token and returns
375
+ the new token.
376
+
377
+ Args:
378
+ user_id: User ID
379
+ callback: Async function that takes old token and returns new token
380
+
381
+ Example:
382
+ >>> async def refresh_token(old_token: str) -> str:
383
+ ... # Call your refresh endpoint
384
+ ... response = await your_auth_client.refresh(old_token)
385
+ ... return response["access_token"]
386
+ >>>
387
+ >>> client.register_user_token_refresh_callback("user-123", refresh_token)
388
+ """
389
+ self.http_client.register_user_token_refresh_callback(user_id, callback)
390
+
391
+ def register_user_refresh_token(self, user_id: str, refresh_token: str) -> None:
392
+ """
393
+ Register refresh token for a user.
394
+
395
+ The SDK will use this refresh token to automatically refresh the user's
396
+ access token when it expires.
202
397
 
203
- async def logout(self) -> None:
204
- """Logout user."""
205
- return await self.auth.logout()
398
+ Args:
399
+ user_id: User ID
400
+ refresh_token: Refresh token string
401
+
402
+ Example:
403
+ >>> client.register_user_refresh_token("user-123", "refresh-token-abc")
404
+ """
405
+ self.http_client.register_user_refresh_token(user_id, refresh_token)
406
+
407
+ def clear_user_token_refresh(self, user_id: str) -> None:
408
+ """
409
+ Clear refresh callback and tokens for a user.
410
+
411
+ Useful when user logs out or refresh tokens are revoked.
412
+
413
+ Args:
414
+ user_id: User ID
415
+
416
+ Example:
417
+ >>> client.clear_user_token_refresh("user-123")
418
+ """
419
+ self.http_client._user_token_refresh.clear_user_tokens(user_id)
206
420
 
207
421
  # ==================== AUTHORIZATION METHODS ====================
208
422
 
209
- async def get_roles(self, token: str) -> list[str]:
423
+ async def get_roles(
424
+ self, token: str, auth_strategy: Optional[AuthStrategy] = None
425
+ ) -> list[str]:
210
426
  """
211
427
  Get user roles (cached in Redis if available).
212
-
428
+
213
429
  Args:
214
430
  token: JWT token
215
-
431
+ auth_strategy: Optional authentication strategy
432
+
216
433
  Returns:
217
434
  List of user roles
218
435
  """
219
- return await self.roles.get_roles(token)
436
+ return await self.roles.get_roles(token, auth_strategy=auth_strategy)
220
437
 
221
- async def has_role(self, token: str, role: str) -> bool:
438
+ async def has_role(
439
+ self, token: str, role: str, auth_strategy: Optional[AuthStrategy] = None
440
+ ) -> bool:
222
441
  """
223
442
  Check if user has specific role.
224
-
443
+
225
444
  Args:
226
445
  token: JWT token
227
446
  role: Role to check
228
-
447
+ auth_strategy: Optional authentication strategy
448
+
229
449
  Returns:
230
450
  True if user has the role, False otherwise
231
451
  """
232
- return await self.roles.has_role(token, role)
452
+ return await self.roles.has_role(token, role, auth_strategy=auth_strategy)
233
453
 
234
- async def has_any_role(self, token: str, roles: list[str]) -> bool:
454
+ async def has_any_role(
455
+ self, token: str, roles: list[str], auth_strategy: Optional[AuthStrategy] = None
456
+ ) -> bool:
235
457
  """
236
458
  Check if user has any of the specified roles.
237
-
459
+
238
460
  Args:
239
461
  token: JWT token
240
462
  roles: List of roles to check
241
-
463
+ auth_strategy: Optional authentication strategy
464
+
242
465
  Returns:
243
466
  True if user has any of the roles, False otherwise
244
467
  """
245
- return await self.roles.has_any_role(token, roles)
468
+ return await self.roles.has_any_role(token, roles, auth_strategy=auth_strategy)
246
469
 
247
- async def has_all_roles(self, token: str, roles: list[str]) -> bool:
470
+ async def has_all_roles(
471
+ self, token: str, roles: list[str], auth_strategy: Optional[AuthStrategy] = None
472
+ ) -> bool:
248
473
  """
249
474
  Check if user has all of the specified roles.
250
-
475
+
251
476
  Args:
252
477
  token: JWT token
253
478
  roles: List of roles to check
254
-
479
+ auth_strategy: Optional authentication strategy
480
+
255
481
  Returns:
256
482
  True if user has all roles, False otherwise
257
483
  """
258
- return await self.roles.has_all_roles(token, roles)
484
+ return await self.roles.has_all_roles(token, roles, auth_strategy=auth_strategy)
259
485
 
260
- async def refresh_roles(self, token: str) -> list[str]:
486
+ async def refresh_roles(
487
+ self, token: str, auth_strategy: Optional[AuthStrategy] = None
488
+ ) -> list[str]:
261
489
  """
262
490
  Force refresh roles from controller (bypass cache).
263
-
491
+
264
492
  Args:
265
493
  token: JWT token
266
-
494
+ auth_strategy: Optional authentication strategy
495
+
267
496
  Returns:
268
497
  Fresh list of user roles
269
498
  """
270
- return await self.roles.refresh_roles(token)
499
+ return await self.roles.refresh_roles(token, auth_strategy=auth_strategy)
271
500
 
272
- async def get_permissions(self, token: str) -> list[str]:
501
+ async def get_permissions(
502
+ self, token: str, auth_strategy: Optional[AuthStrategy] = None
503
+ ) -> list[str]:
273
504
  """
274
505
  Get user permissions (cached in Redis if available).
275
-
506
+
276
507
  Args:
277
508
  token: JWT token
278
-
509
+ auth_strategy: Optional authentication strategy
510
+
279
511
  Returns:
280
512
  List of user permissions
281
513
  """
282
- return await self.permissions.get_permissions(token)
514
+ return await self.permissions.get_permissions(token, auth_strategy=auth_strategy)
283
515
 
284
- async def has_permission(self, token: str, permission: str) -> bool:
516
+ async def has_permission(
517
+ self, token: str, permission: str, auth_strategy: Optional[AuthStrategy] = None
518
+ ) -> bool:
285
519
  """
286
520
  Check if user has specific permission.
287
-
521
+
288
522
  Args:
289
523
  token: JWT token
290
524
  permission: Permission to check
291
-
525
+ auth_strategy: Optional authentication strategy
526
+
292
527
  Returns:
293
528
  True if user has the permission, False otherwise
294
529
  """
295
- return await self.permissions.has_permission(token, permission)
530
+ return await self.permissions.has_permission(token, permission, auth_strategy=auth_strategy)
296
531
 
297
- async def has_any_permission(self, token: str, permissions: list[str]) -> bool:
532
+ async def has_any_permission(
533
+ self, token: str, permissions: list[str], auth_strategy: Optional[AuthStrategy] = None
534
+ ) -> bool:
298
535
  """
299
536
  Check if user has any of the specified permissions.
300
-
537
+
301
538
  Args:
302
539
  token: JWT token
303
540
  permissions: List of permissions to check
304
-
541
+ auth_strategy: Optional authentication strategy
542
+
305
543
  Returns:
306
544
  True if user has any of the permissions, False otherwise
307
545
  """
308
- return await self.permissions.has_any_permission(token, permissions)
546
+ return await self.permissions.has_any_permission(
547
+ token, permissions, auth_strategy=auth_strategy
548
+ )
309
549
 
310
- async def has_all_permissions(self, token: str, permissions: list[str]) -> bool:
550
+ async def has_all_permissions(
551
+ self, token: str, permissions: list[str], auth_strategy: Optional[AuthStrategy] = None
552
+ ) -> bool:
311
553
  """
312
554
  Check if user has all of the specified permissions.
313
-
555
+
314
556
  Args:
315
557
  token: JWT token
316
558
  permissions: List of permissions to check
317
-
559
+ auth_strategy: Optional authentication strategy
560
+
318
561
  Returns:
319
562
  True if user has all permissions, False otherwise
320
563
  """
321
- return await self.permissions.has_all_permissions(token, permissions)
564
+ return await self.permissions.has_all_permissions(
565
+ token, permissions, auth_strategy=auth_strategy
566
+ )
322
567
 
323
- async def refresh_permissions(self, token: str) -> list[str]:
568
+ async def refresh_permissions(
569
+ self, token: str, auth_strategy: Optional[AuthStrategy] = None
570
+ ) -> list[str]:
324
571
  """
325
572
  Force refresh permissions from controller (bypass cache).
326
-
573
+
327
574
  Args:
328
575
  token: JWT token
329
-
576
+ auth_strategy: Optional authentication strategy
577
+
330
578
  Returns:
331
579
  Fresh list of user permissions
332
580
  """
333
- return await self.permissions.refresh_permissions(token)
581
+ return await self.permissions.refresh_permissions(token, auth_strategy=auth_strategy)
334
582
 
335
- async def clear_permissions_cache(self, token: str) -> None:
583
+ async def clear_permissions_cache(
584
+ self, token: str, auth_strategy: Optional[AuthStrategy] = None
585
+ ) -> None:
336
586
  """
337
587
  Clear cached permissions for a user.
338
-
588
+
339
589
  Args:
340
590
  token: JWT token
591
+ auth_strategy: Optional authentication strategy
341
592
  """
342
- return await self.permissions.clear_permissions_cache(token)
593
+ return await self.permissions.clear_permissions_cache(token, auth_strategy=auth_strategy)
343
594
 
344
595
  # ==================== LOGGING METHODS ====================
345
596
 
@@ -347,7 +598,7 @@ class MisoClient:
347
598
  def log(self) -> LoggerService:
348
599
  """
349
600
  Get logger service for application logging.
350
-
601
+
351
602
  Returns:
352
603
  LoggerService instance
353
604
  """
@@ -358,29 +609,45 @@ class MisoClient:
358
609
  def encrypt(self, plaintext: str) -> str:
359
610
  """
360
611
  Encrypt sensitive data.
361
-
612
+
362
613
  Convenience method that delegates to encryption service.
363
-
614
+
364
615
  Args:
365
616
  plaintext: Plain text string to encrypt
366
-
617
+
367
618
  Returns:
368
619
  Base64-encoded encrypted string
620
+
621
+ Raises:
622
+ ConfigurationError: If encryption service is not available (ENCRYPTION_KEY not configured)
369
623
  """
624
+ if self.encryption is None:
625
+ raise ConfigurationError(
626
+ "Encryption service is not available. Set ENCRYPTION_KEY environment variable "
627
+ "to enable encryption functionality."
628
+ )
370
629
  return self.encryption.encrypt(plaintext)
371
630
 
372
631
  def decrypt(self, encrypted_text: str) -> str:
373
632
  """
374
633
  Decrypt sensitive data.
375
-
634
+
376
635
  Convenience method that delegates to encryption service.
377
-
636
+
378
637
  Args:
379
638
  encrypted_text: Base64-encoded encrypted string
380
-
639
+
381
640
  Returns:
382
641
  Decrypted plain text string
642
+
643
+ Raises:
644
+ ConfigurationError: If encryption service is not available (ENCRYPTION_KEY not configured)
383
645
  """
646
+ if self.encryption is None:
647
+ raise ConfigurationError(
648
+ "Encryption service is not available. Set ENCRYPTION_KEY environment variable "
649
+ "to enable encryption functionality."
650
+ )
384
651
  return self.encryption.decrypt(encrypted_text)
385
652
 
386
653
  # ==================== CACHING METHODS ====================
@@ -388,12 +655,12 @@ class MisoClient:
388
655
  async def cache_get(self, key: str) -> Optional[Any]:
389
656
  """
390
657
  Get cached value.
391
-
658
+
392
659
  Convenience method that delegates to cache service.
393
-
660
+
394
661
  Args:
395
662
  key: Cache key
396
-
663
+
397
664
  Returns:
398
665
  Cached value if found, None otherwise
399
666
  """
@@ -402,14 +669,14 @@ class MisoClient:
402
669
  async def cache_set(self, key: str, value: Any, ttl: int) -> bool:
403
670
  """
404
671
  Set cached value with TTL.
405
-
672
+
406
673
  Convenience method that delegates to cache service.
407
-
674
+
408
675
  Args:
409
676
  key: Cache key
410
677
  value: Value to cache
411
678
  ttl: Time to live in seconds
412
-
679
+
413
680
  Returns:
414
681
  True if successful, False otherwise
415
682
  """
@@ -418,12 +685,12 @@ class MisoClient:
418
685
  async def cache_delete(self, key: str) -> bool:
419
686
  """
420
687
  Delete cached value.
421
-
688
+
422
689
  Convenience method that delegates to cache service.
423
-
690
+
424
691
  Args:
425
692
  key: Cache key
426
-
693
+
427
694
  Returns:
428
695
  True if deleted, False otherwise
429
696
  """
@@ -432,7 +699,7 @@ class MisoClient:
432
699
  async def cache_clear(self) -> None:
433
700
  """
434
701
  Clear all cached values.
435
-
702
+
436
703
  Convenience method that delegates to cache service.
437
704
  """
438
705
  await self.cache.clear()
@@ -442,7 +709,7 @@ class MisoClient:
442
709
  def get_config(self) -> MisoClientConfig:
443
710
  """
444
711
  Get current configuration.
445
-
712
+
446
713
  Returns:
447
714
  Copy of current configuration
448
715
  """
@@ -451,12 +718,75 @@ class MisoClient:
451
718
  def is_redis_connected(self) -> bool:
452
719
  """
453
720
  Check if Redis is connected.
454
-
721
+
455
722
  Returns:
456
723
  True if Redis is connected, False otherwise
457
724
  """
458
725
  return self.redis.is_connected()
459
726
 
727
+ # ==================== AUTHENTICATION STRATEGY METHODS ====================
728
+
729
+ def create_auth_strategy(
730
+ self,
731
+ methods: List[Literal["bearer", "client-token", "client-credentials", "api-key"]],
732
+ bearer_token: Optional[str] = None,
733
+ api_key: Optional[str] = None,
734
+ ) -> AuthStrategy:
735
+ """
736
+ Create an authentication strategy object.
737
+
738
+ Args:
739
+ methods: List of authentication methods in priority order
740
+ bearer_token: Optional bearer token for bearer auth
741
+ api_key: Optional API key for api-key auth
742
+
743
+ Returns:
744
+ AuthStrategy instance
745
+
746
+ Example:
747
+ >>> strategy = client.create_auth_strategy(
748
+ ... ['api-key'],
749
+ ... bearer_token=None,
750
+ ... api_key='your-api-key-here'
751
+ ... )
752
+ """
753
+ return AuthStrategy(methods=methods, bearerToken=bearer_token, apiKey=api_key)
754
+
755
+ async def request_with_auth_strategy(
756
+ self,
757
+ method: Literal["GET", "POST", "PUT", "DELETE"],
758
+ url: str,
759
+ auth_strategy: AuthStrategy,
760
+ data: Optional[Dict[str, Any]] = None,
761
+ **kwargs,
762
+ ) -> Any:
763
+ """
764
+ Make request with authentication strategy (priority-based fallback).
765
+
766
+ Tries authentication methods in priority order until one succeeds.
767
+ If a method returns 401, automatically tries the next method in the strategy.
768
+
769
+ Args:
770
+ method: HTTP method
771
+ url: Request URL
772
+ auth_strategy: Authentication strategy configuration
773
+ data: Request data (for POST/PUT)
774
+ **kwargs: Additional httpx request parameters
775
+
776
+ Returns:
777
+ Response data (JSON parsed)
778
+
779
+ Raises:
780
+ MisoClientError: If all authentication methods fail
781
+
782
+ Example:
783
+ >>> strategy = client.create_auth_strategy(['api-key'], api_key='your-key')
784
+ >>> response = await client.request_with_auth_strategy('GET', '/api/data', strategy)
785
+ """
786
+ return await self.http_client.request_with_auth_strategy(
787
+ method, url, auth_strategy, data, **kwargs
788
+ )
789
+
460
790
 
461
791
  # Export types
462
792
  __all__ = [
@@ -465,12 +795,60 @@ __all__ = [
465
795
  "MisoClientConfig",
466
796
  "UserInfo",
467
797
  "AuthResult",
798
+ "AuthStrategy",
468
799
  "LogEntry",
469
800
  "RoleResult",
470
801
  "PermissionResult",
471
802
  "ClientTokenResponse",
472
- "PerformanceMetrics",
803
+ "ClientTokenEndpointResponse",
804
+ "ClientTokenEndpointOptions",
805
+ "DataClientConfigResponse",
806
+ "CircuitBreakerConfig",
473
807
  "ClientLoggingOptions",
808
+ "ErrorResponse",
809
+ # Pagination models
810
+ "Meta",
811
+ "PaginatedListResponse",
812
+ # Filter models
813
+ "FilterOperator",
814
+ "FilterOption",
815
+ "FilterQuery",
816
+ "FilterBuilder",
817
+ "JsonFilter",
818
+ "FilterGroup",
819
+ # Sort models
820
+ "SortOption",
821
+ # Pagination utilities (camelCase)
822
+ "parsePaginationParams",
823
+ "createMetaObject",
824
+ "applyPaginationToArray",
825
+ "createPaginatedListResponse",
826
+ # Pagination utilities (legacy snake_case aliases)
827
+ "parse_pagination_params",
828
+ "create_meta_object",
829
+ "apply_pagination_to_array",
830
+ "create_paginated_list_response",
831
+ # Filter utilities
832
+ "parse_filter_params",
833
+ "build_query_string",
834
+ "apply_filters",
835
+ "filter_query_to_json",
836
+ "json_to_filter_query",
837
+ "json_filter_to_query_string",
838
+ "query_string_to_json_filter",
839
+ "validate_filter_option",
840
+ "validate_json_filter",
841
+ # Sort utilities
842
+ "parse_sort_params",
843
+ "build_sort_string",
844
+ # Error utilities (camelCase)
845
+ "transformError",
846
+ "handleApiError",
847
+ "ApiErrorException",
848
+ # Error utilities (legacy snake_case aliases)
849
+ "transform_error_to_snake_case",
850
+ "handle_api_error_snake_case",
851
+ # Services
474
852
  "AuthService",
475
853
  "RoleService",
476
854
  "PermissionService",
@@ -480,10 +858,25 @@ __all__ = [
480
858
  "EncryptionService",
481
859
  "CacheService",
482
860
  "HttpClient",
861
+ "AuditLogQueue",
483
862
  "load_config",
484
863
  "MisoClientError",
485
864
  "AuthenticationError",
486
865
  "AuthorizationError",
487
866
  "ConnectionError",
488
867
  "ConfigurationError",
868
+ # Server-side utilities
869
+ "get_environment_token",
870
+ "validate_origin",
871
+ "extract_client_token_info",
872
+ "validate_url",
873
+ "resolve_controller_url",
874
+ "is_browser",
875
+ "create_flask_client_token_endpoint",
876
+ "create_fastapi_client_token_endpoint",
877
+ # Request context utilities
878
+ "extract_request_context",
879
+ "RequestContext",
880
+ # Logging utilities
881
+ "extract_logging_context",
489
882
  ]