mcp-security-framework 2.2.0__tar.gz → 2.3.0__tar.gz
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.
- {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/PKG-INFO +1 -1
- {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/__init__.py +1 -1
- {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/core/audit_logger.py +28 -20
- {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/core/auth_manager.py +271 -9
- {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/core/ssl_manager.py +24 -0
- mcp_security_framework-2.3.0/mcp_security_framework/examples/auth_methods_example.py +321 -0
- {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/examples/cert_inspection_example.py +67 -38
- mcp_security_framework-2.3.0/mcp_security_framework/examples/certificate_roles_example.py +393 -0
- {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/examples/crl_retrieval_example.py +42 -19
- {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/examples/protected_keys_example.py +29 -12
- {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/middleware/fastapi_auth_middleware.py +120 -27
- {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/middleware/fastapi_middleware.py +121 -29
- {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/middleware/flask_auth_middleware.py +83 -27
- {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/middleware/flask_middleware.py +82 -29
- {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/schemas/config.py +32 -6
- mcp_security_framework-2.3.0/mcp_security_framework/utils/cert_utils/__init__.py +80 -0
- mcp_security_framework-2.3.0/mcp_security_framework/utils/cert_utils/chain.py +183 -0
- mcp_security_framework-2.3.0/mcp_security_framework/utils/cert_utils/errors.py +28 -0
- mcp_security_framework-2.3.0/mcp_security_framework/utils/cert_utils/inspection.py +335 -0
- mcp_security_framework-2.3.0/mcp_security_framework/utils/cert_utils/parsing.py +255 -0
- mcp_security_framework-2.3.0/mcp_security_framework/utils/cert_utils/revocation.py +206 -0
- mcp_security_framework-2.3.0/mcp_security_framework/utils/cert_utils/roles.py +412 -0
- {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework.egg-info/PKG-INFO +1 -1
- {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework.egg-info/SOURCES.txt +10 -2
- {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/pyproject.toml +1 -1
- mcp_security_framework-2.2.0/mcp_security_framework/utils/cert_utils.py +0 -1353
- {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/LICENSE +0 -0
- {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/README.md +0 -0
- {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/cli/__init__.py +0 -0
- {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/cli/cert_cli.py +0 -0
- {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/cli/security_cli.py +0 -0
- {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/constants.py +0 -0
- {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/core/__init__.py +0 -0
- {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/core/adapter_wrapper.py +0 -0
- {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/core/cert_manager.py +0 -0
- {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/core/permission_manager.py +0 -0
- {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/core/rate_limiter.py +0 -0
- {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/core/security_adapter.py +0 -0
- {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/core/security_manager.py +0 -0
- {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/examples/__init__.py +0 -0
- {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/examples/adapter_example.py +0 -0
- {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/examples/comprehensive_example.py +0 -0
- {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/examples/fastapi_example.py +0 -0
- {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/examples/flask_example.py +0 -0
- {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/examples/gateway_example.py +0 -0
- {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/examples/microservice_example.py +0 -0
- {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/examples/standalone_example.py +0 -0
- {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/middleware/__init__.py +0 -0
- {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/middleware/auth_middleware.py +0 -0
- {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/middleware/mtls_middleware.py +0 -0
- {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/middleware/rate_limit_middleware.py +0 -0
- {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/middleware/security_middleware.py +0 -0
- {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/schemas/__init__.py +0 -0
- {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/schemas/models.py +0 -0
- {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/schemas/operation_context.py +0 -0
- {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/schemas/responses.py +0 -0
- {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/utils/__init__.py +0 -0
- {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/utils/crl_client.py +0 -0
- {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/utils/crypto_utils.py +0 -0
- {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/utils/datetime_compat.py +0 -0
- {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework/utils/validation_utils.py +0 -0
- {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework.egg-info/dependency_links.txt +0 -0
- {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework.egg-info/entry_points.txt +0 -0
- {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework.egg-info/requires.txt +0 -0
- {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/mcp_security_framework.egg-info/top_level.txt +0 -0
- {mcp_security_framework-2.2.0 → mcp_security_framework-2.3.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mcp-security-framework
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
4
4
|
Summary: Universal security framework for microservices with SSL/TLS, authentication, authorization, and rate limiting. Added extensible security adapter system for custom operation validation, structured audit logging, and operation context support. Comprehensive test coverage with 1104+ tests. Fully backward compatible. Requires cryptography>=42.0.0 for certificate operations.
|
|
5
5
|
Author-email: Vasiliy Zdanovskiy <vasilyvz@gmail.com>
|
|
6
6
|
Maintainer-email: Vasiliy Zdanovskiy <vasilyvz@gmail.com>
|
|
@@ -33,6 +33,22 @@ from typing import Any, Dict, List, Optional
|
|
|
33
33
|
from ..schemas.operation_context import OperationContext
|
|
34
34
|
|
|
35
35
|
|
|
36
|
+
class AuditLoggerConfigurationError(ValueError):
|
|
37
|
+
"""
|
|
38
|
+
Raised when the audit logger is misconfigured or an unsupported
|
|
39
|
+
backend is requested.
|
|
40
|
+
|
|
41
|
+
This error indicates that the logger cannot be initialized with the
|
|
42
|
+
provided configuration, typically because a requested backend is not
|
|
43
|
+
supported or required configuration is missing.
|
|
44
|
+
|
|
45
|
+
Inherits from ValueError to maintain compatibility with existing
|
|
46
|
+
code that catches ValueError for configuration errors.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
|
|
36
52
|
class AuditStatus(str, Enum):
|
|
37
53
|
"""
|
|
38
54
|
Audit Status Enumeration
|
|
@@ -224,9 +240,10 @@ class AuditLogger:
|
|
|
224
240
|
... )
|
|
225
241
|
|
|
226
242
|
Note:
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
243
|
+
Only file and console backends are supported. Requesting any other
|
|
244
|
+
backend will raise AuditLoggerConfigurationError at initialization time
|
|
245
|
+
to prevent silent loss of audit records. This is not an extension point;
|
|
246
|
+
audit logs are security-critical and must never silently fail.
|
|
230
247
|
"""
|
|
231
248
|
|
|
232
249
|
def __init__(
|
|
@@ -239,15 +256,16 @@ class AuditLogger:
|
|
|
239
256
|
|
|
240
257
|
Args:
|
|
241
258
|
backend (str): Backend type for audit logging. Supported values:
|
|
242
|
-
"file"
|
|
243
|
-
to "console" if not specified.
|
|
259
|
+
"file" or "console". Defaults to "console" if not specified.
|
|
244
260
|
config (Optional[Dict[str, Any]]): Configuration dictionary for
|
|
245
261
|
the selected backend. For file backend, should contain
|
|
246
262
|
"log_file" key with file path. For console backend, can
|
|
247
263
|
be empty or None. Defaults to None.
|
|
248
264
|
|
|
249
265
|
Raises:
|
|
250
|
-
|
|
266
|
+
AuditLoggerConfigurationError: If backend type is not supported.
|
|
267
|
+
This is a fail-loud check at initialization time to prevent
|
|
268
|
+
silent loss of audit records.
|
|
251
269
|
FileNotFoundError: If file backend is used and log file
|
|
252
270
|
directory does not exist.
|
|
253
271
|
|
|
@@ -266,11 +284,11 @@ class AuditLogger:
|
|
|
266
284
|
self.logger = logging.getLogger(f"{__name__}.{self.backend}")
|
|
267
285
|
|
|
268
286
|
# Validate backend type
|
|
269
|
-
|
|
270
|
-
if self.backend not in
|
|
271
|
-
raise
|
|
287
|
+
implemented_backends = ["file", "console"]
|
|
288
|
+
if self.backend not in implemented_backends:
|
|
289
|
+
raise AuditLoggerConfigurationError(
|
|
272
290
|
f"Unsupported backend: {self.backend}. "
|
|
273
|
-
f"Supported backends: {
|
|
291
|
+
f"Supported backends: {implemented_backends}"
|
|
274
292
|
)
|
|
275
293
|
|
|
276
294
|
# Initialize backend-specific configuration
|
|
@@ -310,8 +328,6 @@ class AuditLogger:
|
|
|
310
328
|
AuditEvent instance with all required fields.
|
|
311
329
|
|
|
312
330
|
Raises:
|
|
313
|
-
NotImplementedError: If database or elasticsearch backend
|
|
314
|
-
is used (not yet implemented).
|
|
315
331
|
IOError: If file backend is used and log file cannot be written.
|
|
316
332
|
|
|
317
333
|
Example:
|
|
@@ -340,14 +356,6 @@ class AuditLogger:
|
|
|
340
356
|
log_message = json.dumps(event_dict, default=str)
|
|
341
357
|
self.logger.info(log_message)
|
|
342
358
|
|
|
343
|
-
elif self.backend == "database":
|
|
344
|
-
# Placeholder for database backend
|
|
345
|
-
raise NotImplementedError("Database backend not yet implemented")
|
|
346
|
-
|
|
347
|
-
elif self.backend == "elasticsearch":
|
|
348
|
-
# Placeholder for elasticsearch backend
|
|
349
|
-
raise NotImplementedError("Elasticsearch backend not yet implemented")
|
|
350
|
-
|
|
351
359
|
def log_operation(
|
|
352
360
|
self,
|
|
353
361
|
operation: str,
|
|
@@ -702,6 +702,246 @@ class AuthManager:
|
|
|
702
702
|
error_code=-32004,
|
|
703
703
|
)
|
|
704
704
|
|
|
705
|
+
def authenticate_basic(self, authorization_header: str) -> AuthResult:
|
|
706
|
+
"""
|
|
707
|
+
Authenticate user using HTTP Basic Authentication.
|
|
708
|
+
|
|
709
|
+
This method validates an HTTP Basic Authentication header per RFC 7617,
|
|
710
|
+
parsing the base64-encoded credentials and verifying them against the
|
|
711
|
+
configured user store.
|
|
712
|
+
|
|
713
|
+
The method performs comprehensive basic auth validation including:
|
|
714
|
+
- Authorization header presence and format verification
|
|
715
|
+
- Base64 decoding of credentials
|
|
716
|
+
- Username and password extraction (split on first colon)
|
|
717
|
+
- Credential verification against stored password hashes
|
|
718
|
+
- User role and permission extraction
|
|
719
|
+
|
|
720
|
+
Args:
|
|
721
|
+
authorization_header (str): Full Authorization header value
|
|
722
|
+
(e.g., "Basic dXNlcm5hbWU6cGFzc3dvcmQ="). Must be in the format
|
|
723
|
+
"Basic <base64-encoded-credentials>" where credentials are
|
|
724
|
+
"username:password". Case-insensitive scheme matching.
|
|
725
|
+
|
|
726
|
+
Returns:
|
|
727
|
+
AuthResult: Authentication result object containing:
|
|
728
|
+
- is_valid (bool): True if authentication succeeded
|
|
729
|
+
- username (str): Authenticated username if valid
|
|
730
|
+
- roles (List[str]): User roles from user_roles mapping
|
|
731
|
+
- auth_method (str): Authentication method used ("basic")
|
|
732
|
+
- error_code (int): Error code if authentication failed
|
|
733
|
+
- error_message (str): Human-readable error message
|
|
734
|
+
|
|
735
|
+
Note on Error Indistinguishability:
|
|
736
|
+
An unknown username and a wrong password return identical status and
|
|
737
|
+
error messages. This prevents username enumeration attacks by making
|
|
738
|
+
it impossible for an attacker to determine whether a username is
|
|
739
|
+
valid or only its password is wrong. Both cases return
|
|
740
|
+
status=INVALID and a generic "Invalid credentials" message.
|
|
741
|
+
|
|
742
|
+
Raises:
|
|
743
|
+
AuthenticationError: When authentication process fails unexpectedly
|
|
744
|
+
|
|
745
|
+
Example:
|
|
746
|
+
>>> auth_manager = AuthManager(config, permission_manager)
|
|
747
|
+
>>> result = auth_manager.authenticate_basic("Basic dXNlcjpwYXNz")
|
|
748
|
+
>>> if result.is_valid:
|
|
749
|
+
... print(f"Authenticated user: {result.username}")
|
|
750
|
+
... print(f"User roles: {result.roles}")
|
|
751
|
+
>>> else:
|
|
752
|
+
... print(f"Authentication failed: {result.error_message}")
|
|
753
|
+
|
|
754
|
+
See Also:
|
|
755
|
+
authenticate_api_key: API key authentication method
|
|
756
|
+
authenticate_jwt_token: JWT token authentication method
|
|
757
|
+
authenticate_certificate: Certificate-based authentication
|
|
758
|
+
"""
|
|
759
|
+
try:
|
|
760
|
+
from ..utils.crypto_utils import verify_password
|
|
761
|
+
|
|
762
|
+
# Validate input
|
|
763
|
+
if not authorization_header:
|
|
764
|
+
return AuthResult(
|
|
765
|
+
is_valid=False,
|
|
766
|
+
status=AuthStatus.INVALID,
|
|
767
|
+
auth_method="basic",
|
|
768
|
+
error_code=-32001,
|
|
769
|
+
error_message="Authorization header is required",
|
|
770
|
+
)
|
|
771
|
+
|
|
772
|
+
# Parse Authorization header: "Basic <base64>"
|
|
773
|
+
parts = authorization_header.split(maxsplit=1)
|
|
774
|
+
if len(parts) != 2:
|
|
775
|
+
return AuthResult(
|
|
776
|
+
is_valid=False,
|
|
777
|
+
status=AuthStatus.INVALID,
|
|
778
|
+
auth_method="basic",
|
|
779
|
+
error_code=-32002,
|
|
780
|
+
error_message="Invalid Authorization header format",
|
|
781
|
+
)
|
|
782
|
+
|
|
783
|
+
scheme, credentials_b64 = parts
|
|
784
|
+
if scheme.lower() != "basic":
|
|
785
|
+
return AuthResult(
|
|
786
|
+
is_valid=False,
|
|
787
|
+
status=AuthStatus.INVALID,
|
|
788
|
+
auth_method="basic",
|
|
789
|
+
error_code=-32003,
|
|
790
|
+
error_message="Invalid Authorization header format",
|
|
791
|
+
)
|
|
792
|
+
|
|
793
|
+
# Decode base64
|
|
794
|
+
try:
|
|
795
|
+
credentials_bytes = __import__("base64").b64decode(credentials_b64)
|
|
796
|
+
credentials_str = credentials_bytes.decode("utf-8")
|
|
797
|
+
except Exception:
|
|
798
|
+
return AuthResult(
|
|
799
|
+
is_valid=False,
|
|
800
|
+
status=AuthStatus.INVALID,
|
|
801
|
+
auth_method="basic",
|
|
802
|
+
error_code=-32004,
|
|
803
|
+
error_message="Invalid credentials encoding",
|
|
804
|
+
)
|
|
805
|
+
|
|
806
|
+
# Split on first colon only (password may contain colons)
|
|
807
|
+
if ":" not in credentials_str:
|
|
808
|
+
return AuthResult(
|
|
809
|
+
is_valid=False,
|
|
810
|
+
status=AuthStatus.INVALID,
|
|
811
|
+
auth_method="basic",
|
|
812
|
+
error_code=-32005,
|
|
813
|
+
error_message="Invalid credentials format",
|
|
814
|
+
)
|
|
815
|
+
|
|
816
|
+
colon_idx = credentials_str.index(":")
|
|
817
|
+
username = credentials_str[:colon_idx]
|
|
818
|
+
password = credentials_str[colon_idx + 1 :]
|
|
819
|
+
|
|
820
|
+
# Validate username is provided
|
|
821
|
+
if not username:
|
|
822
|
+
return AuthResult(
|
|
823
|
+
is_valid=False,
|
|
824
|
+
status=AuthStatus.INVALID,
|
|
825
|
+
auth_method="basic",
|
|
826
|
+
error_code=-32006,
|
|
827
|
+
error_message="Invalid credentials",
|
|
828
|
+
)
|
|
829
|
+
|
|
830
|
+
# Check if user exists in basic_auth_users
|
|
831
|
+
if (
|
|
832
|
+
not self.config.basic_auth_users
|
|
833
|
+
or username not in self.config.basic_auth_users
|
|
834
|
+
):
|
|
835
|
+
# User not found - return same error as wrong password
|
|
836
|
+
return AuthResult(
|
|
837
|
+
is_valid=False,
|
|
838
|
+
status=AuthStatus.INVALID,
|
|
839
|
+
auth_method="basic",
|
|
840
|
+
error_code=-32007,
|
|
841
|
+
error_message="Invalid credentials",
|
|
842
|
+
)
|
|
843
|
+
|
|
844
|
+
# Get stored credentials
|
|
845
|
+
stored_credentials = self.config.basic_auth_users[username]
|
|
846
|
+
stored_hash = stored_credentials.get("hash")
|
|
847
|
+
stored_salt = stored_credentials.get("salt")
|
|
848
|
+
|
|
849
|
+
if not stored_hash or not stored_salt:
|
|
850
|
+
return AuthResult(
|
|
851
|
+
is_valid=False,
|
|
852
|
+
status=AuthStatus.INVALID,
|
|
853
|
+
auth_method="basic",
|
|
854
|
+
error_code=-32008,
|
|
855
|
+
error_message="Invalid credentials",
|
|
856
|
+
)
|
|
857
|
+
|
|
858
|
+
# Verify password
|
|
859
|
+
try:
|
|
860
|
+
if not verify_password(password, stored_hash, stored_salt):
|
|
861
|
+
# Wrong password - return same error as unknown user
|
|
862
|
+
return AuthResult(
|
|
863
|
+
is_valid=False,
|
|
864
|
+
status=AuthStatus.INVALID,
|
|
865
|
+
auth_method="basic",
|
|
866
|
+
error_code=-32009,
|
|
867
|
+
error_message="Invalid credentials",
|
|
868
|
+
)
|
|
869
|
+
except Exception as e:
|
|
870
|
+
self.logger.error(
|
|
871
|
+
"Password verification failed",
|
|
872
|
+
extra={"username": username, "error": str(e)},
|
|
873
|
+
)
|
|
874
|
+
return AuthResult(
|
|
875
|
+
is_valid=False,
|
|
876
|
+
status=AuthStatus.INVALID,
|
|
877
|
+
auth_method="basic",
|
|
878
|
+
error_code=-32010,
|
|
879
|
+
error_message="Invalid credentials",
|
|
880
|
+
)
|
|
881
|
+
|
|
882
|
+
# Get user roles
|
|
883
|
+
user_roles = self._get_user_roles(username)
|
|
884
|
+
|
|
885
|
+
# Get user permissions from permission manager
|
|
886
|
+
user_permissions = set()
|
|
887
|
+
if self.permission_manager:
|
|
888
|
+
try:
|
|
889
|
+
permissions_result = (
|
|
890
|
+
self.permission_manager.get_effective_permissions(user_roles)
|
|
891
|
+
)
|
|
892
|
+
# Handle both set and mock objects
|
|
893
|
+
if hasattr(permissions_result, "__iter__") and not isinstance(
|
|
894
|
+
permissions_result, str
|
|
895
|
+
):
|
|
896
|
+
user_permissions = set(permissions_result)
|
|
897
|
+
else:
|
|
898
|
+
user_permissions = set()
|
|
899
|
+
except Exception as e:
|
|
900
|
+
self.logger.warning(
|
|
901
|
+
"Failed to get user permissions",
|
|
902
|
+
extra={
|
|
903
|
+
"username": username,
|
|
904
|
+
"roles": user_roles,
|
|
905
|
+
"error": str(e),
|
|
906
|
+
},
|
|
907
|
+
)
|
|
908
|
+
|
|
909
|
+
# Create successful authentication result
|
|
910
|
+
auth_result = AuthResult(
|
|
911
|
+
is_valid=True,
|
|
912
|
+
status=AuthStatus.SUCCESS,
|
|
913
|
+
username=username,
|
|
914
|
+
roles=user_roles,
|
|
915
|
+
permissions=user_permissions,
|
|
916
|
+
auth_method="basic",
|
|
917
|
+
auth_timestamp=datetime.now(timezone.utc),
|
|
918
|
+
)
|
|
919
|
+
|
|
920
|
+
self.logger.info(
|
|
921
|
+
"Basic authentication successful",
|
|
922
|
+
extra={
|
|
923
|
+
"username": username,
|
|
924
|
+
"roles": user_roles,
|
|
925
|
+
"auth_method": "basic",
|
|
926
|
+
},
|
|
927
|
+
)
|
|
928
|
+
|
|
929
|
+
return auth_result
|
|
930
|
+
|
|
931
|
+
except Exception as e:
|
|
932
|
+
self.logger.error(
|
|
933
|
+
"Basic authentication failed",
|
|
934
|
+
extra={
|
|
935
|
+
"auth_header": (
|
|
936
|
+
authorization_header[:20] + "..."
|
|
937
|
+
if authorization_header
|
|
938
|
+
else None
|
|
939
|
+
),
|
|
940
|
+
"error": str(e),
|
|
941
|
+
},
|
|
942
|
+
)
|
|
943
|
+
raise AuthenticationError(f"Basic authentication failed: {str(e)}")
|
|
944
|
+
|
|
705
945
|
def create_jwt_token(self, user_data: Dict) -> str:
|
|
706
946
|
"""
|
|
707
947
|
Create JWT token for user.
|
|
@@ -978,26 +1218,48 @@ class AuthManager:
|
|
|
978
1218
|
self, username: str, credentials: Dict[str, Any]
|
|
979
1219
|
) -> bool:
|
|
980
1220
|
"""
|
|
981
|
-
|
|
1221
|
+
Extension point for integrating with external authentication systems.
|
|
1222
|
+
|
|
1223
|
+
This method is provided as an extension point for frameworks and
|
|
1224
|
+
applications that need to integrate with external authentication
|
|
1225
|
+
systems such as LDAP, Active Directory, OAuth2 providers, SAML
|
|
1226
|
+
identity providers, or other enterprise authentication backends.
|
|
982
1227
|
|
|
983
|
-
This
|
|
984
|
-
|
|
1228
|
+
This base implementation always returns False, indicating that the
|
|
1229
|
+
extension point is not configured. Applications that wish to use
|
|
1230
|
+
external authentication must either:
|
|
1231
|
+
1. Subclass AuthManager and override this method
|
|
1232
|
+
2. Call this base method as a fallback only after checking for
|
|
1233
|
+
configuration that enables external authentication integration
|
|
985
1234
|
|
|
986
1235
|
Args:
|
|
987
1236
|
username (str): Username to validate
|
|
988
|
-
credentials (Dict[str, Any]): User credentials
|
|
1237
|
+
credentials (Dict[str, Any]): User credentials dictionary
|
|
989
1238
|
|
|
990
1239
|
Returns:
|
|
991
|
-
bool: True if user is valid in external system
|
|
1240
|
+
bool: True if user is valid in the external system, False if
|
|
1241
|
+
not configured or validation failed.
|
|
1242
|
+
|
|
1243
|
+
Example:
|
|
1244
|
+
To integrate with an external system, subclass AuthManager::
|
|
1245
|
+
|
|
1246
|
+
class LDAPAuthManager(AuthManager):
|
|
1247
|
+
def _validate_external_user(self, username, credentials):
|
|
1248
|
+
if not self.config.ldap_enabled:
|
|
1249
|
+
return False
|
|
1250
|
+
# Connect to LDAP server and validate credentials
|
|
1251
|
+
return self.ldap_client.verify(username, credentials)
|
|
1252
|
+
|
|
1253
|
+
See Also:
|
|
1254
|
+
authenticate_api_key: API key authentication
|
|
1255
|
+
authenticate_basic: HTTP Basic Authentication
|
|
1256
|
+
authenticate_certificate: Certificate-based authentication
|
|
992
1257
|
"""
|
|
993
|
-
# This is a placeholder for external authentication integration
|
|
994
|
-
# In a real implementation, this would connect to external auth systems
|
|
995
1258
|
self.logger.debug(
|
|
996
|
-
"External user validation called",
|
|
1259
|
+
"External user validation called (not configured)",
|
|
997
1260
|
extra={"username": username, "method": "external"},
|
|
998
1261
|
)
|
|
999
1262
|
|
|
1000
|
-
# For now, return False to indicate external validation not implemented
|
|
1001
1263
|
return False
|
|
1002
1264
|
|
|
1003
1265
|
def _extract_username_from_certificate(
|
|
@@ -235,10 +235,22 @@ class SSLManager:
|
|
|
235
235
|
|
|
236
236
|
# Validate required files or data
|
|
237
237
|
if cert_data is None and not cert_file:
|
|
238
|
+
if self.config.material_from_memory:
|
|
239
|
+
raise SSLConfigurationError(
|
|
240
|
+
"material_from_memory=True but cert_data was not supplied; "
|
|
241
|
+
"provide cert_data as PEM bytes or "
|
|
242
|
+
"set material_from_memory=False"
|
|
243
|
+
)
|
|
238
244
|
raise SSLConfigurationError(
|
|
239
245
|
"Server certificate file or data is required"
|
|
240
246
|
)
|
|
241
247
|
if key_data is None and not key_file:
|
|
248
|
+
if self.config.material_from_memory:
|
|
249
|
+
raise SSLConfigurationError(
|
|
250
|
+
"material_from_memory=True but key_data was not supplied; "
|
|
251
|
+
"provide key_data as PEM bytes or "
|
|
252
|
+
"set material_from_memory=False"
|
|
253
|
+
)
|
|
242
254
|
raise SSLConfigurationError(
|
|
243
255
|
"Server private key file or data is required"
|
|
244
256
|
)
|
|
@@ -423,6 +435,18 @@ class SSLManager:
|
|
|
423
435
|
verify_mode = verify_mode or self.config.verify_mode
|
|
424
436
|
min_version = min_version or self.config.min_tls_version
|
|
425
437
|
|
|
438
|
+
# Validate that material_from_memory promises are kept
|
|
439
|
+
if cert_data is None and self.config.material_from_memory:
|
|
440
|
+
raise SSLConfigurationError(
|
|
441
|
+
"material_from_memory=True but cert_data was not supplied; "
|
|
442
|
+
"provide cert_data as PEM bytes or set material_from_memory=False"
|
|
443
|
+
)
|
|
444
|
+
if key_data is None and self.config.material_from_memory:
|
|
445
|
+
raise SSLConfigurationError(
|
|
446
|
+
"material_from_memory=True but key_data was not supplied; "
|
|
447
|
+
"provide key_data as PEM bytes or set material_from_memory=False"
|
|
448
|
+
)
|
|
449
|
+
|
|
426
450
|
# Check cache (only for file-based contexts)
|
|
427
451
|
cache_key = None
|
|
428
452
|
if cert_data is None and key_data is None:
|