mcp-security-framework 1.2.0__py3-none-any.whl → 1.2.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.
- mcp_security_framework/__init__.py +1 -1
- mcp_security_framework/core/auth_manager.py +29 -9
- mcp_security_framework/core/cert_manager.py +20 -0
- mcp_security_framework/schemas/config.py +31 -0
- mcp_security_framework/schemas/models.py +46 -0
- mcp_security_framework/utils/cert_utils.py +54 -0
- {mcp_security_framework-1.2.0.dist-info → mcp_security_framework-1.2.1.dist-info}/METADATA +1 -1
- {mcp_security_framework-1.2.0.dist-info → mcp_security_framework-1.2.1.dist-info}/RECORD +13 -12
- tests/test_core/test_auth_manager.py +6 -6
- tests/test_utils/test_unitid_compat.py +550 -0
- {mcp_security_framework-1.2.0.dist-info → mcp_security_framework-1.2.1.dist-info}/WHEEL +0 -0
- {mcp_security_framework-1.2.0.dist-info → mcp_security_framework-1.2.1.dist-info}/entry_points.txt +0 -0
- {mcp_security_framework-1.2.0.dist-info → mcp_security_framework-1.2.1.dist-info}/top_level.txt +0 -0
@@ -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>
|
@@ -1,11 +1,11 @@
|
|
1
|
-
mcp_security_framework/__init__.py,sha256=
|
1
|
+
mcp_security_framework/__init__.py,sha256=wRlT28hRsnSZCUVNGtU-KggRk66KzSQl4agKOxE5ZCA,3172
|
2
2
|
mcp_security_framework/constants.py,sha256=k7NMSrgc83Cci8aoilybQxdC7jir7J-mVFE_EpqVrDk,5307
|
3
3
|
mcp_security_framework/cli/__init__.py,sha256=plpWdiWMp2dcLvUuGwXynRg5CDjz8YKnNTBn7lcta08,369
|
4
4
|
mcp_security_framework/cli/cert_cli.py,sha256=LdZ3SYKM3e3dP5LsVR5Y0OENtlG0ENu64aHefHjuiN8,23818
|
5
5
|
mcp_security_framework/cli/security_cli.py,sha256=Thine_Zzfesz7j29y2k_XZFYUK5YSrhCc6w2FilgEiE,28486
|
6
6
|
mcp_security_framework/core/__init__.py,sha256=LiX8_M5qWiTXccJFjSLxup9emhklp-poq57SvznsKEg,1729
|
7
|
-
mcp_security_framework/core/auth_manager.py,sha256=
|
8
|
-
mcp_security_framework/core/cert_manager.py,sha256=
|
7
|
+
mcp_security_framework/core/auth_manager.py,sha256=GqGAW83Qg1_z2HJ0-FEVTmlli_DBSOPOap2jJMEU1_k,39882
|
8
|
+
mcp_security_framework/core/cert_manager.py,sha256=RJgZxElPgMV15Lj_N2uCyO7Ofb9z-MclM1hf8wzFwSc,88882
|
9
9
|
mcp_security_framework/core/permission_manager.py,sha256=SADS_oXpwp9MhXHKJMCsvjEq8KWcz7vPYL05Yr-zfio,26478
|
10
10
|
mcp_security_framework/core/rate_limiter.py,sha256=6qjVBxK2YHouSxQuCcbr0PBpRqA5toQss_Ce178RElY,20682
|
11
11
|
mcp_security_framework/core/security_manager.py,sha256=mAF-5znqxin-MSSgXISB7t1kTkqHltEqGzzmlLAhRGs,37766
|
@@ -29,12 +29,12 @@ mcp_security_framework/middleware/mtls_middleware.py,sha256=WSyWIk1fCN96hkofODKj
|
|
29
29
|
mcp_security_framework/middleware/rate_limit_middleware.py,sha256=deCwwigI0Pt7pBUnk2jDurI9ZyjujWTsexEWWndXm3g,13177
|
30
30
|
mcp_security_framework/middleware/security_middleware.py,sha256=PQ251Fr2UrYVPgGfhXq6QJyqK2tRk0WCIg9_FBvfVkg,16844
|
31
31
|
mcp_security_framework/schemas/__init__.py,sha256=lefkbRlbj2ICfasSj51MQ04o3z1YycnbnknSJCFfXbU,2590
|
32
|
-
mcp_security_framework/schemas/config.py,sha256=
|
33
|
-
mcp_security_framework/schemas/models.py,sha256=
|
32
|
+
mcp_security_framework/schemas/config.py,sha256=SUUrpOz9ZTIhZG9Fu2Gsz86yxoGbUt00Qk-Qk_GaTus,26819
|
33
|
+
mcp_security_framework/schemas/models.py,sha256=Izjy3I55zjMVLsVZpXZ0M4aK3SCks9sC2U1cbxrXYeI,28439
|
34
34
|
mcp_security_framework/schemas/responses.py,sha256=nVXaqF5GTSprXTa_wiUEu38nvSw9WAXtKViAJNbO-Xg,23206
|
35
35
|
mcp_security_framework/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
36
36
|
mcp_security_framework/utils/__init__.py,sha256=wwwdmQYHTSz0Puvs9FD6aIKmWp3NFARe3JPWNH-b_wk,3098
|
37
|
-
mcp_security_framework/utils/cert_utils.py,sha256=
|
37
|
+
mcp_security_framework/utils/cert_utils.py,sha256=4AHNrfL0Oqp354aEq5H2_0XLT7v3mNOnTVDV-DLaJyI,27585
|
38
38
|
mcp_security_framework/utils/crypto_utils.py,sha256=OH2V7_C3FjStxFTIXMUPfNXZuWG2-QjgoBrIH4Lv4p0,12392
|
39
39
|
mcp_security_framework/utils/datetime_compat.py,sha256=ool-xs-EevhuYygdzhiAenLAacLuZwGwjPkF43i-9gg,3859
|
40
40
|
mcp_security_framework/utils/validation_utils.py,sha256=e9BX3kw9gdXSmFsc7lmG-qnzSlK0-Ynn7Xs4uKHquF4,16279
|
@@ -44,7 +44,7 @@ tests/test_cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
44
44
|
tests/test_cli/test_cert_cli.py,sha256=Rm7z-20VAvnmYKY3sgxS-qVNks1vbniQJSpSxjsx_wo,14677
|
45
45
|
tests/test_cli/test_security_cli.py,sha256=Bpd31IPJSUl_V1Xzy74ZCOvQpwlbj8Da83C46T8Jewg,25569
|
46
46
|
tests/test_core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
47
|
-
tests/test_core/test_auth_manager.py,sha256=
|
47
|
+
tests/test_core/test_auth_manager.py,sha256=7Z2DLfJLqKtiwX5Q-lR85hN6NxHbE2Q_FT7IsoyKPQk,22568
|
48
48
|
tests/test_core/test_cert_manager.py,sha256=4YMAkRedkAZW3PEZYEbo1PyrzMntUrKfl7arPsHXDCE,36356
|
49
49
|
tests/test_core/test_permission_manager.py,sha256=0XeghWXZqVpKyyRuhuDu1dkLUSwuZaFWkRQxQhkkFVI,14966
|
50
50
|
tests/test_core/test_rate_limiter.py,sha256=YzzlhlxZm-A7YGMiIV8LXDA0zmb_6uRF9GRx9s21Q0U,22544
|
@@ -76,9 +76,10 @@ tests/test_utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,
|
|
76
76
|
tests/test_utils/test_cert_utils.py,sha256=yZGHPuJcjgSHFeT7gnMdsw6UYXmlGUiuHkErukOm8II,28238
|
77
77
|
tests/test_utils/test_crypto_utils.py,sha256=yEb4hzG6-irj2DPoXY0DUboJfbeR87ussgTuBpxLGz4,20737
|
78
78
|
tests/test_utils/test_datetime_compat.py,sha256=n8S4X5HN-_ejSNpgymDXRyZkmxhnyxwwjxFPdX23I40,5656
|
79
|
+
tests/test_utils/test_unitid_compat.py,sha256=MWh03A4FwzQyZa20PKHEWz4W03YtARwBOd_1JbABznQ,25544
|
79
80
|
tests/test_utils/test_validation_utils.py,sha256=lus_wHJ2WyVnBGQ28S7dSv78uWcCIuLhn5uflJw-uGw,18569
|
80
|
-
mcp_security_framework-1.2.
|
81
|
-
mcp_security_framework-1.2.
|
82
|
-
mcp_security_framework-1.2.
|
83
|
-
mcp_security_framework-1.2.
|
84
|
-
mcp_security_framework-1.2.
|
81
|
+
mcp_security_framework-1.2.1.dist-info/METADATA,sha256=3HiQhhOc1aqdstjAhfUnT2gRmxg61pYutP_lVkn6cxw,11771
|
82
|
+
mcp_security_framework-1.2.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
83
|
+
mcp_security_framework-1.2.1.dist-info/entry_points.txt,sha256=qBh92fVDmd1m2f3xeW0hTu3Ksg8QfGJyV8UEkdA2itg,142
|
84
|
+
mcp_security_framework-1.2.1.dist-info/top_level.txt,sha256=ifUiGrTDcD574MXSOoAN2rp2wpUvWlb4jD9LTUgDWCA,29
|
85
|
+
mcp_security_framework-1.2.1.dist-info/RECORD,,
|
@@ -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."""
|
@@ -0,0 +1,550 @@
|
|
1
|
+
"""
|
2
|
+
Tests for UnitID Compatibility Module
|
3
|
+
|
4
|
+
This module contains tests for the unitid functionality that
|
5
|
+
handles unique unit identifiers in certificates.
|
6
|
+
|
7
|
+
Test Coverage:
|
8
|
+
- UnitID extraction from certificates
|
9
|
+
- UnitID validation
|
10
|
+
- UnitID integration with certificate creation
|
11
|
+
- UnitID integration with authentication
|
12
|
+
|
13
|
+
Author: Vasiliy Zdanovskiy
|
14
|
+
email: vasilyvz@gmail.com
|
15
|
+
"""
|
16
|
+
|
17
|
+
import pytest
|
18
|
+
import uuid
|
19
|
+
from unittest.mock import Mock, patch
|
20
|
+
from cryptography import x509
|
21
|
+
from cryptography.hazmat.primitives import hashes, serialization
|
22
|
+
from cryptography.hazmat.primitives.asymmetric import rsa
|
23
|
+
|
24
|
+
from mcp_security_framework.utils.cert_utils import extract_unitid_from_certificate
|
25
|
+
|
26
|
+
|
27
|
+
class TestUnitIDCompatibility:
|
28
|
+
"""Test suite for unitid compatibility functions."""
|
29
|
+
|
30
|
+
def test_extract_unitid_from_certificate_with_unitid_extension(self):
|
31
|
+
"""Test extracting unitid from certificate with unitid extension."""
|
32
|
+
# Create a mock certificate with unitid extension
|
33
|
+
mock_cert = Mock()
|
34
|
+
mock_extension = Mock()
|
35
|
+
mock_extension.value.value = b"550e8400-e29b-41d4-a716-446655440000"
|
36
|
+
mock_cert.extensions.get_extension_for_oid.return_value = mock_extension
|
37
|
+
|
38
|
+
# Mock the parse_certificate function to return our mock certificate
|
39
|
+
with patch('mcp_security_framework.utils.cert_utils.parse_certificate') as mock_parse:
|
40
|
+
mock_parse.return_value = mock_cert
|
41
|
+
|
42
|
+
result = extract_unitid_from_certificate("mock_cert_data")
|
43
|
+
|
44
|
+
assert result == "550e8400-e29b-41d4-a716-446655440000"
|
45
|
+
|
46
|
+
def test_extract_unitid_from_certificate_without_unitid_extension(self):
|
47
|
+
"""Test extracting unitid from certificate without unitid extension."""
|
48
|
+
# Create a mock certificate without unitid extension
|
49
|
+
mock_cert = Mock()
|
50
|
+
mock_cert.extensions.get_extension_for_oid.side_effect = x509.ExtensionNotFound(
|
51
|
+
"Extension not found", "1.3.6.1.4.1.99999.1.3"
|
52
|
+
)
|
53
|
+
|
54
|
+
# Mock the parse_certificate function to return our mock certificate
|
55
|
+
with patch('mcp_security_framework.utils.cert_utils.parse_certificate') as mock_parse:
|
56
|
+
mock_parse.return_value = mock_cert
|
57
|
+
|
58
|
+
result = extract_unitid_from_certificate("mock_cert_data")
|
59
|
+
|
60
|
+
assert result is None
|
61
|
+
|
62
|
+
def test_extract_unitid_from_certificate_with_invalid_unitid(self):
|
63
|
+
"""Test extracting unitid from certificate with invalid unitid format."""
|
64
|
+
# Create a mock certificate with invalid unitid
|
65
|
+
mock_cert = Mock()
|
66
|
+
mock_extension = Mock()
|
67
|
+
mock_extension.value.value = b"invalid-unitid"
|
68
|
+
mock_cert.extensions.get_extension_for_oid.return_value = mock_extension
|
69
|
+
|
70
|
+
# Mock the parse_certificate function to return our mock certificate
|
71
|
+
with patch('mcp_security_framework.utils.cert_utils.parse_certificate') as mock_parse:
|
72
|
+
mock_parse.return_value = mock_cert
|
73
|
+
|
74
|
+
result = extract_unitid_from_certificate("mock_cert_data")
|
75
|
+
|
76
|
+
# Should return None for invalid unitid
|
77
|
+
assert result is None
|
78
|
+
|
79
|
+
def test_extract_unitid_from_certificate_with_real_certificate(self):
|
80
|
+
"""Test extracting unitid from a real certificate."""
|
81
|
+
# Generate a valid UUID4
|
82
|
+
test_unitid = str(uuid.uuid4())
|
83
|
+
|
84
|
+
# Create a real certificate with unitid extension
|
85
|
+
private_key = rsa.generate_private_key(
|
86
|
+
public_exponent=65537,
|
87
|
+
key_size=2048,
|
88
|
+
)
|
89
|
+
|
90
|
+
subject = issuer = x509.Name([
|
91
|
+
x509.NameAttribute(x509.NameOID.COMMON_NAME, 'Test UnitID'),
|
92
|
+
])
|
93
|
+
|
94
|
+
# Create certificate builder
|
95
|
+
from datetime import datetime, timezone, timedelta
|
96
|
+
builder = x509.CertificateBuilder()
|
97
|
+
builder = builder.subject_name(subject)
|
98
|
+
builder = builder.issuer_name(issuer)
|
99
|
+
builder = builder.public_key(private_key.public_key())
|
100
|
+
builder = builder.serial_number(x509.random_serial_number())
|
101
|
+
builder = builder.not_valid_before(datetime.now(timezone.utc))
|
102
|
+
builder = builder.not_valid_after(
|
103
|
+
datetime.now(timezone.utc) + timedelta(days=10)
|
104
|
+
)
|
105
|
+
|
106
|
+
# Add unitid extension
|
107
|
+
unitid_extension = x509.UnrecognizedExtension(
|
108
|
+
oid=x509.ObjectIdentifier("1.3.6.1.4.1.99999.1.3"),
|
109
|
+
value=test_unitid.encode(),
|
110
|
+
)
|
111
|
+
builder = builder.add_extension(unitid_extension, critical=False)
|
112
|
+
|
113
|
+
# Sign the certificate
|
114
|
+
certificate = builder.sign(private_key, hashes.SHA256())
|
115
|
+
|
116
|
+
# Test extraction
|
117
|
+
cert_pem = certificate.public_bytes(serialization.Encoding.PEM)
|
118
|
+
result = extract_unitid_from_certificate(cert_pem)
|
119
|
+
|
120
|
+
assert result == test_unitid
|
121
|
+
|
122
|
+
def test_unitid_validation_in_certificate_info(self):
|
123
|
+
"""Test unitid validation in CertificateInfo model."""
|
124
|
+
from mcp_security_framework.schemas.models import CertificateInfo, CertificateType
|
125
|
+
from datetime import datetime, timezone
|
126
|
+
|
127
|
+
# Test with valid UUID4
|
128
|
+
valid_unitid = str(uuid.uuid4())
|
129
|
+
cert_info = CertificateInfo(
|
130
|
+
subject={"CN": "Test"},
|
131
|
+
issuer={"CN": "Test CA"},
|
132
|
+
serial_number="123456789",
|
133
|
+
not_before=datetime.now(timezone.utc),
|
134
|
+
not_after=datetime.now(timezone.utc),
|
135
|
+
certificate_type=CertificateType.CLIENT,
|
136
|
+
key_size=2048,
|
137
|
+
signature_algorithm="sha256",
|
138
|
+
unitid=valid_unitid
|
139
|
+
)
|
140
|
+
|
141
|
+
assert cert_info.unitid == valid_unitid
|
142
|
+
|
143
|
+
# Test with invalid UUID4
|
144
|
+
with pytest.raises(ValueError, match="unitid must be a valid UUID4 string"):
|
145
|
+
CertificateInfo(
|
146
|
+
subject={"CN": "Test"},
|
147
|
+
issuer={"CN": "Test CA"},
|
148
|
+
serial_number="123456789",
|
149
|
+
not_before=datetime.now(timezone.utc),
|
150
|
+
not_after=datetime.now(timezone.utc),
|
151
|
+
certificate_type=CertificateType.CLIENT,
|
152
|
+
key_size=2048,
|
153
|
+
signature_algorithm="sha256",
|
154
|
+
unitid="invalid-unitid"
|
155
|
+
)
|
156
|
+
|
157
|
+
def test_unitid_validation_in_certificate_pair(self):
|
158
|
+
"""Test unitid validation in CertificatePair model."""
|
159
|
+
from mcp_security_framework.schemas.models import CertificatePair, CertificateType
|
160
|
+
from datetime import datetime, timezone
|
161
|
+
|
162
|
+
# Test with valid UUID4
|
163
|
+
valid_unitid = str(uuid.uuid4())
|
164
|
+
cert_pair = CertificatePair(
|
165
|
+
certificate_path="/path/to/cert.crt",
|
166
|
+
private_key_path="/path/to/key.key",
|
167
|
+
certificate_pem="-----BEGIN CERTIFICATE-----\nMOCK\n-----END CERTIFICATE-----",
|
168
|
+
private_key_pem="-----BEGIN PRIVATE KEY-----\nMOCK\n-----END PRIVATE KEY-----",
|
169
|
+
serial_number="123456789",
|
170
|
+
common_name="Test",
|
171
|
+
organization="Test Org",
|
172
|
+
not_before=datetime.now(timezone.utc),
|
173
|
+
not_after=datetime.now(timezone.utc),
|
174
|
+
certificate_type=CertificateType.CLIENT,
|
175
|
+
key_size=2048,
|
176
|
+
unitid=valid_unitid
|
177
|
+
)
|
178
|
+
|
179
|
+
assert cert_pair.unitid == valid_unitid
|
180
|
+
|
181
|
+
# Test with invalid UUID4
|
182
|
+
with pytest.raises(ValueError, match="unitid must be a valid UUID4 string"):
|
183
|
+
CertificatePair(
|
184
|
+
certificate_path="/path/to/cert.crt",
|
185
|
+
private_key_path="/path/to/key.key",
|
186
|
+
certificate_pem="-----BEGIN CERTIFICATE-----\nMOCK\n-----END CERTIFICATE-----",
|
187
|
+
private_key_pem="-----BEGIN PRIVATE KEY-----\nMOCK\n-----END PRIVATE KEY-----",
|
188
|
+
serial_number="123456789",
|
189
|
+
common_name="Test",
|
190
|
+
organization="Test Org",
|
191
|
+
not_before=datetime.now(timezone.utc),
|
192
|
+
not_after=datetime.now(timezone.utc),
|
193
|
+
certificate_type=CertificateType.CLIENT,
|
194
|
+
key_size=2048,
|
195
|
+
unitid="invalid-unitid"
|
196
|
+
)
|
197
|
+
|
198
|
+
def test_unitid_validation_in_auth_result(self):
|
199
|
+
"""Test unitid validation in AuthResult model."""
|
200
|
+
from mcp_security_framework.schemas.models import AuthResult, AuthStatus
|
201
|
+
from datetime import datetime, timezone
|
202
|
+
|
203
|
+
# Test with valid UUID4
|
204
|
+
valid_unitid = str(uuid.uuid4())
|
205
|
+
auth_result = AuthResult(
|
206
|
+
is_valid=True,
|
207
|
+
status=AuthStatus.SUCCESS,
|
208
|
+
username="test_user",
|
209
|
+
unitid=valid_unitid
|
210
|
+
)
|
211
|
+
|
212
|
+
assert auth_result.unitid == valid_unitid
|
213
|
+
|
214
|
+
# Test with invalid UUID4
|
215
|
+
with pytest.raises(ValueError, match="unitid must be a valid UUID4 string"):
|
216
|
+
AuthResult(
|
217
|
+
is_valid=True,
|
218
|
+
status=AuthStatus.SUCCESS,
|
219
|
+
username="test_user",
|
220
|
+
unitid="invalid-unitid"
|
221
|
+
)
|
222
|
+
|
223
|
+
def test_unitid_in_certificate_creation_config(self):
|
224
|
+
"""Test unitid in certificate creation configuration."""
|
225
|
+
from mcp_security_framework.schemas.config import CAConfig, ClientCertConfig
|
226
|
+
|
227
|
+
# Test with valid UUID4
|
228
|
+
valid_unitid = str(uuid.uuid4())
|
229
|
+
|
230
|
+
ca_config = CAConfig(
|
231
|
+
common_name="Test CA",
|
232
|
+
organization="Test Org",
|
233
|
+
country="US",
|
234
|
+
unitid=valid_unitid
|
235
|
+
)
|
236
|
+
assert ca_config.unitid == valid_unitid
|
237
|
+
|
238
|
+
client_config = ClientCertConfig(
|
239
|
+
common_name="test.example.com",
|
240
|
+
organization="Test Org",
|
241
|
+
country="US",
|
242
|
+
ca_cert_path="/path/to/ca.crt",
|
243
|
+
ca_key_path="/path/to/ca.key",
|
244
|
+
unitid=valid_unitid
|
245
|
+
)
|
246
|
+
assert client_config.unitid == valid_unitid
|
247
|
+
|
248
|
+
# Test with invalid UUID4
|
249
|
+
with pytest.raises(ValueError, match="unitid must be a valid UUID4 string"):
|
250
|
+
CAConfig(
|
251
|
+
common_name="Test CA",
|
252
|
+
organization="Test Org",
|
253
|
+
country="US",
|
254
|
+
unitid="invalid-unitid"
|
255
|
+
)
|
256
|
+
|
257
|
+
def test_unitid_integration_with_certificate_creation(self):
|
258
|
+
"""Test unitid integration with certificate creation."""
|
259
|
+
from mcp_security_framework.core.cert_manager import CertificateManager
|
260
|
+
from mcp_security_framework.schemas.config import CertificateConfig, CAConfig
|
261
|
+
from datetime import datetime, timezone
|
262
|
+
import tempfile
|
263
|
+
import os
|
264
|
+
|
265
|
+
# Create temporary directory for certificates
|
266
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
267
|
+
cert_config = CertificateConfig(
|
268
|
+
enabled=True,
|
269
|
+
ca_cert_path=os.path.join(temp_dir, "ca.crt"),
|
270
|
+
ca_key_path=os.path.join(temp_dir, "ca.key"),
|
271
|
+
cert_storage_path=temp_dir,
|
272
|
+
key_storage_path=temp_dir
|
273
|
+
)
|
274
|
+
|
275
|
+
# Mock the configuration validation to avoid file system checks
|
276
|
+
with patch.object(CertificateManager, '_validate_configuration'):
|
277
|
+
cert_manager = CertificateManager(cert_config)
|
278
|
+
|
279
|
+
# Test unitid in CA creation
|
280
|
+
valid_unitid = str(uuid.uuid4())
|
281
|
+
ca_config = CAConfig(
|
282
|
+
common_name="Test CA",
|
283
|
+
organization="Test Org",
|
284
|
+
country="US",
|
285
|
+
unitid=valid_unitid
|
286
|
+
)
|
287
|
+
|
288
|
+
# Mock the certificate creation to avoid actual file operations
|
289
|
+
with patch.object(cert_manager, '_validate_configuration'):
|
290
|
+
with patch('builtins.open', create=True):
|
291
|
+
with patch('os.makedirs'):
|
292
|
+
with patch('os.chmod'):
|
293
|
+
# Mock the certificate building process
|
294
|
+
mock_cert = Mock()
|
295
|
+
mock_cert.serial_number = 123456789
|
296
|
+
mock_cert.not_valid_before = datetime.now(timezone.utc)
|
297
|
+
mock_cert.not_valid_after = datetime.now(timezone.utc)
|
298
|
+
mock_cert.public_bytes.return_value = b"-----BEGIN CERTIFICATE-----\nMOCK\n-----END CERTIFICATE-----"
|
299
|
+
|
300
|
+
with patch('mcp_security_framework.core.cert_manager.rsa.generate_private_key') as mock_key:
|
301
|
+
mock_private_key = Mock()
|
302
|
+
mock_private_key.public_key.return_value = Mock()
|
303
|
+
mock_private_key.private_bytes.return_value = b"-----BEGIN PRIVATE KEY-----\nMOCK\n-----END PRIVATE KEY-----"
|
304
|
+
mock_key.return_value = mock_private_key
|
305
|
+
|
306
|
+
with patch('mcp_security_framework.core.cert_manager.x509.CertificateBuilder') as mock_builder:
|
307
|
+
mock_builder_instance = Mock()
|
308
|
+
mock_builder_instance.subject_name.return_value = mock_builder_instance
|
309
|
+
mock_builder_instance.issuer_name.return_value = mock_builder_instance
|
310
|
+
mock_builder_instance.public_key.return_value = mock_builder_instance
|
311
|
+
mock_builder_instance.serial_number.return_value = mock_builder_instance
|
312
|
+
mock_builder_instance.not_valid_before.return_value = mock_builder_instance
|
313
|
+
mock_builder_instance.not_valid_after.return_value = mock_builder_instance
|
314
|
+
mock_builder_instance.add_extension.return_value = mock_builder_instance
|
315
|
+
mock_builder_instance.sign.return_value = mock_cert
|
316
|
+
mock_builder.return_value = mock_builder_instance
|
317
|
+
|
318
|
+
# Mock the datetime compatibility functions
|
319
|
+
with patch('mcp_security_framework.core.cert_manager.get_not_valid_before_utc') as mock_before:
|
320
|
+
with patch('mcp_security_framework.core.cert_manager.get_not_valid_after_utc') as mock_after:
|
321
|
+
mock_before.return_value = datetime.now(timezone.utc)
|
322
|
+
mock_after.return_value = datetime.now(timezone.utc)
|
323
|
+
|
324
|
+
result = cert_manager.create_root_ca(ca_config)
|
325
|
+
|
326
|
+
# Verify unitid was added to the certificate pair
|
327
|
+
assert result.unitid == valid_unitid
|
328
|
+
|
329
|
+
def test_unitid_integration_with_authentication(self):
|
330
|
+
"""Test unitid integration with authentication."""
|
331
|
+
from mcp_security_framework.core.auth_manager import AuthManager
|
332
|
+
from mcp_security_framework.schemas.config import AuthConfig
|
333
|
+
from mcp_security_framework.core.permission_manager import PermissionManager
|
334
|
+
from mcp_security_framework.schemas.config import PermissionConfig
|
335
|
+
from unittest.mock import Mock
|
336
|
+
import tempfile
|
337
|
+
import json
|
338
|
+
import os
|
339
|
+
|
340
|
+
# Create test configuration
|
341
|
+
auth_config = AuthConfig(
|
342
|
+
enabled=True,
|
343
|
+
api_keys={"test_key": "test_user"},
|
344
|
+
jwt_secret="test_secret_key_for_jwt_signing_12345"
|
345
|
+
)
|
346
|
+
|
347
|
+
# Create temporary roles file
|
348
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
349
|
+
roles_file = os.path.join(temp_dir, "test_roles.json")
|
350
|
+
with open(roles_file, "w") as f:
|
351
|
+
json.dump({"roles": {}}, f)
|
352
|
+
|
353
|
+
perm_config = PermissionConfig(
|
354
|
+
enabled=True,
|
355
|
+
roles_file=roles_file
|
356
|
+
)
|
357
|
+
|
358
|
+
permission_manager = PermissionManager(perm_config)
|
359
|
+
auth_manager = AuthManager(auth_config, permission_manager)
|
360
|
+
|
361
|
+
# Test unitid in certificate authentication
|
362
|
+
valid_unitid = str(uuid.uuid4())
|
363
|
+
|
364
|
+
# Mock certificate with unitid
|
365
|
+
mock_cert_pem = "-----BEGIN CERTIFICATE-----\nMOCK\n-----END CERTIFICATE-----"
|
366
|
+
|
367
|
+
# Mock the authenticate_certificate method to return a successful result with unitid
|
368
|
+
from mcp_security_framework.schemas.models import AuthResult, AuthStatus
|
369
|
+
from datetime import datetime, timezone, timedelta
|
370
|
+
|
371
|
+
expected_result = AuthResult(
|
372
|
+
is_valid=True,
|
373
|
+
status=AuthStatus.SUCCESS,
|
374
|
+
username="test_user",
|
375
|
+
roles=[],
|
376
|
+
auth_method="certificate",
|
377
|
+
auth_timestamp=datetime.now(timezone.utc),
|
378
|
+
token_expiry=datetime.now(timezone.utc) + timedelta(days=30),
|
379
|
+
unitid=valid_unitid,
|
380
|
+
)
|
381
|
+
|
382
|
+
with patch.object(auth_manager, 'authenticate_certificate', return_value=expected_result):
|
383
|
+
result = auth_manager.authenticate_certificate(mock_cert_pem)
|
384
|
+
|
385
|
+
# Verify unitid was included in authentication result
|
386
|
+
assert result.unitid == valid_unitid
|
387
|
+
|
388
|
+
def test_unitid_optional_in_certificate_creation(self):
|
389
|
+
"""Test that unitid is optional in certificate creation."""
|
390
|
+
from mcp_security_framework.schemas.config import CAConfig, ClientCertConfig
|
391
|
+
|
392
|
+
# Test CA config without unitid
|
393
|
+
ca_config = CAConfig(
|
394
|
+
common_name="Test CA",
|
395
|
+
organization="Test Org",
|
396
|
+
country="US"
|
397
|
+
# unitid not specified
|
398
|
+
)
|
399
|
+
assert ca_config.unitid is None
|
400
|
+
|
401
|
+
# Test client config without unitid
|
402
|
+
client_config = ClientCertConfig(
|
403
|
+
common_name="test.example.com",
|
404
|
+
organization="Test Org",
|
405
|
+
country="US",
|
406
|
+
ca_cert_path="/path/to/ca.crt",
|
407
|
+
ca_key_path="/path/to/ca.key"
|
408
|
+
# unitid not specified
|
409
|
+
)
|
410
|
+
assert client_config.unitid is None
|
411
|
+
|
412
|
+
def test_unitid_optional_in_certificate_info(self):
|
413
|
+
"""Test that unitid is optional in CertificateInfo."""
|
414
|
+
from mcp_security_framework.schemas.models import CertificateInfo, CertificateType
|
415
|
+
from datetime import datetime, timezone
|
416
|
+
|
417
|
+
# Test CertificateInfo without unitid
|
418
|
+
cert_info = CertificateInfo(
|
419
|
+
subject={"CN": "Test"},
|
420
|
+
issuer={"CN": "Test CA"},
|
421
|
+
serial_number="123456789",
|
422
|
+
not_before=datetime.now(timezone.utc),
|
423
|
+
not_after=datetime.now(timezone.utc),
|
424
|
+
certificate_type=CertificateType.CLIENT,
|
425
|
+
key_size=2048,
|
426
|
+
signature_algorithm="sha256"
|
427
|
+
# unitid not specified
|
428
|
+
)
|
429
|
+
assert cert_info.unitid is None
|
430
|
+
|
431
|
+
def test_unitid_optional_in_certificate_pair(self):
|
432
|
+
"""Test that unitid is optional in CertificatePair."""
|
433
|
+
from mcp_security_framework.schemas.models import CertificatePair, CertificateType
|
434
|
+
from datetime import datetime, timezone
|
435
|
+
|
436
|
+
# Test CertificatePair without unitid
|
437
|
+
cert_pair = CertificatePair(
|
438
|
+
certificate_path="/path/to/cert.crt",
|
439
|
+
private_key_path="/path/to/key.key",
|
440
|
+
certificate_pem="-----BEGIN CERTIFICATE-----\nMOCK\n-----END CERTIFICATE-----",
|
441
|
+
private_key_pem="-----BEGIN PRIVATE KEY-----\nMOCK\n-----END PRIVATE KEY-----",
|
442
|
+
serial_number="123456789",
|
443
|
+
common_name="Test",
|
444
|
+
organization="Test Org",
|
445
|
+
not_before=datetime.now(timezone.utc),
|
446
|
+
not_after=datetime.now(timezone.utc),
|
447
|
+
certificate_type=CertificateType.CLIENT,
|
448
|
+
key_size=2048
|
449
|
+
# unitid not specified
|
450
|
+
)
|
451
|
+
assert cert_pair.unitid is None
|
452
|
+
|
453
|
+
def test_unitid_optional_in_auth_result(self):
|
454
|
+
"""Test that unitid is optional in AuthResult."""
|
455
|
+
from mcp_security_framework.schemas.models import AuthResult, AuthStatus
|
456
|
+
|
457
|
+
# Test AuthResult without unitid
|
458
|
+
auth_result = AuthResult(
|
459
|
+
is_valid=True,
|
460
|
+
status=AuthStatus.SUCCESS,
|
461
|
+
username="test_user"
|
462
|
+
# unitid not specified
|
463
|
+
)
|
464
|
+
assert auth_result.unitid is None
|
465
|
+
|
466
|
+
def test_extract_unitid_from_certificate_without_unitid_returns_none(self):
|
467
|
+
"""Test that extract_unitid_from_certificate returns None when unitid is not present."""
|
468
|
+
# Create a mock certificate without unitid extension
|
469
|
+
mock_cert = Mock()
|
470
|
+
mock_cert.extensions.get_extension_for_oid.side_effect = x509.ExtensionNotFound(
|
471
|
+
"Extension not found", "1.3.6.1.4.1.99999.1.3"
|
472
|
+
)
|
473
|
+
|
474
|
+
# Mock the parse_certificate function to return our mock certificate
|
475
|
+
with patch('mcp_security_framework.utils.cert_utils.parse_certificate') as mock_parse:
|
476
|
+
mock_parse.return_value = mock_cert
|
477
|
+
|
478
|
+
result = extract_unitid_from_certificate("mock_cert_data")
|
479
|
+
|
480
|
+
# Should return None when unitid is not present
|
481
|
+
assert result is None
|
482
|
+
|
483
|
+
def test_certificate_creation_without_unitid(self):
|
484
|
+
"""Test certificate creation when unitid is not specified."""
|
485
|
+
from mcp_security_framework.core.cert_manager import CertificateManager
|
486
|
+
from mcp_security_framework.schemas.config import CertificateConfig, CAConfig
|
487
|
+
from datetime import datetime, timezone
|
488
|
+
import tempfile
|
489
|
+
import os
|
490
|
+
|
491
|
+
# Create temporary directory for certificates
|
492
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
493
|
+
cert_config = CertificateConfig(
|
494
|
+
enabled=True,
|
495
|
+
ca_cert_path=os.path.join(temp_dir, "ca.crt"),
|
496
|
+
ca_key_path=os.path.join(temp_dir, "ca.key"),
|
497
|
+
cert_storage_path=temp_dir,
|
498
|
+
key_storage_path=temp_dir
|
499
|
+
)
|
500
|
+
|
501
|
+
# Mock the configuration validation to avoid file system checks
|
502
|
+
with patch.object(CertificateManager, '_validate_configuration'):
|
503
|
+
cert_manager = CertificateManager(cert_config)
|
504
|
+
|
505
|
+
# Test CA creation without unitid
|
506
|
+
ca_config = CAConfig(
|
507
|
+
common_name="Test CA",
|
508
|
+
organization="Test Org",
|
509
|
+
country="US"
|
510
|
+
# unitid not specified
|
511
|
+
)
|
512
|
+
|
513
|
+
# Mock the certificate building process
|
514
|
+
with patch('builtins.open', create=True):
|
515
|
+
with patch('os.makedirs'):
|
516
|
+
with patch('os.chmod'):
|
517
|
+
mock_cert = Mock()
|
518
|
+
mock_cert.serial_number = 123456789
|
519
|
+
mock_cert.not_valid_before = datetime.now(timezone.utc)
|
520
|
+
mock_cert.not_valid_after = datetime.now(timezone.utc)
|
521
|
+
mock_cert.public_bytes.return_value = b"-----BEGIN CERTIFICATE-----\nMOCK\n-----END CERTIFICATE-----"
|
522
|
+
|
523
|
+
with patch('mcp_security_framework.core.cert_manager.rsa.generate_private_key') as mock_key:
|
524
|
+
mock_private_key = Mock()
|
525
|
+
mock_private_key.public_key.return_value = Mock()
|
526
|
+
mock_private_key.private_bytes.return_value = b"-----BEGIN PRIVATE KEY-----\nMOCK\n-----END PRIVATE KEY-----"
|
527
|
+
mock_key.return_value = mock_private_key
|
528
|
+
|
529
|
+
with patch('mcp_security_framework.core.cert_manager.x509.CertificateBuilder') as mock_builder:
|
530
|
+
mock_builder_instance = Mock()
|
531
|
+
mock_builder_instance.subject_name.return_value = mock_builder_instance
|
532
|
+
mock_builder_instance.issuer_name.return_value = mock_builder_instance
|
533
|
+
mock_builder_instance.public_key.return_value = mock_builder_instance
|
534
|
+
mock_builder_instance.serial_number.return_value = mock_builder_instance
|
535
|
+
mock_builder_instance.not_valid_before.return_value = mock_builder_instance
|
536
|
+
mock_builder_instance.not_valid_after.return_value = mock_builder_instance
|
537
|
+
mock_builder_instance.add_extension.return_value = mock_builder_instance
|
538
|
+
mock_builder_instance.sign.return_value = mock_cert
|
539
|
+
mock_builder.return_value = mock_builder_instance
|
540
|
+
|
541
|
+
# Mock the datetime compatibility functions
|
542
|
+
with patch('mcp_security_framework.core.cert_manager.get_not_valid_before_utc') as mock_before:
|
543
|
+
with patch('mcp_security_framework.core.cert_manager.get_not_valid_after_utc') as mock_after:
|
544
|
+
mock_before.return_value = datetime.now(timezone.utc)
|
545
|
+
mock_after.return_value = datetime.now(timezone.utc)
|
546
|
+
|
547
|
+
result = cert_manager.create_root_ca(ca_config)
|
548
|
+
|
549
|
+
# Verify unitid is None when not specified
|
550
|
+
assert result.unitid is None
|
File without changes
|
{mcp_security_framework-1.2.0.dist-info → mcp_security_framework-1.2.1.dist-info}/entry_points.txt
RENAMED
File without changes
|
{mcp_security_framework-1.2.0.dist-info → mcp_security_framework-1.2.1.dist-info}/top_level.txt
RENAMED
File without changes
|