embed-client 2.0.0.0__py3-none-any.whl → 3.1.0.1__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.
embed_client/auth.py ADDED
@@ -0,0 +1,491 @@
1
+ """
2
+ Authentication system for embed-client.
3
+
4
+ This module provides comprehensive authentication management for the embed-client,
5
+ supporting all security modes and authentication methods. It integrates with
6
+ mcp_security_framework when available and provides fallback implementations.
7
+
8
+ Author: Vasiliy Zdanovskiy
9
+ email: vasilyvz@gmail.com
10
+ """
11
+
12
+ import base64
13
+ import logging
14
+ import time
15
+ from typing import Any, Dict, List, Optional, Union
16
+ from urllib.parse import urlparse
17
+
18
+ # Try to import mcp_security_framework components
19
+ try:
20
+ from mcp_security_framework import (
21
+ AuthManager,
22
+ AuthConfig,
23
+ PermissionConfig,
24
+ PermissionManager,
25
+ SecurityManager
26
+ )
27
+ SECURITY_FRAMEWORK_AVAILABLE = True
28
+ except ImportError:
29
+ SECURITY_FRAMEWORK_AVAILABLE = False
30
+ print("Warning: mcp_security_framework not available. Using fallback authentication.")
31
+
32
+ # Fallback JWT implementation
33
+ try:
34
+ import jwt
35
+ JWT_AVAILABLE = True
36
+ except ImportError:
37
+ JWT_AVAILABLE = False
38
+ print("Warning: PyJWT not available. JWT authentication will be disabled.")
39
+
40
+
41
+ class AuthenticationError(Exception):
42
+ """Raised when authentication fails."""
43
+
44
+ def __init__(self, message: str, error_code: int = 401):
45
+ self.message = message
46
+ self.error_code = error_code
47
+ super().__init__(self.message)
48
+
49
+
50
+ class AuthResult:
51
+ """Authentication result container."""
52
+
53
+ def __init__(self, success: bool, user_id: Optional[str] = None,
54
+ roles: Optional[List[str]] = None, error: Optional[str] = None):
55
+ self.success = success
56
+ self.user_id = user_id
57
+ self.roles = roles or []
58
+ self.error = error
59
+
60
+
61
+ class ClientAuthManager:
62
+ """
63
+ Client Authentication Manager.
64
+
65
+ This class provides authentication management for the embed-client,
66
+ supporting multiple authentication methods with integration to
67
+ mcp_security_framework when available.
68
+ """
69
+
70
+ def __init__(self, config: Dict[str, Any]):
71
+ """
72
+ Initialize authentication manager.
73
+
74
+ Args:
75
+ config: Authentication configuration dictionary
76
+ """
77
+ self.config = config
78
+ self.logger = logging.getLogger(__name__)
79
+
80
+ # Initialize security framework components if available
81
+ self.auth_manager = None
82
+ self.permission_manager = None
83
+
84
+ if SECURITY_FRAMEWORK_AVAILABLE:
85
+ self._initialize_security_framework()
86
+ else:
87
+ self.logger.warning("mcp_security_framework not available, using fallback authentication")
88
+
89
+ def _initialize_security_framework(self) -> None:
90
+ """Initialize mcp_security_framework components."""
91
+ try:
92
+ # Create auth config
93
+ auth_config = AuthConfig(
94
+ enabled=self.config.get("auth", {}).get("enabled", True),
95
+ methods=self.config.get("auth", {}).get("methods", ["api_key"]),
96
+ api_keys=self.config.get("auth", {}).get("api_keys", {}),
97
+ jwt_secret=self.config.get("auth", {}).get("jwt", {}).get("secret", ""),
98
+ jwt_algorithm="HS256",
99
+ jwt_expiry_hours=self.config.get("auth", {}).get("jwt", {}).get("expiry_hours", 24)
100
+ )
101
+
102
+ # Create permission config
103
+ permission_config = PermissionConfig(
104
+ enabled=self.config.get("security", {}).get("roles_enabled", False),
105
+ roles_file=self.config.get("security", {}).get("roles_file") or "configs/roles.json"
106
+ )
107
+
108
+ # Create security config
109
+ from mcp_security_framework import SecurityConfig
110
+ security_config = SecurityConfig(
111
+ auth=auth_config,
112
+ permissions=permission_config
113
+ )
114
+
115
+ # Initialize managers
116
+ self.security_manager = SecurityManager(security_config)
117
+ self.auth_manager = self.security_manager.auth_manager
118
+ self.permission_manager = self.security_manager.permission_manager
119
+
120
+ self.logger.info("Security framework initialized successfully")
121
+
122
+ except Exception as e:
123
+ self.logger.warning(f"Failed to initialize security framework: {e}")
124
+ self.auth_manager = None
125
+ self.permission_manager = None
126
+
127
+ def authenticate_api_key(self, api_key: str, header_name: str = "X-API-Key") -> AuthResult:
128
+ """
129
+ Authenticate using API key.
130
+
131
+ Args:
132
+ api_key: API key to authenticate
133
+ header_name: Header name for API key
134
+
135
+ Returns:
136
+ AuthResult with authentication status
137
+ """
138
+ try:
139
+ if self.auth_manager:
140
+ # Use security framework
141
+ result = self.auth_manager.authenticate_api_key(api_key)
142
+ if result.status == AuthStatus.SUCCESS:
143
+ return AuthResult(
144
+ success=True,
145
+ user_id=result.user_id,
146
+ roles=result.roles
147
+ )
148
+ else:
149
+ return AuthResult(success=False, error=result.error_message)
150
+ else:
151
+ # Fallback implementation
152
+ return self._authenticate_api_key_fallback(api_key)
153
+
154
+ except Exception as e:
155
+ self.logger.error(f"API key authentication failed: {e}")
156
+ return AuthResult(success=False, error=str(e))
157
+
158
+ def _authenticate_api_key_fallback(self, api_key: str) -> AuthResult:
159
+ """Fallback API key authentication."""
160
+ api_keys = self.config.get("auth", {}).get("api_keys", {})
161
+
162
+ # Check if API key exists
163
+ for user_id, stored_key in api_keys.items():
164
+ if stored_key == api_key:
165
+ return AuthResult(success=True, user_id=user_id)
166
+
167
+ return AuthResult(success=False, error="Invalid API key")
168
+
169
+ def authenticate_jwt(self, token: str) -> AuthResult:
170
+ """
171
+ Authenticate using JWT token.
172
+
173
+ Args:
174
+ token: JWT token to authenticate
175
+
176
+ Returns:
177
+ AuthResult with authentication status
178
+ """
179
+ try:
180
+ if self.auth_manager:
181
+ # Use security framework
182
+ result = self.auth_manager.validate_jwt_token(token)
183
+ if result.status == AuthStatus.SUCCESS:
184
+ return AuthResult(
185
+ success=True,
186
+ user_id=result.user_id,
187
+ roles=result.roles
188
+ )
189
+ else:
190
+ return AuthResult(success=False, error=result.error_message)
191
+ else:
192
+ # Fallback implementation
193
+ return self._authenticate_jwt_fallback(token)
194
+
195
+ except Exception as e:
196
+ self.logger.error(f"JWT authentication failed: {e}")
197
+ return AuthResult(success=False, error=str(e))
198
+
199
+ def _authenticate_jwt_fallback(self, token: str) -> AuthResult:
200
+ """Fallback JWT authentication."""
201
+ if not JWT_AVAILABLE:
202
+ return AuthResult(success=False, error="JWT not available")
203
+
204
+ try:
205
+ jwt_config = self.config.get("auth", {}).get("jwt", {})
206
+ secret = jwt_config.get("secret", "")
207
+
208
+ if not secret:
209
+ return AuthResult(success=False, error="JWT secret not configured")
210
+
211
+ # Decode and verify token
212
+ payload = jwt.decode(token, secret, algorithms=["HS256"])
213
+
214
+ # Check expiration
215
+ if "exp" in payload and payload["exp"] < time.time():
216
+ return AuthResult(success=False, error="Token expired")
217
+
218
+ return AuthResult(
219
+ success=True,
220
+ user_id=payload.get("sub", payload.get("user_id")),
221
+ roles=payload.get("roles", [])
222
+ )
223
+
224
+ except jwt.ExpiredSignatureError:
225
+ return AuthResult(success=False, error="Token expired")
226
+ except jwt.InvalidTokenError as e:
227
+ return AuthResult(success=False, error=f"Invalid token: {e}")
228
+
229
+ def authenticate_basic(self, username: str, password: str) -> AuthResult:
230
+ """
231
+ Authenticate using basic authentication.
232
+
233
+ Args:
234
+ username: Username
235
+ password: Password
236
+
237
+ Returns:
238
+ AuthResult with authentication status
239
+ """
240
+ try:
241
+ if self.auth_manager:
242
+ # Use security framework
243
+ result = self.auth_manager.authenticate_basic(username, password)
244
+ if result.status == AuthStatus.SUCCESS:
245
+ return AuthResult(
246
+ success=True,
247
+ user_id=result.user_id,
248
+ roles=result.roles
249
+ )
250
+ else:
251
+ return AuthResult(success=False, error=result.error_message)
252
+ else:
253
+ # Fallback implementation
254
+ return self._authenticate_basic_fallback(username, password)
255
+
256
+ except Exception as e:
257
+ self.logger.error(f"Basic authentication failed: {e}")
258
+ return AuthResult(success=False, error=str(e))
259
+
260
+ def _authenticate_basic_fallback(self, username: str, password: str) -> AuthResult:
261
+ """Fallback basic authentication."""
262
+ basic_config = self.config.get("auth", {}).get("basic", {})
263
+ stored_username = basic_config.get("username")
264
+ stored_password = basic_config.get("password")
265
+
266
+ if username == stored_username and password == stored_password:
267
+ return AuthResult(success=True, user_id=username)
268
+
269
+ return AuthResult(success=False, error="Invalid credentials")
270
+
271
+ def authenticate_certificate(self, cert_file: str, key_file: str) -> AuthResult:
272
+ """
273
+ Authenticate using client certificate.
274
+
275
+ Args:
276
+ cert_file: Path to client certificate file
277
+ key_file: Path to client private key file
278
+
279
+ Returns:
280
+ AuthResult with authentication status
281
+ """
282
+ try:
283
+ if self.auth_manager:
284
+ # Use security framework
285
+ result = self.auth_manager.authenticate_certificate(cert_file, key_file)
286
+ if result.status == AuthStatus.SUCCESS:
287
+ return AuthResult(
288
+ success=True,
289
+ user_id=result.user_id,
290
+ roles=result.roles
291
+ )
292
+ else:
293
+ return AuthResult(success=False, error=result.error_message)
294
+ else:
295
+ # Fallback implementation
296
+ return self._authenticate_certificate_fallback(cert_file, key_file)
297
+
298
+ except Exception as e:
299
+ self.logger.error(f"Certificate authentication failed: {e}")
300
+ return AuthResult(success=False, error=str(e))
301
+
302
+ def _authenticate_certificate_fallback(self, cert_file: str, key_file: str) -> AuthResult:
303
+ """Fallback certificate authentication."""
304
+ try:
305
+ # Basic file existence check
306
+ import os
307
+ if not os.path.exists(cert_file):
308
+ return AuthResult(success=False, error="Certificate file not found")
309
+ if not os.path.exists(key_file):
310
+ return AuthResult(success=False, error="Key file not found")
311
+
312
+ # For fallback, we just verify files exist
313
+ # In a real implementation, you would validate the certificate
314
+ return AuthResult(success=True, user_id="certificate_user")
315
+
316
+ except Exception as e:
317
+ return AuthResult(success=False, error=f"Certificate validation failed: {e}")
318
+
319
+ def create_jwt_token(self, user_id: str, roles: Optional[List[str]] = None,
320
+ expiry_hours: Optional[int] = None) -> str:
321
+ """
322
+ Create JWT token for user.
323
+
324
+ Args:
325
+ user_id: User identifier
326
+ roles: List of user roles
327
+ expiry_hours: Token expiry in hours
328
+
329
+ Returns:
330
+ JWT token string
331
+ """
332
+ try:
333
+ if self.auth_manager:
334
+ # Use security framework
335
+ return self.auth_manager.create_jwt_token(user_id, roles, expiry_hours)
336
+ else:
337
+ # Fallback implementation
338
+ return self._create_jwt_token_fallback(user_id, roles, expiry_hours)
339
+
340
+ except Exception as e:
341
+ self.logger.error(f"JWT token creation failed: {e}")
342
+ raise AuthenticationError(f"Failed to create JWT token: {e}")
343
+
344
+ def _create_jwt_token_fallback(self, user_id: str, roles: Optional[List[str]] = None,
345
+ expiry_hours: Optional[int] = None) -> str:
346
+ """Fallback JWT token creation."""
347
+ if not JWT_AVAILABLE:
348
+ raise AuthenticationError("JWT not available")
349
+
350
+ jwt_config = self.config.get("auth", {}).get("jwt", {})
351
+ secret = jwt_config.get("secret", "")
352
+ expiry = expiry_hours or jwt_config.get("expiry_hours", 24)
353
+
354
+ if not secret:
355
+ raise AuthenticationError("JWT secret not configured")
356
+
357
+ payload = {
358
+ "sub": user_id,
359
+ "user_id": user_id,
360
+ "roles": roles or [],
361
+ "exp": time.time() + (expiry * 3600),
362
+ "iat": time.time()
363
+ }
364
+
365
+ return jwt.encode(payload, secret, algorithm="HS256")
366
+
367
+ def get_auth_headers(self, auth_method: str, **kwargs) -> Dict[str, str]:
368
+ """
369
+ Get authentication headers for requests.
370
+
371
+ Args:
372
+ auth_method: Authentication method
373
+ **kwargs: Additional authentication parameters
374
+
375
+ Returns:
376
+ Dictionary of headers
377
+ """
378
+ headers = {}
379
+
380
+ if auth_method == "api_key":
381
+ api_key = kwargs.get("api_key")
382
+ header_name = kwargs.get("header", "X-API-Key")
383
+ if api_key:
384
+ headers[header_name] = api_key
385
+
386
+ elif auth_method == "jwt":
387
+ token = kwargs.get("token")
388
+ if token:
389
+ headers["Authorization"] = f"Bearer {token}"
390
+
391
+ elif auth_method == "basic":
392
+ username = kwargs.get("username")
393
+ password = kwargs.get("password")
394
+ if username and password:
395
+ credentials = base64.b64encode(f"{username}:{password}".encode()).decode()
396
+ headers["Authorization"] = f"Basic {credentials}"
397
+
398
+ elif auth_method == "certificate":
399
+ # Certificate authentication is handled at SSL level
400
+ pass
401
+
402
+ return headers
403
+
404
+ def validate_auth_config(self) -> List[str]:
405
+ """
406
+ Validate authentication configuration.
407
+
408
+ Returns:
409
+ List of validation errors
410
+ """
411
+ errors = []
412
+ auth_config = self.config.get("auth", {})
413
+ auth_method = auth_config.get("method", "none")
414
+
415
+ if auth_method == "api_key":
416
+ api_keys = auth_config.get("api_keys", {})
417
+ if not api_keys:
418
+ errors.append("API keys not configured for api_key authentication")
419
+
420
+ elif auth_method == "jwt":
421
+ jwt_config = auth_config.get("jwt", {})
422
+ if not jwt_config.get("secret"):
423
+ errors.append("JWT secret not configured")
424
+ if not jwt_config.get("username"):
425
+ errors.append("JWT username not configured")
426
+ if not jwt_config.get("password"):
427
+ errors.append("JWT password not configured")
428
+
429
+ elif auth_method == "certificate":
430
+ cert_config = auth_config.get("certificate", {})
431
+ if not cert_config.get("cert_file"):
432
+ errors.append("Certificate file not configured")
433
+ if not cert_config.get("key_file"):
434
+ errors.append("Key file not configured")
435
+
436
+ elif auth_method == "basic":
437
+ basic_config = auth_config.get("basic", {})
438
+ if not basic_config.get("username"):
439
+ errors.append("Basic auth username not configured")
440
+ if not basic_config.get("password"):
441
+ errors.append("Basic auth password not configured")
442
+
443
+ return errors
444
+
445
+ def is_auth_enabled(self) -> bool:
446
+ """Check if authentication is enabled."""
447
+ return self.config.get("auth", {}).get("method", "none") != "none"
448
+
449
+ def get_auth_method(self) -> str:
450
+ """Get current authentication method."""
451
+ return self.config.get("auth", {}).get("method", "none")
452
+
453
+ def get_supported_methods(self) -> List[str]:
454
+ """Get list of supported authentication methods."""
455
+ if SECURITY_FRAMEWORK_AVAILABLE:
456
+ return ["api_key", "jwt", "certificate", "basic"]
457
+ else:
458
+ methods = ["api_key", "basic"]
459
+ if JWT_AVAILABLE:
460
+ methods.append("jwt")
461
+ return methods
462
+
463
+
464
+ def create_auth_manager(config: Dict[str, Any]) -> ClientAuthManager:
465
+ """
466
+ Create authentication manager from configuration.
467
+
468
+ Args:
469
+ config: Configuration dictionary
470
+
471
+ Returns:
472
+ ClientAuthManager instance
473
+ """
474
+ return ClientAuthManager(config)
475
+
476
+
477
+ def create_auth_headers(auth_method: str, **kwargs) -> Dict[str, str]:
478
+ """
479
+ Create authentication headers for requests.
480
+
481
+ Args:
482
+ auth_method: Authentication method
483
+ **kwargs: Authentication parameters
484
+
485
+ Returns:
486
+ Dictionary of headers
487
+ """
488
+ # Create temporary auth manager for header generation
489
+ temp_config = {"auth": {"method": auth_method}}
490
+ auth_manager = ClientAuthManager(temp_config)
491
+ return auth_manager.get_auth_headers(auth_method, **kwargs)