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,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
|
-
|
|
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__(
|
|
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(
|
|
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
|
|
60
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
77
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
148
|
-
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
167
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
192
|
-
|
|
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
|
-
|
|
208
|
-
|
|
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
|