mcp-security-framework 1.2.0__tar.gz → 1.2.1__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-1.2.0 → mcp_security_framework-1.2.1}/PKG-INFO +1 -1
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/__init__.py +1 -1
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/core/auth_manager.py +29 -9
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/core/cert_manager.py +20 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/schemas/config.py +31 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/schemas/models.py +46 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/utils/cert_utils.py +54 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework.egg-info/PKG-INFO +1 -1
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework.egg-info/SOURCES.txt +1 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/pyproject.toml +1 -1
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/test_core/test_auth_manager.py +6 -6
- mcp_security_framework-1.2.1/tests/test_utils/test_unitid_compat.py +550 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/README.md +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/cli/__init__.py +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/cli/cert_cli.py +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/cli/security_cli.py +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/constants.py +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/core/__init__.py +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/core/permission_manager.py +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/core/rate_limiter.py +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/core/security_manager.py +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/core/ssl_manager.py +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/examples/__init__.py +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/examples/comprehensive_example.py +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/examples/django_example.py +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/examples/fastapi_example.py +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/examples/flask_example.py +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/examples/gateway_example.py +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/examples/microservice_example.py +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/examples/standalone_example.py +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/examples/test_all_examples.py +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/middleware/__init__.py +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/middleware/auth_middleware.py +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/middleware/fastapi_auth_middleware.py +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/middleware/fastapi_middleware.py +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/middleware/flask_auth_middleware.py +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/middleware/flask_middleware.py +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/middleware/mtls_middleware.py +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/middleware/rate_limit_middleware.py +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/middleware/security_middleware.py +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/schemas/__init__.py +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/schemas/responses.py +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/tests/__init__.py +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/utils/__init__.py +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/utils/crypto_utils.py +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/utils/datetime_compat.py +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework/utils/validation_utils.py +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework.egg-info/dependency_links.txt +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework.egg-info/entry_points.txt +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework.egg-info/requires.txt +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/mcp_security_framework.egg-info/top_level.txt +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/setup.cfg +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/__init__.py +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/conftest.py +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/test_cli/__init__.py +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/test_cli/test_cert_cli.py +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/test_cli/test_security_cli.py +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/test_core/__init__.py +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/test_core/test_cert_manager.py +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/test_core/test_permission_manager.py +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/test_core/test_rate_limiter.py +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/test_core/test_security_manager.py +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/test_core/test_ssl_manager.py +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/test_examples/__init__.py +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/test_examples/test_comprehensive_example.py +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/test_examples/test_fastapi_example.py +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/test_examples/test_flask_example.py +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/test_examples/test_standalone_example.py +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/test_integration/__init__.py +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/test_integration/test_auth_flow.py +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/test_integration/test_certificate_flow.py +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/test_integration/test_fastapi_integration.py +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/test_integration/test_flask_integration.py +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/test_integration/test_standalone_integration.py +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/test_middleware/__init__.py +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/test_middleware/test_fastapi_auth_middleware.py +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/test_middleware/test_fastapi_middleware.py +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/test_middleware/test_flask_auth_middleware.py +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/test_middleware/test_flask_middleware.py +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/test_middleware/test_security_middleware.py +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/test_schemas/__init__.py +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/test_schemas/test_config.py +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/test_schemas/test_models.py +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/test_schemas/test_responses.py +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/test_schemas/test_serialization.py +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/test_utils/__init__.py +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/test_utils/test_cert_utils.py +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/test_utils/test_crypto_utils.py +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/test_utils/test_datetime_compat.py +0 -0
- {mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/test_utils/test_validation_utils.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: mcp-security-framework
|
3
|
-
Version: 1.2.
|
3
|
+
Version: 1.2.1
|
4
4
|
Summary: Universal security framework for microservices with SSL/TLS, authentication, authorization, and rate limiting. 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>
|
@@ -38,6 +38,7 @@ from ..schemas.models import AuthResult, AuthStatus, ValidationResult
|
|
38
38
|
from ..utils.cert_utils import (
|
39
39
|
extract_permissions_from_certificate,
|
40
40
|
extract_roles_from_certificate,
|
41
|
+
extract_unitid_from_certificate,
|
41
42
|
parse_certificate,
|
42
43
|
validate_certificate_chain,
|
43
44
|
)
|
@@ -124,20 +125,21 @@ class AuthManager:
|
|
124
125
|
self.logger = logging.getLogger(__name__)
|
125
126
|
|
126
127
|
# Initialize storage
|
127
|
-
#
|
128
|
+
# Store API keys in format: {"api_key": "username"}
|
128
129
|
# or handle new format "key": {"username": "user", "roles": ["role1", "role2"]}
|
129
130
|
if config.api_keys:
|
130
131
|
self._api_keys = {}
|
131
132
|
self._api_key_metadata = {}
|
132
133
|
for key, value in config.api_keys.items():
|
133
134
|
if isinstance(value, str):
|
134
|
-
# Old format: "
|
135
|
+
# Old format: "username": "api_key" -> store as {"api_key": "username"}
|
135
136
|
self._api_keys[value] = key
|
136
137
|
elif isinstance(value, dict):
|
137
|
-
# New format: "
|
138
|
+
# New format: "username": {"api_key": "key", "roles": ["role1", "role2"]}
|
139
|
+
api_key = value.get("api_key", key)
|
138
140
|
username = value.get("username", key)
|
139
|
-
self._api_keys[
|
140
|
-
self._api_key_metadata[
|
141
|
+
self._api_keys[api_key] = username
|
142
|
+
self._api_key_metadata[api_key] = value
|
141
143
|
else:
|
142
144
|
self.logger.warning(
|
143
145
|
f"Invalid API key format for key {key}: {value}"
|
@@ -253,7 +255,7 @@ class AuthManager:
|
|
253
255
|
# Find user by API key
|
254
256
|
username = None
|
255
257
|
user_roles = []
|
256
|
-
for
|
258
|
+
for api_key_in_config, user in self._api_keys.items():
|
257
259
|
if api_key_in_config == api_key:
|
258
260
|
username = user
|
259
261
|
# Check if we have metadata for this API key
|
@@ -625,6 +627,16 @@ class AuthManager:
|
|
625
627
|
)
|
626
628
|
roles = []
|
627
629
|
|
630
|
+
# Extract unitid from certificate
|
631
|
+
unitid = None
|
632
|
+
try:
|
633
|
+
unitid = extract_unitid_from_certificate(cert_pem)
|
634
|
+
except Exception as e:
|
635
|
+
self.logger.warning(
|
636
|
+
"Failed to extract unitid from certificate",
|
637
|
+
extra={"username": username, "error": str(e)},
|
638
|
+
)
|
639
|
+
|
628
640
|
# Validate certificate chain if CA is configured
|
629
641
|
if self.config.ca_cert_file:
|
630
642
|
try:
|
@@ -656,6 +668,7 @@ class AuthManager:
|
|
656
668
|
auth_method="certificate",
|
657
669
|
auth_timestamp=datetime.now(timezone.utc),
|
658
670
|
token_expiry=get_not_valid_after_utc(cert),
|
671
|
+
unitid=unitid,
|
659
672
|
)
|
660
673
|
|
661
674
|
self.logger.info(
|
@@ -861,7 +874,7 @@ class AuthManager:
|
|
861
874
|
if not validate_api_key_format(api_key):
|
862
875
|
return False
|
863
876
|
|
864
|
-
self._api_keys[
|
877
|
+
self._api_keys[api_key] = username
|
865
878
|
|
866
879
|
self.logger.info("API key added for user", extra={"username": username})
|
867
880
|
|
@@ -884,8 +897,15 @@ class AuthManager:
|
|
884
897
|
bool: True if API key was removed successfully, False otherwise
|
885
898
|
"""
|
886
899
|
try:
|
887
|
-
|
888
|
-
|
900
|
+
# Find API key for the username and remove it
|
901
|
+
api_key_to_remove = None
|
902
|
+
for api_key, user in self._api_keys.items():
|
903
|
+
if user == username:
|
904
|
+
api_key_to_remove = api_key
|
905
|
+
break
|
906
|
+
|
907
|
+
if api_key_to_remove:
|
908
|
+
del self._api_keys[api_key_to_remove]
|
889
909
|
|
890
910
|
self.logger.info(
|
891
911
|
"API key removed for user", extra={"username": username}
|
@@ -29,6 +29,7 @@ License: MIT
|
|
29
29
|
|
30
30
|
import logging
|
31
31
|
import os
|
32
|
+
import uuid
|
32
33
|
from datetime import datetime, timedelta, timezone
|
33
34
|
from pathlib import Path
|
34
35
|
from typing import Dict, List, Optional, Tuple, Union
|
@@ -54,6 +55,7 @@ from mcp_security_framework.schemas.models import (
|
|
54
55
|
from mcp_security_framework.utils.cert_utils import (
|
55
56
|
extract_permissions_from_certificate,
|
56
57
|
extract_roles_from_certificate,
|
58
|
+
extract_unitid_from_certificate,
|
57
59
|
get_certificate_expiry,
|
58
60
|
get_certificate_serial_number,
|
59
61
|
get_crl_info,
|
@@ -254,6 +256,14 @@ class CertificateManager:
|
|
254
256
|
x509.BasicConstraints(ca=True, path_length=None), critical=True
|
255
257
|
)
|
256
258
|
|
259
|
+
# Add unitid extension if provided
|
260
|
+
if ca_config.unitid:
|
261
|
+
unitid_extension = x509.UnrecognizedExtension(
|
262
|
+
oid=x509.ObjectIdentifier("1.3.6.1.4.1.99999.1.3"),
|
263
|
+
value=ca_config.unitid.encode(),
|
264
|
+
)
|
265
|
+
builder = builder.add_extension(unitid_extension, critical=False)
|
266
|
+
|
257
267
|
builder = builder.add_extension(
|
258
268
|
x509.KeyUsage(
|
259
269
|
digital_signature=True,
|
@@ -325,6 +335,7 @@ class CertificateManager:
|
|
325
335
|
not_after=get_not_valid_after_utc(certificate),
|
326
336
|
certificate_type=CertificateType.ROOT_CA,
|
327
337
|
key_size=ca_config.key_size,
|
338
|
+
unitid=ca_config.unitid,
|
328
339
|
)
|
329
340
|
|
330
341
|
self.logger.info(
|
@@ -766,6 +777,14 @@ class CertificateManager:
|
|
766
777
|
)
|
767
778
|
builder = builder.add_extension(permissions_extension, critical=False)
|
768
779
|
|
780
|
+
# Add unitid extension if provided
|
781
|
+
if client_config.unitid:
|
782
|
+
unitid_extension = x509.UnrecognizedExtension(
|
783
|
+
oid=x509.ObjectIdentifier("1.3.6.1.4.1.99999.1.3"),
|
784
|
+
value=client_config.unitid.encode(),
|
785
|
+
)
|
786
|
+
builder = builder.add_extension(unitid_extension, critical=False)
|
787
|
+
|
769
788
|
# Create certificate
|
770
789
|
certificate = builder.sign(ca_key, hashes.SHA256())
|
771
790
|
|
@@ -816,6 +835,7 @@ class CertificateManager:
|
|
816
835
|
not_after=get_not_valid_after_utc(certificate),
|
817
836
|
certificate_type=CertificateType.CLIENT,
|
818
837
|
key_size=client_config.key_size,
|
838
|
+
unitid=client_config.unitid,
|
819
839
|
)
|
820
840
|
|
821
841
|
self.logger.info(
|
@@ -33,6 +33,7 @@ License: MIT
|
|
33
33
|
from enum import Enum
|
34
34
|
from pathlib import Path
|
35
35
|
from typing import Any, Dict, List, Optional, Union
|
36
|
+
import uuid
|
36
37
|
|
37
38
|
from pydantic import BaseModel, Field, field_validator, model_validator
|
38
39
|
from pydantic.types import SecretStr
|
@@ -599,6 +600,21 @@ class CAConfig(BaseModel):
|
|
599
600
|
hash_algorithm: str = Field(
|
600
601
|
default="sha256", description="Hash algorithm for signing"
|
601
602
|
)
|
603
|
+
unitid: Optional[str] = Field(
|
604
|
+
default=None, description="Unique unit identifier (UUID4) for the certificate"
|
605
|
+
)
|
606
|
+
|
607
|
+
@field_validator("unitid")
|
608
|
+
@classmethod
|
609
|
+
def validate_unitid(cls, v):
|
610
|
+
"""Validate unitid format."""
|
611
|
+
if v is not None:
|
612
|
+
try:
|
613
|
+
# Validate UUID4 format
|
614
|
+
uuid.UUID(v, version=4)
|
615
|
+
except ValueError:
|
616
|
+
raise ValueError("unitid must be a valid UUID4 string")
|
617
|
+
return v
|
602
618
|
|
603
619
|
|
604
620
|
class IntermediateCAConfig(CAConfig):
|
@@ -668,6 +684,21 @@ class ClientCertConfig(BaseModel):
|
|
668
684
|
)
|
669
685
|
ca_cert_path: str = Field(..., description="Path to signing CA certificate")
|
670
686
|
ca_key_path: str = Field(..., description="Path to signing CA private key")
|
687
|
+
unitid: Optional[str] = Field(
|
688
|
+
default=None, description="Unique unit identifier (UUID4) for the certificate"
|
689
|
+
)
|
690
|
+
|
691
|
+
@field_validator("unitid")
|
692
|
+
@classmethod
|
693
|
+
def validate_unitid(cls, v):
|
694
|
+
"""Validate unitid format."""
|
695
|
+
if v is not None:
|
696
|
+
try:
|
697
|
+
# Validate UUID4 format
|
698
|
+
uuid.UUID(v, version=4)
|
699
|
+
except ValueError:
|
700
|
+
raise ValueError("unitid must be a valid UUID4 string")
|
701
|
+
return v
|
671
702
|
|
672
703
|
|
673
704
|
class ServerCertConfig(ClientCertConfig):
|
@@ -38,6 +38,7 @@ License: MIT
|
|
38
38
|
from datetime import datetime, timedelta, timezone
|
39
39
|
from enum import Enum
|
40
40
|
from typing import Any, Dict, List, Optional, Set, TypeAlias
|
41
|
+
import uuid
|
41
42
|
|
42
43
|
from pydantic import BaseModel, Field, field_validator, model_validator
|
43
44
|
|
@@ -140,6 +141,9 @@ class AuthResult(BaseModel):
|
|
140
141
|
metadata: Dict[str, Any] = Field(
|
141
142
|
default_factory=dict, description="Additional authentication metadata"
|
142
143
|
)
|
144
|
+
unitid: Optional[str] = Field(
|
145
|
+
default=None, description="Unique unit identifier (UUID4) from certificate"
|
146
|
+
)
|
143
147
|
|
144
148
|
@field_validator("username")
|
145
149
|
@classmethod
|
@@ -149,6 +153,18 @@ class AuthResult(BaseModel):
|
|
149
153
|
raise ValueError("Username cannot be empty")
|
150
154
|
return v
|
151
155
|
|
156
|
+
@field_validator("unitid")
|
157
|
+
@classmethod
|
158
|
+
def validate_unitid(cls, v):
|
159
|
+
"""Validate unitid format."""
|
160
|
+
if v is not None:
|
161
|
+
try:
|
162
|
+
# Validate UUID4 format
|
163
|
+
uuid.UUID(v, version=4)
|
164
|
+
except ValueError:
|
165
|
+
raise ValueError("unitid must be a valid UUID4 string")
|
166
|
+
return v
|
167
|
+
|
152
168
|
@model_validator(mode="after")
|
153
169
|
def validate_auth_result(self):
|
154
170
|
"""Validate authentication result consistency."""
|
@@ -309,6 +325,9 @@ class CertificateInfo(BaseModel):
|
|
309
325
|
fingerprint_sha256: Optional[str] = Field(
|
310
326
|
default=None, description="SHA256 fingerprint"
|
311
327
|
)
|
328
|
+
unitid: Optional[str] = Field(
|
329
|
+
default=None, description="Unique unit identifier (UUID4) for the certificate"
|
330
|
+
)
|
312
331
|
|
313
332
|
@field_validator("key_size")
|
314
333
|
@classmethod
|
@@ -318,6 +337,18 @@ class CertificateInfo(BaseModel):
|
|
318
337
|
raise ValueError("Key size must be between 512 and 8192 bits")
|
319
338
|
return v
|
320
339
|
|
340
|
+
@field_validator("unitid")
|
341
|
+
@classmethod
|
342
|
+
def validate_unitid(cls, v):
|
343
|
+
"""Validate unitid format."""
|
344
|
+
if v is not None:
|
345
|
+
try:
|
346
|
+
# Validate UUID4 format
|
347
|
+
uuid.UUID(v, version=4)
|
348
|
+
except ValueError:
|
349
|
+
raise ValueError("unitid must be a valid UUID4 string")
|
350
|
+
return v
|
351
|
+
|
321
352
|
@property
|
322
353
|
def is_expired(self) -> bool:
|
323
354
|
"""Check if certificate is expired."""
|
@@ -424,6 +455,9 @@ class CertificatePair(BaseModel):
|
|
424
455
|
metadata: Dict[str, Any] = Field(
|
425
456
|
default_factory=dict, description="Additional certificate metadata"
|
426
457
|
)
|
458
|
+
unitid: Optional[str] = Field(
|
459
|
+
default=None, description="Unique unit identifier (UUID4) for the certificate"
|
460
|
+
)
|
427
461
|
|
428
462
|
@field_validator("certificate_pem")
|
429
463
|
@classmethod
|
@@ -449,6 +483,18 @@ class CertificatePair(BaseModel):
|
|
449
483
|
raise ValueError("Invalid private key PEM format")
|
450
484
|
return v
|
451
485
|
|
486
|
+
@field_validator("unitid")
|
487
|
+
@classmethod
|
488
|
+
def validate_unitid(cls, v):
|
489
|
+
"""Validate unitid format."""
|
490
|
+
if v is not None:
|
491
|
+
try:
|
492
|
+
# Validate UUID4 format
|
493
|
+
uuid.UUID(v, version=4)
|
494
|
+
except ValueError:
|
495
|
+
raise ValueError("unitid must be a valid UUID4 string")
|
496
|
+
return v
|
497
|
+
|
452
498
|
@property
|
453
499
|
def is_expired(self) -> bool:
|
454
500
|
"""Check if certificate is expired."""
|
@@ -160,6 +160,15 @@ def extract_certificate_info(cert_data: Union[str, bytes, Path]) -> Dict:
|
|
160
160
|
if country:
|
161
161
|
info["country"] = country[0].value
|
162
162
|
|
163
|
+
# Extract unitid
|
164
|
+
try:
|
165
|
+
unitid = extract_unitid_from_certificate(cert_data)
|
166
|
+
if unitid:
|
167
|
+
info["unitid"] = unitid
|
168
|
+
except Exception:
|
169
|
+
# If unitid extraction fails, continue without it
|
170
|
+
pass
|
171
|
+
|
163
172
|
return info
|
164
173
|
except Exception as e:
|
165
174
|
raise CertificateError(f"Certificate information extraction failed: {str(e)}")
|
@@ -275,6 +284,51 @@ def extract_permissions_from_certificate(
|
|
275
284
|
raise CertificateError(f"Permission extraction failed: {str(e)}")
|
276
285
|
|
277
286
|
|
287
|
+
def extract_unitid_from_certificate(
|
288
|
+
cert_data: Union[str, bytes, Path],
|
289
|
+
) -> Optional[str]:
|
290
|
+
"""
|
291
|
+
Extract unitid from certificate extensions.
|
292
|
+
|
293
|
+
Args:
|
294
|
+
cert_data: Certificate data as string, bytes, or file path
|
295
|
+
|
296
|
+
Returns:
|
297
|
+
Unit ID (UUID4) found in certificate, or None if not found
|
298
|
+
|
299
|
+
Raises:
|
300
|
+
CertificateError: If unitid extraction fails
|
301
|
+
"""
|
302
|
+
try:
|
303
|
+
cert = parse_certificate(cert_data)
|
304
|
+
unitid = None
|
305
|
+
|
306
|
+
# Check for custom extension with unitid
|
307
|
+
try:
|
308
|
+
unitid_extension = cert.extensions.get_extension_for_oid(
|
309
|
+
x509.ObjectIdentifier("1.3.6.1.4.1.99999.1.3") # Custom unitid OID
|
310
|
+
)
|
311
|
+
if unitid_extension:
|
312
|
+
unitid_data = unitid_extension.value.value
|
313
|
+
if isinstance(unitid_data, bytes):
|
314
|
+
unitid_str = unitid_data.decode("utf-8")
|
315
|
+
unitid = unitid_str.strip()
|
316
|
+
|
317
|
+
# Validate UUID4 format
|
318
|
+
try:
|
319
|
+
import uuid
|
320
|
+
uuid.UUID(unitid, version=4)
|
321
|
+
except ValueError:
|
322
|
+
# Invalid UUID4 format, return None
|
323
|
+
unitid = None
|
324
|
+
except x509.extensions.ExtensionNotFound:
|
325
|
+
pass
|
326
|
+
|
327
|
+
return unitid
|
328
|
+
except Exception as e:
|
329
|
+
raise CertificateError(f"Unitid extraction failed: {str(e)}")
|
330
|
+
|
331
|
+
|
278
332
|
def validate_certificate_chain(
|
279
333
|
cert_data: Union[str, bytes, Path],
|
280
334
|
ca_cert_data: Union[str, bytes, Path, List[Union[str, bytes, Path]]],
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: mcp-security-framework
|
3
|
-
Version: 1.2.
|
3
|
+
Version: 1.2.1
|
4
4
|
Summary: Universal security framework for microservices with SSL/TLS, authentication, authorization, and rate limiting. 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>
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
4
4
|
|
5
5
|
[project]
|
6
6
|
name = "mcp-security-framework"
|
7
|
-
version = "1.2.
|
7
|
+
version = "1.2.1"
|
8
8
|
description = "Universal security framework for microservices with SSL/TLS, authentication, authorization, and rate limiting. Requires cryptography>=42.0.0 for certificate operations."
|
9
9
|
readme = "README.md"
|
10
10
|
license = {text = "MIT"}
|
{mcp_security_framework-1.2.0 → mcp_security_framework-1.2.1}/tests/test_core/test_auth_manager.py
RENAMED
@@ -46,7 +46,7 @@ class TestAuthManager:
|
|
46
46
|
|
47
47
|
# Create test configuration
|
48
48
|
self.auth_config = AuthConfig(
|
49
|
-
api_keys={"
|
49
|
+
api_keys={"test_user": "test_api_key_123"},
|
50
50
|
jwt_secret="test_jwt_secret_key_32_chars_long",
|
51
51
|
jwt_expiry_hours=1,
|
52
52
|
ca_cert_file=None,
|
@@ -65,7 +65,7 @@ class TestAuthManager:
|
|
65
65
|
"""Test successful AuthManager initialization."""
|
66
66
|
assert self.auth_manager.config == self.auth_config
|
67
67
|
assert self.auth_manager.permission_manager == self.mock_permission_manager
|
68
|
-
assert self.auth_manager._api_keys == {"
|
68
|
+
assert self.auth_manager._api_keys == {"test_api_key_123": "test_user"}
|
69
69
|
assert self.auth_manager._jwt_secret == "test_jwt_secret_key_32_chars_long"
|
70
70
|
assert isinstance(self.auth_manager._token_cache, dict)
|
71
71
|
assert isinstance(self.auth_manager._session_store, dict)
|
@@ -429,8 +429,8 @@ class TestAuthManager:
|
|
429
429
|
success = self.auth_manager.add_api_key("new_user", "new_api_key_456789")
|
430
430
|
|
431
431
|
assert success is True
|
432
|
-
assert "
|
433
|
-
assert self.auth_manager._api_keys["
|
432
|
+
assert "new_api_key_456789" in self.auth_manager._api_keys
|
433
|
+
assert self.auth_manager._api_keys["new_api_key_456789"] == "new_user"
|
434
434
|
|
435
435
|
def test_add_api_key_invalid_input(self):
|
436
436
|
"""Test API key addition with invalid input."""
|
@@ -456,12 +456,12 @@ class TestAuthManager:
|
|
456
456
|
"""Test successful API key removal."""
|
457
457
|
# Add a key first
|
458
458
|
self.auth_manager.add_api_key("temp_user", "temp_key_123456789")
|
459
|
-
assert "
|
459
|
+
assert "temp_key_123456789" in self.auth_manager._api_keys
|
460
460
|
|
461
461
|
# Remove the key
|
462
462
|
success = self.auth_manager.remove_api_key("temp_user")
|
463
463
|
assert success is True
|
464
|
-
assert "
|
464
|
+
assert "temp_key_123456789" not in self.auth_manager._api_keys
|
465
465
|
|
466
466
|
def test_remove_api_key_not_found(self):
|
467
467
|
"""Test API key removal for non-existent user."""
|