kailash 0.6.2__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.
- kailash/__init__.py +3 -3
- kailash/api/custom_nodes_secure.py +3 -3
- kailash/api/gateway.py +1 -1
- kailash/api/studio.py +2 -3
- kailash/api/workflow_api.py +3 -4
- kailash/core/resilience/bulkhead.py +460 -0
- kailash/core/resilience/circuit_breaker.py +92 -10
- kailash/edge/discovery.py +86 -0
- kailash/mcp_server/__init__.py +334 -0
- kailash/mcp_server/advanced_features.py +1022 -0
- kailash/{mcp → mcp_server}/ai_registry_server.py +29 -4
- kailash/mcp_server/auth.py +789 -0
- kailash/mcp_server/client.py +712 -0
- kailash/mcp_server/discovery.py +1593 -0
- kailash/mcp_server/errors.py +673 -0
- kailash/mcp_server/oauth.py +1727 -0
- kailash/mcp_server/protocol.py +1126 -0
- kailash/mcp_server/registry_integration.py +587 -0
- kailash/mcp_server/server.py +1747 -0
- kailash/{mcp → mcp_server}/servers/ai_registry.py +2 -2
- kailash/mcp_server/transports.py +1169 -0
- kailash/mcp_server/utils/cache.py +510 -0
- kailash/middleware/auth/auth_manager.py +3 -3
- kailash/middleware/communication/api_gateway.py +2 -9
- kailash/middleware/communication/realtime.py +1 -1
- kailash/middleware/mcp/client_integration.py +1 -1
- kailash/middleware/mcp/enhanced_server.py +2 -2
- kailash/nodes/__init__.py +2 -0
- kailash/nodes/admin/audit_log.py +6 -6
- kailash/nodes/admin/permission_check.py +8 -8
- kailash/nodes/admin/role_management.py +32 -28
- kailash/nodes/admin/schema.sql +6 -1
- kailash/nodes/admin/schema_manager.py +13 -13
- kailash/nodes/admin/security_event.py +16 -20
- kailash/nodes/admin/tenant_isolation.py +3 -3
- kailash/nodes/admin/transaction_utils.py +3 -3
- kailash/nodes/admin/user_management.py +21 -22
- kailash/nodes/ai/a2a.py +11 -11
- kailash/nodes/ai/ai_providers.py +9 -12
- kailash/nodes/ai/embedding_generator.py +13 -14
- kailash/nodes/ai/intelligent_agent_orchestrator.py +19 -19
- kailash/nodes/ai/iterative_llm_agent.py +3 -3
- kailash/nodes/ai/llm_agent.py +213 -36
- kailash/nodes/ai/self_organizing.py +2 -2
- kailash/nodes/alerts/discord.py +4 -4
- kailash/nodes/api/graphql.py +6 -6
- kailash/nodes/api/http.py +12 -17
- kailash/nodes/api/rate_limiting.py +4 -4
- kailash/nodes/api/rest.py +15 -15
- kailash/nodes/auth/mfa.py +3 -4
- kailash/nodes/auth/risk_assessment.py +2 -2
- kailash/nodes/auth/session_management.py +5 -5
- kailash/nodes/auth/sso.py +143 -0
- kailash/nodes/base.py +6 -2
- kailash/nodes/base_async.py +16 -2
- kailash/nodes/base_with_acl.py +2 -2
- kailash/nodes/cache/__init__.py +9 -0
- kailash/nodes/cache/cache.py +1172 -0
- kailash/nodes/cache/cache_invalidation.py +870 -0
- kailash/nodes/cache/redis_pool_manager.py +595 -0
- kailash/nodes/code/async_python.py +2 -1
- kailash/nodes/code/python.py +196 -35
- kailash/nodes/compliance/data_retention.py +6 -6
- kailash/nodes/compliance/gdpr.py +5 -5
- kailash/nodes/data/__init__.py +10 -0
- kailash/nodes/data/optimistic_locking.py +906 -0
- kailash/nodes/data/readers.py +8 -8
- kailash/nodes/data/redis.py +349 -0
- kailash/nodes/data/sql.py +314 -3
- kailash/nodes/data/streaming.py +21 -0
- kailash/nodes/enterprise/__init__.py +8 -0
- kailash/nodes/enterprise/audit_logger.py +285 -0
- kailash/nodes/enterprise/batch_processor.py +22 -3
- kailash/nodes/enterprise/data_lineage.py +1 -1
- kailash/nodes/enterprise/mcp_executor.py +205 -0
- kailash/nodes/enterprise/service_discovery.py +150 -0
- kailash/nodes/enterprise/tenant_assignment.py +108 -0
- kailash/nodes/logic/async_operations.py +2 -2
- kailash/nodes/logic/convergence.py +1 -1
- kailash/nodes/logic/operations.py +1 -1
- kailash/nodes/monitoring/__init__.py +11 -1
- kailash/nodes/monitoring/health_check.py +456 -0
- kailash/nodes/monitoring/log_processor.py +817 -0
- kailash/nodes/monitoring/metrics_collector.py +627 -0
- kailash/nodes/monitoring/performance_benchmark.py +137 -11
- kailash/nodes/rag/advanced.py +7 -7
- kailash/nodes/rag/agentic.py +49 -2
- kailash/nodes/rag/conversational.py +3 -3
- kailash/nodes/rag/evaluation.py +3 -3
- kailash/nodes/rag/federated.py +3 -3
- kailash/nodes/rag/graph.py +3 -3
- kailash/nodes/rag/multimodal.py +3 -3
- kailash/nodes/rag/optimized.py +5 -5
- kailash/nodes/rag/privacy.py +3 -3
- kailash/nodes/rag/query_processing.py +6 -6
- kailash/nodes/rag/realtime.py +1 -1
- kailash/nodes/rag/registry.py +2 -6
- kailash/nodes/rag/router.py +1 -1
- kailash/nodes/rag/similarity.py +7 -7
- kailash/nodes/rag/strategies.py +4 -4
- kailash/nodes/security/abac_evaluator.py +6 -6
- kailash/nodes/security/behavior_analysis.py +5 -6
- kailash/nodes/security/credential_manager.py +1 -1
- kailash/nodes/security/rotating_credentials.py +11 -11
- kailash/nodes/security/threat_detection.py +8 -8
- kailash/nodes/testing/credential_testing.py +2 -2
- kailash/nodes/transform/processors.py +5 -5
- kailash/runtime/local.py +162 -14
- kailash/runtime/parameter_injection.py +425 -0
- kailash/runtime/parameter_injector.py +657 -0
- kailash/runtime/testing.py +2 -2
- kailash/testing/fixtures.py +2 -2
- kailash/workflow/builder.py +99 -18
- kailash/workflow/builder_improvements.py +207 -0
- kailash/workflow/input_handling.py +170 -0
- {kailash-0.6.2.dist-info → kailash-0.6.4.dist-info}/METADATA +21 -8
- {kailash-0.6.2.dist-info → kailash-0.6.4.dist-info}/RECORD +126 -101
- kailash/mcp/__init__.py +0 -53
- kailash/mcp/client.py +0 -445
- kailash/mcp/server.py +0 -292
- kailash/mcp/server_enhanced.py +0 -449
- kailash/mcp/utils/cache.py +0 -267
- /kailash/{mcp → mcp_server}/client_new.py +0 -0
- /kailash/{mcp → mcp_server}/utils/__init__.py +0 -0
- /kailash/{mcp → mcp_server}/utils/config.py +0 -0
- /kailash/{mcp → mcp_server}/utils/formatters.py +0 -0
- /kailash/{mcp → mcp_server}/utils/metrics.py +0 -0
- {kailash-0.6.2.dist-info → kailash-0.6.4.dist-info}/WHEEL +0 -0
- {kailash-0.6.2.dist-info → kailash-0.6.4.dist-info}/entry_points.txt +0 -0
- {kailash-0.6.2.dist-info → kailash-0.6.4.dist-info}/licenses/LICENSE +0 -0
- {kailash-0.6.2.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:]
|