From 059e40bbafa7388f4076bf5fd9e20706417379ed Mon Sep 17 00:00:00 2001 From: dushu Date: Mon, 12 Feb 2024 10:11:04 -0500 Subject: [PATCH] feat: apply juju secrets to transfer bind account password --- lib/charms/glauth_k8s/v0/ldap.py | 59 ++++++++++++++++++++++++++++++-- metadata.yaml | 2 +- src/charm.py | 1 + src/integrations.py | 16 ++++++--- tests/integration/test_charm.py | 1 + tests/unit/test_charm.py | 9 +++-- 6 files changed, 76 insertions(+), 12 deletions(-) diff --git a/lib/charms/glauth_k8s/v0/ldap.py b/lib/charms/glauth_k8s/v0/ldap.py index 4320dabe..399794ca 100644 --- a/lib/charms/glauth_k8s/v0/ldap.py +++ b/lib/charms/glauth_k8s/v0/ldap.py @@ -123,8 +123,10 @@ def _on_ldap_requested(self, event: LdapRequestedEvent) -> None: """ from functools import wraps +from string import Template from typing import Any, Callable, Literal, Optional, Union +import ops from ops.charm import ( CharmBase, RelationBrokenEvent, @@ -133,7 +135,7 @@ def _on_ldap_requested(self, event: LdapRequestedEvent) -> None: RelationEvent, ) from ops.framework import EventSource, Object, ObjectEvents -from ops.model import Relation +from ops.model import Relation, SecretNotFoundError from pydantic import ( BaseModel, ConfigDict, @@ -151,11 +153,12 @@ def _on_ldap_requested(self, event: LdapRequestedEvent) -> None: # Increment this PATCH version before using `charmcraft publish-lib` or reset # to 0 if you are raising the major API version -LIBPATCH = 2 +LIBPATCH = 3 PYDEPS = ["pydantic~=2.5.3"] DEFAULT_RELATION_NAME = "ldap" +BIND_ACCOUNT_SECRET_LABEL_TEMPLATE = Template("relation-$relation_id-bind-account-secret") def leader_unit(func: Callable) -> Callable: @@ -182,6 +185,36 @@ def _update_relation_app_databag( relation.data[ldap.app].update(data) +class Secret: + def __init__(self, secret: ops.Secret = None) -> None: + self._secret: ops.Secret = secret + + @property + def uri(self) -> str: + return self._secret.id if self._secret else "" + + @classmethod + def load( + cls, + charm: CharmBase, + label: str, + *, + content: Optional[dict[str, str]] = None, + ) -> "Secret": + try: + secret = charm.model.get_secret(label=label) + except SecretNotFoundError: + secret = charm.app.add_secret(label=label, content=content) + + return Secret(secret) + + def grant(self, relation: Relation) -> None: + self._secret.grant(relation) + + def remove(self) -> None: + self._secret.remove_all_revisions() + + class LdapProviderBaseData(BaseModel): model_config = ConfigDict(frozen=True) @@ -268,12 +301,25 @@ def __init__( self.charm.on[self._relation_name].relation_changed, self._on_relation_changed, ) + self.framework.observe( + self.charm.on[self._relation_name].relation_broken, + self._on_relation_broken, + ) @leader_unit def _on_relation_changed(self, event: RelationChangedEvent) -> None: """Handle the event emitted when the requirer charm provides the necessary data.""" self.on.ldap_requested.emit(event.relation) + @leader_unit + def _on_relation_broken(self, event: RelationBrokenEvent) -> None: + """Handle the event emitted when the LDAP integration is broken.""" + secret = Secret.load( + self.charm, + label=BIND_ACCOUNT_SECRET_LABEL_TEMPLATE.substitute(relation_id=event.relation.id), + ) + secret.remove() + def update_relations_app_data( self, /, data: Optional[LdapProviderBaseData] = None, relation_id: Optional[int] = None ) -> None: @@ -286,9 +332,16 @@ def update_relations_app_data( if relation_id is not None: relations = [relation for relation in relations if relation.id == relation_id] + secret = Secret.load( + self.charm, + BIND_ACCOUNT_SECRET_LABEL_TEMPLATE.substitute(relation_id=relation_id), + content={"password": data.bind_password_secret}, + ) + secret.grant(relations[0]) + data = data.model_copy(update={"bind_password_secret": secret.uri}) for relation in relations: - _update_relation_app_databag(self.charm, relation, data.model_dump()) + _update_relation_app_databag(self.charm, relation, data.model_dump()) # type: ignore[union-attr] class LdapRequirer(Object): diff --git a/metadata.yaml b/metadata.yaml index 210db58f..6850b73b 100644 --- a/metadata.yaml +++ b/metadata.yaml @@ -17,7 +17,7 @@ resources: oci-image: type: oci-image description: GLAuth oci-image - upstream-source: ghcr.io/canonical/glauth:2.3.0 + upstream-source: ghcr.io/canonical/glauth:2.3.1 requires: pg-database: diff --git a/src/charm.py b/src/charm.py index 128c255f..bcfbfe0c 100755 --- a/src/charm.py +++ b/src/charm.py @@ -238,6 +238,7 @@ def _on_pebble_ready(self, event: PebbleReadyEvent) -> None: self._handle_event_update(event) + @leader_unit @validate_database_resource def _on_ldap_requested(self, event: LdapRequestedEvent) -> None: if not (requirer_data := event.data): diff --git a/src/integrations.py b/src/integrations.py index 3b2c7707..50cee853 100644 --- a/src/integrations.py +++ b/src/integrations.py @@ -1,3 +1,6 @@ +# Copyright 2024 Canonical Ltd. +# See LICENSE file for licensing details. + import hashlib import logging import subprocess @@ -39,9 +42,9 @@ @dataclass(frozen=True) class BindAccount: - cn: Optional[str] = None - ou: Optional[str] = None - password: Optional[str] = None + cn: str + ou: str + password: str def _create_bind_account(dsn: str, user_name: str, group_name: str) -> BindAccount: @@ -106,7 +109,7 @@ def provider_data(self) -> Optional[LdapProviderData]: url=self.ldap_url, base_dn=self.base_dn, bind_dn=f"cn={self._bind_account.cn},ou={self._bind_account.ou},{self.base_dn}", - bind_password_secret=self._bind_account.password or "", + bind_password_secret=self._bind_account.password, auth_method="simple", starttls=self.starttls_enabled, ) @@ -146,7 +149,10 @@ def __init__(self, charm: CharmBase) -> None: key="glauth-server-cert", peer_relation_name="glauth-peers", cert_subject=hostname, - extra_sans_dns=[hostname], + extra_sans_dns=[ + hostname, + f"{charm.app.name}.{charm.model.name}.svc.cluster.local", + ], ) @property diff --git a/tests/integration/test_charm.py b/tests/integration/test_charm.py index 9ddf4fd4..09555834 100644 --- a/tests/integration/test_charm.py +++ b/tests/integration/test_charm.py @@ -118,6 +118,7 @@ async def test_ldap_integration( assert ldap_integration_data["bind_dn"].startswith( f"cn={GLAUTH_CLIENT_APP},ou={ops_test.model_name}" ) + assert ldap_integration_data["bind_password_secret"].startswith("secret:") async def test_certificate_transfer_integration( diff --git a/tests/unit/test_charm.py b/tests/unit/test_charm.py index 3888eac4..3326c9e0 100644 --- a/tests/unit/test_charm.py +++ b/tests/unit/test_charm.py @@ -196,9 +196,12 @@ def test_when_ldap_requested( ldap_relation_data: MagicMock, ) -> None: assert isinstance(harness.model.unit.status, ActiveStatus) - assert LDAP_PROVIDER_DATA.model_dump() == harness.get_relation_data( - ldap_relation, harness.model.app.name - ) + + actual = dict(harness.get_relation_data(ldap_relation, harness.model.app.name)) + secret_id = actual.get("bind_password_secret") + secret_content = harness.model.get_secret(id=secret_id).get_content() + actual["bind_password_secret"] = secret_content.get("password") + assert LDAP_PROVIDER_DATA.model_dump() == actual class TestLdapAuxiliaryRequestedEvent: