mcp-security-framework 0.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/__init__.py +96 -0
- mcp_security_framework/cli/__init__.py +18 -0
- mcp_security_framework/cli/cert_cli.py +511 -0
- mcp_security_framework/cli/security_cli.py +791 -0
- mcp_security_framework/constants.py +209 -0
- mcp_security_framework/core/__init__.py +61 -0
- mcp_security_framework/core/auth_manager.py +1011 -0
- mcp_security_framework/core/cert_manager.py +1663 -0
- mcp_security_framework/core/permission_manager.py +735 -0
- mcp_security_framework/core/rate_limiter.py +602 -0
- mcp_security_framework/core/security_manager.py +943 -0
- mcp_security_framework/core/ssl_manager.py +735 -0
- mcp_security_framework/examples/__init__.py +75 -0
- mcp_security_framework/examples/django_example.py +615 -0
- mcp_security_framework/examples/fastapi_example.py +472 -0
- mcp_security_framework/examples/flask_example.py +506 -0
- mcp_security_framework/examples/gateway_example.py +803 -0
- mcp_security_framework/examples/microservice_example.py +690 -0
- mcp_security_framework/examples/standalone_example.py +576 -0
- mcp_security_framework/middleware/__init__.py +250 -0
- mcp_security_framework/middleware/auth_middleware.py +292 -0
- mcp_security_framework/middleware/fastapi_auth_middleware.py +447 -0
- mcp_security_framework/middleware/fastapi_middleware.py +757 -0
- mcp_security_framework/middleware/flask_auth_middleware.py +465 -0
- mcp_security_framework/middleware/flask_middleware.py +591 -0
- mcp_security_framework/middleware/mtls_middleware.py +439 -0
- mcp_security_framework/middleware/rate_limit_middleware.py +403 -0
- mcp_security_framework/middleware/security_middleware.py +507 -0
- mcp_security_framework/schemas/__init__.py +109 -0
- mcp_security_framework/schemas/config.py +694 -0
- mcp_security_framework/schemas/models.py +709 -0
- mcp_security_framework/schemas/responses.py +686 -0
- mcp_security_framework/tests/__init__.py +0 -0
- mcp_security_framework/utils/__init__.py +121 -0
- mcp_security_framework/utils/cert_utils.py +525 -0
- mcp_security_framework/utils/crypto_utils.py +475 -0
- mcp_security_framework/utils/validation_utils.py +571 -0
- mcp_security_framework-0.1.0.dist-info/METADATA +411 -0
- mcp_security_framework-0.1.0.dist-info/RECORD +76 -0
- mcp_security_framework-0.1.0.dist-info/WHEEL +5 -0
- mcp_security_framework-0.1.0.dist-info/entry_points.txt +3 -0
- mcp_security_framework-0.1.0.dist-info/top_level.txt +2 -0
- tests/__init__.py +0 -0
- tests/test_cli/__init__.py +0 -0
- tests/test_cli/test_cert_cli.py +379 -0
- tests/test_cli/test_security_cli.py +657 -0
- tests/test_core/__init__.py +0 -0
- tests/test_core/test_auth_manager.py +582 -0
- tests/test_core/test_cert_manager.py +795 -0
- tests/test_core/test_permission_manager.py +395 -0
- tests/test_core/test_rate_limiter.py +626 -0
- tests/test_core/test_security_manager.py +841 -0
- tests/test_core/test_ssl_manager.py +532 -0
- tests/test_examples/__init__.py +8 -0
- tests/test_examples/test_fastapi_example.py +264 -0
- tests/test_examples/test_flask_example.py +238 -0
- tests/test_examples/test_standalone_example.py +292 -0
- tests/test_integration/__init__.py +0 -0
- tests/test_integration/test_auth_flow.py +502 -0
- tests/test_integration/test_certificate_flow.py +527 -0
- tests/test_integration/test_fastapi_integration.py +341 -0
- tests/test_integration/test_flask_integration.py +398 -0
- tests/test_integration/test_standalone_integration.py +493 -0
- tests/test_middleware/__init__.py +0 -0
- tests/test_middleware/test_fastapi_middleware.py +523 -0
- tests/test_middleware/test_flask_middleware.py +582 -0
- tests/test_middleware/test_security_middleware.py +493 -0
- tests/test_schemas/__init__.py +0 -0
- tests/test_schemas/test_config.py +811 -0
- tests/test_schemas/test_models.py +879 -0
- tests/test_schemas/test_responses.py +1054 -0
- tests/test_schemas/test_serialization.py +493 -0
- tests/test_utils/__init__.py +0 -0
- tests/test_utils/test_cert_utils.py +510 -0
- tests/test_utils/test_crypto_utils.py +603 -0
- tests/test_utils/test_validation_utils.py +477 -0
@@ -0,0 +1,709 @@
|
|
1
|
+
"""
|
2
|
+
Data Models Module
|
3
|
+
|
4
|
+
This module provides comprehensive data models for all core data structures
|
5
|
+
used throughout the MCP Security Framework. It includes models for
|
6
|
+
authentication results, validation results, certificate information,
|
7
|
+
and other essential data types.
|
8
|
+
|
9
|
+
Key Features:
|
10
|
+
- Type-safe data models with validation
|
11
|
+
- Comprehensive field documentation
|
12
|
+
- Default values for common use cases
|
13
|
+
- Serialization and deserialization support
|
14
|
+
- Type aliases for better code readability
|
15
|
+
|
16
|
+
Classes:
|
17
|
+
AuthResult: Authentication result model
|
18
|
+
ValidationResult: Validation result model
|
19
|
+
CertificateInfo: Certificate information model
|
20
|
+
CertificatePair: Certificate and key pair model
|
21
|
+
RateLimitStatus: Rate limiting status model
|
22
|
+
UserCredentials: User credentials model
|
23
|
+
RolePermissions: Role permissions mapping model
|
24
|
+
CertificateChain: Certificate chain model
|
25
|
+
|
26
|
+
Type Aliases:
|
27
|
+
ApiKey: Type alias for API key strings
|
28
|
+
Username: Type alias for username strings
|
29
|
+
RoleName: Type alias for role name strings
|
30
|
+
PermissionName: Type alias for permission name strings
|
31
|
+
CertificatePath: Type alias for certificate file paths
|
32
|
+
|
33
|
+
Author: MCP Security Team
|
34
|
+
Version: 1.0.0
|
35
|
+
License: MIT
|
36
|
+
"""
|
37
|
+
|
38
|
+
from datetime import datetime, timedelta, timezone
|
39
|
+
from enum import Enum
|
40
|
+
from typing import Any, Dict, List, Optional, Set, TypeAlias
|
41
|
+
|
42
|
+
from pydantic import BaseModel, Field, field_validator, model_validator
|
43
|
+
|
44
|
+
# Type aliases for better code readability
|
45
|
+
ApiKey: TypeAlias = str
|
46
|
+
Username: TypeAlias = str
|
47
|
+
RoleName: TypeAlias = str
|
48
|
+
PermissionName: TypeAlias = str
|
49
|
+
CertificatePath: TypeAlias = str
|
50
|
+
|
51
|
+
|
52
|
+
class AuthStatus(str, Enum):
|
53
|
+
"""Authentication status enumeration."""
|
54
|
+
|
55
|
+
SUCCESS = "success"
|
56
|
+
FAILED = "failed"
|
57
|
+
EXPIRED = "expired"
|
58
|
+
INVALID = "invalid"
|
59
|
+
UNAUTHORIZED = "unauthorized"
|
60
|
+
|
61
|
+
|
62
|
+
class ValidationStatus(str, Enum):
|
63
|
+
"""Validation status enumeration."""
|
64
|
+
|
65
|
+
VALID = "valid"
|
66
|
+
INVALID = "invalid"
|
67
|
+
EXPIRED = "expired"
|
68
|
+
REVOKED = "revoked"
|
69
|
+
UNTRUSTED = "untrusted"
|
70
|
+
|
71
|
+
|
72
|
+
class CertificateType(str, Enum):
|
73
|
+
"""Certificate type enumeration."""
|
74
|
+
|
75
|
+
ROOT_CA = "root_ca"
|
76
|
+
INTERMEDIATE_CA = "intermediate_ca"
|
77
|
+
SERVER = "server"
|
78
|
+
CLIENT = "client"
|
79
|
+
CODE_SIGNING = "code_signing"
|
80
|
+
EMAIL = "email"
|
81
|
+
|
82
|
+
|
83
|
+
class AuthMethod(str, Enum):
|
84
|
+
"""Authentication method enumeration."""
|
85
|
+
|
86
|
+
API_KEY = "api_key"
|
87
|
+
JWT = "jwt"
|
88
|
+
CERTIFICATE = "certificate"
|
89
|
+
BASIC = "basic"
|
90
|
+
OAUTH2 = "oauth2"
|
91
|
+
|
92
|
+
|
93
|
+
class AuthResult(BaseModel):
|
94
|
+
"""
|
95
|
+
Authentication Result Model
|
96
|
+
|
97
|
+
This model represents the result of an authentication attempt,
|
98
|
+
including success status, user information, and authentication
|
99
|
+
metadata.
|
100
|
+
|
101
|
+
Attributes:
|
102
|
+
is_valid: Whether authentication was successful
|
103
|
+
status: Authentication status (success, failed, expired, etc.)
|
104
|
+
username: Authenticated username if successful
|
105
|
+
user_id: Unique user identifier
|
106
|
+
roles: List of user roles
|
107
|
+
permissions: Set of user permissions
|
108
|
+
auth_method: Method used for authentication
|
109
|
+
auth_timestamp: When authentication occurred
|
110
|
+
token_expiry: When authentication token expires
|
111
|
+
error_code: Error code if authentication failed
|
112
|
+
error_message: Human-readable error message
|
113
|
+
metadata: Additional authentication metadata
|
114
|
+
"""
|
115
|
+
|
116
|
+
is_valid: bool = Field(..., description="Whether authentication was successful")
|
117
|
+
status: AuthStatus = Field(..., description="Authentication status")
|
118
|
+
username: Optional[str] = Field(default=None, description="Authenticated username")
|
119
|
+
user_id: Optional[str] = Field(default=None, description="Unique user identifier")
|
120
|
+
roles: List[str] = Field(default_factory=list, description="List of user roles")
|
121
|
+
permissions: Set[str] = Field(
|
122
|
+
default_factory=set, description="Set of user permissions"
|
123
|
+
)
|
124
|
+
auth_method: Optional[AuthMethod] = Field(
|
125
|
+
default=None, description="Method used for authentication"
|
126
|
+
)
|
127
|
+
auth_timestamp: datetime = Field(
|
128
|
+
default_factory=datetime.utcnow, description="When authentication occurred"
|
129
|
+
)
|
130
|
+
token_expiry: Optional[datetime] = Field(
|
131
|
+
default=None, description="When authentication token expires"
|
132
|
+
)
|
133
|
+
error_code: Optional[int] = Field(
|
134
|
+
default=None, description="Error code if authentication failed"
|
135
|
+
)
|
136
|
+
error_message: Optional[str] = Field(
|
137
|
+
default=None, description="Human-readable error message"
|
138
|
+
)
|
139
|
+
metadata: Dict[str, Any] = Field(
|
140
|
+
default_factory=dict, description="Additional authentication metadata"
|
141
|
+
)
|
142
|
+
|
143
|
+
@field_validator("username")
|
144
|
+
@classmethod
|
145
|
+
def validate_username(cls, v):
|
146
|
+
"""Validate username format."""
|
147
|
+
if v is not None and len(v.strip()) == 0:
|
148
|
+
raise ValueError("Username cannot be empty")
|
149
|
+
return v
|
150
|
+
|
151
|
+
@model_validator(mode="after")
|
152
|
+
def validate_auth_result(self):
|
153
|
+
"""Validate authentication result consistency."""
|
154
|
+
if self.is_valid and self.status != AuthStatus.SUCCESS:
|
155
|
+
raise ValueError("Valid authentication must have SUCCESS status")
|
156
|
+
|
157
|
+
if not self.is_valid and self.status == AuthStatus.SUCCESS:
|
158
|
+
raise ValueError("Invalid authentication cannot have SUCCESS status")
|
159
|
+
|
160
|
+
if self.error_code is not None and self.is_valid:
|
161
|
+
raise ValueError("Valid authentication cannot have error code")
|
162
|
+
|
163
|
+
if self.error_message is not None and self.is_valid:
|
164
|
+
raise ValueError("Valid authentication cannot have error message")
|
165
|
+
|
166
|
+
return self
|
167
|
+
|
168
|
+
@property
|
169
|
+
def is_expired(self) -> bool:
|
170
|
+
"""Check if authentication token is expired."""
|
171
|
+
if self.token_expiry is None:
|
172
|
+
return False
|
173
|
+
return datetime.now(timezone.utc) > self.token_expiry
|
174
|
+
|
175
|
+
@property
|
176
|
+
def expires_soon(self) -> bool:
|
177
|
+
"""Check if authentication token expires soon (within 1 hour)."""
|
178
|
+
if self.token_expiry is None:
|
179
|
+
return False
|
180
|
+
return datetime.now(timezone.utc) + timedelta(hours=1) > self.token_expiry
|
181
|
+
|
182
|
+
|
183
|
+
class ValidationResult(BaseModel):
|
184
|
+
"""
|
185
|
+
Validation Result Model
|
186
|
+
|
187
|
+
This model represents the result of a validation operation,
|
188
|
+
such as certificate validation, permission validation, or
|
189
|
+
input data validation.
|
190
|
+
|
191
|
+
Attributes:
|
192
|
+
is_valid: Whether validation was successful
|
193
|
+
status: Validation status (valid, invalid, expired, etc.)
|
194
|
+
field_name: Name of the field being validated
|
195
|
+
value: Value that was validated
|
196
|
+
error_code: Error code if validation failed
|
197
|
+
error_message: Human-readable error message
|
198
|
+
warnings: List of validation warnings
|
199
|
+
metadata: Additional validation metadata
|
200
|
+
"""
|
201
|
+
|
202
|
+
is_valid: bool = Field(..., description="Whether validation was successful")
|
203
|
+
status: ValidationStatus = Field(..., description="Validation status")
|
204
|
+
field_name: Optional[str] = Field(
|
205
|
+
default=None, description="Name of the field being validated"
|
206
|
+
)
|
207
|
+
value: Optional[Any] = Field(default=None, description="Value that was validated")
|
208
|
+
error_code: Optional[int] = Field(
|
209
|
+
default=None, description="Error code if validation failed"
|
210
|
+
)
|
211
|
+
error_message: Optional[str] = Field(
|
212
|
+
default=None, description="Human-readable error message"
|
213
|
+
)
|
214
|
+
warnings: List[str] = Field(
|
215
|
+
default_factory=list, description="List of validation warnings"
|
216
|
+
)
|
217
|
+
metadata: Dict[str, Any] = Field(
|
218
|
+
default_factory=dict, description="Additional validation metadata"
|
219
|
+
)
|
220
|
+
granted_permissions: List[str] = Field(
|
221
|
+
default_factory=list, description="List of granted permissions"
|
222
|
+
)
|
223
|
+
denied_permissions: List[str] = Field(
|
224
|
+
default_factory=list, description="List of denied permissions"
|
225
|
+
)
|
226
|
+
|
227
|
+
@model_validator(mode="after")
|
228
|
+
def validate_validation_result(self):
|
229
|
+
"""Validate validation result consistency."""
|
230
|
+
if self.is_valid and self.status != ValidationStatus.VALID:
|
231
|
+
raise ValueError("Valid validation must have VALID status")
|
232
|
+
|
233
|
+
if not self.is_valid and self.status == ValidationStatus.VALID:
|
234
|
+
raise ValueError("Invalid validation cannot have VALID status")
|
235
|
+
|
236
|
+
if self.error_code is not None and self.is_valid:
|
237
|
+
raise ValueError("Valid validation cannot have error code")
|
238
|
+
|
239
|
+
if self.error_message is not None and self.is_valid:
|
240
|
+
raise ValueError("Valid validation cannot have error message")
|
241
|
+
|
242
|
+
return self
|
243
|
+
|
244
|
+
|
245
|
+
class CertificateInfo(BaseModel):
|
246
|
+
"""
|
247
|
+
Certificate Information Model
|
248
|
+
|
249
|
+
This model represents detailed information about a certificate,
|
250
|
+
including subject, issuer, validity dates, and extensions.
|
251
|
+
|
252
|
+
Attributes:
|
253
|
+
subject: Certificate subject (CN, O, OU, etc.)
|
254
|
+
issuer: Certificate issuer information
|
255
|
+
serial_number: Certificate serial number
|
256
|
+
not_before: Certificate validity start date
|
257
|
+
not_after: Certificate validity end date
|
258
|
+
certificate_type: Type of certificate
|
259
|
+
key_size: RSA key size in bits
|
260
|
+
signature_algorithm: Signature algorithm used
|
261
|
+
subject_alt_names: List of subject alternative names
|
262
|
+
key_usage: Key usage extensions
|
263
|
+
extended_key_usage: Extended key usage extensions
|
264
|
+
is_ca: Whether certificate is a CA certificate
|
265
|
+
path_length: Maximum path length constraint
|
266
|
+
roles: Roles extracted from certificate
|
267
|
+
permissions: Permissions extracted from certificate
|
268
|
+
certificate_path: Path to certificate file
|
269
|
+
fingerprint_sha1: SHA1 fingerprint
|
270
|
+
fingerprint_sha256: SHA256 fingerprint
|
271
|
+
"""
|
272
|
+
|
273
|
+
subject: Dict[str, str] = Field(..., description="Certificate subject information")
|
274
|
+
issuer: Dict[str, str] = Field(..., description="Certificate issuer information")
|
275
|
+
serial_number: str = Field(..., description="Certificate serial number")
|
276
|
+
not_before: datetime = Field(..., description="Certificate validity start date")
|
277
|
+
not_after: datetime = Field(..., description="Certificate validity end date")
|
278
|
+
certificate_type: CertificateType = Field(..., description="Type of certificate")
|
279
|
+
key_size: int = Field(..., description="RSA key size in bits")
|
280
|
+
signature_algorithm: str = Field(..., description="Signature algorithm used")
|
281
|
+
subject_alt_names: List[str] = Field(
|
282
|
+
default_factory=list, description="List of subject alternative names"
|
283
|
+
)
|
284
|
+
key_usage: List[str] = Field(
|
285
|
+
default_factory=list, description="Key usage extensions"
|
286
|
+
)
|
287
|
+
extended_key_usage: List[str] = Field(
|
288
|
+
default_factory=list, description="Extended key usage extensions"
|
289
|
+
)
|
290
|
+
is_ca: bool = Field(
|
291
|
+
default=False, description="Whether certificate is a CA certificate"
|
292
|
+
)
|
293
|
+
path_length: Optional[int] = Field(
|
294
|
+
default=None, description="Maximum path length constraint"
|
295
|
+
)
|
296
|
+
roles: List[str] = Field(
|
297
|
+
default_factory=list, description="Roles extracted from certificate"
|
298
|
+
)
|
299
|
+
permissions: List[str] = Field(
|
300
|
+
default_factory=list, description="Permissions extracted from certificate"
|
301
|
+
)
|
302
|
+
certificate_path: Optional[str] = Field(
|
303
|
+
default=None, description="Path to certificate file"
|
304
|
+
)
|
305
|
+
fingerprint_sha1: Optional[str] = Field(
|
306
|
+
default=None, description="SHA1 fingerprint"
|
307
|
+
)
|
308
|
+
fingerprint_sha256: Optional[str] = Field(
|
309
|
+
default=None, description="SHA256 fingerprint"
|
310
|
+
)
|
311
|
+
|
312
|
+
@field_validator("key_size")
|
313
|
+
@classmethod
|
314
|
+
def validate_key_size(cls, v):
|
315
|
+
"""Validate key size."""
|
316
|
+
if v < 512 or v > 8192:
|
317
|
+
raise ValueError("Key size must be between 512 and 8192 bits")
|
318
|
+
return v
|
319
|
+
|
320
|
+
@property
|
321
|
+
def is_expired(self) -> bool:
|
322
|
+
"""Check if certificate is expired."""
|
323
|
+
now = datetime.now(timezone.utc)
|
324
|
+
# Ensure not_after has timezone info
|
325
|
+
not_after = self.not_after.replace(tzinfo=timezone.utc) if self.not_after.tzinfo is None else self.not_after
|
326
|
+
return now > not_after
|
327
|
+
|
328
|
+
@property
|
329
|
+
def expires_soon(self) -> bool:
|
330
|
+
"""Check if certificate expires soon (within 30 days)."""
|
331
|
+
now = datetime.now(timezone.utc)
|
332
|
+
# Ensure not_after has timezone info
|
333
|
+
not_after = self.not_after.replace(tzinfo=timezone.utc) if self.not_after.tzinfo is None else self.not_after
|
334
|
+
return now + timedelta(days=30) > not_after
|
335
|
+
|
336
|
+
@property
|
337
|
+
def days_until_expiry(self) -> int:
|
338
|
+
"""Calculate days until certificate expiry."""
|
339
|
+
if self.is_expired:
|
340
|
+
return 0
|
341
|
+
now = datetime.now(timezone.utc)
|
342
|
+
# Ensure not_after has timezone info
|
343
|
+
not_after = self.not_after.replace(tzinfo=timezone.utc) if self.not_after.tzinfo is None else self.not_after
|
344
|
+
delta = not_after - now
|
345
|
+
return delta.days
|
346
|
+
|
347
|
+
@property
|
348
|
+
def common_name(self) -> Optional[str]:
|
349
|
+
"""Get certificate common name."""
|
350
|
+
return self.subject.get("CN")
|
351
|
+
|
352
|
+
@property
|
353
|
+
def organization(self) -> Optional[str]:
|
354
|
+
"""Get certificate organization."""
|
355
|
+
return self.subject.get("O")
|
356
|
+
|
357
|
+
@property
|
358
|
+
def valid(self) -> bool:
|
359
|
+
"""Check if certificate is valid (not expired)."""
|
360
|
+
return not self.is_expired
|
361
|
+
|
362
|
+
@property
|
363
|
+
def revoked(self) -> bool:
|
364
|
+
"""Check if certificate is revoked."""
|
365
|
+
# This would typically check against a CRL or OCSP
|
366
|
+
# For now, return False as default
|
367
|
+
return False
|
368
|
+
|
369
|
+
|
370
|
+
class CertificatePair(BaseModel):
|
371
|
+
"""
|
372
|
+
Certificate and Key Pair Model
|
373
|
+
|
374
|
+
This model represents a certificate and its corresponding private key,
|
375
|
+
including file paths and metadata.
|
376
|
+
|
377
|
+
Attributes:
|
378
|
+
certificate_path: Path to certificate file
|
379
|
+
private_key_path: Path to private key file
|
380
|
+
certificate_pem: Certificate content in PEM format
|
381
|
+
private_key_pem: Private key content in PEM format
|
382
|
+
serial_number: Certificate serial number
|
383
|
+
common_name: Certificate common name
|
384
|
+
organization: Certificate organization
|
385
|
+
not_before: Certificate validity start date
|
386
|
+
not_after: Certificate validity end date
|
387
|
+
certificate_type: Type of certificate
|
388
|
+
key_size: RSA key size in bits
|
389
|
+
roles: Roles extracted from certificate
|
390
|
+
permissions: Permissions extracted from certificate
|
391
|
+
metadata: Additional certificate metadata
|
392
|
+
"""
|
393
|
+
|
394
|
+
certificate_path: str = Field(..., description="Path to certificate file")
|
395
|
+
private_key_path: str = Field(..., description="Path to private key file")
|
396
|
+
certificate_pem: str = Field(..., description="Certificate content in PEM format")
|
397
|
+
private_key_pem: str = Field(..., description="Private key content in PEM format")
|
398
|
+
serial_number: str = Field(..., description="Certificate serial number")
|
399
|
+
common_name: str = Field(..., description="Certificate common name")
|
400
|
+
organization: str = Field(..., description="Certificate organization")
|
401
|
+
not_before: datetime = Field(..., description="Certificate validity start date")
|
402
|
+
not_after: datetime = Field(..., description="Certificate validity end date")
|
403
|
+
certificate_type: CertificateType = Field(..., description="Type of certificate")
|
404
|
+
key_size: int = Field(..., description="RSA key size in bits")
|
405
|
+
roles: List[str] = Field(
|
406
|
+
default_factory=list, description="Roles extracted from certificate"
|
407
|
+
)
|
408
|
+
permissions: List[str] = Field(
|
409
|
+
default_factory=list, description="Permissions extracted from certificate"
|
410
|
+
)
|
411
|
+
metadata: Dict[str, Any] = Field(
|
412
|
+
default_factory=dict, description="Additional certificate metadata"
|
413
|
+
)
|
414
|
+
|
415
|
+
@field_validator("certificate_pem")
|
416
|
+
@classmethod
|
417
|
+
def validate_certificate_pem(cls, v):
|
418
|
+
"""Validate certificate PEM format."""
|
419
|
+
if not v.startswith("-----BEGIN CERTIFICATE-----"):
|
420
|
+
raise ValueError("Invalid certificate PEM format")
|
421
|
+
if not v.strip().endswith("-----END CERTIFICATE-----"):
|
422
|
+
raise ValueError("Invalid certificate PEM format")
|
423
|
+
return v
|
424
|
+
|
425
|
+
@field_validator("private_key_pem")
|
426
|
+
@classmethod
|
427
|
+
def validate_private_key_pem(cls, v):
|
428
|
+
"""Validate private key PEM format."""
|
429
|
+
if not v.startswith("-----BEGIN PRIVATE KEY-----") and not v.startswith(
|
430
|
+
"-----BEGIN RSA PRIVATE KEY-----"
|
431
|
+
):
|
432
|
+
raise ValueError("Invalid private key PEM format")
|
433
|
+
if not v.strip().endswith("-----END PRIVATE KEY-----") and not v.strip().endswith(
|
434
|
+
"-----END RSA PRIVATE KEY-----"
|
435
|
+
):
|
436
|
+
raise ValueError("Invalid private key PEM format")
|
437
|
+
return v
|
438
|
+
|
439
|
+
@property
|
440
|
+
def is_expired(self) -> bool:
|
441
|
+
"""Check if certificate is expired."""
|
442
|
+
return datetime.now(timezone.utc) > self.not_after
|
443
|
+
|
444
|
+
@property
|
445
|
+
def expires_soon(self) -> bool:
|
446
|
+
"""Check if certificate expires soon (within 30 days)."""
|
447
|
+
return datetime.now(timezone.utc) + timedelta(days=30) > self.not_after
|
448
|
+
|
449
|
+
|
450
|
+
class RateLimitStatus(BaseModel):
|
451
|
+
"""
|
452
|
+
Rate Limiting Status Model
|
453
|
+
|
454
|
+
This model represents the current status of rate limiting for
|
455
|
+
a specific identifier (IP, user, etc.).
|
456
|
+
|
457
|
+
Attributes:
|
458
|
+
identifier: Rate limiting identifier (IP, user ID, etc.)
|
459
|
+
current_count: Current request count in window
|
460
|
+
limit: Maximum allowed requests in window
|
461
|
+
window_start: Start time of current window
|
462
|
+
window_end: End time of current window
|
463
|
+
is_exceeded: Whether rate limit is exceeded
|
464
|
+
remaining_requests: Number of remaining requests
|
465
|
+
reset_time: Time when rate limit resets
|
466
|
+
window_size_seconds: Size of rate limiting window
|
467
|
+
"""
|
468
|
+
|
469
|
+
identifier: str = Field(..., description="Rate limiting identifier")
|
470
|
+
current_count: int = Field(..., ge=0, description="Current request count in window")
|
471
|
+
limit: int = Field(..., ge=1, description="Maximum allowed requests in window")
|
472
|
+
window_start: datetime = Field(..., description="Start time of current window")
|
473
|
+
window_end: datetime = Field(..., description="End time of current window")
|
474
|
+
is_exceeded: bool = Field(..., description="Whether rate limit is exceeded")
|
475
|
+
remaining_requests: int = Field(
|
476
|
+
..., ge=0, description="Number of remaining requests"
|
477
|
+
)
|
478
|
+
reset_time: datetime = Field(..., description="Time when rate limit resets")
|
479
|
+
window_size_seconds: int = Field(
|
480
|
+
..., ge=1, description="Size of rate limiting window"
|
481
|
+
)
|
482
|
+
|
483
|
+
@model_validator(mode="after")
|
484
|
+
def validate_rate_limit_status(self):
|
485
|
+
"""Validate rate limit status consistency."""
|
486
|
+
if self.current_count > self.limit and not self.is_exceeded:
|
487
|
+
raise ValueError("Rate limit exceeded but is_exceeded is False")
|
488
|
+
|
489
|
+
if self.current_count <= self.limit and self.is_exceeded:
|
490
|
+
raise ValueError("Rate limit not exceeded but is_exceeded is True")
|
491
|
+
|
492
|
+
if self.remaining_requests < 0:
|
493
|
+
raise ValueError("Remaining requests cannot be negative")
|
494
|
+
|
495
|
+
if self.remaining_requests > self.limit:
|
496
|
+
raise ValueError("Remaining requests cannot exceed limit")
|
497
|
+
|
498
|
+
return self
|
499
|
+
|
500
|
+
@property
|
501
|
+
def seconds_until_reset(self) -> int:
|
502
|
+
"""Calculate seconds until rate limit resets."""
|
503
|
+
delta = self.reset_time - datetime.now(timezone.utc)
|
504
|
+
return max(0, int(delta.total_seconds()))
|
505
|
+
|
506
|
+
@property
|
507
|
+
def utilization_percentage(self) -> float:
|
508
|
+
"""Calculate rate limit utilization percentage."""
|
509
|
+
if self.limit == 0:
|
510
|
+
return 0.0
|
511
|
+
return (self.current_count / self.limit) * 100.0
|
512
|
+
|
513
|
+
|
514
|
+
class UserCredentials(BaseModel):
|
515
|
+
"""
|
516
|
+
User Credentials Model
|
517
|
+
|
518
|
+
This model represents user credentials for authentication,
|
519
|
+
supporting multiple authentication methods.
|
520
|
+
|
521
|
+
Attributes:
|
522
|
+
username: User username
|
523
|
+
password: User password (hashed)
|
524
|
+
api_key: User API key
|
525
|
+
certificate_path: Path to user certificate
|
526
|
+
roles: List of user roles
|
527
|
+
permissions: Set of user permissions
|
528
|
+
is_active: Whether user account is active
|
529
|
+
created_at: When user account was created
|
530
|
+
last_login: When user last logged in
|
531
|
+
metadata: Additional user metadata
|
532
|
+
"""
|
533
|
+
|
534
|
+
username: str = Field(..., description="User username")
|
535
|
+
password: Optional[str] = Field(default=None, description="User password (hashed)")
|
536
|
+
api_key: Optional[str] = Field(default=None, description="User API key")
|
537
|
+
certificate_path: Optional[str] = Field(
|
538
|
+
default=None, description="Path to user certificate"
|
539
|
+
)
|
540
|
+
roles: List[str] = Field(default_factory=list, description="List of user roles")
|
541
|
+
permissions: Set[str] = Field(
|
542
|
+
default_factory=set, description="Set of user permissions"
|
543
|
+
)
|
544
|
+
is_active: bool = Field(default=True, description="Whether user account is active")
|
545
|
+
created_at: datetime = Field(
|
546
|
+
default_factory=datetime.utcnow, description="When user account was created"
|
547
|
+
)
|
548
|
+
last_login: Optional[datetime] = Field(
|
549
|
+
default=None, description="When user last logged in"
|
550
|
+
)
|
551
|
+
metadata: Dict[str, Any] = Field(
|
552
|
+
default_factory=dict, description="Additional user metadata"
|
553
|
+
)
|
554
|
+
|
555
|
+
@field_validator("username")
|
556
|
+
@classmethod
|
557
|
+
def validate_username(cls, v):
|
558
|
+
"""Validate username format."""
|
559
|
+
if len(v.strip()) == 0:
|
560
|
+
raise ValueError("Username cannot be empty")
|
561
|
+
if len(v) > 100:
|
562
|
+
raise ValueError("Username too long (max 100 characters)")
|
563
|
+
return v.strip()
|
564
|
+
|
565
|
+
@property
|
566
|
+
def has_password(self) -> bool:
|
567
|
+
"""Check if user has password set."""
|
568
|
+
return self.password is not None and len(self.password) > 0
|
569
|
+
|
570
|
+
@property
|
571
|
+
def has_api_key(self) -> bool:
|
572
|
+
"""Check if user has API key set."""
|
573
|
+
return self.api_key is not None and len(self.api_key) > 0
|
574
|
+
|
575
|
+
@property
|
576
|
+
def has_certificate(self) -> bool:
|
577
|
+
"""Check if user has certificate set."""
|
578
|
+
return self.certificate_path is not None and len(self.certificate_path) > 0
|
579
|
+
|
580
|
+
|
581
|
+
class RolePermissions(BaseModel):
|
582
|
+
"""
|
583
|
+
Role Permissions Mapping Model
|
584
|
+
|
585
|
+
This model represents the mapping between roles and their
|
586
|
+
associated permissions, including role hierarchy.
|
587
|
+
|
588
|
+
Attributes:
|
589
|
+
role_name: Name of the role
|
590
|
+
permissions: Set of permissions for this role
|
591
|
+
parent_roles: List of parent roles
|
592
|
+
child_roles: List of child roles
|
593
|
+
description: Role description
|
594
|
+
is_active: Whether role is active
|
595
|
+
created_at: When role was created
|
596
|
+
metadata: Additional role metadata
|
597
|
+
"""
|
598
|
+
|
599
|
+
role_name: str = Field(..., description="Name of the role")
|
600
|
+
permissions: Set[str] = Field(
|
601
|
+
default_factory=set, description="Set of permissions for this role"
|
602
|
+
)
|
603
|
+
parent_roles: List[str] = Field(
|
604
|
+
default_factory=list, description="List of parent roles"
|
605
|
+
)
|
606
|
+
child_roles: List[str] = Field(
|
607
|
+
default_factory=list, description="List of child roles"
|
608
|
+
)
|
609
|
+
description: Optional[str] = Field(default=None, description="Role description")
|
610
|
+
is_active: bool = Field(default=True, description="Whether role is active")
|
611
|
+
created_at: datetime = Field(
|
612
|
+
default_factory=datetime.utcnow, description="When role was created"
|
613
|
+
)
|
614
|
+
metadata: Dict[str, Any] = Field(
|
615
|
+
default_factory=dict, description="Additional role metadata"
|
616
|
+
)
|
617
|
+
|
618
|
+
@field_validator("role_name")
|
619
|
+
@classmethod
|
620
|
+
def validate_role_name(cls, v):
|
621
|
+
"""Validate role name format."""
|
622
|
+
if len(v.strip()) == 0:
|
623
|
+
raise ValueError("Role name cannot be empty")
|
624
|
+
if len(v) > 100:
|
625
|
+
raise ValueError("Role name too long (max 100 characters)")
|
626
|
+
return v.strip()
|
627
|
+
|
628
|
+
@property
|
629
|
+
def effective_permissions(self) -> Set[str]:
|
630
|
+
"""Get effective permissions including inherited permissions."""
|
631
|
+
# This would be calculated by traversing the role hierarchy
|
632
|
+
# For now, return direct permissions
|
633
|
+
return self.permissions.copy()
|
634
|
+
|
635
|
+
def has_permission(self, permission: str) -> bool:
|
636
|
+
"""Check if role has specific permission."""
|
637
|
+
return permission in self.effective_permissions
|
638
|
+
|
639
|
+
|
640
|
+
class CertificateChain(BaseModel):
|
641
|
+
"""
|
642
|
+
Certificate Chain Model
|
643
|
+
|
644
|
+
This model represents a complete certificate chain from
|
645
|
+
end-entity certificate to root CA certificate.
|
646
|
+
|
647
|
+
Attributes:
|
648
|
+
certificates: List of certificates in chain (end-entity first)
|
649
|
+
chain_length: Number of certificates in chain
|
650
|
+
is_valid: Whether certificate chain is valid
|
651
|
+
validation_errors: List of validation errors
|
652
|
+
root_ca: Root CA certificate information
|
653
|
+
intermediate_cas: List of intermediate CA certificates
|
654
|
+
end_entity: End-entity certificate information
|
655
|
+
trust_path: Trust path information
|
656
|
+
"""
|
657
|
+
|
658
|
+
certificates: List[CertificateInfo] = Field(
|
659
|
+
..., description="List of certificates in chain"
|
660
|
+
)
|
661
|
+
chain_length: int = Field(..., ge=1, description="Number of certificates in chain")
|
662
|
+
is_valid: bool = Field(..., description="Whether certificate chain is valid")
|
663
|
+
validation_errors: List[str] = Field(
|
664
|
+
default_factory=list, description="List of validation errors"
|
665
|
+
)
|
666
|
+
root_ca: Optional[CertificateInfo] = Field(
|
667
|
+
default=None, description="Root CA certificate information"
|
668
|
+
)
|
669
|
+
intermediate_cas: List[CertificateInfo] = Field(
|
670
|
+
default_factory=list, description="List of intermediate CA certificates"
|
671
|
+
)
|
672
|
+
end_entity: Optional[CertificateInfo] = Field(
|
673
|
+
default=None, description="End-entity certificate information"
|
674
|
+
)
|
675
|
+
trust_path: Dict[str, Any] = Field(
|
676
|
+
default_factory=dict, description="Trust path information"
|
677
|
+
)
|
678
|
+
|
679
|
+
@field_validator("chain_length")
|
680
|
+
@classmethod
|
681
|
+
def validate_chain_length(cls, v, info):
|
682
|
+
"""Validate chain length matches certificates list."""
|
683
|
+
certificates = info.data.get("certificates", [])
|
684
|
+
if v != len(certificates):
|
685
|
+
raise ValueError("Chain length must match number of certificates")
|
686
|
+
return v
|
687
|
+
|
688
|
+
@model_validator(mode="after")
|
689
|
+
def validate_certificate_chain(self):
|
690
|
+
"""Validate certificate chain consistency."""
|
691
|
+
if self.certificates and not self.end_entity:
|
692
|
+
self.end_entity = self.certificates[0]
|
693
|
+
|
694
|
+
if self.certificates and not self.root_ca:
|
695
|
+
self.root_ca = self.certificates[-1]
|
696
|
+
|
697
|
+
return self
|
698
|
+
|
699
|
+
@property
|
700
|
+
def has_intermediate_cas(self) -> bool:
|
701
|
+
"""Check if chain has intermediate CAs."""
|
702
|
+
return len(self.certificates) > 2
|
703
|
+
|
704
|
+
@property
|
705
|
+
def is_self_signed(self) -> bool:
|
706
|
+
"""Check if end-entity certificate is self-signed."""
|
707
|
+
if not self.end_entity or not self.root_ca:
|
708
|
+
return False
|
709
|
+
return self.end_entity.serial_number == self.root_ca.serial_number
|