Skip to content

Commit

Permalink
Merge pull request #25 from canonical/IAM-604
Browse files Browse the repository at this point in the history
feat: apply juju secrets to transfer bind account password
  • Loading branch information
wood-push-melon authored Feb 21, 2024
2 parents 77ce6da + 059e40b commit 57e4631
Show file tree
Hide file tree
Showing 6 changed files with 76 additions and 12 deletions.
59 changes: 56 additions & 3 deletions lib/charms/glauth_k8s/v0/ldap.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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:
Expand All @@ -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)

Expand Down Expand Up @@ -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:
Expand All @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion metadata.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
16 changes: 11 additions & 5 deletions src/integrations.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# Copyright 2024 Canonical Ltd.
# See LICENSE file for licensing details.

import hashlib
import logging
import subprocess
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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,
)
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions tests/integration/test_charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
9 changes: 6 additions & 3 deletions tests/unit/test_charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down

0 comments on commit 57e4631

Please sign in to comment.