mcp-security-framework 0.1.0__py3-none-any.whl → 1.1.0__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/core/auth_manager.py +12 -2
- mcp_security_framework/core/cert_manager.py +247 -16
- mcp_security_framework/core/permission_manager.py +4 -0
- mcp_security_framework/core/rate_limiter.py +10 -0
- mcp_security_framework/core/security_manager.py +2 -0
- mcp_security_framework/examples/comprehensive_example.py +884 -0
- mcp_security_framework/examples/django_example.py +45 -12
- mcp_security_framework/examples/fastapi_example.py +826 -354
- mcp_security_framework/examples/flask_example.py +51 -11
- mcp_security_framework/examples/gateway_example.py +109 -17
- mcp_security_framework/examples/microservice_example.py +112 -16
- mcp_security_framework/examples/standalone_example.py +646 -430
- mcp_security_framework/examples/test_all_examples.py +556 -0
- mcp_security_framework/middleware/auth_middleware.py +1 -1
- mcp_security_framework/middleware/fastapi_auth_middleware.py +82 -14
- mcp_security_framework/middleware/flask_auth_middleware.py +154 -7
- mcp_security_framework/schemas/models.py +1 -0
- mcp_security_framework/utils/cert_utils.py +5 -5
- {mcp_security_framework-0.1.0.dist-info → mcp_security_framework-1.1.0.dist-info}/METADATA +1 -1
- {mcp_security_framework-0.1.0.dist-info → mcp_security_framework-1.1.0.dist-info}/RECORD +38 -32
- tests/conftest.py +306 -0
- tests/test_cli/test_cert_cli.py +13 -31
- tests/test_core/test_cert_manager.py +12 -12
- tests/test_examples/test_comprehensive_example.py +560 -0
- tests/test_examples/test_fastapi_example.py +214 -116
- tests/test_examples/test_flask_example.py +250 -131
- tests/test_examples/test_standalone_example.py +44 -99
- tests/test_integration/test_auth_flow.py +4 -4
- tests/test_integration/test_certificate_flow.py +1 -1
- tests/test_integration/test_fastapi_integration.py +39 -45
- tests/test_integration/test_flask_integration.py +4 -2
- tests/test_integration/test_standalone_integration.py +48 -48
- tests/test_middleware/test_fastapi_auth_middleware.py +724 -0
- tests/test_middleware/test_flask_auth_middleware.py +638 -0
- tests/test_middleware/test_security_middleware.py +9 -3
- {mcp_security_framework-0.1.0.dist-info → mcp_security_framework-1.1.0.dist-info}/WHEEL +0 -0
- {mcp_security_framework-0.1.0.dist-info → mcp_security_framework-1.1.0.dist-info}/entry_points.txt +0 -0
- {mcp_security_framework-0.1.0.dist-info → mcp_security_framework-1.1.0.dist-info}/top_level.txt +0 -0
@@ -159,6 +159,16 @@ class AuthManager:
|
|
159
159
|
"jwt_expiry_hours": config.jwt_expiry_hours,
|
160
160
|
},
|
161
161
|
)
|
162
|
+
|
163
|
+
@property
|
164
|
+
def is_auth_enabled(self) -> bool:
|
165
|
+
"""
|
166
|
+
Check if authentication is enabled.
|
167
|
+
|
168
|
+
Returns:
|
169
|
+
bool: True if authentication is enabled, False otherwise
|
170
|
+
"""
|
171
|
+
return self.config.enabled
|
162
172
|
|
163
173
|
def authenticate_api_key(self, api_key: str) -> AuthResult:
|
164
174
|
"""
|
@@ -568,7 +578,7 @@ class AuthManager:
|
|
568
578
|
|
569
579
|
# Check certificate expiration
|
570
580
|
now = datetime.now(timezone.utc)
|
571
|
-
if cert.
|
581
|
+
if cert.not_valid_after_utc.replace(tzinfo=timezone.utc) < now:
|
572
582
|
return AuthResult(
|
573
583
|
is_valid=False,
|
574
584
|
status=AuthStatus.EXPIRED,
|
@@ -626,7 +636,7 @@ class AuthManager:
|
|
626
636
|
roles=roles,
|
627
637
|
auth_method="certificate",
|
628
638
|
auth_timestamp=datetime.now(timezone.utc),
|
629
|
-
token_expiry=cert.
|
639
|
+
token_expiry=cert.not_valid_after_utc.replace(tzinfo=timezone.utc),
|
630
640
|
)
|
631
641
|
|
632
642
|
self.logger.info(
|
@@ -313,8 +313,8 @@ class CertificateManager:
|
|
313
313
|
serial_number=str(certificate.serial_number),
|
314
314
|
common_name=ca_config.common_name,
|
315
315
|
organization=ca_config.organization,
|
316
|
-
not_before=certificate.
|
317
|
-
not_after=certificate.
|
316
|
+
not_before=certificate.not_valid_before_utc,
|
317
|
+
not_after=certificate.not_valid_after_utc.replace(tzinfo=timezone.utc),
|
318
318
|
certificate_type=CertificateType.ROOT_CA,
|
319
319
|
key_size=ca_config.key_size,
|
320
320
|
)
|
@@ -534,8 +534,8 @@ class CertificateManager:
|
|
534
534
|
encryption_algorithm=serialization.NoEncryption(),
|
535
535
|
).decode(),
|
536
536
|
serial_number=str(certificate.serial_number),
|
537
|
-
not_before=certificate.
|
538
|
-
not_after=certificate.
|
537
|
+
not_before=certificate.not_valid_before_utc.replace(tzinfo=timezone.utc),
|
538
|
+
not_after=certificate.not_valid_after_utc.replace(tzinfo=timezone.utc),
|
539
539
|
common_name=intermediate_config.common_name,
|
540
540
|
organization=intermediate_config.organization,
|
541
541
|
certificate_type=CertificateType.INTERMEDIATE_CA,
|
@@ -775,8 +775,8 @@ class CertificateManager:
|
|
775
775
|
serial_number=str(certificate.serial_number),
|
776
776
|
common_name=client_config.common_name,
|
777
777
|
organization=client_config.organization,
|
778
|
-
not_before=certificate.
|
779
|
-
not_after=certificate.
|
778
|
+
not_before=certificate.not_valid_before_utc,
|
779
|
+
not_after=certificate.not_valid_after_utc.replace(tzinfo=timezone.utc),
|
780
780
|
certificate_type=CertificateType.CLIENT,
|
781
781
|
key_size=client_config.key_size,
|
782
782
|
)
|
@@ -1009,8 +1009,8 @@ class CertificateManager:
|
|
1009
1009
|
serial_number=str(certificate.serial_number),
|
1010
1010
|
common_name=server_config.common_name,
|
1011
1011
|
organization=server_config.organization,
|
1012
|
-
not_before=certificate.
|
1013
|
-
not_after=certificate.
|
1012
|
+
not_before=certificate.not_valid_before_utc,
|
1013
|
+
not_after=certificate.not_valid_after_utc.replace(tzinfo=timezone.utc),
|
1014
1014
|
certificate_type=CertificateType.SERVER,
|
1015
1015
|
key_size=server_config.key_size,
|
1016
1016
|
)
|
@@ -1144,8 +1144,8 @@ WvWwM6xqxW0Sf6s5AxJmTn3amZ0G+aP4Y2AEojlbQR7g5aigKbFQqGDFW07egp6
|
|
1144
1144
|
serial_number=str(new_cert.serial_number),
|
1145
1145
|
common_name="", # Will be extracted from subject
|
1146
1146
|
organization="", # Will be extracted from subject
|
1147
|
-
not_before=new_cert.
|
1148
|
-
not_after=new_cert.
|
1147
|
+
not_before=new_cert.not_valid_before_utc,
|
1148
|
+
not_after=new_cert.not_valid_after_utc.replace(tzinfo=timezone.utc),
|
1149
1149
|
certificate_type=CertificateType.CLIENT, # Default
|
1150
1150
|
key_size=0, # Will be extracted
|
1151
1151
|
)
|
@@ -1411,8 +1411,8 @@ WvWwM6xqxW0Sf6s5AxJmTn3amZ0G+aP4Y2AEojlbQR7g5aigKbFQqGDFW07egp6
|
|
1411
1411
|
)
|
1412
1412
|
},
|
1413
1413
|
serial_number=serial_number,
|
1414
|
-
not_before=cert.
|
1415
|
-
not_after=cert.
|
1414
|
+
not_before=cert.not_valid_before_utc,
|
1415
|
+
not_after=cert.not_valid_after_utc,
|
1416
1416
|
certificate_type=CertificateType.CLIENT, # Default to client, could be enhanced
|
1417
1417
|
key_size=expiry_info.get("key_size", 2048), # Default to 2048 bits
|
1418
1418
|
signature_algorithm=cert.signature_algorithm_oid._name,
|
@@ -1446,18 +1446,199 @@ WvWwM6xqxW0Sf6s5AxJmTn3amZ0G+aP4Y2AEojlbQR7g5aigKbFQqGDFW07egp6
|
|
1446
1446
|
f"Failed to get certificate info: {str(e)}"
|
1447
1447
|
)
|
1448
1448
|
|
1449
|
+
def create_certificate_signing_request(self,
|
1450
|
+
common_name: str,
|
1451
|
+
organization: str,
|
1452
|
+
country: str,
|
1453
|
+
state: Optional[str] = None,
|
1454
|
+
locality: Optional[str] = None,
|
1455
|
+
organizational_unit: Optional[str] = None,
|
1456
|
+
email: Optional[str] = None,
|
1457
|
+
key_size: int = 2048,
|
1458
|
+
key_type: str = "rsa",
|
1459
|
+
output_path: Optional[str] = None) -> Tuple[str, str]:
|
1460
|
+
"""
|
1461
|
+
Create a Certificate Signing Request (CSR).
|
1462
|
+
|
1463
|
+
This method creates a Certificate Signing Request (CSR) that can be
|
1464
|
+
submitted to a Certificate Authority (CA) for signing.
|
1465
|
+
|
1466
|
+
Args:
|
1467
|
+
common_name (str): Common name for the certificate (e.g., domain name)
|
1468
|
+
organization (str): Organization name
|
1469
|
+
country (str): Country code (e.g., "US", "GB")
|
1470
|
+
state (Optional[str]): State or province name
|
1471
|
+
locality (Optional[str]): Locality or city name
|
1472
|
+
organizational_unit (Optional[str]): Organizational unit name
|
1473
|
+
email (Optional[str]): Email address
|
1474
|
+
key_size (int): Key size in bits. Defaults to 2048
|
1475
|
+
key_type (str): Key type ("rsa" or "ec"). Defaults to "rsa"
|
1476
|
+
output_path (Optional[str]): Output directory for CSR and key files.
|
1477
|
+
If None, uses default path from configuration
|
1478
|
+
|
1479
|
+
Returns:
|
1480
|
+
Tuple[str, str]: Paths to the created CSR file and private key file
|
1481
|
+
|
1482
|
+
Raises:
|
1483
|
+
ValueError: When required parameters are invalid
|
1484
|
+
CertificateGenerationError: When CSR creation fails
|
1485
|
+
FileNotFoundError: When output directory is not accessible
|
1486
|
+
PermissionError: When output directory is not writable
|
1487
|
+
|
1488
|
+
Example:
|
1489
|
+
>>> cert_manager = CertificateManager(config)
|
1490
|
+
>>> csr_path, key_path = cert_manager.create_certificate_signing_request(
|
1491
|
+
... common_name="api.example.com",
|
1492
|
+
... organization="Example Corp",
|
1493
|
+
... country="US",
|
1494
|
+
... state="California"
|
1495
|
+
... )
|
1496
|
+
>>> print(f"CSR created: {csr_path}")
|
1497
|
+
>>> print(f"Private key created: {key_path}")
|
1498
|
+
"""
|
1499
|
+
try:
|
1500
|
+
# Validate required parameters
|
1501
|
+
if not common_name:
|
1502
|
+
raise ValueError("Common name is required")
|
1503
|
+
if not organization:
|
1504
|
+
raise ValueError("Organization is required")
|
1505
|
+
if not country:
|
1506
|
+
raise ValueError("Country is required")
|
1507
|
+
|
1508
|
+
# Generate private key
|
1509
|
+
if key_type.lower() == "rsa":
|
1510
|
+
private_key = rsa.generate_private_key(
|
1511
|
+
public_exponent=65537, key_size=key_size, backend=None
|
1512
|
+
)
|
1513
|
+
elif key_type.lower() == "ec":
|
1514
|
+
private_key = ec.generate_private_key(
|
1515
|
+
ec.SECP256R1(), backend=None
|
1516
|
+
)
|
1517
|
+
else:
|
1518
|
+
raise ValueError(f"Unsupported key type: {key_type}")
|
1519
|
+
|
1520
|
+
# Create CSR subject
|
1521
|
+
subject_attributes = [
|
1522
|
+
x509.NameAttribute(NameOID.COMMON_NAME, common_name),
|
1523
|
+
x509.NameAttribute(NameOID.ORGANIZATION_NAME, organization),
|
1524
|
+
x509.NameAttribute(NameOID.COUNTRY_NAME, country),
|
1525
|
+
]
|
1526
|
+
|
1527
|
+
# Add optional attributes
|
1528
|
+
if state:
|
1529
|
+
subject_attributes.append(
|
1530
|
+
x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, state)
|
1531
|
+
)
|
1532
|
+
if locality:
|
1533
|
+
subject_attributes.append(
|
1534
|
+
x509.NameAttribute(NameOID.LOCALITY_NAME, locality)
|
1535
|
+
)
|
1536
|
+
if organizational_unit:
|
1537
|
+
subject_attributes.append(
|
1538
|
+
x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, organizational_unit)
|
1539
|
+
)
|
1540
|
+
if email:
|
1541
|
+
subject_attributes.append(
|
1542
|
+
x509.NameAttribute(NameOID.EMAIL_ADDRESS, email)
|
1543
|
+
)
|
1544
|
+
|
1545
|
+
subject = x509.Name(subject_attributes)
|
1546
|
+
|
1547
|
+
# Create CSR builder
|
1548
|
+
csr_builder = x509.CertificateSigningRequestBuilder()
|
1549
|
+
csr_builder = csr_builder.subject_name(subject)
|
1550
|
+
csr_builder = csr_builder.add_extension(
|
1551
|
+
x509.BasicConstraints(ca=False, path_length=None),
|
1552
|
+
critical=True
|
1553
|
+
)
|
1554
|
+
|
1555
|
+
# Add Subject Alternative Name extension if common name looks like a domain
|
1556
|
+
if "." in common_name and not common_name.startswith("*"):
|
1557
|
+
san_extension = x509.SubjectAlternativeName([
|
1558
|
+
x509.DNSName(common_name)
|
1559
|
+
])
|
1560
|
+
csr_builder = csr_builder.add_extension(san_extension, critical=False)
|
1561
|
+
|
1562
|
+
# Build and sign CSR
|
1563
|
+
csr = csr_builder.sign(private_key, hashes.SHA256())
|
1564
|
+
|
1565
|
+
# Determine output paths
|
1566
|
+
if output_path is None:
|
1567
|
+
output_dir = Path(self.config.cert_storage_path) / "csr"
|
1568
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
1569
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
1570
|
+
csr_filename = f"{common_name.replace('.', '_')}_{timestamp}.csr"
|
1571
|
+
key_filename = f"{common_name.replace('.', '_')}_{timestamp}.key"
|
1572
|
+
csr_path = str(output_dir / csr_filename)
|
1573
|
+
key_path = str(output_dir / key_filename)
|
1574
|
+
else:
|
1575
|
+
output_dir = Path(output_path)
|
1576
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
1577
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
1578
|
+
csr_filename = f"{common_name.replace('.', '_')}_{timestamp}.csr"
|
1579
|
+
key_filename = f"{common_name.replace('.', '_')}_{timestamp}.key"
|
1580
|
+
csr_path = str(output_dir / csr_filename)
|
1581
|
+
key_path = str(output_dir / key_filename)
|
1582
|
+
|
1583
|
+
# Write CSR to file
|
1584
|
+
with open(csr_path, 'wb') as f:
|
1585
|
+
f.write(csr.public_bytes(serialization.Encoding.PEM))
|
1586
|
+
|
1587
|
+
# Write private key to file
|
1588
|
+
with open(key_path, 'wb') as f:
|
1589
|
+
f.write(private_key.private_bytes(
|
1590
|
+
encoding=serialization.Encoding.PEM,
|
1591
|
+
format=serialization.PrivateFormat.PKCS8,
|
1592
|
+
encryption_algorithm=serialization.NoEncryption()
|
1593
|
+
))
|
1594
|
+
|
1595
|
+
# Set proper permissions
|
1596
|
+
os.chmod(key_path, 0o600) # Read/write for owner only
|
1597
|
+
os.chmod(csr_path, 0o644) # Read for all, write for owner
|
1598
|
+
|
1599
|
+
self.logger.info(
|
1600
|
+
"CSR created successfully",
|
1601
|
+
extra={
|
1602
|
+
"csr_path": csr_path,
|
1603
|
+
"key_path": key_path,
|
1604
|
+
"common_name": common_name,
|
1605
|
+
"organization": organization,
|
1606
|
+
"key_type": key_type,
|
1607
|
+
"key_size": key_size
|
1608
|
+
}
|
1609
|
+
)
|
1610
|
+
|
1611
|
+
return csr_path, key_path
|
1612
|
+
|
1613
|
+
except Exception as e:
|
1614
|
+
self.logger.error(
|
1615
|
+
"Failed to create CSR",
|
1616
|
+
extra={
|
1617
|
+
"common_name": common_name,
|
1618
|
+
"organization": organization,
|
1619
|
+
"error": str(e)
|
1620
|
+
}
|
1621
|
+
)
|
1622
|
+
raise CertificateGenerationError(f"Failed to create CSR: {str(e)}")
|
1623
|
+
|
1449
1624
|
def create_crl(self, ca_cert_path: str, ca_key_path: str,
|
1625
|
+
revoked_serials: Optional[List[Dict[str, Union[str, int]]]] = None,
|
1450
1626
|
output_path: Optional[str] = None, validity_days: int = 30) -> str:
|
1451
1627
|
"""
|
1452
1628
|
Create a Certificate Revocation List (CRL).
|
1453
1629
|
|
1454
1630
|
This method creates a Certificate Revocation List (CRL) from the CA
|
1455
1631
|
certificate and private key. The CRL contains information about
|
1456
|
-
revoked certificates.
|
1632
|
+
revoked certificates with revocation reasons.
|
1457
1633
|
|
1458
1634
|
Args:
|
1459
1635
|
ca_cert_path (str): Path to CA certificate file
|
1460
1636
|
ca_key_path (str): Path to CA private key file
|
1637
|
+
revoked_serials (Optional[List[Dict]]): List of revoked certificate serials.
|
1638
|
+
Each dict should contain:
|
1639
|
+
- "serial": Serial number as string or int
|
1640
|
+
- "reason": Revocation reason (optional)
|
1641
|
+
- "revocation_date": Revocation date (optional, defaults to now)
|
1461
1642
|
output_path (Optional[str]): Output path for CRL file.
|
1462
1643
|
If None, uses default path from configuration
|
1463
1644
|
validity_days (int): CRL validity period in days. Defaults to 30
|
@@ -1471,9 +1652,14 @@ WvWwM6xqxW0Sf6s5AxJmTn3amZ0G+aP4Y2AEojlbQR7g5aigKbFQqGDFW07egp6
|
|
1471
1652
|
|
1472
1653
|
Example:
|
1473
1654
|
>>> cert_manager = CertificateManager(config)
|
1655
|
+
>>> revoked_serials = [
|
1656
|
+
... {"serial": "123456789", "reason": "key_compromise"},
|
1657
|
+
... {"serial": "987654321", "reason": "certificate_hold"}
|
1658
|
+
... ]
|
1474
1659
|
>>> crl_path = cert_manager.create_crl(
|
1475
1660
|
... ca_cert_path="/path/to/ca.crt",
|
1476
1661
|
... ca_key_path="/path/to/ca.key",
|
1662
|
+
... revoked_serials=revoked_serials,
|
1477
1663
|
... validity_days=30
|
1478
1664
|
... )
|
1479
1665
|
>>> print(f"CRL created: {crl_path}")
|
@@ -1508,10 +1694,31 @@ WvWwM6xqxW0Sf6s5AxJmTn3amZ0G+aP4Y2AEojlbQR7g5aigKbFQqGDFW07egp6
|
|
1508
1694
|
crl_builder = crl_builder.next_update(next_update)
|
1509
1695
|
crl_builder = crl_builder.issuer_name(ca_cert.subject)
|
1510
1696
|
|
1511
|
-
# Add revoked certificates
|
1512
|
-
# This is a basic implementation - in a real scenario, you would
|
1513
|
-
# load revoked certificates from a database or file
|
1697
|
+
# Add revoked certificates
|
1514
1698
|
revoked_certificates = []
|
1699
|
+
if revoked_serials:
|
1700
|
+
for revoked_info in revoked_serials:
|
1701
|
+
serial = revoked_info.get("serial")
|
1702
|
+
reason = revoked_info.get("reason", "unspecified")
|
1703
|
+
revocation_date = revoked_info.get("revocation_date", now)
|
1704
|
+
|
1705
|
+
# Convert serial to int if it's a string
|
1706
|
+
if isinstance(serial, str):
|
1707
|
+
serial = int(serial, 16) if serial.startswith("0x") else int(serial)
|
1708
|
+
|
1709
|
+
# Map reason string to x509 enum
|
1710
|
+
reason_enum = self._get_revocation_reason(reason)
|
1711
|
+
|
1712
|
+
# Create revoked certificate entry
|
1713
|
+
revoked_cert = x509.RevokedCertificateBuilder().serial_number(
|
1714
|
+
serial
|
1715
|
+
).revocation_date(
|
1716
|
+
revocation_date
|
1717
|
+
).add_extension(
|
1718
|
+
x509.CRLReason(reason_enum), critical=False
|
1719
|
+
).build()
|
1720
|
+
|
1721
|
+
revoked_certificates.append(revoked_cert)
|
1515
1722
|
|
1516
1723
|
# Build CRL
|
1517
1724
|
crl = crl_builder.sign(ca_private_key, hashes.SHA256())
|
@@ -1551,6 +1758,30 @@ WvWwM6xqxW0Sf6s5AxJmTn3amZ0G+aP4Y2AEojlbQR7g5aigKbFQqGDFW07egp6
|
|
1551
1758
|
)
|
1552
1759
|
raise CertificateGenerationError(f"Failed to create CRL: {str(e)}")
|
1553
1760
|
|
1761
|
+
def _get_revocation_reason(self, reason: str) -> x509.ReasonFlags:
|
1762
|
+
"""
|
1763
|
+
Map reason string to x509.ReasonFlags enum.
|
1764
|
+
|
1765
|
+
Args:
|
1766
|
+
reason (str): Reason string
|
1767
|
+
|
1768
|
+
Returns:
|
1769
|
+
x509.ReasonFlags: Corresponding reason enum
|
1770
|
+
"""
|
1771
|
+
reason_map = {
|
1772
|
+
"unspecified": x509.ReasonFlags.unspecified,
|
1773
|
+
"key_compromise": x509.ReasonFlags.key_compromise,
|
1774
|
+
"ca_compromise": x509.ReasonFlags.ca_compromise,
|
1775
|
+
"affiliation_changed": x509.ReasonFlags.affiliation_changed,
|
1776
|
+
"superseded": x509.ReasonFlags.superseded,
|
1777
|
+
"cessation_of_operation": x509.ReasonFlags.cessation_of_operation,
|
1778
|
+
"certificate_hold": x509.ReasonFlags.certificate_hold,
|
1779
|
+
"privilege_withdrawn": x509.ReasonFlags.privilege_withdrawn,
|
1780
|
+
"aa_compromise": x509.ReasonFlags.aa_compromise,
|
1781
|
+
}
|
1782
|
+
|
1783
|
+
return reason_map.get(reason.lower(), x509.ReasonFlags.unspecified)
|
1784
|
+
|
1554
1785
|
def export_certificate(self, cert_path: str, format: str = "pem") -> Union[str, bytes]:
|
1555
1786
|
"""
|
1556
1787
|
Export certificate to different formats.
|
@@ -660,6 +660,10 @@ class PermissionManager:
|
|
660
660
|
if required_perm in available_permissions:
|
661
661
|
return True
|
662
662
|
|
663
|
+
# Check for universal permission "*" (all permissions)
|
664
|
+
if "*" in available_permissions:
|
665
|
+
return True
|
666
|
+
|
663
667
|
# Wildcard match
|
664
668
|
if "*" in required_perm:
|
665
669
|
perm_parts = required_perm.split(":")
|
@@ -182,6 +182,16 @@ class RateLimiter:
|
|
182
182
|
"storage_backend": config.storage_backend,
|
183
183
|
},
|
184
184
|
)
|
185
|
+
|
186
|
+
@property
|
187
|
+
def is_rate_limiting_enabled(self) -> bool:
|
188
|
+
"""
|
189
|
+
Check if rate limiting is enabled.
|
190
|
+
|
191
|
+
Returns:
|
192
|
+
bool: True if rate limiting is enabled, False otherwise
|
193
|
+
"""
|
194
|
+
return self.config.enabled
|
185
195
|
|
186
196
|
def check_rate_limit(self, identifier: str, limit: Optional[int] = None) -> bool:
|
187
197
|
"""
|
@@ -25,6 +25,7 @@ License: MIT
|
|
25
25
|
|
26
26
|
import logging
|
27
27
|
from typing import Dict, List, Optional, Union, Any
|
28
|
+
from datetime import datetime, timezone
|
28
29
|
|
29
30
|
from ..schemas.config import SecurityConfig, AuthConfig, PermissionConfig, CertificateConfig
|
30
31
|
from ..schemas.models import AuthResult, ValidationResult, ValidationStatus, CertificatePair, CertificateInfo, AuthStatus
|
@@ -129,6 +130,7 @@ class SecurityManager:
|
|
129
130
|
self.logger = logging.getLogger(__name__)
|
130
131
|
self._component_status: Dict[str, bool] = {}
|
131
132
|
self._security_events: List[Dict[str, Any]] = []
|
133
|
+
self._start_time = datetime.now(timezone.utc)
|
132
134
|
|
133
135
|
# Initialize all core components
|
134
136
|
self._initialize_components()
|