crackerjack 0.31.10__py3-none-any.whl → 0.31.13__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.

Files changed (155) hide show
  1. crackerjack/CLAUDE.md +288 -705
  2. crackerjack/__main__.py +22 -8
  3. crackerjack/agents/__init__.py +0 -3
  4. crackerjack/agents/architect_agent.py +0 -43
  5. crackerjack/agents/base.py +1 -9
  6. crackerjack/agents/coordinator.py +2 -148
  7. crackerjack/agents/documentation_agent.py +109 -81
  8. crackerjack/agents/dry_agent.py +122 -97
  9. crackerjack/agents/formatting_agent.py +3 -16
  10. crackerjack/agents/import_optimization_agent.py +1174 -130
  11. crackerjack/agents/performance_agent.py +956 -188
  12. crackerjack/agents/performance_helpers.py +229 -0
  13. crackerjack/agents/proactive_agent.py +1 -48
  14. crackerjack/agents/refactoring_agent.py +516 -246
  15. crackerjack/agents/refactoring_helpers.py +282 -0
  16. crackerjack/agents/security_agent.py +393 -90
  17. crackerjack/agents/test_creation_agent.py +1776 -120
  18. crackerjack/agents/test_specialist_agent.py +59 -15
  19. crackerjack/agents/tracker.py +0 -102
  20. crackerjack/api.py +145 -37
  21. crackerjack/cli/handlers.py +48 -30
  22. crackerjack/cli/interactive.py +11 -11
  23. crackerjack/cli/options.py +66 -4
  24. crackerjack/code_cleaner.py +808 -148
  25. crackerjack/config/global_lock_config.py +110 -0
  26. crackerjack/config/hooks.py +43 -64
  27. crackerjack/core/async_workflow_orchestrator.py +247 -97
  28. crackerjack/core/autofix_coordinator.py +192 -109
  29. crackerjack/core/enhanced_container.py +46 -63
  30. crackerjack/core/file_lifecycle.py +549 -0
  31. crackerjack/core/performance.py +9 -8
  32. crackerjack/core/performance_monitor.py +395 -0
  33. crackerjack/core/phase_coordinator.py +281 -94
  34. crackerjack/core/proactive_workflow.py +9 -58
  35. crackerjack/core/resource_manager.py +501 -0
  36. crackerjack/core/service_watchdog.py +490 -0
  37. crackerjack/core/session_coordinator.py +4 -8
  38. crackerjack/core/timeout_manager.py +504 -0
  39. crackerjack/core/websocket_lifecycle.py +475 -0
  40. crackerjack/core/workflow_orchestrator.py +343 -209
  41. crackerjack/dynamic_config.py +50 -9
  42. crackerjack/errors.py +3 -4
  43. crackerjack/executors/async_hook_executor.py +63 -13
  44. crackerjack/executors/cached_hook_executor.py +14 -14
  45. crackerjack/executors/hook_executor.py +100 -37
  46. crackerjack/executors/hook_lock_manager.py +856 -0
  47. crackerjack/executors/individual_hook_executor.py +120 -86
  48. crackerjack/intelligence/__init__.py +0 -7
  49. crackerjack/intelligence/adaptive_learning.py +13 -86
  50. crackerjack/intelligence/agent_orchestrator.py +15 -78
  51. crackerjack/intelligence/agent_registry.py +12 -59
  52. crackerjack/intelligence/agent_selector.py +31 -92
  53. crackerjack/intelligence/integration.py +1 -41
  54. crackerjack/interactive.py +9 -9
  55. crackerjack/managers/async_hook_manager.py +25 -8
  56. crackerjack/managers/hook_manager.py +9 -9
  57. crackerjack/managers/publish_manager.py +57 -59
  58. crackerjack/managers/test_command_builder.py +6 -36
  59. crackerjack/managers/test_executor.py +9 -61
  60. crackerjack/managers/test_manager.py +17 -63
  61. crackerjack/managers/test_manager_backup.py +77 -127
  62. crackerjack/managers/test_progress.py +4 -23
  63. crackerjack/mcp/cache.py +5 -12
  64. crackerjack/mcp/client_runner.py +10 -10
  65. crackerjack/mcp/context.py +64 -6
  66. crackerjack/mcp/dashboard.py +14 -11
  67. crackerjack/mcp/enhanced_progress_monitor.py +55 -55
  68. crackerjack/mcp/file_monitor.py +72 -42
  69. crackerjack/mcp/progress_components.py +103 -84
  70. crackerjack/mcp/progress_monitor.py +122 -49
  71. crackerjack/mcp/rate_limiter.py +12 -12
  72. crackerjack/mcp/server_core.py +16 -22
  73. crackerjack/mcp/service_watchdog.py +26 -26
  74. crackerjack/mcp/state.py +15 -0
  75. crackerjack/mcp/tools/core_tools.py +95 -39
  76. crackerjack/mcp/tools/error_analyzer.py +6 -32
  77. crackerjack/mcp/tools/execution_tools.py +1 -56
  78. crackerjack/mcp/tools/execution_tools_backup.py +35 -131
  79. crackerjack/mcp/tools/intelligence_tool_registry.py +0 -36
  80. crackerjack/mcp/tools/intelligence_tools.py +2 -55
  81. crackerjack/mcp/tools/monitoring_tools.py +308 -145
  82. crackerjack/mcp/tools/proactive_tools.py +12 -42
  83. crackerjack/mcp/tools/progress_tools.py +23 -15
  84. crackerjack/mcp/tools/utility_tools.py +3 -40
  85. crackerjack/mcp/tools/workflow_executor.py +40 -60
  86. crackerjack/mcp/websocket/app.py +0 -3
  87. crackerjack/mcp/websocket/endpoints.py +206 -268
  88. crackerjack/mcp/websocket/jobs.py +213 -66
  89. crackerjack/mcp/websocket/server.py +84 -6
  90. crackerjack/mcp/websocket/websocket_handler.py +137 -29
  91. crackerjack/models/config_adapter.py +3 -16
  92. crackerjack/models/protocols.py +162 -3
  93. crackerjack/models/resource_protocols.py +454 -0
  94. crackerjack/models/task.py +3 -3
  95. crackerjack/monitoring/__init__.py +0 -0
  96. crackerjack/monitoring/ai_agent_watchdog.py +25 -71
  97. crackerjack/monitoring/regression_prevention.py +28 -87
  98. crackerjack/orchestration/advanced_orchestrator.py +44 -78
  99. crackerjack/orchestration/coverage_improvement.py +10 -60
  100. crackerjack/orchestration/execution_strategies.py +16 -16
  101. crackerjack/orchestration/test_progress_streamer.py +61 -53
  102. crackerjack/plugins/base.py +1 -1
  103. crackerjack/plugins/managers.py +22 -20
  104. crackerjack/py313.py +65 -21
  105. crackerjack/services/backup_service.py +467 -0
  106. crackerjack/services/bounded_status_operations.py +627 -0
  107. crackerjack/services/cache.py +7 -9
  108. crackerjack/services/config.py +35 -52
  109. crackerjack/services/config_integrity.py +5 -16
  110. crackerjack/services/config_merge.py +542 -0
  111. crackerjack/services/contextual_ai_assistant.py +17 -19
  112. crackerjack/services/coverage_ratchet.py +44 -73
  113. crackerjack/services/debug.py +25 -39
  114. crackerjack/services/dependency_monitor.py +52 -50
  115. crackerjack/services/enhanced_filesystem.py +14 -11
  116. crackerjack/services/file_hasher.py +1 -1
  117. crackerjack/services/filesystem.py +1 -12
  118. crackerjack/services/git.py +71 -47
  119. crackerjack/services/health_metrics.py +31 -27
  120. crackerjack/services/initialization.py +276 -428
  121. crackerjack/services/input_validator.py +760 -0
  122. crackerjack/services/log_manager.py +16 -16
  123. crackerjack/services/logging.py +7 -6
  124. crackerjack/services/metrics.py +43 -43
  125. crackerjack/services/pattern_cache.py +2 -31
  126. crackerjack/services/pattern_detector.py +26 -63
  127. crackerjack/services/performance_benchmarks.py +20 -45
  128. crackerjack/services/regex_patterns.py +2887 -0
  129. crackerjack/services/regex_utils.py +537 -0
  130. crackerjack/services/secure_path_utils.py +683 -0
  131. crackerjack/services/secure_status_formatter.py +534 -0
  132. crackerjack/services/secure_subprocess.py +605 -0
  133. crackerjack/services/security.py +47 -10
  134. crackerjack/services/security_logger.py +492 -0
  135. crackerjack/services/server_manager.py +109 -50
  136. crackerjack/services/smart_scheduling.py +8 -25
  137. crackerjack/services/status_authentication.py +603 -0
  138. crackerjack/services/status_security_manager.py +442 -0
  139. crackerjack/services/thread_safe_status_collector.py +546 -0
  140. crackerjack/services/tool_version_service.py +1 -23
  141. crackerjack/services/unified_config.py +36 -58
  142. crackerjack/services/validation_rate_limiter.py +269 -0
  143. crackerjack/services/version_checker.py +9 -40
  144. crackerjack/services/websocket_resource_limiter.py +572 -0
  145. crackerjack/slash_commands/__init__.py +52 -2
  146. crackerjack/tools/__init__.py +0 -0
  147. crackerjack/tools/validate_input_validator_patterns.py +262 -0
  148. crackerjack/tools/validate_regex_patterns.py +198 -0
  149. {crackerjack-0.31.10.dist-info → crackerjack-0.31.13.dist-info}/METADATA +197 -12
  150. crackerjack-0.31.13.dist-info/RECORD +178 -0
  151. crackerjack/cli/facade.py +0 -104
  152. crackerjack-0.31.10.dist-info/RECORD +0 -149
  153. {crackerjack-0.31.10.dist-info → crackerjack-0.31.13.dist-info}/WHEEL +0 -0
  154. {crackerjack-0.31.10.dist-info → crackerjack-0.31.13.dist-info}/entry_points.txt +0 -0
  155. {crackerjack-0.31.10.dist-info → crackerjack-0.31.13.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
+ )