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.
Files changed (76) hide show
  1. mcp_security_framework/__init__.py +96 -0
  2. mcp_security_framework/cli/__init__.py +18 -0
  3. mcp_security_framework/cli/cert_cli.py +511 -0
  4. mcp_security_framework/cli/security_cli.py +791 -0
  5. mcp_security_framework/constants.py +209 -0
  6. mcp_security_framework/core/__init__.py +61 -0
  7. mcp_security_framework/core/auth_manager.py +1011 -0
  8. mcp_security_framework/core/cert_manager.py +1663 -0
  9. mcp_security_framework/core/permission_manager.py +735 -0
  10. mcp_security_framework/core/rate_limiter.py +602 -0
  11. mcp_security_framework/core/security_manager.py +943 -0
  12. mcp_security_framework/core/ssl_manager.py +735 -0
  13. mcp_security_framework/examples/__init__.py +75 -0
  14. mcp_security_framework/examples/django_example.py +615 -0
  15. mcp_security_framework/examples/fastapi_example.py +472 -0
  16. mcp_security_framework/examples/flask_example.py +506 -0
  17. mcp_security_framework/examples/gateway_example.py +803 -0
  18. mcp_security_framework/examples/microservice_example.py +690 -0
  19. mcp_security_framework/examples/standalone_example.py +576 -0
  20. mcp_security_framework/middleware/__init__.py +250 -0
  21. mcp_security_framework/middleware/auth_middleware.py +292 -0
  22. mcp_security_framework/middleware/fastapi_auth_middleware.py +447 -0
  23. mcp_security_framework/middleware/fastapi_middleware.py +757 -0
  24. mcp_security_framework/middleware/flask_auth_middleware.py +465 -0
  25. mcp_security_framework/middleware/flask_middleware.py +591 -0
  26. mcp_security_framework/middleware/mtls_middleware.py +439 -0
  27. mcp_security_framework/middleware/rate_limit_middleware.py +403 -0
  28. mcp_security_framework/middleware/security_middleware.py +507 -0
  29. mcp_security_framework/schemas/__init__.py +109 -0
  30. mcp_security_framework/schemas/config.py +694 -0
  31. mcp_security_framework/schemas/models.py +709 -0
  32. mcp_security_framework/schemas/responses.py +686 -0
  33. mcp_security_framework/tests/__init__.py +0 -0
  34. mcp_security_framework/utils/__init__.py +121 -0
  35. mcp_security_framework/utils/cert_utils.py +525 -0
  36. mcp_security_framework/utils/crypto_utils.py +475 -0
  37. mcp_security_framework/utils/validation_utils.py +571 -0
  38. mcp_security_framework-0.1.0.dist-info/METADATA +411 -0
  39. mcp_security_framework-0.1.0.dist-info/RECORD +76 -0
  40. mcp_security_framework-0.1.0.dist-info/WHEEL +5 -0
  41. mcp_security_framework-0.1.0.dist-info/entry_points.txt +3 -0
  42. mcp_security_framework-0.1.0.dist-info/top_level.txt +2 -0
  43. tests/__init__.py +0 -0
  44. tests/test_cli/__init__.py +0 -0
  45. tests/test_cli/test_cert_cli.py +379 -0
  46. tests/test_cli/test_security_cli.py +657 -0
  47. tests/test_core/__init__.py +0 -0
  48. tests/test_core/test_auth_manager.py +582 -0
  49. tests/test_core/test_cert_manager.py +795 -0
  50. tests/test_core/test_permission_manager.py +395 -0
  51. tests/test_core/test_rate_limiter.py +626 -0
  52. tests/test_core/test_security_manager.py +841 -0
  53. tests/test_core/test_ssl_manager.py +532 -0
  54. tests/test_examples/__init__.py +8 -0
  55. tests/test_examples/test_fastapi_example.py +264 -0
  56. tests/test_examples/test_flask_example.py +238 -0
  57. tests/test_examples/test_standalone_example.py +292 -0
  58. tests/test_integration/__init__.py +0 -0
  59. tests/test_integration/test_auth_flow.py +502 -0
  60. tests/test_integration/test_certificate_flow.py +527 -0
  61. tests/test_integration/test_fastapi_integration.py +341 -0
  62. tests/test_integration/test_flask_integration.py +398 -0
  63. tests/test_integration/test_standalone_integration.py +493 -0
  64. tests/test_middleware/__init__.py +0 -0
  65. tests/test_middleware/test_fastapi_middleware.py +523 -0
  66. tests/test_middleware/test_flask_middleware.py +582 -0
  67. tests/test_middleware/test_security_middleware.py +493 -0
  68. tests/test_schemas/__init__.py +0 -0
  69. tests/test_schemas/test_config.py +811 -0
  70. tests/test_schemas/test_models.py +879 -0
  71. tests/test_schemas/test_responses.py +1054 -0
  72. tests/test_schemas/test_serialization.py +493 -0
  73. tests/test_utils/__init__.py +0 -0
  74. tests/test_utils/test_cert_utils.py +510 -0
  75. tests/test_utils/test_crypto_utils.py +603 -0
  76. 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