miso-client 0.2.0__py3-none-any.whl → 0.5.0__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.
Potentially problematic release.
This version of miso-client might be problematic. Click here for more details.
- miso_client/__init__.py +59 -3
- miso_client/errors.py +22 -1
- miso_client/models/__init__.py +4 -0
- miso_client/models/error_response.py +50 -0
- miso_client/models/filter.py +140 -0
- miso_client/models/pagination.py +66 -0
- miso_client/models/sort.py +25 -0
- miso_client/services/logger.py +7 -6
- miso_client/utils/data_masker.py +77 -5
- miso_client/utils/error_utils.py +104 -0
- miso_client/utils/filter.py +256 -0
- miso_client/utils/http_client.py +517 -212
- miso_client/utils/internal_http_client.py +471 -0
- miso_client/utils/pagination.py +157 -0
- miso_client/utils/sensitive_fields_loader.py +116 -0
- miso_client/utils/sort.py +116 -0
- {miso_client-0.2.0.dist-info → miso_client-0.5.0.dist-info}/METADATA +348 -3
- miso_client-0.5.0.dist-info/RECORD +33 -0
- miso_client-0.2.0.dist-info/RECORD +0 -23
- {miso_client-0.2.0.dist-info → miso_client-0.5.0.dist-info}/WHEEL +0 -0
- {miso_client-0.2.0.dist-info → miso_client-0.5.0.dist-info}/licenses/LICENSE +0 -0
- {miso_client-0.2.0.dist-info → miso_client-0.5.0.dist-info}/top_level.txt +0 -0
miso_client/__init__.py
CHANGED
|
@@ -26,6 +26,10 @@ from .models.config import (
|
|
|
26
26
|
RoleResult,
|
|
27
27
|
UserInfo,
|
|
28
28
|
)
|
|
29
|
+
from .models.error_response import ErrorResponse
|
|
30
|
+
from .models.filter import FilterBuilder, FilterOperator, FilterOption, FilterQuery
|
|
31
|
+
from .models.pagination import Meta, PaginatedListResponse
|
|
32
|
+
from .models.sort import SortOption
|
|
29
33
|
from .services.auth import AuthService
|
|
30
34
|
from .services.cache import CacheService
|
|
31
35
|
from .services.encryption import EncryptionService
|
|
@@ -34,9 +38,19 @@ from .services.permission import PermissionService
|
|
|
34
38
|
from .services.redis import RedisService
|
|
35
39
|
from .services.role import RoleService
|
|
36
40
|
from .utils.config_loader import load_config
|
|
41
|
+
from .utils.error_utils import handle_api_error_snake_case, transform_error_to_snake_case
|
|
42
|
+
from .utils.filter import apply_filters, build_query_string, parse_filter_params
|
|
37
43
|
from .utils.http_client import HttpClient
|
|
44
|
+
from .utils.internal_http_client import InternalHttpClient
|
|
45
|
+
from .utils.pagination import (
|
|
46
|
+
apply_pagination_to_array,
|
|
47
|
+
create_meta_object,
|
|
48
|
+
create_paginated_list_response,
|
|
49
|
+
parse_pagination_params,
|
|
50
|
+
)
|
|
51
|
+
from .utils.sort import build_sort_string, parse_sort_params
|
|
38
52
|
|
|
39
|
-
__version__ = "0.
|
|
53
|
+
__version__ = "0.5.0"
|
|
40
54
|
__author__ = "AI Fabrix Team"
|
|
41
55
|
__license__ = "MIT"
|
|
42
56
|
|
|
@@ -60,14 +74,29 @@ class MisoClient:
|
|
|
60
74
|
config: MisoClient configuration including controller URL, client credentials, etc.
|
|
61
75
|
"""
|
|
62
76
|
self.config = config
|
|
63
|
-
|
|
77
|
+
|
|
78
|
+
# Create InternalHttpClient first (pure HTTP functionality, no logging)
|
|
79
|
+
self._internal_http_client = InternalHttpClient(config)
|
|
80
|
+
|
|
81
|
+
# Create Redis service
|
|
64
82
|
self.redis = RedisService(config.redis)
|
|
83
|
+
|
|
84
|
+
# Create LoggerService with InternalHttpClient (to avoid circular dependency)
|
|
85
|
+
# LoggerService uses InternalHttpClient for sending logs to prevent audit loops
|
|
86
|
+
self.logger = LoggerService(self._internal_http_client, self.redis)
|
|
87
|
+
|
|
88
|
+
# Create public HttpClient wrapping InternalHttpClient with logger
|
|
89
|
+
# This HttpClient adds automatic ISO 27001 compliant audit and debug logging
|
|
90
|
+
self.http_client = HttpClient(config, self.logger)
|
|
91
|
+
|
|
65
92
|
# Cache service (uses Redis if available, falls back to in-memory)
|
|
66
93
|
self.cache = CacheService(self.redis)
|
|
94
|
+
|
|
95
|
+
# Services use public HttpClient (with audit logging)
|
|
67
96
|
self.auth = AuthService(self.http_client, self.redis)
|
|
68
97
|
self.roles = RoleService(self.http_client, self.cache)
|
|
69
98
|
self.permissions = PermissionService(self.http_client, self.cache)
|
|
70
|
-
|
|
99
|
+
|
|
71
100
|
# Encryption service (reads ENCRYPTION_KEY from environment by default)
|
|
72
101
|
self.encryption = EncryptionService()
|
|
73
102
|
self.initialized = False
|
|
@@ -473,6 +502,33 @@ __all__ = [
|
|
|
473
502
|
"ClientTokenResponse",
|
|
474
503
|
"PerformanceMetrics",
|
|
475
504
|
"ClientLoggingOptions",
|
|
505
|
+
"ErrorResponse",
|
|
506
|
+
# Pagination models
|
|
507
|
+
"Meta",
|
|
508
|
+
"PaginatedListResponse",
|
|
509
|
+
# Filter models
|
|
510
|
+
"FilterOperator",
|
|
511
|
+
"FilterOption",
|
|
512
|
+
"FilterQuery",
|
|
513
|
+
"FilterBuilder",
|
|
514
|
+
# Sort models
|
|
515
|
+
"SortOption",
|
|
516
|
+
# Pagination utilities
|
|
517
|
+
"parse_pagination_params",
|
|
518
|
+
"create_meta_object",
|
|
519
|
+
"apply_pagination_to_array",
|
|
520
|
+
"create_paginated_list_response",
|
|
521
|
+
# Filter utilities
|
|
522
|
+
"parse_filter_params",
|
|
523
|
+
"build_query_string",
|
|
524
|
+
"apply_filters",
|
|
525
|
+
# Sort utilities
|
|
526
|
+
"parse_sort_params",
|
|
527
|
+
"build_sort_string",
|
|
528
|
+
# Error utilities
|
|
529
|
+
"transform_error_to_snake_case",
|
|
530
|
+
"handle_api_error_snake_case",
|
|
531
|
+
# Services
|
|
476
532
|
"AuthService",
|
|
477
533
|
"RoleService",
|
|
478
534
|
"PermissionService",
|
miso_client/errors.py
CHANGED
|
@@ -4,12 +4,21 @@ SDK exceptions and error handling.
|
|
|
4
4
|
This module defines custom exceptions for the MisoClient SDK.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from ..models.error_response import ErrorResponse
|
|
11
|
+
|
|
7
12
|
|
|
8
13
|
class MisoClientError(Exception):
|
|
9
14
|
"""Base exception for MisoClient SDK errors."""
|
|
10
15
|
|
|
11
16
|
def __init__(
|
|
12
|
-
self,
|
|
17
|
+
self,
|
|
18
|
+
message: str,
|
|
19
|
+
status_code: int | None = None,
|
|
20
|
+
error_body: dict | None = None,
|
|
21
|
+
error_response: "ErrorResponse | None" = None,
|
|
13
22
|
):
|
|
14
23
|
"""
|
|
15
24
|
Initialize MisoClient error.
|
|
@@ -18,11 +27,23 @@ class MisoClientError(Exception):
|
|
|
18
27
|
message: Error message
|
|
19
28
|
status_code: HTTP status code if applicable
|
|
20
29
|
error_body: Sanitized error response body (secrets masked)
|
|
30
|
+
error_response: Structured error response object (RFC 7807-style)
|
|
21
31
|
"""
|
|
22
32
|
super().__init__(message)
|
|
23
33
|
self.message = message
|
|
24
34
|
self.status_code = status_code
|
|
25
35
|
self.error_body = error_body if error_body is not None else None
|
|
36
|
+
self.error_response = error_response
|
|
37
|
+
|
|
38
|
+
# Enhance message with structured error information if available
|
|
39
|
+
if error_response and error_response.errors:
|
|
40
|
+
if len(error_response.errors) == 1:
|
|
41
|
+
self.message = error_response.errors[0]
|
|
42
|
+
else:
|
|
43
|
+
self.message = f"{error_response.title}: {'; '.join(error_response.errors)}"
|
|
44
|
+
# Override status_code from structured response if available
|
|
45
|
+
if error_response.statusCode:
|
|
46
|
+
self.status_code = error_response.statusCode
|
|
26
47
|
|
|
27
48
|
|
|
28
49
|
class AuthenticationError(MisoClientError):
|
miso_client/models/__init__.py
CHANGED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Structured error response model following RFC 7807-style format.
|
|
3
|
+
|
|
4
|
+
This module provides a generic error response interface that can be used
|
|
5
|
+
across different applications for consistent error handling.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import List, Optional
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, Field
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ErrorResponse(BaseModel):
|
|
14
|
+
"""
|
|
15
|
+
Structured error response following RFC 7807-style format.
|
|
16
|
+
|
|
17
|
+
This model represents a standardized error response structure that includes:
|
|
18
|
+
- Multiple error messages
|
|
19
|
+
- Error type identifier
|
|
20
|
+
- Human-readable title
|
|
21
|
+
- HTTP status code
|
|
22
|
+
- Request instance URI (optional)
|
|
23
|
+
|
|
24
|
+
Example:
|
|
25
|
+
{
|
|
26
|
+
"errors": ["Error message 1", "Error message 2"],
|
|
27
|
+
"type": "/Errors/Bad Input",
|
|
28
|
+
"title": "Bad Request",
|
|
29
|
+
"statusCode": 400,
|
|
30
|
+
"instance": "/OpenApi/rest/Xzy"
|
|
31
|
+
}
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
errors: List[str] = Field(..., description="List of error messages")
|
|
35
|
+
type: str = Field(..., description="Error type URI (e.g., '/Errors/Bad Input')")
|
|
36
|
+
title: Optional[str] = Field(default=None, description="Human-readable error title")
|
|
37
|
+
statusCode: int = Field(..., alias="status_code", description="HTTP status code")
|
|
38
|
+
instance: Optional[str] = Field(default=None, description="Request instance URI")
|
|
39
|
+
request_key: Optional[str] = Field(
|
|
40
|
+
default=None, alias="requestKey", description="Request key for error tracking"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
class Config:
|
|
44
|
+
populate_by_name = True # Allow both camelCase and snake_case
|
|
45
|
+
|
|
46
|
+
# Support snake_case attribute access
|
|
47
|
+
@property
|
|
48
|
+
def status_code(self) -> int:
|
|
49
|
+
"""Get statusCode as status_code (snake_case)."""
|
|
50
|
+
return self.statusCode
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Filter types for MisoClient SDK.
|
|
3
|
+
|
|
4
|
+
This module contains Pydantic models and classes that define filter structures
|
|
5
|
+
for query filtering matching the Miso/Dataplane API conventions.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any, List, Literal, Optional, Union
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, Field
|
|
11
|
+
|
|
12
|
+
FilterOperator = Literal[
|
|
13
|
+
"eq",
|
|
14
|
+
"neq",
|
|
15
|
+
"in",
|
|
16
|
+
"nin",
|
|
17
|
+
"gt",
|
|
18
|
+
"lt",
|
|
19
|
+
"gte",
|
|
20
|
+
"lte",
|
|
21
|
+
"contains",
|
|
22
|
+
"like",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class FilterOption(BaseModel):
|
|
27
|
+
"""
|
|
28
|
+
Single filter option with field, operator, and value.
|
|
29
|
+
|
|
30
|
+
Fields:
|
|
31
|
+
field: Field name to filter on
|
|
32
|
+
op: Filter operator (eq, neq, in, nin, gt, lt, gte, lte, contains, like)
|
|
33
|
+
value: Filter value (supports single values or arrays for 'in'/'nin' operators)
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
field: str = Field(..., description="Field name to filter on")
|
|
37
|
+
op: FilterOperator = Field(..., description="Filter operator")
|
|
38
|
+
value: Union[str, int, float, bool, List[Any]] = Field(
|
|
39
|
+
..., description="Filter value (supports arrays for 'in'/'nin' operators)"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class FilterQuery(BaseModel):
|
|
44
|
+
"""
|
|
45
|
+
Complete filter query with filters, sort, pagination, and field selection.
|
|
46
|
+
|
|
47
|
+
Fields:
|
|
48
|
+
filters: Optional list of filter options
|
|
49
|
+
sort: Optional list of sort options (field strings with optional '-' prefix for desc)
|
|
50
|
+
page: Optional page number (1-based)
|
|
51
|
+
page_size: Optional number of items per page
|
|
52
|
+
fields: Optional list of fields to include in response
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
filters: Optional[List[FilterOption]] = Field(
|
|
56
|
+
default=None, description="List of filter options"
|
|
57
|
+
)
|
|
58
|
+
sort: Optional[List[str]] = Field(
|
|
59
|
+
default=None,
|
|
60
|
+
description="List of sort options (e.g., ['-updated_at', 'created_at'])",
|
|
61
|
+
)
|
|
62
|
+
page: Optional[int] = Field(default=None, description="Page number (1-based)")
|
|
63
|
+
page_size: Optional[int] = Field(default=None, description="Number of items per page")
|
|
64
|
+
fields: Optional[List[str]] = Field(
|
|
65
|
+
default=None, description="List of fields to include in response"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class FilterBuilder:
|
|
70
|
+
"""
|
|
71
|
+
Builder pattern for dynamic filter construction.
|
|
72
|
+
|
|
73
|
+
Allows chaining filter additions for building complex filter queries.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
def __init__(self):
|
|
77
|
+
"""Initialize empty filter builder."""
|
|
78
|
+
self._filters: List[FilterOption] = []
|
|
79
|
+
|
|
80
|
+
def add(self, field: str, op: FilterOperator, value: Any) -> "FilterBuilder":
|
|
81
|
+
"""
|
|
82
|
+
Add a filter option to the builder.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
field: Field name to filter on
|
|
86
|
+
op: Filter operator
|
|
87
|
+
value: Filter value
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
FilterBuilder instance for method chaining
|
|
91
|
+
"""
|
|
92
|
+
self._filters.append(FilterOption(field=field, op=op, value=value))
|
|
93
|
+
return self
|
|
94
|
+
|
|
95
|
+
def add_many(self, filters: List[FilterOption]) -> "FilterBuilder":
|
|
96
|
+
"""
|
|
97
|
+
Add multiple filter options to the builder.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
filters: List of FilterOption objects
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
FilterBuilder instance for method chaining
|
|
104
|
+
"""
|
|
105
|
+
self._filters.extend(filters)
|
|
106
|
+
return self
|
|
107
|
+
|
|
108
|
+
def build(self) -> List[FilterOption]:
|
|
109
|
+
"""
|
|
110
|
+
Build the filter list.
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
List of FilterOption objects
|
|
114
|
+
"""
|
|
115
|
+
return self._filters.copy()
|
|
116
|
+
|
|
117
|
+
def to_query_string(self) -> str:
|
|
118
|
+
"""
|
|
119
|
+
Convert filters to query string format.
|
|
120
|
+
|
|
121
|
+
Format: ?filter=field:op:value&filter=field:op:value
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
Query string with filter parameters
|
|
125
|
+
"""
|
|
126
|
+
if not self._filters:
|
|
127
|
+
return ""
|
|
128
|
+
|
|
129
|
+
query_parts: List[str] = []
|
|
130
|
+
for filter_option in self._filters:
|
|
131
|
+
# Format value for query string
|
|
132
|
+
if isinstance(filter_option.value, list):
|
|
133
|
+
# For arrays (in/nin), join with commas
|
|
134
|
+
value_str = ",".join(str(v) for v in filter_option.value)
|
|
135
|
+
else:
|
|
136
|
+
value_str = str(filter_option.value)
|
|
137
|
+
|
|
138
|
+
query_parts.append(f"filter={filter_option.field}:{filter_option.op}:{value_str}")
|
|
139
|
+
|
|
140
|
+
return "&".join(query_parts)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pagination types for MisoClient SDK.
|
|
3
|
+
|
|
4
|
+
This module contains Pydantic models that define pagination structures
|
|
5
|
+
for paginated list responses matching the Miso/Dataplane API conventions.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Generic, List, TypeVar
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, Field
|
|
11
|
+
|
|
12
|
+
T = TypeVar("T")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Meta(BaseModel):
|
|
16
|
+
"""
|
|
17
|
+
Pagination metadata for list responses.
|
|
18
|
+
|
|
19
|
+
Fields:
|
|
20
|
+
total_items: Total number of items across all pages
|
|
21
|
+
current_page: Current page number (1-based, maps from `page` query param)
|
|
22
|
+
page_size: Number of items per page (maps from `page_size` query param)
|
|
23
|
+
type: Resource type identifier (e.g., 'item', 'user', 'group')
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
total_items: int = Field(..., alias="totalItems", description="Total number of items")
|
|
27
|
+
current_page: int = Field(..., alias="currentPage", description="Current page number (1-based)")
|
|
28
|
+
page_size: int = Field(..., alias="pageSize", description="Number of items per page")
|
|
29
|
+
type: str = Field(..., description="Resource type identifier")
|
|
30
|
+
|
|
31
|
+
class Config:
|
|
32
|
+
populate_by_name = True # Allow both snake_case and camelCase
|
|
33
|
+
|
|
34
|
+
# Support camelCase attribute access
|
|
35
|
+
@property
|
|
36
|
+
def totalItems(self) -> int:
|
|
37
|
+
"""Get total_items as totalItems (camelCase)."""
|
|
38
|
+
return self.total_items
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def currentPage(self) -> int:
|
|
42
|
+
"""Get current_page as currentPage (camelCase)."""
|
|
43
|
+
return self.current_page
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def pageSize(self) -> int:
|
|
47
|
+
"""Get page_size as pageSize (camelCase)."""
|
|
48
|
+
return self.page_size
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class PaginatedListResponse(BaseModel, Generic[T]):
|
|
52
|
+
"""
|
|
53
|
+
Paginated list response structure.
|
|
54
|
+
|
|
55
|
+
Generic type parameter T represents the item type in the data array.
|
|
56
|
+
|
|
57
|
+
Fields:
|
|
58
|
+
meta: Pagination metadata
|
|
59
|
+
data: Array of items for current page
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
meta: Meta = Field(..., description="Pagination metadata")
|
|
63
|
+
data: List[T] = Field(..., description="Array of items for current page")
|
|
64
|
+
|
|
65
|
+
class Config:
|
|
66
|
+
populate_by_name = True # Allow both snake_case and camelCase
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Sort types for MisoClient SDK.
|
|
3
|
+
|
|
4
|
+
This module contains Pydantic models that define sort structures
|
|
5
|
+
for query sorting matching the Miso/Dataplane API conventions.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Literal
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, Field
|
|
11
|
+
|
|
12
|
+
SortOrder = Literal["asc", "desc"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SortOption(BaseModel):
|
|
16
|
+
"""
|
|
17
|
+
Sort option with field and order.
|
|
18
|
+
|
|
19
|
+
Fields:
|
|
20
|
+
field: Field name to sort by
|
|
21
|
+
order: Sort order ('asc' for ascending, 'desc' for descending)
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
field: str = Field(..., description="Field name to sort by")
|
|
25
|
+
order: SortOrder = Field(..., description="Sort order ('asc' or 'desc')")
|
miso_client/services/logger.py
CHANGED
|
@@ -14,23 +14,23 @@ from typing import Any, Dict, Literal, Optional
|
|
|
14
14
|
from ..models.config import ClientLoggingOptions, LogEntry
|
|
15
15
|
from ..services.redis import RedisService
|
|
16
16
|
from ..utils.data_masker import DataMasker
|
|
17
|
-
from ..utils.
|
|
17
|
+
from ..utils.internal_http_client import InternalHttpClient
|
|
18
18
|
from ..utils.jwt_tools import decode_token
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
class LoggerService:
|
|
22
22
|
"""Logger service for application logging and audit events."""
|
|
23
23
|
|
|
24
|
-
def __init__(self,
|
|
24
|
+
def __init__(self, internal_http_client: InternalHttpClient, redis: RedisService):
|
|
25
25
|
"""
|
|
26
26
|
Initialize logger service.
|
|
27
27
|
|
|
28
28
|
Args:
|
|
29
|
-
|
|
29
|
+
internal_http_client: Internal HTTP client instance (used for log sending)
|
|
30
30
|
redis: Redis service instance
|
|
31
31
|
"""
|
|
32
|
-
self.config =
|
|
33
|
-
self.
|
|
32
|
+
self.config = internal_http_client.config
|
|
33
|
+
self.internal_http_client = internal_http_client
|
|
34
34
|
self.redis = redis
|
|
35
35
|
self.mask_sensitive_data = True # Default: mask sensitive data
|
|
36
36
|
self.correlation_counter = 0
|
|
@@ -357,12 +357,13 @@ class LoggerService:
|
|
|
357
357
|
return # Successfully queued in Redis
|
|
358
358
|
|
|
359
359
|
# Fallback to unified logging endpoint with client credentials
|
|
360
|
+
# Use InternalHttpClient to avoid circular dependency with HttpClient
|
|
360
361
|
try:
|
|
361
362
|
# Backend extracts environment and application from client credentials
|
|
362
363
|
log_payload = log_entry.model_dump(
|
|
363
364
|
exclude={"environment", "application"}, exclude_none=True
|
|
364
365
|
)
|
|
365
|
-
await self.
|
|
366
|
+
await self.internal_http_client.request("POST", "/api/logs", log_payload)
|
|
366
367
|
except Exception:
|
|
367
368
|
# Failed to send log to controller
|
|
368
369
|
# Silently fail to avoid infinite logging loops
|
miso_client/utils/data_masker.py
CHANGED
|
@@ -5,7 +5,9 @@ Implements ISO 27001 data protection controls by masking sensitive fields
|
|
|
5
5
|
in log entries and context data.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
from typing import Any, Set
|
|
8
|
+
from typing import Any, Optional, Set
|
|
9
|
+
|
|
10
|
+
from .sensitive_fields_loader import get_sensitive_fields_array
|
|
9
11
|
|
|
10
12
|
|
|
11
13
|
class DataMasker:
|
|
@@ -13,8 +15,8 @@ class DataMasker:
|
|
|
13
15
|
|
|
14
16
|
MASKED_VALUE = "***MASKED***"
|
|
15
17
|
|
|
16
|
-
#
|
|
17
|
-
|
|
18
|
+
# Hardcoded set of sensitive field names (normalized) - fallback if JSON cannot be loaded
|
|
19
|
+
_hardcoded_sensitive_fields: Set[str] = {
|
|
18
20
|
"password",
|
|
19
21
|
"passwd",
|
|
20
22
|
"pwd",
|
|
@@ -38,6 +40,73 @@ class DataMasker:
|
|
|
38
40
|
"secretkey",
|
|
39
41
|
}
|
|
40
42
|
|
|
43
|
+
# Cached merged sensitive fields (loaded on first use)
|
|
44
|
+
_sensitive_fields: Optional[Set[str]] = None
|
|
45
|
+
_config_loaded: bool = False
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def _load_config(cls, config_path: Optional[str] = None) -> None:
|
|
49
|
+
"""
|
|
50
|
+
Load sensitive fields configuration from JSON and merge with hardcoded defaults.
|
|
51
|
+
|
|
52
|
+
This method is called automatically on first use. It loads JSON configuration
|
|
53
|
+
and merges it with hardcoded defaults, ensuring backward compatibility.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
config_path: Optional custom path to JSON config file
|
|
57
|
+
"""
|
|
58
|
+
if cls._config_loaded:
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
# Start with hardcoded fields as base
|
|
62
|
+
merged_fields = set(cls._hardcoded_sensitive_fields)
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
# Try to load fields from JSON configuration
|
|
66
|
+
json_fields = get_sensitive_fields_array(config_path)
|
|
67
|
+
if json_fields:
|
|
68
|
+
# Normalize and add JSON fields (same normalization as hardcoded fields)
|
|
69
|
+
for field in json_fields:
|
|
70
|
+
if isinstance(field, str):
|
|
71
|
+
# Normalize: lowercase and remove underscores/hyphens
|
|
72
|
+
normalized = field.lower().replace("_", "").replace("-", "")
|
|
73
|
+
merged_fields.add(normalized)
|
|
74
|
+
except Exception:
|
|
75
|
+
# If JSON loading fails, fall back to hardcoded fields only
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
cls._sensitive_fields = merged_fields
|
|
79
|
+
cls._config_loaded = True
|
|
80
|
+
|
|
81
|
+
@classmethod
|
|
82
|
+
def _get_sensitive_fields(cls) -> Set[str]:
|
|
83
|
+
"""
|
|
84
|
+
Get the set of sensitive fields (loads config on first call).
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
Set of normalized sensitive field names
|
|
88
|
+
"""
|
|
89
|
+
if not cls._config_loaded:
|
|
90
|
+
cls._load_config()
|
|
91
|
+
assert cls._sensitive_fields is not None
|
|
92
|
+
return cls._sensitive_fields
|
|
93
|
+
|
|
94
|
+
@classmethod
|
|
95
|
+
def set_config_path(cls, config_path: str) -> None:
|
|
96
|
+
"""
|
|
97
|
+
Set custom path for sensitive fields configuration.
|
|
98
|
+
|
|
99
|
+
Must be called before first use of DataMasker methods if custom path is needed.
|
|
100
|
+
Otherwise, default path or environment variable will be used.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
config_path: Path to JSON configuration file
|
|
104
|
+
"""
|
|
105
|
+
# Reset cache to force reload with new path
|
|
106
|
+
cls._config_loaded = False
|
|
107
|
+
cls._sensitive_fields = None
|
|
108
|
+
cls._load_config(config_path)
|
|
109
|
+
|
|
41
110
|
@classmethod
|
|
42
111
|
def is_sensitive_field(cls, key: str) -> bool:
|
|
43
112
|
"""
|
|
@@ -52,12 +121,15 @@ class DataMasker:
|
|
|
52
121
|
# Normalize key: lowercase and remove underscores/hyphens
|
|
53
122
|
normalized_key = key.lower().replace("_", "").replace("-", "")
|
|
54
123
|
|
|
124
|
+
# Get sensitive fields (loads config on first use)
|
|
125
|
+
sensitive_fields = cls._get_sensitive_fields()
|
|
126
|
+
|
|
55
127
|
# Check exact match
|
|
56
|
-
if normalized_key in
|
|
128
|
+
if normalized_key in sensitive_fields:
|
|
57
129
|
return True
|
|
58
130
|
|
|
59
131
|
# Check if field contains sensitive keywords
|
|
60
|
-
for sensitive_field in
|
|
132
|
+
for sensitive_field in sensitive_fields:
|
|
61
133
|
if sensitive_field in normalized_key:
|
|
62
134
|
return True
|
|
63
135
|
|