kailash 0.6.3__py3-none-any.whl → 0.6.4__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.
Files changed (120) hide show
  1. kailash/__init__.py +3 -3
  2. kailash/api/custom_nodes_secure.py +3 -3
  3. kailash/api/gateway.py +1 -1
  4. kailash/api/studio.py +2 -3
  5. kailash/api/workflow_api.py +3 -4
  6. kailash/core/resilience/bulkhead.py +460 -0
  7. kailash/core/resilience/circuit_breaker.py +92 -10
  8. kailash/edge/discovery.py +86 -0
  9. kailash/mcp_server/__init__.py +309 -33
  10. kailash/mcp_server/advanced_features.py +1022 -0
  11. kailash/mcp_server/ai_registry_server.py +27 -2
  12. kailash/mcp_server/auth.py +789 -0
  13. kailash/mcp_server/client.py +645 -378
  14. kailash/mcp_server/discovery.py +1593 -0
  15. kailash/mcp_server/errors.py +673 -0
  16. kailash/mcp_server/oauth.py +1727 -0
  17. kailash/mcp_server/protocol.py +1126 -0
  18. kailash/mcp_server/registry_integration.py +587 -0
  19. kailash/mcp_server/server.py +1213 -98
  20. kailash/mcp_server/transports.py +1169 -0
  21. kailash/mcp_server/utils/__init__.py +6 -1
  22. kailash/mcp_server/utils/cache.py +250 -7
  23. kailash/middleware/auth/auth_manager.py +3 -3
  24. kailash/middleware/communication/api_gateway.py +2 -9
  25. kailash/middleware/communication/realtime.py +1 -1
  26. kailash/middleware/mcp/enhanced_server.py +1 -1
  27. kailash/nodes/__init__.py +2 -0
  28. kailash/nodes/admin/audit_log.py +6 -6
  29. kailash/nodes/admin/permission_check.py +8 -8
  30. kailash/nodes/admin/role_management.py +32 -28
  31. kailash/nodes/admin/schema.sql +6 -1
  32. kailash/nodes/admin/schema_manager.py +13 -13
  33. kailash/nodes/admin/security_event.py +16 -20
  34. kailash/nodes/admin/tenant_isolation.py +3 -3
  35. kailash/nodes/admin/transaction_utils.py +3 -3
  36. kailash/nodes/admin/user_management.py +21 -22
  37. kailash/nodes/ai/a2a.py +11 -11
  38. kailash/nodes/ai/ai_providers.py +9 -12
  39. kailash/nodes/ai/embedding_generator.py +13 -14
  40. kailash/nodes/ai/intelligent_agent_orchestrator.py +19 -19
  41. kailash/nodes/ai/iterative_llm_agent.py +2 -2
  42. kailash/nodes/ai/llm_agent.py +210 -33
  43. kailash/nodes/ai/self_organizing.py +2 -2
  44. kailash/nodes/alerts/discord.py +4 -4
  45. kailash/nodes/api/graphql.py +6 -6
  46. kailash/nodes/api/http.py +12 -17
  47. kailash/nodes/api/rate_limiting.py +4 -4
  48. kailash/nodes/api/rest.py +15 -15
  49. kailash/nodes/auth/mfa.py +3 -4
  50. kailash/nodes/auth/risk_assessment.py +2 -2
  51. kailash/nodes/auth/session_management.py +5 -5
  52. kailash/nodes/auth/sso.py +143 -0
  53. kailash/nodes/base.py +6 -2
  54. kailash/nodes/base_async.py +16 -2
  55. kailash/nodes/base_with_acl.py +2 -2
  56. kailash/nodes/cache/__init__.py +9 -0
  57. kailash/nodes/cache/cache.py +1172 -0
  58. kailash/nodes/cache/cache_invalidation.py +870 -0
  59. kailash/nodes/cache/redis_pool_manager.py +595 -0
  60. kailash/nodes/code/async_python.py +2 -1
  61. kailash/nodes/code/python.py +196 -35
  62. kailash/nodes/compliance/data_retention.py +6 -6
  63. kailash/nodes/compliance/gdpr.py +5 -5
  64. kailash/nodes/data/__init__.py +10 -0
  65. kailash/nodes/data/optimistic_locking.py +906 -0
  66. kailash/nodes/data/readers.py +8 -8
  67. kailash/nodes/data/redis.py +349 -0
  68. kailash/nodes/data/sql.py +314 -3
  69. kailash/nodes/data/streaming.py +21 -0
  70. kailash/nodes/enterprise/__init__.py +8 -0
  71. kailash/nodes/enterprise/audit_logger.py +285 -0
  72. kailash/nodes/enterprise/batch_processor.py +22 -3
  73. kailash/nodes/enterprise/data_lineage.py +1 -1
  74. kailash/nodes/enterprise/mcp_executor.py +205 -0
  75. kailash/nodes/enterprise/service_discovery.py +150 -0
  76. kailash/nodes/enterprise/tenant_assignment.py +108 -0
  77. kailash/nodes/logic/async_operations.py +2 -2
  78. kailash/nodes/logic/convergence.py +1 -1
  79. kailash/nodes/logic/operations.py +1 -1
  80. kailash/nodes/monitoring/__init__.py +11 -1
  81. kailash/nodes/monitoring/health_check.py +456 -0
  82. kailash/nodes/monitoring/log_processor.py +817 -0
  83. kailash/nodes/monitoring/metrics_collector.py +627 -0
  84. kailash/nodes/monitoring/performance_benchmark.py +137 -11
  85. kailash/nodes/rag/advanced.py +7 -7
  86. kailash/nodes/rag/agentic.py +49 -2
  87. kailash/nodes/rag/conversational.py +3 -3
  88. kailash/nodes/rag/evaluation.py +3 -3
  89. kailash/nodes/rag/federated.py +3 -3
  90. kailash/nodes/rag/graph.py +3 -3
  91. kailash/nodes/rag/multimodal.py +3 -3
  92. kailash/nodes/rag/optimized.py +5 -5
  93. kailash/nodes/rag/privacy.py +3 -3
  94. kailash/nodes/rag/query_processing.py +6 -6
  95. kailash/nodes/rag/realtime.py +1 -1
  96. kailash/nodes/rag/registry.py +2 -6
  97. kailash/nodes/rag/router.py +1 -1
  98. kailash/nodes/rag/similarity.py +7 -7
  99. kailash/nodes/rag/strategies.py +4 -4
  100. kailash/nodes/security/abac_evaluator.py +6 -6
  101. kailash/nodes/security/behavior_analysis.py +5 -6
  102. kailash/nodes/security/credential_manager.py +1 -1
  103. kailash/nodes/security/rotating_credentials.py +11 -11
  104. kailash/nodes/security/threat_detection.py +8 -8
  105. kailash/nodes/testing/credential_testing.py +2 -2
  106. kailash/nodes/transform/processors.py +5 -5
  107. kailash/runtime/local.py +162 -14
  108. kailash/runtime/parameter_injection.py +425 -0
  109. kailash/runtime/parameter_injector.py +657 -0
  110. kailash/runtime/testing.py +2 -2
  111. kailash/testing/fixtures.py +2 -2
  112. kailash/workflow/builder.py +99 -18
  113. kailash/workflow/builder_improvements.py +207 -0
  114. kailash/workflow/input_handling.py +170 -0
  115. {kailash-0.6.3.dist-info → kailash-0.6.4.dist-info}/METADATA +22 -9
  116. {kailash-0.6.3.dist-info → kailash-0.6.4.dist-info}/RECORD +120 -94
  117. {kailash-0.6.3.dist-info → kailash-0.6.4.dist-info}/WHEEL +0 -0
  118. {kailash-0.6.3.dist-info → kailash-0.6.4.dist-info}/entry_points.txt +0 -0
  119. {kailash-0.6.3.dist-info → kailash-0.6.4.dist-info}/licenses/LICENSE +0 -0
  120. {kailash-0.6.3.dist-info → kailash-0.6.4.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,789 @@
1
+ """Authentication framework for MCP servers and clients.
2
+
3
+ This module provides a comprehensive authentication system for Model Context Protocol
4
+ implementations, supporting multiple authentication methods and security features.
5
+
6
+ Features:
7
+ - Multiple auth methods: API Key, Bearer Token, Basic Auth, JWT, OAuth2
8
+ - Permission-based access control
9
+ - Rate limiting per client
10
+ - Session management
11
+ - Audit logging
12
+ - Token validation and refresh
13
+ - Custom authentication providers
14
+
15
+ Examples:
16
+ API Key authentication:
17
+
18
+ >>> auth = APIKeyAuth(keys=["secret123", "secret456"])
19
+ >>> client = HTTPMCPClient(auth=auth.get_client_config())
20
+ >>> server = HTTPMCPServer(auth_config=auth.get_server_config())
21
+
22
+ JWT authentication:
23
+
24
+ >>> auth = JWTAuth(secret="my-secret", algorithm="HS256")
25
+ >>> token = auth.create_token({"user": "alice", "permissions": ["read", "write"]})
26
+ """
27
+
28
+ import hashlib
29
+ import hmac
30
+ import json
31
+ import logging
32
+ import time
33
+ import uuid
34
+ from abc import ABC, abstractmethod
35
+ from collections import defaultdict
36
+ from datetime import datetime, timedelta, timezone
37
+ from typing import Any, Dict, List, Optional, Set, Union
38
+
39
+ try:
40
+ import base64
41
+ import os
42
+
43
+ import jwt
44
+ from cryptography.hazmat.primitives import hashes
45
+ from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
46
+ except ImportError:
47
+ jwt = None
48
+ logger = logging.getLogger(__name__)
49
+ logger.warning(
50
+ "JWT dependencies not available. Install with: pip install pyjwt cryptography"
51
+ )
52
+
53
+ logger = logging.getLogger(__name__)
54
+
55
+
56
+ class AuthenticationError(Exception):
57
+ """Base exception for authentication errors."""
58
+
59
+ def __init__(self, message: str, error_code: str = "AUTH_FAILED"):
60
+ super().__init__(message)
61
+ self.error_code = error_code
62
+
63
+
64
+ class PermissionError(AuthenticationError):
65
+ """Exception for permission-related errors."""
66
+
67
+ def __init__(self, message: str, required_permission: str = ""):
68
+ super().__init__(message, "PERMISSION_DENIED")
69
+ self.required_permission = required_permission
70
+
71
+
72
+ class RateLimitError(AuthenticationError):
73
+ """Exception for rate limiting errors."""
74
+
75
+ def __init__(self, message: str, retry_after: Optional[int] = None):
76
+ super().__init__(message, "RATE_LIMITED")
77
+ self.retry_after = retry_after
78
+
79
+
80
+ class AuthProvider(ABC):
81
+ """Abstract base class for authentication providers."""
82
+
83
+ @abstractmethod
84
+ def authenticate(self, credentials: Union[str, Dict[str, Any]]) -> Dict[str, Any]:
85
+ """Authenticate credentials and return user info.
86
+
87
+ Args:
88
+ credentials: Authentication credentials (string token or dict)
89
+
90
+ Returns:
91
+ Authentication context dict
92
+ """
93
+ pass
94
+
95
+ @abstractmethod
96
+ def get_client_config(self) -> Dict[str, Any]:
97
+ """Get client-side authentication configuration."""
98
+ pass
99
+
100
+ @abstractmethod
101
+ def get_server_config(self) -> Dict[str, Any]:
102
+ """Get server-side authentication configuration."""
103
+ pass
104
+
105
+
106
+ class APIKeyAuth(AuthProvider):
107
+ """API Key authentication provider.
108
+
109
+ Supports multiple API keys with optional permissions and metadata.
110
+
111
+ Args:
112
+ keys: List of valid API keys or dict mapping keys to metadata
113
+ header_name: HTTP header name for the API key
114
+ permissions: Default permissions for all keys
115
+
116
+ Examples:
117
+ Simple API key auth:
118
+
119
+ >>> auth = APIKeyAuth(keys=["secret123", "secret456"])
120
+
121
+ API keys with permissions:
122
+
123
+ >>> auth = APIKeyAuth(keys={
124
+ ... "admin_key": {"permissions": ["read", "write", "admin"]},
125
+ ... "read_key": {"permissions": ["read"]}
126
+ ... })
127
+ """
128
+
129
+ def __init__(
130
+ self,
131
+ keys: Union[List[str], Dict[str, Dict[str, Any]]],
132
+ header_name: str = "X-API-Key",
133
+ permissions: Optional[List[str]] = None,
134
+ ):
135
+ """Initialize API key authentication."""
136
+ self.header_name = header_name
137
+ self.default_permissions = permissions or ["read"]
138
+
139
+ # Normalize keys to dict format
140
+ if isinstance(keys, list):
141
+ self.keys = {key: {"permissions": self.default_permissions} for key in keys}
142
+ else:
143
+ self.keys = keys
144
+
145
+ # Add default permissions to keys that don't have them
146
+ for key_data in self.keys.values():
147
+ if "permissions" not in key_data:
148
+ key_data["permissions"] = self.default_permissions
149
+
150
+ logger.info(f"Initialized API Key auth with {len(self.keys)} keys")
151
+
152
+ def authenticate(self, credentials: Union[str, Dict[str, Any]]) -> Dict[str, Any]:
153
+ """Authenticate API key credentials.
154
+
155
+ Args:
156
+ credentials: Either API key string or dict with 'api_key' field
157
+
158
+ Returns:
159
+ Authentication context dict
160
+
161
+ Raises:
162
+ AuthenticationError: If credentials are invalid or missing
163
+ """
164
+ # Handle both string and dict inputs for better developer experience
165
+ if isinstance(credentials, str):
166
+ api_key = credentials
167
+ elif isinstance(credentials, dict):
168
+ api_key = credentials.get("api_key")
169
+ if not api_key:
170
+ raise AuthenticationError(
171
+ "Expected dict with 'api_key' field, got dict without api_key"
172
+ )
173
+ else:
174
+ raise AuthenticationError(
175
+ f"Expected string or dict, got {type(credentials).__name__}"
176
+ )
177
+
178
+ if api_key not in self.keys:
179
+ raise AuthenticationError("Invalid API key")
180
+
181
+ key_data = self.keys[api_key]
182
+ return {
183
+ "user_id": f"api_key_{hashlib.sha256(api_key.encode()).hexdigest()[:8]}",
184
+ "auth_type": "api_key",
185
+ "permissions": key_data.get("permissions", []),
186
+ "metadata": key_data,
187
+ }
188
+
189
+ def get_client_config(self) -> Dict[str, Any]:
190
+ """Get client configuration."""
191
+ # Return first key for client (in practice, client would specify which key to use)
192
+ first_key = next(iter(self.keys.keys()))
193
+ return {"type": "api_key", "key": first_key, "header": self.header_name}
194
+
195
+ def get_server_config(self) -> Dict[str, Any]:
196
+ """Get server configuration."""
197
+ return {
198
+ "type": "api_key",
199
+ "header": self.header_name,
200
+ "keys": list(self.keys.keys()),
201
+ "key_metadata": self.keys,
202
+ }
203
+
204
+
205
+ class BearerTokenAuth(AuthProvider):
206
+ """Bearer token authentication provider.
207
+
208
+ Supports JWT and opaque bearer tokens with validation.
209
+
210
+ Args:
211
+ tokens: List of valid tokens or dict mapping tokens to metadata
212
+ validate_jwt: Whether to validate JWT tokens
213
+ jwt_secret: Secret for JWT validation
214
+ jwt_algorithm: Algorithm for JWT validation
215
+
216
+ Examples:
217
+ Simple bearer token:
218
+
219
+ >>> auth = BearerTokenAuth(tokens=["bearer_token_123"])
220
+
221
+ JWT bearer tokens:
222
+
223
+ >>> auth = BearerTokenAuth(
224
+ ... validate_jwt=True,
225
+ ... jwt_secret="my-secret",
226
+ ... jwt_algorithm="HS256"
227
+ ... )
228
+ """
229
+
230
+ def __init__(
231
+ self,
232
+ tokens: Optional[Union[List[str], Dict[str, Dict[str, Any]]]] = None,
233
+ validate_jwt: bool = False,
234
+ jwt_secret: Optional[str] = None,
235
+ jwt_algorithm: str = "HS256",
236
+ ):
237
+ """Initialize bearer token authentication."""
238
+ self.validate_jwt = validate_jwt
239
+ self.jwt_secret = jwt_secret
240
+ self.jwt_algorithm = jwt_algorithm
241
+
242
+ # Normalize tokens
243
+ if tokens is None:
244
+ self.tokens = {}
245
+ elif isinstance(tokens, list):
246
+ self.tokens = {token: {} for token in tokens}
247
+ else:
248
+ self.tokens = tokens
249
+
250
+ if validate_jwt and not jwt_secret:
251
+ raise ValueError("JWT secret required when validate_jwt=True")
252
+
253
+ logger.info(f"Initialized Bearer Token auth (JWT: {validate_jwt})")
254
+
255
+ def authenticate(self, credentials: Union[str, Dict[str, Any]]) -> Dict[str, Any]:
256
+ """Authenticate bearer token credentials.
257
+
258
+ Args:
259
+ credentials: Either bearer token string or dict with 'token' field
260
+
261
+ Returns:
262
+ Authentication context dict
263
+ """
264
+ # Handle both string and dict inputs
265
+ if isinstance(credentials, str):
266
+ token = credentials
267
+ elif isinstance(credentials, dict):
268
+ token = credentials.get("token")
269
+ if not token:
270
+ raise AuthenticationError(
271
+ "Expected dict with 'token' field, got dict without token"
272
+ )
273
+ else:
274
+ raise AuthenticationError(
275
+ f"Expected string or dict, got {type(credentials).__name__}"
276
+ )
277
+
278
+ if self.validate_jwt:
279
+ return self._validate_jwt_token(token)
280
+ else:
281
+ return self._validate_opaque_token(token)
282
+
283
+ def _validate_jwt_token(self, token: str) -> Dict[str, Any]:
284
+ """Validate JWT token."""
285
+ if jwt is None:
286
+ raise AuthenticationError("JWT validation not available")
287
+
288
+ try:
289
+ payload = jwt.decode(
290
+ token, self.jwt_secret, algorithms=[self.jwt_algorithm]
291
+ )
292
+
293
+ return {
294
+ "user_id": payload.get("sub", payload.get("user", "unknown")),
295
+ "auth_type": "jwt",
296
+ "permissions": payload.get("permissions", ["read"]),
297
+ "metadata": payload,
298
+ }
299
+
300
+ except jwt.ExpiredSignatureError:
301
+ raise AuthenticationError("Token expired")
302
+ except jwt.InvalidTokenError as e:
303
+ raise AuthenticationError(f"Invalid token: {e}")
304
+
305
+ def _validate_opaque_token(self, token: str) -> Dict[str, Any]:
306
+ """Validate opaque bearer token."""
307
+ if token not in self.tokens:
308
+ raise AuthenticationError("Invalid bearer token")
309
+
310
+ token_data = self.tokens[token]
311
+ return {
312
+ "user_id": f"token_{hashlib.sha256(token.encode()).hexdigest()[:8]}",
313
+ "auth_type": "bearer",
314
+ "permissions": token_data.get("permissions", ["read"]),
315
+ "metadata": token_data,
316
+ }
317
+
318
+ def get_client_config(self) -> Dict[str, Any]:
319
+ """Get client configuration."""
320
+ if self.tokens:
321
+ first_token = next(iter(self.tokens.keys()))
322
+ return {"type": "bearer", "token": first_token}
323
+ return {"type": "bearer"}
324
+
325
+ def get_server_config(self) -> Dict[str, Any]:
326
+ """Get server configuration."""
327
+ config = {"type": "bearer"}
328
+ if self.validate_jwt:
329
+ config.update(
330
+ {
331
+ "validate_jwt": True,
332
+ "jwt_secret": self.jwt_secret,
333
+ "jwt_algorithm": self.jwt_algorithm,
334
+ }
335
+ )
336
+ else:
337
+ config["tokens"] = list(self.tokens.keys())
338
+ return config
339
+
340
+
341
+ class JWTAuth(BearerTokenAuth):
342
+ """JWT-specific authentication provider with token creation.
343
+
344
+ Extends BearerTokenAuth with JWT token creation capabilities.
345
+
346
+ Args:
347
+ secret: JWT signing secret
348
+ algorithm: JWT algorithm
349
+ expiration: Token expiration time in seconds
350
+ issuer: Token issuer
351
+
352
+ Examples:
353
+ Create JWT auth provider:
354
+
355
+ >>> auth = JWTAuth(secret="my-secret", expiration=3600)
356
+ >>> token = auth.create_token({"user": "alice", "permissions": ["read", "write"]})
357
+ """
358
+
359
+ def __init__(
360
+ self,
361
+ secret: str,
362
+ algorithm: str = "HS256",
363
+ expiration: int = 3600,
364
+ issuer: str = "mcp-server",
365
+ ):
366
+ """Initialize JWT authentication."""
367
+ super().__init__(validate_jwt=True, jwt_secret=secret, jwt_algorithm=algorithm)
368
+ self.expiration = expiration
369
+ self.issuer = issuer
370
+
371
+ def create_token(
372
+ self, payload: Dict[str, Any], expiration: Optional[int] = None
373
+ ) -> str:
374
+ """Create a JWT token.
375
+
376
+ Args:
377
+ payload: Token payload (should include 'user' and 'permissions')
378
+ expiration: Custom expiration in seconds
379
+
380
+ Returns:
381
+ JWT token string
382
+
383
+ Examples:
384
+ >>> token = auth.create_token({
385
+ ... "user": "alice",
386
+ ... "permissions": ["read", "write"]
387
+ ... })
388
+ """
389
+ if jwt is None:
390
+ raise RuntimeError("JWT library not available")
391
+
392
+ now = datetime.now(timezone.utc)
393
+ exp_time = expiration or self.expiration
394
+
395
+ jwt_payload = {
396
+ "iss": self.issuer,
397
+ "iat": int(now.timestamp()),
398
+ "exp": int((now + timedelta(seconds=exp_time)).timestamp()),
399
+ "jti": str(uuid.uuid4()),
400
+ **payload,
401
+ }
402
+
403
+ return jwt.encode(jwt_payload, self.jwt_secret, algorithm=self.jwt_algorithm)
404
+
405
+
406
+ class BasicAuth(AuthProvider):
407
+ """Basic HTTP authentication provider.
408
+
409
+ Supports username/password authentication with secure password hashing.
410
+
411
+ Args:
412
+ users: Dict mapping usernames to password hashes or user data
413
+ hash_passwords: Whether to hash plain text passwords
414
+
415
+ Examples:
416
+ Basic auth with plaintext passwords (for development):
417
+
418
+ >>> auth = BasicAuth(users={
419
+ ... "admin": "password123",
420
+ ... "user": "secret456"
421
+ ... }, hash_passwords=True)
422
+
423
+ Basic auth with pre-hashed passwords:
424
+
425
+ >>> auth = BasicAuth(users={
426
+ ... "admin": {
427
+ ... "password_hash": "hashed_password",
428
+ ... "permissions": ["read", "write", "admin"]
429
+ ... }
430
+ ... })
431
+ """
432
+
433
+ def __init__(
434
+ self, users: Dict[str, Union[str, Dict[str, Any]]], hash_passwords: bool = False
435
+ ):
436
+ """Initialize basic authentication."""
437
+ self.users = {}
438
+
439
+ # Normalize user data
440
+ for username, user_data in users.items():
441
+ if isinstance(user_data, str):
442
+ # Plain password
443
+ password = user_data
444
+ if hash_passwords:
445
+ password_hash = self._hash_password(password)
446
+ else:
447
+ password_hash = password
448
+
449
+ self.users[username] = {
450
+ "password_hash": password_hash,
451
+ "permissions": ["read"],
452
+ }
453
+ else:
454
+ # User data dict
455
+ self.users[username] = user_data
456
+ if hash_passwords and "password" in user_data:
457
+ self.users[username]["password_hash"] = self._hash_password(
458
+ user_data["password"]
459
+ )
460
+ del self.users[username]["password"]
461
+
462
+ logger.info(f"Initialized Basic Auth with {len(self.users)} users")
463
+
464
+ def _hash_password(self, password: str) -> str:
465
+ """Hash a password using PBKDF2."""
466
+ salt = os.urandom(32)
467
+ kdf = PBKDF2HMAC(
468
+ algorithm=hashes.SHA256(),
469
+ length=32,
470
+ salt=salt,
471
+ iterations=100000,
472
+ )
473
+ key = kdf.derive(password.encode())
474
+ return base64.b64encode(salt + key).decode()
475
+
476
+ def _verify_password(self, password: str, password_hash: str) -> bool:
477
+ """Verify a password against its hash."""
478
+ try:
479
+ decoded = base64.b64decode(password_hash.encode())
480
+ salt = decoded[:32]
481
+ stored_key = decoded[32:]
482
+
483
+ kdf = PBKDF2HMAC(
484
+ algorithm=hashes.SHA256(),
485
+ length=32,
486
+ salt=salt,
487
+ iterations=100000,
488
+ )
489
+
490
+ kdf.verify(password.encode(), stored_key)
491
+ return True
492
+ except:
493
+ # Fallback to plain text comparison (for development)
494
+ return password == password_hash
495
+
496
+ def authenticate(self, credentials: Union[str, Dict[str, Any]]) -> Dict[str, Any]:
497
+ """Authenticate basic auth credentials.
498
+
499
+ Args:
500
+ credentials: Dict with 'username' and 'password' fields (string not supported for BasicAuth)
501
+
502
+ Returns:
503
+ Authentication context dict
504
+ """
505
+ if isinstance(credentials, str):
506
+ raise AuthenticationError(
507
+ "BasicAuth requires dict with 'username' and 'password' fields, not string"
508
+ )
509
+ elif not isinstance(credentials, dict):
510
+ raise AuthenticationError(
511
+ f"Expected dict, got {type(credentials).__name__}"
512
+ )
513
+
514
+ username = credentials.get("username")
515
+ password = credentials.get("password")
516
+
517
+ if not username or not password:
518
+ raise AuthenticationError("Missing username or password")
519
+
520
+ if username not in self.users:
521
+ raise AuthenticationError("Invalid username")
522
+
523
+ user_data = self.users[username]
524
+ password_hash = user_data.get("password_hash", "")
525
+
526
+ if not self._verify_password(password, password_hash):
527
+ raise AuthenticationError("Invalid password")
528
+
529
+ return {
530
+ "user_id": username,
531
+ "auth_type": "basic",
532
+ "permissions": user_data.get("permissions", ["read"]),
533
+ "metadata": user_data,
534
+ }
535
+
536
+ def get_client_config(self) -> Dict[str, Any]:
537
+ """Get client configuration."""
538
+ # Return first user for client config (in practice, client specifies credentials)
539
+ first_user = next(iter(self.users.keys()))
540
+ return {
541
+ "type": "basic",
542
+ "username": first_user,
543
+ "password": "***", # Client should provide actual password
544
+ }
545
+
546
+ def get_server_config(self) -> Dict[str, Any]:
547
+ """Get server configuration."""
548
+ return {"type": "basic", "users": list(self.users.keys())}
549
+
550
+
551
+ class PermissionManager:
552
+ """Permission management for authenticated users.
553
+
554
+ Provides role-based and permission-based access control.
555
+
556
+ Args:
557
+ roles: Dict mapping role names to permissions
558
+ default_permissions: Default permissions for all users
559
+
560
+ Examples:
561
+ Create permission manager:
562
+
563
+ >>> pm = PermissionManager(roles={
564
+ ... "admin": ["read", "write", "delete", "manage"],
565
+ ... "editor": ["read", "write"],
566
+ ... "viewer": ["read"]
567
+ ... })
568
+ >>> pm.check_permission(user_info, "write")
569
+ """
570
+
571
+ def __init__(
572
+ self,
573
+ roles: Optional[Dict[str, List[str]]] = None,
574
+ default_permissions: Optional[List[str]] = None,
575
+ ):
576
+ """Initialize permission manager."""
577
+ self.roles = roles or {
578
+ "admin": ["read", "write", "delete", "manage"],
579
+ "editor": ["read", "write"],
580
+ "viewer": ["read"],
581
+ }
582
+ self.default_permissions = default_permissions or ["read"]
583
+
584
+ def check_permission(self, user_info: Dict[str, Any], permission: str) -> bool:
585
+ """Check if user has specific permission.
586
+
587
+ Args:
588
+ user_info: User information from authentication
589
+ permission: Permission to check
590
+
591
+ Returns:
592
+ True if user has permission
593
+
594
+ Raises:
595
+ PermissionError: If user lacks permission
596
+ """
597
+ user_permissions = self._get_user_permissions(user_info)
598
+
599
+ if permission in user_permissions:
600
+ return True
601
+
602
+ raise PermissionError(
603
+ f"User lacks required permission: {permission}",
604
+ required_permission=permission,
605
+ )
606
+
607
+ def _get_user_permissions(self, user_info: Dict[str, Any]) -> List[str]:
608
+ """Get all permissions for a user."""
609
+ permissions = set(user_info.get("permissions", self.default_permissions))
610
+
611
+ # Add role-based permissions
612
+ roles = user_info.get("roles", [])
613
+ for role in roles:
614
+ if role in self.roles:
615
+ permissions.update(self.roles[role])
616
+
617
+ return list(permissions)
618
+
619
+
620
+ class RateLimiter:
621
+ """Rate limiting for authenticated users.
622
+
623
+ Implements token bucket algorithm for rate limiting.
624
+
625
+ Args:
626
+ default_limit: Default requests per minute
627
+ burst_limit: Maximum burst requests
628
+ per_user_limits: Custom limits per user
629
+
630
+ Examples:
631
+ Create rate limiter:
632
+
633
+ >>> limiter = RateLimiter(default_limit=60, burst_limit=10)
634
+ >>> limiter.check_rate_limit(user_info)
635
+ """
636
+
637
+ def __init__(
638
+ self,
639
+ default_limit: int = 60, # requests per minute
640
+ burst_limit: int = 10,
641
+ per_user_limits: Optional[Dict[str, int]] = None,
642
+ ):
643
+ """Initialize rate limiter."""
644
+ self.default_limit = default_limit
645
+ self.burst_limit = burst_limit
646
+ self.per_user_limits = per_user_limits or {}
647
+
648
+ # Token buckets per user
649
+ self._buckets: Dict[str, Dict[str, Any]] = defaultdict(
650
+ lambda: {"tokens": self.burst_limit, "last_refill": time.time()}
651
+ )
652
+
653
+ def check_rate_limit(self, user_info: Dict[str, Any]) -> bool:
654
+ """Check if user is within rate limits.
655
+
656
+ Args:
657
+ user_info: User information from authentication
658
+
659
+ Returns:
660
+ True if request is allowed
661
+
662
+ Raises:
663
+ RateLimitError: If rate limit exceeded
664
+ """
665
+ user_id = user_info.get("user_id", "anonymous")
666
+ limit = self.per_user_limits.get(user_id, self.default_limit)
667
+
668
+ bucket = self._buckets[user_id]
669
+ now = time.time()
670
+
671
+ # Refill tokens
672
+ time_passed = now - bucket["last_refill"]
673
+ tokens_to_add = (time_passed / 60.0) * limit # per minute
674
+ bucket["tokens"] = min(self.burst_limit, bucket["tokens"] + tokens_to_add)
675
+ bucket["last_refill"] = now
676
+
677
+ # Check if request allowed
678
+ if bucket["tokens"] >= 1:
679
+ bucket["tokens"] -= 1
680
+ return True
681
+ else:
682
+ retry_after = int(60.0 / limit) # seconds until next token
683
+ raise RateLimitError(
684
+ f"Rate limit exceeded for user {user_id}", retry_after=retry_after
685
+ )
686
+
687
+
688
+ class AuthManager:
689
+ """Comprehensive authentication manager.
690
+
691
+ Combines authentication providers with permission and rate limiting.
692
+
693
+ Args:
694
+ provider: Authentication provider
695
+ permission_manager: Permission manager
696
+ rate_limiter: Rate limiter
697
+ enable_audit: Enable audit logging
698
+
699
+ Examples:
700
+ Create full auth manager:
701
+
702
+ >>> auth_provider = APIKeyAuth(keys=["secret123"])
703
+ >>> manager = AuthManager(
704
+ ... provider=auth_provider,
705
+ ... permission_manager=PermissionManager(),
706
+ ... rate_limiter=RateLimiter(default_limit=100)
707
+ ... )
708
+ """
709
+
710
+ def __init__(
711
+ self,
712
+ provider: AuthProvider,
713
+ permission_manager: Optional[PermissionManager] = None,
714
+ rate_limiter: Optional[RateLimiter] = None,
715
+ enable_audit: bool = True,
716
+ ):
717
+ """Initialize auth manager."""
718
+ self.provider = provider
719
+ self.permission_manager = permission_manager or PermissionManager()
720
+ self.rate_limiter = rate_limiter
721
+ self.enable_audit = enable_audit
722
+
723
+ # Audit log
724
+ self._audit_log: List[Dict[str, Any]] = []
725
+
726
+ def authenticate_and_authorize(
727
+ self, credentials: Dict[str, Any], required_permission: Optional[str] = None
728
+ ) -> Dict[str, Any]:
729
+ """Authenticate credentials and check authorization.
730
+
731
+ Args:
732
+ credentials: Authentication credentials
733
+ required_permission: Required permission for the operation
734
+
735
+ Returns:
736
+ User information dict
737
+
738
+ Raises:
739
+ AuthenticationError: If authentication fails
740
+ PermissionError: If user lacks required permission
741
+ RateLimitError: If rate limit exceeded
742
+ """
743
+ # Authenticate
744
+ user_info = self.provider.authenticate(credentials)
745
+
746
+ # Check rate limits
747
+ if self.rate_limiter:
748
+ self.rate_limiter.check_rate_limit(user_info)
749
+
750
+ # Check permissions
751
+ if required_permission:
752
+ self.permission_manager.check_permission(user_info, required_permission)
753
+
754
+ # Audit log
755
+ if self.enable_audit:
756
+ self._log_auth_event("success", user_info, required_permission)
757
+
758
+ return user_info
759
+
760
+ def _log_auth_event(
761
+ self,
762
+ event_type: str,
763
+ user_info: Dict[str, Any],
764
+ permission: Optional[str] = None,
765
+ ):
766
+ """Log authentication event."""
767
+ event = {
768
+ "timestamp": datetime.now(timezone.utc).isoformat(),
769
+ "event_type": event_type,
770
+ "user_id": user_info.get("user_id"),
771
+ "auth_type": user_info.get("auth_type"),
772
+ "permission": permission,
773
+ }
774
+ self._audit_log.append(event)
775
+
776
+ # Keep only last 1000 events
777
+ if len(self._audit_log) > 1000:
778
+ self._audit_log = self._audit_log[-1000:]
779
+
780
+ def get_audit_log(self, limit: int = 100) -> List[Dict[str, Any]]:
781
+ """Get audit log entries.
782
+
783
+ Args:
784
+ limit: Maximum number of entries to return
785
+
786
+ Returns:
787
+ List of audit log entries
788
+ """
789
+ return self._audit_log[-limit:]