crackerjack 0.31.10__py3-none-any.whl → 0.31.12__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 crackerjack might be problematic. Click here for more details.
- crackerjack/CLAUDE.md +288 -705
- crackerjack/__main__.py +22 -8
- crackerjack/agents/__init__.py +0 -3
- crackerjack/agents/architect_agent.py +0 -43
- crackerjack/agents/base.py +1 -9
- crackerjack/agents/coordinator.py +2 -148
- crackerjack/agents/documentation_agent.py +109 -81
- crackerjack/agents/dry_agent.py +122 -97
- crackerjack/agents/formatting_agent.py +3 -16
- crackerjack/agents/import_optimization_agent.py +1174 -130
- crackerjack/agents/performance_agent.py +956 -188
- crackerjack/agents/performance_helpers.py +229 -0
- crackerjack/agents/proactive_agent.py +1 -48
- crackerjack/agents/refactoring_agent.py +516 -246
- crackerjack/agents/refactoring_helpers.py +282 -0
- crackerjack/agents/security_agent.py +393 -90
- crackerjack/agents/test_creation_agent.py +1776 -120
- crackerjack/agents/test_specialist_agent.py +59 -15
- crackerjack/agents/tracker.py +0 -102
- crackerjack/api.py +145 -37
- crackerjack/cli/handlers.py +48 -30
- crackerjack/cli/interactive.py +11 -11
- crackerjack/cli/options.py +66 -4
- crackerjack/code_cleaner.py +808 -148
- crackerjack/config/global_lock_config.py +110 -0
- crackerjack/config/hooks.py +43 -64
- crackerjack/core/async_workflow_orchestrator.py +247 -97
- crackerjack/core/autofix_coordinator.py +192 -109
- crackerjack/core/enhanced_container.py +46 -63
- crackerjack/core/file_lifecycle.py +549 -0
- crackerjack/core/performance.py +9 -8
- crackerjack/core/performance_monitor.py +395 -0
- crackerjack/core/phase_coordinator.py +281 -94
- crackerjack/core/proactive_workflow.py +9 -58
- crackerjack/core/resource_manager.py +501 -0
- crackerjack/core/service_watchdog.py +490 -0
- crackerjack/core/session_coordinator.py +4 -8
- crackerjack/core/timeout_manager.py +504 -0
- crackerjack/core/websocket_lifecycle.py +475 -0
- crackerjack/core/workflow_orchestrator.py +343 -209
- crackerjack/dynamic_config.py +47 -6
- crackerjack/errors.py +3 -4
- crackerjack/executors/async_hook_executor.py +63 -13
- crackerjack/executors/cached_hook_executor.py +14 -14
- crackerjack/executors/hook_executor.py +100 -37
- crackerjack/executors/hook_lock_manager.py +856 -0
- crackerjack/executors/individual_hook_executor.py +120 -86
- crackerjack/intelligence/__init__.py +0 -7
- crackerjack/intelligence/adaptive_learning.py +13 -86
- crackerjack/intelligence/agent_orchestrator.py +15 -78
- crackerjack/intelligence/agent_registry.py +12 -59
- crackerjack/intelligence/agent_selector.py +31 -92
- crackerjack/intelligence/integration.py +1 -41
- crackerjack/interactive.py +9 -9
- crackerjack/managers/async_hook_manager.py +25 -8
- crackerjack/managers/hook_manager.py +9 -9
- crackerjack/managers/publish_manager.py +57 -59
- crackerjack/managers/test_command_builder.py +6 -36
- crackerjack/managers/test_executor.py +9 -61
- crackerjack/managers/test_manager.py +17 -63
- crackerjack/managers/test_manager_backup.py +77 -127
- crackerjack/managers/test_progress.py +4 -23
- crackerjack/mcp/cache.py +5 -12
- crackerjack/mcp/client_runner.py +10 -10
- crackerjack/mcp/context.py +64 -6
- crackerjack/mcp/dashboard.py +14 -11
- crackerjack/mcp/enhanced_progress_monitor.py +55 -55
- crackerjack/mcp/file_monitor.py +72 -42
- crackerjack/mcp/progress_components.py +103 -84
- crackerjack/mcp/progress_monitor.py +122 -49
- crackerjack/mcp/rate_limiter.py +12 -12
- crackerjack/mcp/server_core.py +16 -22
- crackerjack/mcp/service_watchdog.py +26 -26
- crackerjack/mcp/state.py +15 -0
- crackerjack/mcp/tools/core_tools.py +95 -39
- crackerjack/mcp/tools/error_analyzer.py +6 -32
- crackerjack/mcp/tools/execution_tools.py +1 -56
- crackerjack/mcp/tools/execution_tools_backup.py +35 -131
- crackerjack/mcp/tools/intelligence_tool_registry.py +0 -36
- crackerjack/mcp/tools/intelligence_tools.py +2 -55
- crackerjack/mcp/tools/monitoring_tools.py +308 -145
- crackerjack/mcp/tools/proactive_tools.py +12 -42
- crackerjack/mcp/tools/progress_tools.py +23 -15
- crackerjack/mcp/tools/utility_tools.py +3 -40
- crackerjack/mcp/tools/workflow_executor.py +40 -60
- crackerjack/mcp/websocket/app.py +0 -3
- crackerjack/mcp/websocket/endpoints.py +206 -268
- crackerjack/mcp/websocket/jobs.py +213 -66
- crackerjack/mcp/websocket/server.py +84 -6
- crackerjack/mcp/websocket/websocket_handler.py +137 -29
- crackerjack/models/config_adapter.py +3 -16
- crackerjack/models/protocols.py +162 -3
- crackerjack/models/resource_protocols.py +454 -0
- crackerjack/models/task.py +3 -3
- crackerjack/monitoring/__init__.py +0 -0
- crackerjack/monitoring/ai_agent_watchdog.py +25 -71
- crackerjack/monitoring/regression_prevention.py +28 -87
- crackerjack/orchestration/advanced_orchestrator.py +44 -78
- crackerjack/orchestration/coverage_improvement.py +10 -60
- crackerjack/orchestration/execution_strategies.py +16 -16
- crackerjack/orchestration/test_progress_streamer.py +61 -53
- crackerjack/plugins/base.py +1 -1
- crackerjack/plugins/managers.py +22 -20
- crackerjack/py313.py +65 -21
- crackerjack/services/backup_service.py +467 -0
- crackerjack/services/bounded_status_operations.py +627 -0
- crackerjack/services/cache.py +7 -9
- crackerjack/services/config.py +35 -52
- crackerjack/services/config_integrity.py +5 -16
- crackerjack/services/config_merge.py +542 -0
- crackerjack/services/contextual_ai_assistant.py +17 -19
- crackerjack/services/coverage_ratchet.py +44 -73
- crackerjack/services/debug.py +25 -39
- crackerjack/services/dependency_monitor.py +52 -50
- crackerjack/services/enhanced_filesystem.py +14 -11
- crackerjack/services/file_hasher.py +1 -1
- crackerjack/services/filesystem.py +1 -12
- crackerjack/services/git.py +71 -47
- crackerjack/services/health_metrics.py +31 -27
- crackerjack/services/initialization.py +276 -428
- crackerjack/services/input_validator.py +760 -0
- crackerjack/services/log_manager.py +16 -16
- crackerjack/services/logging.py +7 -6
- crackerjack/services/metrics.py +43 -43
- crackerjack/services/pattern_cache.py +2 -31
- crackerjack/services/pattern_detector.py +26 -63
- crackerjack/services/performance_benchmarks.py +20 -45
- crackerjack/services/regex_patterns.py +2887 -0
- crackerjack/services/regex_utils.py +537 -0
- crackerjack/services/secure_path_utils.py +683 -0
- crackerjack/services/secure_status_formatter.py +534 -0
- crackerjack/services/secure_subprocess.py +605 -0
- crackerjack/services/security.py +47 -10
- crackerjack/services/security_logger.py +492 -0
- crackerjack/services/server_manager.py +109 -50
- crackerjack/services/smart_scheduling.py +8 -25
- crackerjack/services/status_authentication.py +603 -0
- crackerjack/services/status_security_manager.py +442 -0
- crackerjack/services/thread_safe_status_collector.py +546 -0
- crackerjack/services/tool_version_service.py +1 -23
- crackerjack/services/unified_config.py +36 -58
- crackerjack/services/validation_rate_limiter.py +269 -0
- crackerjack/services/version_checker.py +9 -40
- crackerjack/services/websocket_resource_limiter.py +572 -0
- crackerjack/slash_commands/__init__.py +52 -2
- crackerjack/tools/__init__.py +0 -0
- crackerjack/tools/validate_input_validator_patterns.py +262 -0
- crackerjack/tools/validate_regex_patterns.py +198 -0
- {crackerjack-0.31.10.dist-info → crackerjack-0.31.12.dist-info}/METADATA +197 -12
- crackerjack-0.31.12.dist-info/RECORD +178 -0
- crackerjack/cli/facade.py +0 -104
- crackerjack-0.31.10.dist-info/RECORD +0 -149
- {crackerjack-0.31.10.dist-info → crackerjack-0.31.12.dist-info}/WHEEL +0 -0
- {crackerjack-0.31.10.dist-info → crackerjack-0.31.12.dist-info}/entry_points.txt +0 -0
- {crackerjack-0.31.10.dist-info → crackerjack-0.31.12.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,603 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Status Authentication System for secure access control.
|
|
3
|
+
|
|
4
|
+
Provides authentication and authorization for status endpoints with
|
|
5
|
+
JWT tokens, API keys, role-based access control, and audit logging.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import hashlib
|
|
9
|
+
import hmac
|
|
10
|
+
import json
|
|
11
|
+
import secrets
|
|
12
|
+
import time
|
|
13
|
+
import typing as t
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from enum import Enum
|
|
16
|
+
|
|
17
|
+
from .security_logger import SecurityEventLevel, SecurityEventType, get_security_logger
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AccessLevel(str, Enum):
|
|
21
|
+
"""Access levels for status endpoints."""
|
|
22
|
+
|
|
23
|
+
PUBLIC = "public" # Basic system status
|
|
24
|
+
INTERNAL = "internal" # Internal service status
|
|
25
|
+
ADMIN = "admin" # Full administrative access
|
|
26
|
+
DEBUG = "debug" # Debug-level information
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class AuthenticationMethod(str, Enum):
|
|
30
|
+
"""Supported authentication methods."""
|
|
31
|
+
|
|
32
|
+
API_KEY = "api_key"
|
|
33
|
+
JWT_TOKEN = "jwt_token"
|
|
34
|
+
HMAC_SIGNATURE = "hmac_signature"
|
|
35
|
+
LOCAL_ONLY = "local_only"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class AuthCredentials:
|
|
40
|
+
"""Authentication credentials for status access."""
|
|
41
|
+
|
|
42
|
+
client_id: str
|
|
43
|
+
access_level: AccessLevel
|
|
44
|
+
method: AuthenticationMethod
|
|
45
|
+
expires_at: float | None = None
|
|
46
|
+
allowed_operations: set[str] | None = None
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def is_expired(self) -> bool:
|
|
50
|
+
"""Check if credentials are expired."""
|
|
51
|
+
return self.expires_at is not None and time.time() > self.expires_at
|
|
52
|
+
|
|
53
|
+
def has_operation_access(self, operation: str) -> bool:
|
|
54
|
+
"""Check if credentials allow specific operation."""
|
|
55
|
+
return self.allowed_operations is None or operation in self.allowed_operations
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class AuthenticationError(Exception):
|
|
59
|
+
"""Base authentication error."""
|
|
60
|
+
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class AccessDeniedError(AuthenticationError):
|
|
65
|
+
"""Access denied error."""
|
|
66
|
+
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class ExpiredCredentialsError(AuthenticationError):
|
|
71
|
+
"""Expired credentials error."""
|
|
72
|
+
|
|
73
|
+
pass
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class StatusAuthenticator:
|
|
77
|
+
"""
|
|
78
|
+
Authentication system for status endpoints.
|
|
79
|
+
|
|
80
|
+
Features:
|
|
81
|
+
- Multiple authentication methods (API keys, JWT, HMAC)
|
|
82
|
+
- Role-based access control
|
|
83
|
+
- Time-based credential expiration
|
|
84
|
+
- Operation-level permissions
|
|
85
|
+
- Comprehensive audit logging
|
|
86
|
+
- Local-only access mode for development
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
def __init__(
|
|
90
|
+
self,
|
|
91
|
+
secret_key: str | None = None,
|
|
92
|
+
default_access_level: AccessLevel = AccessLevel.PUBLIC,
|
|
93
|
+
enable_local_only: bool = True,
|
|
94
|
+
):
|
|
95
|
+
"""
|
|
96
|
+
Initialize status authenticator.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
secret_key: Secret key for HMAC and JWT validation
|
|
100
|
+
default_access_level: Default access level for unauthenticated requests
|
|
101
|
+
enable_local_only: Allow local-only access for development
|
|
102
|
+
"""
|
|
103
|
+
self.secret_key = secret_key or self._generate_secret_key()
|
|
104
|
+
self.default_access_level = default_access_level
|
|
105
|
+
self.enable_local_only = enable_local_only
|
|
106
|
+
|
|
107
|
+
self.security_logger = get_security_logger()
|
|
108
|
+
|
|
109
|
+
# Valid API keys and their associated credentials
|
|
110
|
+
self._api_keys: dict[str, AuthCredentials] = {}
|
|
111
|
+
|
|
112
|
+
# Access level requirements for operations
|
|
113
|
+
self._operation_requirements: dict[str, AccessLevel] = {
|
|
114
|
+
"get_basic_status": AccessLevel.PUBLIC,
|
|
115
|
+
"get_service_status": AccessLevel.INTERNAL,
|
|
116
|
+
"get_comprehensive_status": AccessLevel.INTERNAL,
|
|
117
|
+
"get_server_stats": AccessLevel.ADMIN,
|
|
118
|
+
"get_debug_info": AccessLevel.DEBUG,
|
|
119
|
+
"restart_services": AccessLevel.ADMIN,
|
|
120
|
+
"clear_cache": AccessLevel.ADMIN,
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
# Initialize with default API keys if none exist
|
|
124
|
+
self._initialize_default_keys()
|
|
125
|
+
|
|
126
|
+
def _generate_secret_key(self) -> str:
|
|
127
|
+
"""Generate a secure random secret key."""
|
|
128
|
+
return secrets.token_urlsafe(32)
|
|
129
|
+
|
|
130
|
+
def _initialize_default_keys(self) -> None:
|
|
131
|
+
"""Initialize with default API keys for different access levels."""
|
|
132
|
+
|
|
133
|
+
# Generate default API keys for different access levels
|
|
134
|
+
default_keys = {
|
|
135
|
+
AccessLevel.PUBLIC: self._generate_api_key("public_default"),
|
|
136
|
+
AccessLevel.INTERNAL: self._generate_api_key("internal_default"),
|
|
137
|
+
AccessLevel.ADMIN: self._generate_api_key("admin_default"),
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
for level, api_key in default_keys.items():
|
|
141
|
+
self._api_keys[api_key] = AuthCredentials(
|
|
142
|
+
client_id=f"default_{level.value}",
|
|
143
|
+
access_level=level,
|
|
144
|
+
method=AuthenticationMethod.API_KEY,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
def _generate_api_key(self, prefix: str = "ck") -> str:
|
|
148
|
+
"""Generate a secure API key with prefix."""
|
|
149
|
+
random_part = secrets.token_urlsafe(32)
|
|
150
|
+
return f"{prefix}_{random_part}"
|
|
151
|
+
|
|
152
|
+
def authenticate_request(
|
|
153
|
+
self,
|
|
154
|
+
auth_header: str | None = None,
|
|
155
|
+
client_ip: str | None = None,
|
|
156
|
+
operation: str = "unknown",
|
|
157
|
+
) -> AuthCredentials:
|
|
158
|
+
"""
|
|
159
|
+
Authenticate a status request.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
auth_header: Authorization header value
|
|
163
|
+
client_ip: Client IP address
|
|
164
|
+
operation: Operation being requested
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
AuthCredentials for the authenticated request
|
|
168
|
+
|
|
169
|
+
Raises:
|
|
170
|
+
AuthenticationError: If authentication fails
|
|
171
|
+
AccessDeniedError: If access is denied
|
|
172
|
+
"""
|
|
173
|
+
|
|
174
|
+
# Check for local-only access (development mode)
|
|
175
|
+
if self.enable_local_only and client_ip:
|
|
176
|
+
if client_ip in ("127.0.0.1", "::1", "localhost"):
|
|
177
|
+
self.security_logger.log_security_event(
|
|
178
|
+
event_type=SecurityEventType.LOCAL_ACCESS_GRANTED,
|
|
179
|
+
level=SecurityEventLevel.INFO,
|
|
180
|
+
message=f"Local access granted for {operation}",
|
|
181
|
+
client_id="local",
|
|
182
|
+
operation=operation,
|
|
183
|
+
additional_data={"client_ip": client_ip},
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
return AuthCredentials(
|
|
187
|
+
client_id="local",
|
|
188
|
+
access_level=AccessLevel.ADMIN, # Local gets admin access
|
|
189
|
+
method=AuthenticationMethod.LOCAL_ONLY,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
# Parse authentication header
|
|
193
|
+
if not auth_header:
|
|
194
|
+
# Return default access level for unauthenticated requests
|
|
195
|
+
credentials = AuthCredentials(
|
|
196
|
+
client_id="anonymous",
|
|
197
|
+
access_level=self.default_access_level,
|
|
198
|
+
method=AuthenticationMethod.API_KEY,
|
|
199
|
+
)
|
|
200
|
+
else:
|
|
201
|
+
credentials = self._parse_auth_header(auth_header, operation)
|
|
202
|
+
|
|
203
|
+
# Validate credentials
|
|
204
|
+
self._validate_credentials(credentials, operation)
|
|
205
|
+
|
|
206
|
+
# Check operation permissions
|
|
207
|
+
self._check_operation_access(credentials, operation)
|
|
208
|
+
|
|
209
|
+
# Log successful authentication
|
|
210
|
+
self.security_logger.log_security_event(
|
|
211
|
+
event_type=SecurityEventType.AUTH_SUCCESS,
|
|
212
|
+
level=SecurityEventLevel.INFO,
|
|
213
|
+
message=f"Authentication successful for {operation}",
|
|
214
|
+
client_id=credentials.client_id,
|
|
215
|
+
operation=operation,
|
|
216
|
+
additional_data={
|
|
217
|
+
"access_level": credentials.access_level.value,
|
|
218
|
+
"method": credentials.method.value,
|
|
219
|
+
"client_ip": client_ip,
|
|
220
|
+
},
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
return credentials
|
|
224
|
+
|
|
225
|
+
def _parse_auth_header(self, auth_header: str, operation: str) -> AuthCredentials:
|
|
226
|
+
"""Parse authentication header and create credentials."""
|
|
227
|
+
|
|
228
|
+
try:
|
|
229
|
+
# Handle different authentication schemes
|
|
230
|
+
if auth_header.startswith("Bearer "):
|
|
231
|
+
# JWT token
|
|
232
|
+
token = auth_header[7:] # Remove "Bearer " prefix
|
|
233
|
+
return self._validate_jwt_token(token, operation)
|
|
234
|
+
|
|
235
|
+
elif auth_header.startswith("ApiKey "):
|
|
236
|
+
# API key authentication
|
|
237
|
+
api_key = auth_header[7:] # Remove "ApiKey " prefix
|
|
238
|
+
return self._validate_api_key(api_key, operation)
|
|
239
|
+
|
|
240
|
+
elif auth_header.startswith("HMAC-SHA256 "):
|
|
241
|
+
# HMAC signature authentication
|
|
242
|
+
signature_data = auth_header[12:] # Remove "HMAC-SHA256 " prefix
|
|
243
|
+
return self._validate_hmac_signature(signature_data, operation)
|
|
244
|
+
|
|
245
|
+
else:
|
|
246
|
+
# Try as plain API key
|
|
247
|
+
return self._validate_api_key(auth_header, operation)
|
|
248
|
+
|
|
249
|
+
except Exception as e:
|
|
250
|
+
self.security_logger.log_security_event(
|
|
251
|
+
event_type=SecurityEventType.AUTH_FAILURE,
|
|
252
|
+
level=SecurityEventLevel.WARNING,
|
|
253
|
+
message=f"Authentication parsing failed: {e}",
|
|
254
|
+
operation=operation,
|
|
255
|
+
)
|
|
256
|
+
raise AuthenticationError(f"Invalid authentication format: {e}")
|
|
257
|
+
|
|
258
|
+
def _validate_api_key(self, api_key: str, operation: str) -> AuthCredentials:
|
|
259
|
+
"""Validate API key and return credentials."""
|
|
260
|
+
|
|
261
|
+
if api_key in self._api_keys:
|
|
262
|
+
credentials = self._api_keys[api_key]
|
|
263
|
+
|
|
264
|
+
if credentials.is_expired:
|
|
265
|
+
self.security_logger.log_security_event(
|
|
266
|
+
event_type=SecurityEventType.AUTH_EXPIRED,
|
|
267
|
+
level=SecurityEventLevel.WARNING,
|
|
268
|
+
message="API key expired",
|
|
269
|
+
client_id=credentials.client_id,
|
|
270
|
+
operation=operation,
|
|
271
|
+
)
|
|
272
|
+
raise ExpiredCredentialsError("API key expired")
|
|
273
|
+
|
|
274
|
+
return credentials
|
|
275
|
+
|
|
276
|
+
self.security_logger.log_security_event(
|
|
277
|
+
event_type=SecurityEventType.AUTH_FAILURE,
|
|
278
|
+
level=SecurityEventLevel.WARNING,
|
|
279
|
+
message="Invalid API key",
|
|
280
|
+
operation=operation,
|
|
281
|
+
additional_data={
|
|
282
|
+
"api_key_prefix": api_key[:8] if len(api_key) > 8 else api_key
|
|
283
|
+
},
|
|
284
|
+
)
|
|
285
|
+
raise AuthenticationError("Invalid API key")
|
|
286
|
+
|
|
287
|
+
def _validate_jwt_token(self, token: str, operation: str) -> AuthCredentials:
|
|
288
|
+
"""Validate JWT token and return credentials."""
|
|
289
|
+
|
|
290
|
+
try:
|
|
291
|
+
# Simple JWT validation (in production, use proper JWT library)
|
|
292
|
+
parts = token.split(".")
|
|
293
|
+
if len(parts) != 3:
|
|
294
|
+
raise AuthenticationError("Invalid JWT format")
|
|
295
|
+
|
|
296
|
+
# Decode header and payload (base64)
|
|
297
|
+
import base64
|
|
298
|
+
|
|
299
|
+
json.loads(base64.b64decode(parts[0] + "=="))
|
|
300
|
+
payload = json.loads(base64.b64decode(parts[1] + "=="))
|
|
301
|
+
signature = parts[2]
|
|
302
|
+
|
|
303
|
+
# Validate signature with HMAC
|
|
304
|
+
expected_signature = (
|
|
305
|
+
base64.b64encode(
|
|
306
|
+
hmac.new(
|
|
307
|
+
self.secret_key.encode(),
|
|
308
|
+
f"{parts[0]}.{parts[1]}".encode(),
|
|
309
|
+
hashlib.sha256,
|
|
310
|
+
).digest()
|
|
311
|
+
)
|
|
312
|
+
.decode()
|
|
313
|
+
.rstrip("=")
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
if not hmac.compare_digest(signature, expected_signature):
|
|
317
|
+
raise AuthenticationError("Invalid JWT signature")
|
|
318
|
+
|
|
319
|
+
# Check expiration
|
|
320
|
+
if "exp" in payload and time.time() > payload["exp"]:
|
|
321
|
+
raise ExpiredCredentialsError("JWT token expired")
|
|
322
|
+
|
|
323
|
+
# Extract credentials
|
|
324
|
+
client_id = payload.get("sub", "jwt_user")
|
|
325
|
+
access_level = AccessLevel(
|
|
326
|
+
payload.get("access_level", AccessLevel.PUBLIC.value)
|
|
327
|
+
)
|
|
328
|
+
allowed_operations = payload.get("operations")
|
|
329
|
+
|
|
330
|
+
return AuthCredentials(
|
|
331
|
+
client_id=client_id,
|
|
332
|
+
access_level=access_level,
|
|
333
|
+
method=AuthenticationMethod.JWT_TOKEN,
|
|
334
|
+
expires_at=payload.get("exp"),
|
|
335
|
+
allowed_operations=set(allowed_operations)
|
|
336
|
+
if allowed_operations
|
|
337
|
+
else None,
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
except (json.JSONDecodeError, ValueError, KeyError) as e:
|
|
341
|
+
self.security_logger.log_security_event(
|
|
342
|
+
event_type=SecurityEventType.AUTH_FAILURE,
|
|
343
|
+
level=SecurityEventLevel.WARNING,
|
|
344
|
+
message=f"JWT validation failed: {e}",
|
|
345
|
+
operation=operation,
|
|
346
|
+
)
|
|
347
|
+
raise AuthenticationError(f"Invalid JWT token: {e}")
|
|
348
|
+
|
|
349
|
+
def _validate_hmac_signature(
|
|
350
|
+
self, signature_data: str, operation: str
|
|
351
|
+
) -> AuthCredentials:
|
|
352
|
+
"""Validate HMAC signature and return credentials."""
|
|
353
|
+
|
|
354
|
+
try:
|
|
355
|
+
# Parse signature data: client_id:timestamp:signature
|
|
356
|
+
parts = signature_data.split(":")
|
|
357
|
+
if len(parts) != 3:
|
|
358
|
+
raise AuthenticationError("Invalid HMAC signature format")
|
|
359
|
+
|
|
360
|
+
client_id, timestamp_str, signature = parts
|
|
361
|
+
timestamp = float(timestamp_str)
|
|
362
|
+
|
|
363
|
+
# Check timestamp (prevent replay attacks)
|
|
364
|
+
current_time = time.time()
|
|
365
|
+
if abs(current_time - timestamp) > 300: # 5 minute window
|
|
366
|
+
raise ExpiredCredentialsError("HMAC signature timestamp too old")
|
|
367
|
+
|
|
368
|
+
# Create expected signature
|
|
369
|
+
message = f"{client_id}:{operation}:{timestamp_str}"
|
|
370
|
+
expected_signature = hmac.new(
|
|
371
|
+
self.secret_key.encode(),
|
|
372
|
+
message.encode(),
|
|
373
|
+
hashlib.sha256,
|
|
374
|
+
).hexdigest()
|
|
375
|
+
|
|
376
|
+
if not hmac.compare_digest(signature, expected_signature):
|
|
377
|
+
raise AuthenticationError("Invalid HMAC signature")
|
|
378
|
+
|
|
379
|
+
return AuthCredentials(
|
|
380
|
+
client_id=client_id,
|
|
381
|
+
access_level=AccessLevel.INTERNAL, # HMAC gets internal access
|
|
382
|
+
method=AuthenticationMethod.HMAC_SIGNATURE,
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
except (ValueError, IndexError) as e:
|
|
386
|
+
self.security_logger.log_security_event(
|
|
387
|
+
event_type=SecurityEventType.AUTH_FAILURE,
|
|
388
|
+
level=SecurityEventLevel.WARNING,
|
|
389
|
+
message=f"HMAC validation failed: {e}",
|
|
390
|
+
operation=operation,
|
|
391
|
+
)
|
|
392
|
+
raise AuthenticationError(f"Invalid HMAC signature: {e}")
|
|
393
|
+
|
|
394
|
+
def _validate_credentials(
|
|
395
|
+
self, credentials: AuthCredentials, operation: str
|
|
396
|
+
) -> None:
|
|
397
|
+
"""Validate credentials are still valid."""
|
|
398
|
+
|
|
399
|
+
if credentials.is_expired:
|
|
400
|
+
self.security_logger.log_security_event(
|
|
401
|
+
event_type=SecurityEventType.AUTH_EXPIRED,
|
|
402
|
+
level=SecurityEventLevel.WARNING,
|
|
403
|
+
message="Credentials expired",
|
|
404
|
+
client_id=credentials.client_id,
|
|
405
|
+
operation=operation,
|
|
406
|
+
)
|
|
407
|
+
raise ExpiredCredentialsError("Credentials expired")
|
|
408
|
+
|
|
409
|
+
def _check_operation_access(
|
|
410
|
+
self, credentials: AuthCredentials, operation: str
|
|
411
|
+
) -> None:
|
|
412
|
+
"""Check if credentials have access to the requested operation."""
|
|
413
|
+
|
|
414
|
+
# Check operation-specific permissions
|
|
415
|
+
if not credentials.has_operation_access(operation):
|
|
416
|
+
self.security_logger.log_security_event(
|
|
417
|
+
event_type=SecurityEventType.ACCESS_DENIED,
|
|
418
|
+
level=SecurityEventLevel.WARNING,
|
|
419
|
+
message=f"Operation not allowed: {operation}",
|
|
420
|
+
client_id=credentials.client_id,
|
|
421
|
+
operation=operation,
|
|
422
|
+
additional_data={
|
|
423
|
+
"access_level": credentials.access_level.value,
|
|
424
|
+
"allowed_operations": list(credentials.allowed_operations or []),
|
|
425
|
+
},
|
|
426
|
+
)
|
|
427
|
+
raise AccessDeniedError(f"Operation not allowed: {operation}")
|
|
428
|
+
|
|
429
|
+
# Check access level requirements
|
|
430
|
+
required_level = self._operation_requirements.get(operation, AccessLevel.PUBLIC)
|
|
431
|
+
if not self._has_sufficient_access_level(
|
|
432
|
+
credentials.access_level, required_level
|
|
433
|
+
):
|
|
434
|
+
self.security_logger.log_security_event(
|
|
435
|
+
event_type=SecurityEventType.INSUFFICIENT_PRIVILEGES,
|
|
436
|
+
level=SecurityEventLevel.WARNING,
|
|
437
|
+
message=f"Insufficient access level for {operation}",
|
|
438
|
+
client_id=credentials.client_id,
|
|
439
|
+
operation=operation,
|
|
440
|
+
additional_data={
|
|
441
|
+
"user_level": credentials.access_level.value,
|
|
442
|
+
"required_level": required_level.value,
|
|
443
|
+
},
|
|
444
|
+
)
|
|
445
|
+
raise AccessDeniedError(f"Insufficient access level for {operation}")
|
|
446
|
+
|
|
447
|
+
def _has_sufficient_access_level(
|
|
448
|
+
self,
|
|
449
|
+
user_level: AccessLevel,
|
|
450
|
+
required_level: AccessLevel,
|
|
451
|
+
) -> bool:
|
|
452
|
+
"""Check if user access level meets requirements."""
|
|
453
|
+
|
|
454
|
+
# Access level hierarchy
|
|
455
|
+
level_hierarchy = {
|
|
456
|
+
AccessLevel.PUBLIC: 0,
|
|
457
|
+
AccessLevel.INTERNAL: 1,
|
|
458
|
+
AccessLevel.ADMIN: 2,
|
|
459
|
+
AccessLevel.DEBUG: 3,
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
user_level_num = level_hierarchy.get(user_level, 0)
|
|
463
|
+
required_level_num = level_hierarchy.get(required_level, 0)
|
|
464
|
+
|
|
465
|
+
return user_level_num >= required_level_num
|
|
466
|
+
|
|
467
|
+
def is_operation_allowed(self, operation: str, access_level: AccessLevel) -> bool:
|
|
468
|
+
"""Check if an operation is allowed for the given access level."""
|
|
469
|
+
required_level = self._operation_requirements.get(operation, AccessLevel.PUBLIC)
|
|
470
|
+
return self._has_sufficient_access_level(access_level, required_level)
|
|
471
|
+
|
|
472
|
+
def add_api_key(
|
|
473
|
+
self,
|
|
474
|
+
client_id: str,
|
|
475
|
+
access_level: AccessLevel,
|
|
476
|
+
expires_at: float | None = None,
|
|
477
|
+
allowed_operations: set[str] | None = None,
|
|
478
|
+
) -> str:
|
|
479
|
+
"""
|
|
480
|
+
Add a new API key.
|
|
481
|
+
|
|
482
|
+
Args:
|
|
483
|
+
client_id: Client identifier
|
|
484
|
+
access_level: Access level for the key
|
|
485
|
+
expires_at: Optional expiration timestamp
|
|
486
|
+
allowed_operations: Optional set of allowed operations
|
|
487
|
+
|
|
488
|
+
Returns:
|
|
489
|
+
Generated API key
|
|
490
|
+
"""
|
|
491
|
+
|
|
492
|
+
api_key = self._generate_api_key(client_id.replace(" ", "_").lower())
|
|
493
|
+
|
|
494
|
+
credentials = AuthCredentials(
|
|
495
|
+
client_id=client_id,
|
|
496
|
+
access_level=access_level,
|
|
497
|
+
method=AuthenticationMethod.API_KEY,
|
|
498
|
+
expires_at=expires_at,
|
|
499
|
+
allowed_operations=allowed_operations,
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
self._api_keys[api_key] = credentials
|
|
503
|
+
|
|
504
|
+
self.security_logger.log_security_event(
|
|
505
|
+
event_type=SecurityEventType.API_KEY_CREATED,
|
|
506
|
+
level=SecurityEventLevel.INFO,
|
|
507
|
+
message=f"API key created for {client_id}",
|
|
508
|
+
client_id=client_id,
|
|
509
|
+
operation="create_api_key",
|
|
510
|
+
additional_data={
|
|
511
|
+
"access_level": access_level.value,
|
|
512
|
+
"expires_at": expires_at,
|
|
513
|
+
},
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
return api_key
|
|
517
|
+
|
|
518
|
+
def revoke_api_key(self, api_key: str) -> bool:
|
|
519
|
+
"""
|
|
520
|
+
Revoke an API key.
|
|
521
|
+
|
|
522
|
+
Args:
|
|
523
|
+
api_key: API key to revoke
|
|
524
|
+
|
|
525
|
+
Returns:
|
|
526
|
+
True if key was revoked, False if not found
|
|
527
|
+
"""
|
|
528
|
+
|
|
529
|
+
if api_key in self._api_keys:
|
|
530
|
+
credentials = self._api_keys[api_key]
|
|
531
|
+
del self._api_keys[api_key]
|
|
532
|
+
|
|
533
|
+
self.security_logger.log_security_event(
|
|
534
|
+
event_type=SecurityEventType.API_KEY_REVOKED,
|
|
535
|
+
level=SecurityEventLevel.INFO,
|
|
536
|
+
message=f"API key revoked for {credentials.client_id}",
|
|
537
|
+
client_id=credentials.client_id,
|
|
538
|
+
operation="revoke_api_key",
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
return True
|
|
542
|
+
|
|
543
|
+
return False
|
|
544
|
+
|
|
545
|
+
def get_auth_status(self) -> dict[str, t.Any]:
|
|
546
|
+
"""Get current authentication system status."""
|
|
547
|
+
|
|
548
|
+
active_keys = len([k for k, c in self._api_keys.items() if not c.is_expired])
|
|
549
|
+
expired_keys = len([k for k, c in self._api_keys.items() if c.is_expired])
|
|
550
|
+
|
|
551
|
+
return {
|
|
552
|
+
"authentication_enabled": True,
|
|
553
|
+
"local_only_enabled": self.enable_local_only,
|
|
554
|
+
"default_access_level": self.default_access_level.value,
|
|
555
|
+
"api_keys": {
|
|
556
|
+
"total": len(self._api_keys),
|
|
557
|
+
"active": active_keys,
|
|
558
|
+
"expired": expired_keys,
|
|
559
|
+
},
|
|
560
|
+
"supported_methods": [method.value for method in AuthenticationMethod],
|
|
561
|
+
"access_levels": [level.value for level in AccessLevel],
|
|
562
|
+
"operation_requirements": {
|
|
563
|
+
op: level.value for op, level in self._operation_requirements.items()
|
|
564
|
+
},
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
# Global singleton instance
|
|
569
|
+
_authenticator: StatusAuthenticator | None = None
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def get_status_authenticator() -> StatusAuthenticator:
|
|
573
|
+
"""Get the global status authenticator instance."""
|
|
574
|
+
|
|
575
|
+
global _authenticator
|
|
576
|
+
if _authenticator is None:
|
|
577
|
+
_authenticator = StatusAuthenticator()
|
|
578
|
+
return _authenticator
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
async def authenticate_status_request(
|
|
582
|
+
auth_header: str | None = None,
|
|
583
|
+
client_ip: str | None = None,
|
|
584
|
+
operation: str = "unknown",
|
|
585
|
+
) -> AuthCredentials:
|
|
586
|
+
"""
|
|
587
|
+
Convenience function for status request authentication.
|
|
588
|
+
|
|
589
|
+
Args:
|
|
590
|
+
auth_header: Authorization header value
|
|
591
|
+
client_ip: Client IP address
|
|
592
|
+
operation: Operation being requested
|
|
593
|
+
|
|
594
|
+
Returns:
|
|
595
|
+
AuthCredentials for the authenticated request
|
|
596
|
+
|
|
597
|
+
Raises:
|
|
598
|
+
AuthenticationError: If authentication fails
|
|
599
|
+
"""
|
|
600
|
+
|
|
601
|
+
return get_status_authenticator().authenticate_request(
|
|
602
|
+
auth_header, client_ip, operation
|
|
603
|
+
)
|