Skip to content

Commit

Permalink
Merge pull request #33 from canonical/fetch-lib-certs
Browse files Browse the repository at this point in the history
Fetch CSR-related lib updates to fix tests
  • Loading branch information
mmkay authored Sep 5, 2024
2 parents 82f832c + c8b10e3 commit c53f0e4
Show file tree
Hide file tree
Showing 2 changed files with 59 additions and 6 deletions.
42 changes: 39 additions & 3 deletions lib/charms/observability_libs/v1/cert_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
self.framework.observe(self.cert_handler.on.cert_changed, self._on_server_cert_changed)
container.push(keypath, self.cert_handler.private_key)
container.push(certpath, self.cert_handler.servert_cert)
container.push(certpath, self.cert_handler.server_cert)
```
Since this library uses [Juju Secrets](https://juju.is/docs/juju/secret) it requires Juju >= 3.0.3.
Expand Down Expand Up @@ -59,15 +59,15 @@
import logging

from ops.charm import CharmBase
from ops.framework import EventBase, EventSource, Object, ObjectEvents
from ops.framework import BoundEvent, EventBase, EventSource, Object, ObjectEvents, StoredState
from ops.jujuversion import JujuVersion
from ops.model import Relation, Secret, SecretNotFoundError

logger = logging.getLogger(__name__)

LIBID = "b5cd5cd580f3428fa5f59a8876dcbe6a"
LIBAPI = 1
LIBPATCH = 11
LIBPATCH = 12

VAULT_SECRET_LABEL = "cert-handler-private-vault"

Expand Down Expand Up @@ -273,6 +273,7 @@ class CertHandler(Object):
"""A wrapper for the requirer side of the TLS Certificates charm library."""

on = CertHandlerEvents() # pyright: ignore
_stored = StoredState()

def __init__(
self,
Expand All @@ -283,6 +284,7 @@ def __init__(
peer_relation_name: str = "peers",
cert_subject: Optional[str] = None,
sans: Optional[List[str]] = None,
refresh_events: Optional[List[BoundEvent]] = None,
):
"""CertHandler is used to wrap TLS Certificates management operations for charms.
Expand All @@ -299,8 +301,17 @@ def __init__(
Must match metadata.yaml.
cert_subject: Custom subject. Name collisions are under the caller's responsibility.
sans: DNS names. If none are given, use FQDN.
refresh_events: an optional list of bound events which
will be observed to replace the current CSR with a new one
if there are changes in the CSR's DNS SANs or IP SANs.
Then, subsequently, replace its corresponding certificate with a new one.
"""
super().__init__(charm, key)
# use StoredState to store the hash of the CSR
# to potentially trigger a CSR renewal on `refresh_events`
self._stored.set_default(
csr_hash=None,
)
self.charm = charm

# We need to sanitize the unit name, otherwise route53 complains:
Expand Down Expand Up @@ -355,6 +366,15 @@ def __init__(
self._on_upgrade_charm,
)

if refresh_events:
for ev in refresh_events:
self.framework.observe(ev, self._on_refresh_event)

def _on_refresh_event(self, _):
"""Replace the latest current CSR with a new one if there are any SANs changes."""
if self._stored.csr_hash != self._csr_hash:
self._generate_csr(renew=True)

def _on_upgrade_charm(self, _):
has_privkey = self.vault.get_value("private-key")

Expand Down Expand Up @@ -419,6 +439,20 @@ def enabled(self) -> bool:

return True

@property
def _csr_hash(self) -> int:
"""A hash of the config that constructs the CSR.
Only include here the config options that, should they change, should trigger a renewal of
the CSR.
"""
return hash(
(
tuple(self.sans_dns),
tuple(self.sans_ip),
)
)

@property
def available(self) -> bool:
"""Return True if all certs are available in relation data; False otherwise."""
Expand Down Expand Up @@ -484,6 +518,8 @@ def _generate_csr(
)
self.certificates.request_certificate_creation(certificate_signing_request=csr)

self._stored.csr_hash = self._csr_hash

if clear_cert:
self.vault.clear()

Expand Down
23 changes: 20 additions & 3 deletions lib/charms/tls_certificates_interface/v3/tls_certificates.py
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,7 @@ def _on_all_certificates_invalidated(self, event: AllCertificatesInvalidatedEven
ModelError,
Relation,
RelationDataContent,
Secret,
SecretNotFoundError,
Unit,
)
Expand All @@ -317,7 +318,7 @@ def _on_all_certificates_invalidated(self, event: AllCertificatesInvalidatedEven

# Increment this PATCH version before using `charmcraft publish-lib` or reset
# to 0 if you are raising the major API version
LIBPATCH = 17
LIBPATCH = 18

PYDEPS = ["cryptography", "jsonschema"]

Expand Down Expand Up @@ -1966,9 +1967,10 @@ def _on_secret_expired(self, event: SecretExpiredEvent) -> None:
Args:
event (SecretExpiredEvent): Juju event
"""
if not event.secret.label or not event.secret.label.startswith(f"{LIBID}-"):
csr = self._get_csr_from_secret(event.secret)
if not csr:
logger.error("Failed to get CSR from secret %s", event.secret.label)
return
csr = event.secret.get_content()["csr"]
provider_certificate = self._find_certificate_in_relation_data(csr)
if not provider_certificate:
# A secret expired but we did not find matching certificate. Cleaning up
Expand Down Expand Up @@ -2008,3 +2010,18 @@ def _find_certificate_in_relation_data(self, csr: str) -> Optional[ProviderCerti
continue
return provider_certificate
return None

def _get_csr_from_secret(self, secret: Secret) -> str:
"""Extract the CSR from the secret label or content.
This function is a workaround to maintain backwards compatiblity
and fix the issue reported in
https://github.com/canonical/tls-certificates-interface/issues/228
"""
if not (csr := secret.get_content().get("csr", "")):
# In versions <14 of the Lib we were storing the CSR in the label of the secret
# The CSR now is stored int the content of the secret, which was a breaking change
# Here we get the CSR if the secret was created by an app using libpatch 14 or lower
if secret.label and secret.label.startswith(f"{LIBID}-"):
csr = secret.label[len(f"{LIBID}-") :]
return csr

0 comments on commit c53f0e4

Please sign in to comment.