88external relation.
99"""
1010import base64
11+ import json
1112import logging
1213import re
1314import socket
14- from typing import List , Optional , Tuple
15+ from typing import Optional , Tuple
1516
1617from charms .tls_certificates_interface .v3 .tls_certificates import (
1718 CertificateAvailableEvent ,
2021 generate_csr ,
2122 generate_private_key ,
2223)
24+ from cryptography import x509
25+ from cryptography .hazmat .backends import default_backend
2326from ops .charm import ActionEvent , RelationBrokenEvent , RelationJoinedEvent
2427from ops .framework import Object
2528from ops .model import ActiveStatus , MaintenanceStatus , WaitingStatus
2831
2932UNIT_SCOPE = Config .Relations .UNIT_SCOPE
3033Scopes = Config .Relations .Scopes
31-
34+ SANS_DNS_KEY = "sans_dns"
35+ SANS_IPS_KEY = "sans_ips"
3236
3337# The unique Charmhub library identifier, never change it
3438LIBID = "e02a50f0795e4dd292f58e93b4f493dd"
3842
3943# Increment this PATCH version before using `charmcraft publish-lib` or reset
4044# to 0 if you are raising the major API version
41- LIBPATCH = 2
45+ LIBPATCH = 5
46+
47+ WAIT_CERT_UPDATE = "wait-cert-updated"
4248
4349logger = logging .getLogger (__name__ )
4450
@@ -104,12 +110,13 @@ def request_certificate(
104110 else :
105111 key = self ._parse_tls_file (param )
106112
113+ sans = self .get_new_sans ()
107114 csr = generate_csr (
108115 private_key = key ,
109116 subject = self ._get_subject_name (),
110117 organization = self ._get_subject_name (),
111- sans = self . _get_sans () ,
112- sans_ip = [ str ( self . charm . model . get_binding ( self . peer_relation ). network . bind_address ) ],
118+ sans = sans [ SANS_DNS_KEY ] ,
119+ sans_ip = sans [ SANS_IPS_KEY ],
113120 )
114121 self .set_tls_secret (internal , Config .TLS .SECRET_KEY_LABEL , key .decode ("utf-8" ))
115122 self .set_tls_secret (internal , Config .TLS .SECRET_CSR_LABEL , csr .decode ("utf-8" ))
@@ -118,9 +125,8 @@ def request_certificate(
118125 label = "int" if internal else "ext"
119126 self .charm .unit_peer_data [f"{ label } _certs_subject" ] = self ._get_subject_name ()
120127 self .charm .unit_peer_data [f"{ label } _certs_subject" ] = self ._get_subject_name ()
121-
122- if self .charm .model .get_relation (Config .TLS .TLS_PEER_RELATION ):
123- self .certs .request_certificate_creation (certificate_signing_request = csr )
128+ self .certs .request_certificate_creation (certificate_signing_request = csr )
129+ self .set_waiting_for_cert_to_update (internal = internal , waiting = True )
124130
125131 @staticmethod
126132 def _parse_tls_file (raw_content : str ) -> bytes :
@@ -158,12 +164,18 @@ def _on_tls_relation_joined(self, event: RelationJoinedEvent) -> None:
158164
159165 def _on_tls_relation_broken (self , event : RelationBrokenEvent ) -> None :
160166 """Disable TLS when TLS relation broken."""
161- logger .debug ("Disabling external and internal TLS for unit: %s" , self .charm .unit .name )
167+ if not self .charm .db_initialised :
168+ logger .info ("Deferring %s. db is not initialised." , str (type (event )))
169+ event .defer ()
170+ return
171+
162172 if self .charm .upgrade_in_progress :
163173 logger .warning (
164174 "Disabling TLS is not supported during an upgrade. The charm may be in a broken, unrecoverable state."
165175 )
166176
177+ logger .debug ("Disabling external and internal TLS for unit: %s" , self .charm .unit .name )
178+
167179 for internal in [True , False ]:
168180 self .set_tls_secret (internal , Config .TLS .SECRET_CA_LABEL , None )
169181 self .set_tls_secret (internal , Config .TLS .SECRET_CERT_LABEL , None )
@@ -188,6 +200,11 @@ def _on_certificate_available(self, event: CertificateAvailableEvent) -> None:
188200 event .defer ()
189201 return
190202
203+ if not self .charm .db_initialised :
204+ logger .info ("Deferring %s. db is not initialised." , str (type (event )))
205+ event .defer ()
206+ return
207+
191208 int_csr = self .get_tls_secret (internal = True , label_name = Config .TLS .SECRET_CSR_LABEL )
192209 ext_csr = self .get_tls_secret (internal = False , label_name = Config .TLS .SECRET_CSR_LABEL )
193210
@@ -208,12 +225,13 @@ def _on_certificate_available(self, event: CertificateAvailableEvent) -> None:
208225 )
209226 self .set_tls_secret (internal , Config .TLS .SECRET_CERT_LABEL , event .certificate )
210227 self .set_tls_secret (internal , Config .TLS .SECRET_CA_LABEL , event .ca )
228+ self .set_waiting_for_cert_to_update (internal = internal , waiting = False )
211229
212230 if self .charm .is_role (Config .Role .CONFIG_SERVER ) and internal :
213231 self .charm .cluster .update_ca_secret (new_ca = event .ca )
214232 self .charm .config_server .update_ca_secret (new_ca = event .ca )
215233
216- if self .waiting_for_certs ():
234+ if self .is_waiting_for_both_certs ():
217235 logger .debug (
218236 "Defer till both internal and external TLS certificates available to avoid second restart."
219237 )
@@ -235,7 +253,7 @@ def _on_certificate_available(self, event: CertificateAvailableEvent) -> None:
235253 # clear waiting status if db service is ready
236254 self .charm .status .set_and_share_status (ActiveStatus ())
237255
238- def waiting_for_certs (self ):
256+ def is_waiting_for_both_certs (self ) -> bool :
239257 """Returns a boolean indicating whether additional certs are needed."""
240258 if not self .get_tls_secret (internal = True , label_name = Config .TLS .SECRET_CERT_LABEL ):
241259 logger .debug ("Waiting for internal certificate." )
@@ -268,21 +286,25 @@ def _on_certificate_expiring(self, event: CertificateExpiringEvent) -> None:
268286 == self .get_tls_secret (internal = True , label_name = Config .TLS .SECRET_CERT_LABEL ).rstrip ()
269287 ):
270288 logger .debug ("The internal TLS certificate expiring." )
271-
272289 internal = True
273290 else :
274291 logger .error ("An unknown certificate expiring." )
275292 return
276293
277294 logger .debug ("Generating a new Certificate Signing Request." )
295+ self .request_new_certificates (internal )
296+
297+ def request_new_certificates (self , internal : bool ) -> None :
298+ """Requests the renewel of a new certificate."""
278299 key = self .get_tls_secret (internal , Config .TLS .SECRET_KEY_LABEL ).encode ("utf-8" )
279300 old_csr = self .get_tls_secret (internal , Config .TLS .SECRET_CSR_LABEL ).encode ("utf-8" )
301+ sans = self .get_new_sans ()
280302 new_csr = generate_csr (
281303 private_key = key ,
282304 subject = self ._get_subject_name (),
283305 organization = self ._get_subject_name (),
284- sans = self . _get_sans () ,
285- sans_ip = [ str ( self . charm . model . get_binding ( self . peer_relation ). network . bind_address ) ],
306+ sans = sans [ SANS_DNS_KEY ] ,
307+ sans_ip = sans [ SANS_IPS_KEY ],
286308 )
287309 logger .debug ("Requesting a certificate renewal." )
288310
@@ -292,21 +314,53 @@ def _on_certificate_expiring(self, event: CertificateExpiringEvent) -> None:
292314 )
293315
294316 self .set_tls_secret (internal , Config .TLS .SECRET_CSR_LABEL , new_csr .decode ("utf-8" ))
317+ self .set_waiting_for_cert_to_update (waiting = True , internal = internal )
295318
296- def _get_sans (self ) -> List [str ]:
319+ def get_new_sans (self ) -> dict [str , list [ str ] ]:
297320 """Create a list of DNS names for a MongoDB unit.
298321
299322 Returns:
300323 A list representing the hostnames of the MongoDB unit.
301324 """
302325 unit_id = self .charm .unit .name .split ("/" )[1 ]
303- return [
304- f"{ self .charm .app .name } -{ unit_id } " ,
305- socket .getfqdn (),
306- f"{ self .charm .app .name } -{ unit_id } .{ self .charm .app .name } -endpoints" ,
307- str (self .charm .model .get_binding (self .peer_relation ).network .bind_address ),
308- "localhost" ,
309- ]
326+
327+ sans = {
328+ SANS_DNS_KEY : [
329+ f"{ self .charm .app .name } -{ unit_id } " ,
330+ socket .getfqdn (),
331+ "localhost" ,
332+ f"{ self .charm .app .name } -{ unit_id } .{ self .charm .app .name } -endpoints" ,
333+ ],
334+ SANS_IPS_KEY : [
335+ str (self .charm .model .get_binding (self .peer_relation ).network .bind_address )
336+ ],
337+ }
338+
339+ if self .charm .is_role (Config .Role .MONGOS ) and self .charm .is_external_client :
340+ sans [SANS_IPS_KEY ].append (
341+ self .charm .get_ext_mongos_host (self .charm .unit , incl_port = False )
342+ )
343+
344+ return sans
345+
346+ def get_current_sans (self , internal : bool ) -> dict [str , list [str ]] | None :
347+ """Gets the current SANs for the unit cert."""
348+ # if unit has no certificates do not proceed.
349+ if not self .is_tls_enabled (internal = internal ):
350+ return
351+
352+ pem_file = self .get_tls_secret (internal , Config .TLS .SECRET_CERT_LABEL )
353+
354+ try :
355+ cert = x509 .load_pem_x509_certificate (pem_file .encode (), default_backend ())
356+ sans = cert .extensions .get_extension_for_class (x509 .SubjectAlternativeName ).value
357+ sans_ip = [str (san ) for san in sans .get_values_for_type (x509 .IPAddress )]
358+ sans_dns = [str (san ) for san in sans .get_values_for_type (x509 .DNSName )]
359+ except x509 .ExtensionNotFound :
360+ sans_ip = []
361+ sans_dns = []
362+
363+ return {SANS_IPS_KEY : sorted (sans_ip ), SANS_DNS_KEY : sorted (sans_dns )}
310364
311365 def get_tls_files (self , internal : bool ) -> Tuple [Optional [str ], Optional [str ]]:
312366 """Prepare TLS files in special MongoDB way.
@@ -356,3 +410,23 @@ def _get_subject_name(self) -> str:
356410 return self .charm .get_config_server_name () or self .charm .app .name
357411
358412 return self .charm .app .name
413+
414+ def is_set_waiting_for_cert_to_update (
415+ self ,
416+ internal : bool = False ,
417+ ) -> bool :
418+ """Returns True if we are waiting for a cert to update."""
419+ scope = "int" if internal else "ext"
420+ label_name = f"{ scope } -{ WAIT_CERT_UPDATE } "
421+
422+ return json .loads (self .charm .unit_peer_data .get (label_name , "false" ))
423+
424+ def set_waiting_for_cert_to_update (
425+ self ,
426+ waiting : bool ,
427+ internal : bool ,
428+ ) -> None :
429+ """Sets a boolean indicator, for whether or not we are waiting for a cert to update."""
430+ scope = "int" if internal else "ext"
431+ label_name = f"{ scope } -{ WAIT_CERT_UPDATE } "
432+ self .charm .unit_peer_data [label_name ] = json .dumps (waiting )
0 commit comments