Skip to content

Commit

Permalink
feat: Handles outstanding cerfificate requests in main hook
Browse files Browse the repository at this point in the history
  • Loading branch information
gruyaume committed Nov 2, 2023
1 parent 2db375d commit 188369c
Show file tree
Hide file tree
Showing 2 changed files with 87 additions and 26 deletions.
40 changes: 27 additions & 13 deletions src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand Down
73 changes: 60 additions & 13 deletions tests/unit/test_charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -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(
Expand Down

0 comments on commit 188369c

Please sign in to comment.