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,92 @@
|
|
|
1
|
+
"""
|
|
2
|
+
HTTP error handler utilities for InternalHttpClient.
|
|
3
|
+
|
|
4
|
+
This module provides error parsing and handling functionality for HTTP responses.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
from ..models.error_response import ErrorResponse
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def extract_correlation_id_from_response(
|
|
15
|
+
response: Optional[httpx.Response] = None,
|
|
16
|
+
) -> Optional[str]:
|
|
17
|
+
"""
|
|
18
|
+
Extract correlation ID from response headers.
|
|
19
|
+
|
|
20
|
+
Checks common correlation ID header names.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
response: HTTP response object (optional)
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
Correlation ID string if found, None otherwise
|
|
27
|
+
"""
|
|
28
|
+
if not response:
|
|
29
|
+
return None
|
|
30
|
+
|
|
31
|
+
# Check common correlation ID header names (case-insensitive)
|
|
32
|
+
correlation_headers = [
|
|
33
|
+
"x-correlation-id",
|
|
34
|
+
"x-request-id",
|
|
35
|
+
"correlation-id",
|
|
36
|
+
"correlationId",
|
|
37
|
+
"x-correlationid",
|
|
38
|
+
"request-id",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
for header_name in correlation_headers:
|
|
42
|
+
correlation_id = response.headers.get(header_name) or response.headers.get(
|
|
43
|
+
header_name.lower()
|
|
44
|
+
)
|
|
45
|
+
if correlation_id:
|
|
46
|
+
return str(correlation_id)
|
|
47
|
+
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def parse_error_response(response: httpx.Response, url: str) -> Optional[ErrorResponse]:
|
|
52
|
+
"""
|
|
53
|
+
Parse structured error response from HTTP response.
|
|
54
|
+
|
|
55
|
+
Extracts correlation ID from response headers if not present in response body.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
response: HTTP response object
|
|
59
|
+
url: Request URL (used for instance URI if not in response)
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
ErrorResponse if response matches structure, None otherwise
|
|
63
|
+
"""
|
|
64
|
+
if not response.headers.get("content-type", "").startswith("application/json"):
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
response_data = response.json()
|
|
69
|
+
# Check if response matches ErrorResponse structure
|
|
70
|
+
if (
|
|
71
|
+
isinstance(response_data, dict)
|
|
72
|
+
and "errors" in response_data
|
|
73
|
+
and "type" in response_data
|
|
74
|
+
and "title" in response_data
|
|
75
|
+
and "statusCode" in response_data
|
|
76
|
+
):
|
|
77
|
+
# Set instance from URL if not provided
|
|
78
|
+
if "instance" not in response_data or not response_data["instance"]:
|
|
79
|
+
response_data["instance"] = url
|
|
80
|
+
|
|
81
|
+
# Extract correlation ID from headers if not present in response body
|
|
82
|
+
if "correlationId" not in response_data or not response_data["correlationId"]:
|
|
83
|
+
correlation_id = extract_correlation_id_from_response(response)
|
|
84
|
+
if correlation_id:
|
|
85
|
+
response_data["correlationId"] = correlation_id
|
|
86
|
+
|
|
87
|
+
return ErrorResponse(**response_data)
|
|
88
|
+
except (ValueError, TypeError, KeyError):
|
|
89
|
+
# JSON parsing failed or structure doesn't match
|
|
90
|
+
pass
|
|
91
|
+
|
|
92
|
+
return None
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""
|
|
2
|
+
HTTP log formatting utilities for ISO 27001 compliant audit and debug logging.
|
|
3
|
+
|
|
4
|
+
This module provides formatting functions for building audit and debug log contexts.
|
|
5
|
+
All sensitive data should be masked before passing to these formatters.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any, Dict, Optional
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _add_optional_fields(context: Dict[str, Any], **fields: Any) -> None:
|
|
12
|
+
"""
|
|
13
|
+
Add optional fields to context dictionary if they are not None.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
context: Context dictionary to add fields to
|
|
17
|
+
**fields: Optional fields to add (value is None if field should be skipped)
|
|
18
|
+
"""
|
|
19
|
+
for key, value in fields.items():
|
|
20
|
+
if value is not None:
|
|
21
|
+
context[key] = value
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def build_audit_context(
|
|
25
|
+
method: str,
|
|
26
|
+
url: str,
|
|
27
|
+
status_code: Optional[int],
|
|
28
|
+
duration_ms: int,
|
|
29
|
+
user_id: Optional[str],
|
|
30
|
+
request_size: Optional[int],
|
|
31
|
+
response_size: Optional[int],
|
|
32
|
+
error_message: Optional[str],
|
|
33
|
+
correlation_id: Optional[str] = None,
|
|
34
|
+
) -> Dict[str, Any]:
|
|
35
|
+
"""
|
|
36
|
+
Build audit context dictionary for logging.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
method: HTTP method
|
|
40
|
+
url: Request URL
|
|
41
|
+
status_code: HTTP status code
|
|
42
|
+
duration_ms: Request duration in milliseconds
|
|
43
|
+
user_id: User ID if available
|
|
44
|
+
request_size: Request size in bytes (optional)
|
|
45
|
+
response_size: Response size in bytes (optional)
|
|
46
|
+
error_message: Error message if request failed (optional)
|
|
47
|
+
correlation_id: Correlation ID if available (optional)
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Audit context dictionary
|
|
51
|
+
"""
|
|
52
|
+
audit_context: Dict[str, Any] = {
|
|
53
|
+
"method": method,
|
|
54
|
+
"url": url,
|
|
55
|
+
"statusCode": status_code,
|
|
56
|
+
"duration": duration_ms,
|
|
57
|
+
}
|
|
58
|
+
_add_optional_fields(
|
|
59
|
+
audit_context,
|
|
60
|
+
userId=user_id,
|
|
61
|
+
requestSize=request_size,
|
|
62
|
+
responseSize=response_size,
|
|
63
|
+
error=error_message,
|
|
64
|
+
correlationId=correlation_id,
|
|
65
|
+
)
|
|
66
|
+
return audit_context
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def build_debug_context(
|
|
70
|
+
method: str,
|
|
71
|
+
url: str,
|
|
72
|
+
status_code: Optional[int],
|
|
73
|
+
duration_ms: int,
|
|
74
|
+
base_url: str,
|
|
75
|
+
user_id: Optional[str],
|
|
76
|
+
masked_headers: Optional[Dict[str, Any]],
|
|
77
|
+
masked_body: Optional[Any],
|
|
78
|
+
masked_response: Optional[str],
|
|
79
|
+
query_params: Optional[Dict[str, Any]],
|
|
80
|
+
) -> Dict[str, Any]:
|
|
81
|
+
"""
|
|
82
|
+
Build debug context dictionary for detailed logging.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
method: HTTP method
|
|
86
|
+
url: Request URL
|
|
87
|
+
status_code: HTTP status code
|
|
88
|
+
duration_ms: Request duration in milliseconds
|
|
89
|
+
base_url: Base URL from config
|
|
90
|
+
user_id: User ID if available
|
|
91
|
+
masked_headers: Masked request headers
|
|
92
|
+
masked_body: Masked request body
|
|
93
|
+
masked_response: Masked response body
|
|
94
|
+
query_params: Masked query parameters
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Debug context dictionary
|
|
98
|
+
"""
|
|
99
|
+
debug_context: Dict[str, Any] = {
|
|
100
|
+
"method": method,
|
|
101
|
+
"url": url,
|
|
102
|
+
"statusCode": status_code,
|
|
103
|
+
"duration": duration_ms,
|
|
104
|
+
"baseURL": base_url,
|
|
105
|
+
"timeout": 30.0, # Default timeout
|
|
106
|
+
}
|
|
107
|
+
_add_optional_fields(
|
|
108
|
+
debug_context,
|
|
109
|
+
userId=user_id,
|
|
110
|
+
requestHeaders=masked_headers,
|
|
111
|
+
requestBody=masked_body,
|
|
112
|
+
responseBody=masked_response,
|
|
113
|
+
queryParams=query_params,
|
|
114
|
+
)
|
|
115
|
+
return debug_context
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""
|
|
2
|
+
HTTP log data masking utilities for ISO 27001 compliant logging.
|
|
3
|
+
|
|
4
|
+
This module provides data masking functions specifically for HTTP request/response
|
|
5
|
+
logging. All sensitive data is masked using DataMasker before logging.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any, Dict, Optional
|
|
9
|
+
from urllib.parse import parse_qs, urlparse
|
|
10
|
+
|
|
11
|
+
from .data_masker import DataMasker
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def mask_error_message(error: Exception) -> Optional[str]:
|
|
15
|
+
"""
|
|
16
|
+
Mask sensitive data in error message.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
error: Exception object
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
Masked error message string, or None if no error
|
|
23
|
+
"""
|
|
24
|
+
if error is None:
|
|
25
|
+
return None
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
error_message = str(error)
|
|
29
|
+
# Mask if error message contains sensitive keywords
|
|
30
|
+
if isinstance(error_message, str) and any(
|
|
31
|
+
keyword in error_message.lower() for keyword in ["password", "token", "secret", "key"]
|
|
32
|
+
):
|
|
33
|
+
return DataMasker.MASKED_VALUE
|
|
34
|
+
return error_message
|
|
35
|
+
except Exception:
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def mask_request_data(
|
|
40
|
+
request_headers: Optional[Dict[str, Any]], request_data: Optional[Dict[str, Any]]
|
|
41
|
+
) -> tuple[Optional[Dict[str, Any]], Optional[Any]]:
|
|
42
|
+
"""
|
|
43
|
+
Mask sensitive data in request headers and body.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
request_headers: Request headers dictionary
|
|
47
|
+
request_data: Request body data
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Tuple of (masked_headers, masked_body)
|
|
51
|
+
"""
|
|
52
|
+
masked_headers: Optional[Dict[str, Any]] = None
|
|
53
|
+
if request_headers:
|
|
54
|
+
masked_headers = DataMasker.mask_sensitive_data(request_headers)
|
|
55
|
+
|
|
56
|
+
masked_body: Optional[Any] = None
|
|
57
|
+
if request_data is not None:
|
|
58
|
+
masked_body = DataMasker.mask_sensitive_data(request_data)
|
|
59
|
+
|
|
60
|
+
return masked_headers, masked_body
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def extract_and_mask_query_params(url: str) -> Optional[Dict[str, Any]]:
|
|
64
|
+
"""
|
|
65
|
+
Extract query parameters from URL and mask sensitive data.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
url: Request URL with query string
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
Masked query parameters dictionary, or None if no query params
|
|
72
|
+
"""
|
|
73
|
+
try:
|
|
74
|
+
parsed_url = urlparse(url)
|
|
75
|
+
if not parsed_url.query:
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
query_dict = parse_qs(parsed_url.query)
|
|
79
|
+
# Convert lists to single values for simplicity
|
|
80
|
+
query_simple: Dict[str, Any] = {
|
|
81
|
+
k: v[0] if len(v) == 1 else v for k, v in query_dict.items()
|
|
82
|
+
}
|
|
83
|
+
masked = DataMasker.mask_sensitive_data(query_simple)
|
|
84
|
+
return masked if isinstance(masked, dict) else None
|
|
85
|
+
except Exception:
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def estimate_object_size(obj: Any) -> int:
|
|
90
|
+
"""
|
|
91
|
+
Quick size estimation without full JSON serialization.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
obj: Object to estimate size for
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Estimated size in bytes
|
|
98
|
+
"""
|
|
99
|
+
if obj is None:
|
|
100
|
+
return 0
|
|
101
|
+
|
|
102
|
+
if isinstance(obj, str):
|
|
103
|
+
return len(obj.encode("utf-8"))
|
|
104
|
+
|
|
105
|
+
if not isinstance(obj, (dict, list)):
|
|
106
|
+
return 10 # Estimate for primitives
|
|
107
|
+
|
|
108
|
+
if isinstance(obj, list):
|
|
109
|
+
if len(obj) == 0:
|
|
110
|
+
return 10
|
|
111
|
+
# Sample first few items for estimation
|
|
112
|
+
sample_size = min(3, len(obj))
|
|
113
|
+
estimated_item_size = sum(estimate_object_size(item) for item in obj[:sample_size])
|
|
114
|
+
avg_item_size = estimated_item_size / sample_size if sample_size > 0 else 100
|
|
115
|
+
return int(len(obj) * avg_item_size)
|
|
116
|
+
|
|
117
|
+
# Object: estimate based on property count and values
|
|
118
|
+
size = 0
|
|
119
|
+
for key, value in obj.items():
|
|
120
|
+
size += len(str(key).encode("utf-8")) + estimate_object_size(value)
|
|
121
|
+
return size
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def truncate_response_body(body: Any, max_size: int = 10000) -> tuple[Any, bool]:
|
|
125
|
+
"""
|
|
126
|
+
Truncate response body to reduce processing cost.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
body: Response body to truncate
|
|
130
|
+
max_size: Maximum size in bytes
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
Tuple of (truncated_data, was_truncated)
|
|
134
|
+
"""
|
|
135
|
+
if body is None:
|
|
136
|
+
return body, False
|
|
137
|
+
|
|
138
|
+
# For strings, truncate directly
|
|
139
|
+
if isinstance(body, str):
|
|
140
|
+
body_bytes = body.encode("utf-8")
|
|
141
|
+
if len(body_bytes) <= max_size:
|
|
142
|
+
return body, False
|
|
143
|
+
truncated = body_bytes[:max_size].decode("utf-8", errors="ignore") + "..."
|
|
144
|
+
return truncated, True
|
|
145
|
+
|
|
146
|
+
# For objects/arrays, estimate size first
|
|
147
|
+
estimated_size = estimate_object_size(body)
|
|
148
|
+
if estimated_size <= max_size:
|
|
149
|
+
return body, False
|
|
150
|
+
|
|
151
|
+
# If estimated size is too large, return placeholder
|
|
152
|
+
return {
|
|
153
|
+
"_message": "Response body too large, truncated for performance",
|
|
154
|
+
"_estimatedSize": estimated_size,
|
|
155
|
+
}, True
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def mask_response_data(
|
|
159
|
+
response: Optional[Any], max_size: Optional[int] = None, max_masking_size: Optional[int] = None
|
|
160
|
+
) -> Optional[str]:
|
|
161
|
+
"""
|
|
162
|
+
Mask sensitive data in response body and limit size.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
response: Response data
|
|
166
|
+
max_size: Maximum size before truncation (default: 10000)
|
|
167
|
+
max_masking_size: Maximum size before skipping masking (default: 50000)
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
Masked response body as string, or None
|
|
171
|
+
"""
|
|
172
|
+
if response is None:
|
|
173
|
+
return None
|
|
174
|
+
|
|
175
|
+
max_size = max_size or 10000
|
|
176
|
+
max_masking_size = max_masking_size or 50000
|
|
177
|
+
|
|
178
|
+
try:
|
|
179
|
+
# Check if we should skip masking due to size
|
|
180
|
+
estimated_size = estimate_object_size(response)
|
|
181
|
+
if estimated_size > max_masking_size:
|
|
182
|
+
return str({" _message": "Response body too large, masking skipped"})
|
|
183
|
+
|
|
184
|
+
# Truncate if needed
|
|
185
|
+
truncated_body, was_truncated = truncate_response_body(response, max_size)
|
|
186
|
+
|
|
187
|
+
# Mask sensitive data
|
|
188
|
+
try:
|
|
189
|
+
if isinstance(truncated_body, dict):
|
|
190
|
+
masked_dict = DataMasker.mask_sensitive_data(truncated_body)
|
|
191
|
+
result = str(masked_dict)
|
|
192
|
+
if was_truncated and len(result) > 1000:
|
|
193
|
+
result = result[:1000] + "..."
|
|
194
|
+
return result
|
|
195
|
+
elif isinstance(truncated_body, str):
|
|
196
|
+
# Already truncated string
|
|
197
|
+
return truncated_body
|
|
198
|
+
else:
|
|
199
|
+
return str(truncated_body)
|
|
200
|
+
except Exception:
|
|
201
|
+
return str(truncated_body) if was_truncated else str(response)
|
|
202
|
+
except Exception:
|
|
203
|
+
return None
|