From 188369c358e6892c790680e408ddba2b228bfa13 Mon Sep 17 00:00:00 2001 From: guillaume Date: Thu, 2 Nov 2023 10:19:47 +0200 Subject: [PATCH] feat: Handles outstanding cerfificate requests in main hook --- src/charm.py | 40 +++++++++++++++------- tests/unit/test_charm.py | 73 +++++++++++++++++++++++++++++++++------- 2 files changed, 87 insertions(+), 26 deletions(-) diff --git a/src/charm.py b/src/charm.py index c0a318b..d7d5665 100755 --- a/src/charm.py +++ b/src/charm.py @@ -22,7 +22,7 @@ ) from ops.charm import ActionEvent, CharmBase, EventBase, RelationJoinedEvent from ops.main import main -from ops.model import ActiveStatus, BlockedStatus, SecretNotFoundError, WaitingStatus +from ops.model import ActiveStatus, BlockedStatus, SecretNotFoundError logger = logging.getLogger(__name__) @@ -164,8 +164,19 @@ def _configure_ca(self, event: EventBase) -> None: self.tls_certificates.revoke_all_certificates() logger.info("Revoked all previously issued certificates.") self._send_ca_cert() + self._process_outstanding_certificate_requests() self.unit.status = ActiveStatus() + def _process_outstanding_certificate_requests(self) -> None: + """Process outstanding certificate requests.""" + for relation in self.tls_certificates.get_requirer_csrs_with_no_certs(): + for request in relation["unit_csrs"]: + self._generate_self_signed_certificate( + csr=request["certificate_signing_request"], + is_ca=request["is_ca"], + relation_id=relation["relation_id"], + ) + def _invalid_configs(self) -> list[str]: """Returns list of invalid configurations. @@ -189,34 +200,37 @@ def _on_certificate_creation_request(self, event: CertificateCreationRequestEven """ if not self.unit.is_leader(): return - if invalid_configs := self._invalid_configs(): - self.unit.status = BlockedStatus( - f"The following configuration values are not valid: {invalid_configs}" - ) - event.defer() + if self._invalid_configs(): + logger.warning("Invalid configuration. Certificate cannot be generated.") return if not self._root_certificate_is_stored: - self.unit.status = WaitingStatus("Root Certificate is not yet generated") - event.defer() + logger.warning( + "Root certificate is not yet generated. Certificate cannot be generated." + ) return + self._generate_self_signed_certificate( + csr=event.certificate_signing_request, is_ca=event.is_ca, relation_id=event.relation_id + ) + + def _generate_self_signed_certificate(self, csr: str, is_ca: bool, relation_id: int): ca_certificate_secret = self.model.get_secret(label=CA_CERTIFICATES_SECRET_LABEL) ca_certificate_secret_content = ca_certificate_secret.get_content() certificate = generate_certificate( ca=ca_certificate_secret_content["ca-certificate"].encode(), ca_key=ca_certificate_secret_content["private-key"].encode(), ca_key_password=ca_certificate_secret_content["private-key-password"].encode(), - csr=event.certificate_signing_request.encode(), + csr=csr.encode(), validity=self._config_certificate_validity, - is_ca=event.is_ca, + is_ca=is_ca, ).decode() self.tls_certificates.set_relation_certificate( - certificate_signing_request=event.certificate_signing_request, + certificate_signing_request=csr, certificate=certificate, ca=ca_certificate_secret_content["ca-certificate"], chain=[ca_certificate_secret_content["ca-certificate"], certificate], - relation_id=event.relation_id, + relation_id=relation_id, ) - logger.info(f"Generated certificate for relation {event.relation_id}") + logger.info("Generated certificate for relation %s", relation_id) def _on_get_ca_certificate(self, event: ActionEvent): """Handler for the get-ca-certificate action. diff --git a/tests/unit/test_charm.py b/tests/unit/test_charm.py index 2a24cc1..b952966 100644 --- a/tests/unit/test_charm.py +++ b/tests/unit/test_charm.py @@ -6,7 +6,7 @@ import ops import ops.testing -from ops.model import ActiveStatus, BlockedStatus, WaitingStatus +from ops.model import ActiveStatus, BlockedStatus from charm import SelfSignedCertificatesCharm @@ -107,6 +107,65 @@ def test_given_valid_config_when_config_changed_then_status_is_active( self.assertEqual(self.harness.model.unit.status, ActiveStatus()) + @patch(f"{TLS_LIB_PATH}.TLSCertificatesProvidesV2.set_relation_certificate") + @patch(f"{TLS_LIB_PATH}.TLSCertificatesProvidesV2.get_requirer_csrs_with_no_certs") + @patch("charm.generate_private_key") + @patch("charm.generate_password") + @patch("charm.generate_ca") + @patch("charm.generate_certificate") + def test_given_outstanding_certificate_requests_when_config_changed_then_requests_processed( + self, + patch_generate_certificate, + patch_generate_ca, + patch_generate_password, + patch_generate_private_key, + patch_get_requirer_csrs_with_no_certs, + patch_set_relation_certificate, + ): + validity = 100 + relation_id = 123 + ca = "whatever CA certificate" + private_key_password = "password" + private_key = "whatever private key" + requirer_csr = "whatever CSR" + requirer_is_ca = True + generated_certificate = "whatever certificate" + patch_generate_ca.return_value = ca.encode() + patch_generate_password.return_value = private_key_password + patch_generate_private_key.return_value = private_key.encode() + patch_get_requirer_csrs_with_no_certs.return_value = [ + { + "relation_id": relation_id, + "unit_csrs": [ + { + "certificate_signing_request": requirer_csr, + "is_ca": requirer_is_ca, + } + ], + } + ] + patch_generate_certificate.return_value = generated_certificate.encode() + key_values = {"ca-common-name": "pizza.com", "certificate-validity": validity} + self.harness.set_leader(is_leader=True) + + self.harness.update_config(key_values=key_values) + + patch_generate_certificate.assert_called_with( + ca=ca.encode(), + ca_key=private_key.encode(), + ca_key_password=private_key_password.encode(), + csr=requirer_csr.encode(), + validity=validity, + is_ca=requirer_is_ca, + ) + patch_set_relation_certificate.assert_called_with( + certificate_signing_request=requirer_csr, + certificate=generated_certificate, + ca=ca, + chain=[ca, generated_certificate], + relation_id=relation_id, + ) + def test_given_invalid_config_when_certificate_request_then_status_is_blocked(self): self.harness.set_leader(is_leader=True) key_values = {"ca-common-name": "pizza.com", "certificate-validity": 0} @@ -163,18 +222,6 @@ def test_given_valid_config_and_unit_is_leader_when_secret_expired_then_new_ca_c private_key_string, ) - def test_given_root_certificate_not_yet_generated_when_certificate_request_then_status_is_waiting( # noqa: E501 - self, - ): - self.harness.set_leader(is_leader=True) - - self.harness.charm._on_certificate_creation_request(event=Mock()) - - self.assertEqual( - self.harness.model.unit.status, - WaitingStatus("Root Certificate is not yet generated"), - ) - @patch(f"{TLS_LIB_PATH}.TLSCertificatesProvidesV2.set_relation_certificate") @patch("charm.generate_certificate") def test_given_root_certificates_when_certificate_request_then_certificates_are_generated(