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,288 @@
1
+ """
2
+ Logger chain for fluent logging API.
3
+
4
+ This module provides the LoggerChain class for method chaining in logging operations.
5
+ """
6
+
7
+ from typing import TYPE_CHECKING, Any, Optional
8
+
9
+ if TYPE_CHECKING:
10
+ from .logger import LoggerService
11
+
12
+ from ..models.config import ClientLoggingOptions
13
+ from ..utils.request_context import extract_request_context
14
+
15
+
16
+ class LoggerChain:
17
+ """Method chaining class for fluent logging API."""
18
+
19
+ def __init__(
20
+ self,
21
+ logger: "LoggerService",
22
+ context: Optional[dict[str, Any]] = None,
23
+ options: Optional[ClientLoggingOptions] = None,
24
+ ):
25
+ """
26
+ Initialize logger chain.
27
+
28
+ Args:
29
+ logger: Logger service instance
30
+ context: Initial context
31
+ options: Initial logging options
32
+ """
33
+ self.logger = logger
34
+ self.context = context or {}
35
+ self.options = options or ClientLoggingOptions()
36
+
37
+ def add_context(self, key: str, value: Any) -> "LoggerChain":
38
+ """Add context key-value pair."""
39
+ self.context[key] = value
40
+ return self
41
+
42
+ def add_user(self, user_id: str) -> "LoggerChain":
43
+ """Add user ID."""
44
+ if self.options is None:
45
+ self.options = ClientLoggingOptions()
46
+ self.options.userId = user_id
47
+ return self
48
+
49
+ def add_application(self, application_id: str) -> "LoggerChain":
50
+ """Add application ID."""
51
+ if self.options is None:
52
+ self.options = ClientLoggingOptions()
53
+ self.options.applicationId = application_id
54
+ return self
55
+
56
+ def add_correlation(self, correlation_id: str) -> "LoggerChain":
57
+ """Add correlation ID."""
58
+ if self.options is None:
59
+ self.options = ClientLoggingOptions()
60
+ self.options.correlationId = correlation_id
61
+ return self
62
+
63
+ def with_token(self, token: str) -> "LoggerChain":
64
+ """Add token for context extraction."""
65
+ if self.options is None:
66
+ self.options = ClientLoggingOptions()
67
+ self.options.token = token
68
+ return self
69
+
70
+ def without_masking(self) -> "LoggerChain":
71
+ """Disable data masking."""
72
+ if self.options is None:
73
+ self.options = ClientLoggingOptions()
74
+ self.options.maskSensitiveData = False
75
+ return self
76
+
77
+ def with_request(self, request: Any) -> "LoggerChain":
78
+ """
79
+ Auto-extract logging context from HTTP Request object.
80
+
81
+ Extracts: IP, method, path, user-agent, correlation ID, user from JWT.
82
+
83
+ Supports:
84
+ - FastAPI/Starlette Request
85
+ - Flask Request
86
+ - Generic dict-like request objects
87
+
88
+ Args:
89
+ request: HTTP request object
90
+
91
+ Returns:
92
+ Self for method chaining
93
+
94
+ Example:
95
+ >>> await logger.with_request(request).info("Processing request")
96
+ """
97
+ ctx = extract_request_context(request)
98
+
99
+ if self.options is None:
100
+ self.options = ClientLoggingOptions()
101
+
102
+ # Merge into options (these become top-level LogEntry fields)
103
+ if ctx.user_id:
104
+ self.options.userId = ctx.user_id
105
+ if ctx.session_id:
106
+ self.options.sessionId = ctx.session_id
107
+ if ctx.correlation_id:
108
+ self.options.correlationId = ctx.correlation_id
109
+ if ctx.request_id:
110
+ self.options.requestId = ctx.request_id
111
+ if ctx.ip_address:
112
+ self.options.ipAddress = ctx.ip_address
113
+ if ctx.user_agent:
114
+ self.options.userAgent = ctx.user_agent
115
+
116
+ # Merge into context (additional request info, not top-level LogEntry fields)
117
+ if ctx.method:
118
+ self.context["method"] = ctx.method
119
+ if ctx.path:
120
+ self.context["path"] = ctx.path
121
+ if ctx.referer:
122
+ self.context["referer"] = ctx.referer
123
+ if ctx.request_size:
124
+ self.context["requestSize"] = ctx.request_size
125
+
126
+ return self
127
+
128
+ def with_indexed_context(
129
+ self,
130
+ source_key: Optional[str] = None,
131
+ source_display_name: Optional[str] = None,
132
+ external_system_key: Optional[str] = None,
133
+ external_system_display_name: Optional[str] = None,
134
+ record_key: Optional[str] = None,
135
+ record_display_name: Optional[str] = None,
136
+ ) -> "LoggerChain":
137
+ """
138
+ Add indexed context fields for fast querying.
139
+
140
+ Args:
141
+ source_key: ExternalDataSource.key
142
+ source_display_name: Human-readable source name
143
+ external_system_key: ExternalSystem.key
144
+ external_system_display_name: Human-readable system name
145
+ record_key: ExternalRecord.key
146
+ record_display_name: Human-readable record identifier
147
+
148
+ Returns:
149
+ Self for method chaining
150
+ """
151
+ if self.options is None:
152
+ self.options = ClientLoggingOptions()
153
+ if source_key:
154
+ self.options.sourceKey = source_key
155
+ if source_display_name:
156
+ self.options.sourceDisplayName = source_display_name
157
+ if external_system_key:
158
+ self.options.externalSystemKey = external_system_key
159
+ if external_system_display_name:
160
+ self.options.externalSystemDisplayName = external_system_display_name
161
+ if record_key:
162
+ self.options.recordKey = record_key
163
+ if record_display_name:
164
+ self.options.recordDisplayName = record_display_name
165
+ return self
166
+
167
+ def with_credential_context(
168
+ self,
169
+ credential_id: Optional[str] = None,
170
+ credential_type: Optional[str] = None,
171
+ ) -> "LoggerChain":
172
+ """
173
+ Add credential context for performance analysis.
174
+
175
+ Args:
176
+ credential_id: Credential identifier
177
+ credential_type: Credential type (apiKey, oauth2, etc.)
178
+
179
+ Returns:
180
+ Self for method chaining
181
+ """
182
+ if self.options is None:
183
+ self.options = ClientLoggingOptions()
184
+ if credential_id:
185
+ self.options.credentialId = credential_id
186
+ if credential_type:
187
+ self.options.credentialType = credential_type
188
+ return self
189
+
190
+ def with_request_metrics(
191
+ self,
192
+ request_size: Optional[int] = None,
193
+ response_size: Optional[int] = None,
194
+ duration_ms: Optional[int] = None,
195
+ duration_seconds: Optional[float] = None,
196
+ timeout: Optional[float] = None,
197
+ retry_count: Optional[int] = None,
198
+ ) -> "LoggerChain":
199
+ """
200
+ Add request/response metrics.
201
+
202
+ Args:
203
+ request_size: Request body size in bytes
204
+ response_size: Response body size in bytes
205
+ duration_ms: Duration in milliseconds
206
+ duration_seconds: Duration in seconds
207
+ timeout: Request timeout in seconds
208
+ retry_count: Number of retry attempts
209
+
210
+ Returns:
211
+ Self for method chaining
212
+ """
213
+ if self.options is None:
214
+ self.options = ClientLoggingOptions()
215
+ if request_size is not None:
216
+ self.options.requestSize = request_size
217
+ if response_size is not None:
218
+ self.options.responseSize = response_size
219
+ if duration_ms is not None:
220
+ self.options.durationMs = duration_ms
221
+ if duration_seconds is not None:
222
+ self.options.durationSeconds = duration_seconds
223
+ if timeout is not None:
224
+ self.options.timeout = timeout
225
+ if retry_count is not None:
226
+ self.options.retryCount = retry_count
227
+ return self
228
+
229
+ def with_error_context(
230
+ self,
231
+ error_category: Optional[str] = None,
232
+ http_status_category: Optional[str] = None,
233
+ ) -> "LoggerChain":
234
+ """
235
+ Add error classification context.
236
+
237
+ Args:
238
+ error_category: Error category (network, timeout, auth, validation, server)
239
+ http_status_category: HTTP status category (2xx, 4xx, 5xx)
240
+
241
+ Returns:
242
+ Self for method chaining
243
+ """
244
+ if self.options is None:
245
+ self.options = ClientLoggingOptions()
246
+ if error_category:
247
+ self.options.errorCategory = error_category
248
+ if http_status_category:
249
+ self.options.httpStatusCategory = http_status_category
250
+ return self
251
+
252
+ def add_session(self, session_id: str) -> "LoggerChain":
253
+ """
254
+ Add session ID to logging context.
255
+
256
+ Args:
257
+ session_id: Session identifier
258
+
259
+ Returns:
260
+ Self for method chaining
261
+ """
262
+ if self.options is None:
263
+ self.options = ClientLoggingOptions()
264
+ self.options.sessionId = session_id
265
+ return self
266
+
267
+ async def error(self, message: str, stack_trace: Optional[str] = None) -> None:
268
+ """Log error."""
269
+ await self.logger.error(message, self.context, stack_trace, self.options)
270
+
271
+ async def info(self, message: str) -> None:
272
+ """Log info."""
273
+ await self.logger.info(message, self.context, self.options)
274
+
275
+ async def audit(self, action: str, resource: str) -> None:
276
+ """Log audit."""
277
+ await self.logger.audit(action, resource, self.context, self.options)
278
+
279
+ async def debug(self, message: str) -> None:
280
+ """
281
+ Log debug message.
282
+
283
+ Only logs if log level is set to 'debug' in config.
284
+
285
+ Args:
286
+ message: Debug message
287
+ """
288
+ await self.logger.debug(message, self.context, self.options)
@@ -6,39 +6,55 @@ Permissions are cached with Redis and in-memory fallback using CacheService.
6
6
  Optimized to extract userId from JWT token before API calls for cache optimization.
7
7
  """
8
8
 
9
+ import logging
9
10
  import time
10
- from typing import List, cast
11
- from ..models.config import PermissionResult
11
+ from typing import TYPE_CHECKING, List, Optional, cast
12
+
13
+ from ..models.config import AuthStrategy, PermissionResult
12
14
  from ..services.cache import CacheService
15
+ from ..utils.auth_utils import validate_token_request
16
+ from ..utils.error_utils import extract_correlation_id_from_error
13
17
  from ..utils.http_client import HttpClient
14
18
  from ..utils.jwt_tools import extract_user_id
15
19
 
20
+ if TYPE_CHECKING:
21
+ from ..api import ApiClient
22
+
23
+ logger = logging.getLogger(__name__)
24
+
16
25
 
17
26
  class PermissionService:
18
27
  """Permission service for user authorization with caching."""
19
-
20
- def __init__(self, http_client: HttpClient, cache: CacheService):
28
+
29
+ def __init__(
30
+ self, http_client: HttpClient, cache: CacheService, api_client: Optional["ApiClient"] = None
31
+ ):
21
32
  """
22
33
  Initialize permission service.
23
-
34
+
24
35
  Args:
25
- http_client: HTTP client instance
36
+ http_client: HTTP client instance (for backward compatibility)
26
37
  cache: Cache service instance (handles Redis + in-memory fallback)
38
+ api_client: Optional API client instance (for typed API calls)
27
39
  """
28
40
  self.config = http_client.config
29
41
  self.http_client = http_client
30
42
  self.cache = cache
43
+ self.api_client = api_client
31
44
  self.permission_ttl = self.config.permission_ttl
32
45
 
33
- async def get_permissions(self, token: str) -> List[str]:
46
+ async def get_permissions(
47
+ self, token: str, auth_strategy: Optional[AuthStrategy] = None
48
+ ) -> List[str]:
34
49
  """
35
50
  Get user permissions with Redis caching.
36
-
51
+
37
52
  Optimized to extract userId from token first to check cache before API call.
38
-
53
+
39
54
  Args:
40
55
  token: JWT token
41
-
56
+ auth_strategy: Optional authentication strategy
57
+
42
58
  Returns:
43
59
  List of user permissions
44
60
  """
@@ -56,10 +72,8 @@ class PermissionService:
56
72
  # Cache miss or no userId in token - fetch from controller
57
73
  # If we don't have userId, get it from validate endpoint
58
74
  if not user_id:
59
- user_info = await self.http_client.authenticated_request(
60
- "POST",
61
- "/api/auth/validate",
62
- token
75
+ user_info = await validate_token_request(
76
+ token, self.http_client, self.api_client, auth_strategy
63
77
  )
64
78
  user_id = user_info.get("user", {}).get("id") if user_info else None
65
79
  if not user_id:
@@ -67,89 +81,114 @@ class PermissionService:
67
81
  cache_key = f"permissions:{user_id}"
68
82
 
69
83
  # Cache miss - fetch from controller
70
- permission_result = await self.http_client.authenticated_request(
71
- "GET",
72
- "/api/auth/permissions", # Backend knows app/env from client token
73
- token
74
- )
84
+ if self.api_client:
85
+ # Use ApiClient for typed API calls
86
+ response = await self.api_client.permissions.get_permissions(
87
+ token, auth_strategy=auth_strategy
88
+ )
89
+ permissions = response.data.permissions or []
90
+ else:
91
+ # Fallback to HttpClient for backward compatibility
92
+ if auth_strategy is not None:
93
+ permission_result = await self.http_client.authenticated_request(
94
+ "GET", "/api/v1/auth/permissions", token, auth_strategy=auth_strategy
95
+ )
96
+ else:
97
+ permission_result = await self.http_client.authenticated_request(
98
+ "GET", "/api/v1/auth/permissions", token
99
+ )
75
100
 
76
- permission_data = PermissionResult(**permission_result)
77
- permissions = permission_data.permissions or []
101
+ permission_data = PermissionResult(**permission_result)
102
+ permissions = permission_data.permissions or []
78
103
 
79
104
  # Cache the result (CacheService handles Redis + in-memory automatically)
80
105
  assert cache_key is not None
81
106
  await self.cache.set(
82
107
  cache_key,
83
108
  {"permissions": permissions, "timestamp": int(time.time() * 1000)},
84
- self.permission_ttl
109
+ self.permission_ttl,
85
110
  )
86
111
 
87
112
  return permissions
88
-
89
- except Exception:
90
- # Failed to get permissions, return empty list
113
+
114
+ except Exception as error:
115
+ correlation_id = extract_correlation_id_from_error(error)
116
+ logger.error(
117
+ "Failed to get permissions",
118
+ exc_info=error,
119
+ extra={"correlationId": correlation_id} if correlation_id else None,
120
+ )
91
121
  return []
92
122
 
93
- async def has_permission(self, token: str, permission: str) -> bool:
123
+ async def has_permission(
124
+ self, token: str, permission: str, auth_strategy: Optional[AuthStrategy] = None
125
+ ) -> bool:
94
126
  """
95
127
  Check if user has specific permission.
96
-
128
+
97
129
  Args:
98
130
  token: JWT token
99
131
  permission: Permission to check
100
-
132
+ auth_strategy: Optional authentication strategy
133
+
101
134
  Returns:
102
135
  True if user has the permission, False otherwise
103
136
  """
104
- permissions = await self.get_permissions(token)
137
+ permissions = await self.get_permissions(token, auth_strategy=auth_strategy)
105
138
  return permission in permissions
106
139
 
107
- async def has_any_permission(self, token: str, permissions: List[str]) -> bool:
140
+ async def has_any_permission(
141
+ self, token: str, permissions: List[str], auth_strategy: Optional[AuthStrategy] = None
142
+ ) -> bool:
108
143
  """
109
144
  Check if user has any of the specified permissions.
110
-
145
+
111
146
  Args:
112
147
  token: JWT token
113
148
  permissions: List of permissions to check
114
-
149
+ auth_strategy: Optional authentication strategy
150
+
115
151
  Returns:
116
152
  True if user has any of the permissions, False otherwise
117
153
  """
118
- user_permissions = await self.get_permissions(token)
154
+ user_permissions = await self.get_permissions(token, auth_strategy=auth_strategy)
119
155
  return any(permission in user_permissions for permission in permissions)
120
156
 
121
- async def has_all_permissions(self, token: str, permissions: List[str]) -> bool:
157
+ async def has_all_permissions(
158
+ self, token: str, permissions: List[str], auth_strategy: Optional[AuthStrategy] = None
159
+ ) -> bool:
122
160
  """
123
161
  Check if user has all of the specified permissions.
124
-
162
+
125
163
  Args:
126
164
  token: JWT token
127
165
  permissions: List of permissions to check
128
-
166
+ auth_strategy: Optional authentication strategy
167
+
129
168
  Returns:
130
169
  True if user has all permissions, False otherwise
131
170
  """
132
- user_permissions = await self.get_permissions(token)
171
+ user_permissions = await self.get_permissions(token, auth_strategy=auth_strategy)
133
172
  return all(permission in user_permissions for permission in permissions)
134
173
 
135
- async def refresh_permissions(self, token: str) -> List[str]:
174
+ async def refresh_permissions(
175
+ self, token: str, auth_strategy: Optional[AuthStrategy] = None
176
+ ) -> List[str]:
136
177
  """
137
178
  Force refresh permissions from controller (bypass cache).
138
-
179
+
139
180
  Args:
140
181
  token: JWT token
141
-
182
+ auth_strategy: Optional authentication strategy
183
+
142
184
  Returns:
143
185
  Fresh list of user permissions
144
186
  """
145
187
  try:
146
188
  # Get user info to extract userId
147
- user_info = await self.http_client.authenticated_request(
148
- "POST",
149
- "/api/auth/validate",
150
- token
189
+ user_info = await validate_token_request(
190
+ token, self.http_client, self.api_client, auth_strategy
151
191
  )
152
-
153
192
  user_id = user_info.get("user", {}).get("id") if user_info else None
154
193
  if not user_id:
155
194
  return []
@@ -157,43 +196,62 @@ class PermissionService:
157
196
  cache_key = f"permissions:{user_id}"
158
197
 
159
198
  # Fetch fresh permissions from controller using refresh endpoint
160
- permission_result = await self.http_client.authenticated_request(
161
- "GET",
162
- "/api/auth/permissions/refresh",
163
- token
164
- )
199
+ if self.api_client:
200
+ # Use ApiClient for typed API calls
201
+ response = await self.api_client.permissions.refresh_permissions(
202
+ token, auth_strategy=auth_strategy
203
+ )
204
+ permissions = response.data.permissions or []
205
+ else:
206
+ # Fallback to HttpClient for backward compatibility
207
+ if auth_strategy is not None:
208
+ permission_result = await self.http_client.authenticated_request(
209
+ "GET",
210
+ "/api/v1/auth/permissions/refresh",
211
+ token,
212
+ auth_strategy=auth_strategy,
213
+ )
214
+ else:
215
+ permission_result = await self.http_client.authenticated_request(
216
+ "GET", "/api/v1/auth/permissions/refresh", token
217
+ )
165
218
 
166
- permission_data = PermissionResult(**permission_result)
167
- permissions = permission_data.permissions or []
219
+ permission_data = PermissionResult(**permission_result)
220
+ permissions = permission_data.permissions or []
168
221
 
169
222
  # Update cache with fresh data (CacheService handles Redis + in-memory automatically)
170
223
  await self.cache.set(
171
224
  cache_key,
172
225
  {"permissions": permissions, "timestamp": int(time.time() * 1000)},
173
- self.permission_ttl
226
+ self.permission_ttl,
174
227
  )
175
228
 
176
229
  return permissions
177
-
178
- except Exception:
179
- # Failed to refresh permissions, return empty list
230
+
231
+ except Exception as error:
232
+ correlation_id = extract_correlation_id_from_error(error)
233
+ logger.error(
234
+ "Failed to refresh permissions",
235
+ exc_info=error,
236
+ extra={"correlationId": correlation_id} if correlation_id else None,
237
+ )
180
238
  return []
181
239
 
182
- async def clear_permissions_cache(self, token: str) -> None:
240
+ async def clear_permissions_cache(
241
+ self, token: str, auth_strategy: Optional[AuthStrategy] = None
242
+ ) -> None:
183
243
  """
184
244
  Clear cached permissions for a user.
185
-
245
+
186
246
  Args:
187
247
  token: JWT token
248
+ auth_strategy: Optional authentication strategy
188
249
  """
189
250
  try:
190
251
  # Get user info to extract userId
191
- user_info = await self.http_client.authenticated_request(
192
- "POST",
193
- "/api/auth/validate",
194
- token
252
+ user_info = await validate_token_request(
253
+ token, self.http_client, self.api_client, auth_strategy
195
254
  )
196
-
197
255
  user_id = user_info.get("user", {}).get("id") if user_info else None
198
256
  if not user_id:
199
257
  return
@@ -202,7 +260,12 @@ class PermissionService:
202
260
 
203
261
  # Clear from cache (CacheService handles Redis + in-memory automatically)
204
262
  await self.cache.delete(cache_key)
205
-
206
- except Exception:
207
- # Failed to clear cache, silently continue
208
- pass
263
+
264
+ except Exception as error:
265
+ correlation_id = extract_correlation_id_from_error(error)
266
+ logger.error(
267
+ "Failed to clear permissions cache",
268
+ exc_info=error,
269
+ extra={"correlationId": correlation_id} if correlation_id else None,
270
+ )
271
+ # Silently continue per service method pattern