Coverage for frappe_manager / ssl_manager / certificate.py: 53%

47 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-07-02 18:35 +0530

1""" 

2SSL Certificate models with support for multi-certificate management. 

3 

4Provides base SSLCertificate model and specialized CustomDomainCertificate 

5for CNAME delegation scenarios. 

6""" 

7 

8from datetime import datetime, timedelta 

9from pathlib import Path 

10 

11from pydantic import BaseModel 

12 

13from frappe_manager import SSL_RENEW_BEFORE_DAYS 

14from frappe_manager.ssl_manager import LETSENCRYPT_PREFERRED_CHALLENGE, SUPPORTED_SSL_TYPES 

15from frappe_manager.utils.helpers import get_certificate_expiry_date 

16 

17 

18class SSLCertificate(BaseModel): 

19 """ 

20 SSL certificate configuration and state. 

21 

22 Represents a single SSL certificate for a single domain, with support 

23 for system state tracking and computed properties. 

24 """ 

25 

26 # User configuration 

27 domain: str 

28 ssl_type: SUPPORTED_SSL_TYPES 

29 challenge_type: LETSENCRYPT_PREFERRED_CHALLENGE = LETSENCRYPT_PREFERRED_CHALLENGE.http01 

30 enabled: bool = True 

31 acme_client: str = "acme.sh" # ACME client for certificate issuance (acme.sh) 

32 hsts: str = "off" 

33 

34 # System state fields (populated after certificate issuance) 

35 cert_path: Path | None = None 

36 key_path: Path | None = None 

37 issued_date: datetime | None = None 

38 last_renewal_attempt: datetime | None = None 

39 status: str = "pending" # pending, active, expiring_soon, expired, error 

40 

41 # Fields to exclude when serializing to TOML 

42 toml_exclude: set | None = {"domain", "toml_exclude"} 

43 

44 @property 

45 def expiry_date(self) -> datetime | None: 

46 """ 

47 Compute expiry date from certificate file at runtime. 

48 

49 This ensures the expiry date is always accurate and in sync with the 

50 actual certificate file, avoiding stale data in config. 

51 

52 Returns: 

53 Certificate expiry datetime, or None if cert doesn't exist 

54 """ 

55 if self.cert_path and self.cert_path.exists(): 

56 try: 

57 return get_certificate_expiry_date(self.cert_path) 

58 except Exception: 

59 # If we can't read the cert, return None 

60 return None 

61 return None 

62 

63 @property 

64 def needs_renewal(self) -> bool: 

65 """ 

66 Check if certificate needs renewal based on expiry date. 

67 

68 A certificate needs renewal if it will expire within 

69 SSL_RENEW_BEFORE_DAYS days. 

70 

71 Returns: 

72 True if certificate should be renewed, False otherwise 

73 """ 

74 expiry = self.expiry_date 

75 if not expiry: 

76 return False 

77 

78 renewal_threshold = expiry - timedelta(days=SSL_RENEW_BEFORE_DAYS) 

79 now = datetime.now() 

80 

81 # Handle timezone-aware dates 

82 if expiry.tzinfo: 

83 now = now.replace(tzinfo=expiry.tzinfo) 

84 

85 return now >= renewal_threshold 

86 

87 @property 

88 def days_until_expiry(self) -> int | None: 

89 """ 

90 Calculate days until certificate expires. 

91 

92 Returns: 

93 Number of days until expiry, or None if cert doesn't exist 

94 """ 

95 expiry = self.expiry_date 

96 if not expiry: 

97 return None 

98 

99 now = datetime.now() 

100 if expiry.tzinfo: 

101 now = now.replace(tzinfo=expiry.tzinfo) 

102 

103 delta = expiry - now 

104 return delta.days