Skip to content

Commit

Permalink
refactor: use pydantic for ldap interface lib
Browse files Browse the repository at this point in the history
  • Loading branch information
wood-push-melon committed Jan 8, 2024
1 parent a013ee1 commit 324c06a
Show file tree
Hide file tree
Showing 6 changed files with 83 additions and 40 deletions.
2 changes: 1 addition & 1 deletion config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@ options:
type: string
hostname:
description: The hostname of the LDAP server
default: "ldap.canonical.com"
default: "ldap.glauth.com"
type: string
6 changes: 2 additions & 4 deletions integration-requirements.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
protobuf~=3.20.1
pytest
-r requirements.txt
juju
pytest
pytest-operator
requests
-r requirements.txt
81 changes: 54 additions & 27 deletions lib/charms/glauth_k8s/v0/ldap.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ def _on_ldap_requested(self, event: LdapRequestedEvent) -> None:
ldap_data = ...
# Update the integration data
self.ldap_provider.update_relation_app_data(
self.ldap_provider.update_relations_app_data(
relation.id,
ldap_data,
)
Expand All @@ -122,11 +122,9 @@ def _on_ldap_requested(self, event: LdapRequestedEvent) -> None:
LDAP related information in order to connect and authenticate to the LDAP server
"""

from dataclasses import asdict, dataclass
from functools import wraps
from typing import Any, Callable, Optional, Union
from typing import Any, Callable, Literal, Optional, Union

from dacite import Config, from_dict
from ops.charm import (
CharmBase,
RelationBrokenEvent,
Expand All @@ -136,6 +134,14 @@ def _on_ldap_requested(self, event: LdapRequestedEvent) -> None:
)
from ops.framework import EventSource, Object, ObjectEvents
from ops.model import Relation
from pydantic import (
BaseModel,
ConfigDict,
StrictBool,
ValidationError,
field_serializer,
field_validator,
)

# The unique CharmHub library identifier, never change it
LIBID = "5a535b3c4d0b40da98e29867128e57b9"
Expand All @@ -147,7 +153,7 @@ def _on_ldap_requested(self, event: LdapRequestedEvent) -> None:
# to 0 if you are raising the major API version
LIBPATCH = 2

PYDEPS = ["dacite~=1.8.0"]
PYDEPS = ["pydantic~=2.5.3"]

DEFAULT_RELATION_NAME = "ldap"

Expand Down Expand Up @@ -176,18 +182,43 @@ def _update_relation_app_databag(
relation.data[ldap.app].update(data)


@dataclass(frozen=True)
class LdapProviderData:
class LdapProviderBaseData(BaseModel):
model_config = ConfigDict(frozen=True)

url: str
base_dn: str

@field_validator("url")
@classmethod
def validate_ldap_url(cls, v: str) -> str:
if not v.startswith("ldap://"):
raise ValidationError("Invalid LDAP URL scheme.")

return v


class LdapProviderData(LdapProviderBaseData):
bind_dn: str
bind_password_secret: str
auth_method: str
starttls: bool
auth_method: Literal["simple"]
starttls: StrictBool

@field_validator("starttls", mode="before")
@classmethod
def deserialize_bool(cls, v: str | bool) -> bool:
if isinstance(v, str):
return True if v.casefold() == "true" else False

return v

@field_serializer("starttls")
def serialize_bool(self, starttls: bool) -> str:
return str(starttls)


class LdapRequirerData(BaseModel):
model_config = ConfigDict(frozen=True)

@dataclass(frozen=True)
class LdapRequirerData:
user: str
group: str

Expand All @@ -198,9 +229,7 @@ class LdapRequestedEvent(RelationEvent):
@property
def data(self) -> Optional[LdapRequirerData]:
relation_data = self.relation.data.get(self.relation.app)
return (
from_dict(data_class=LdapRequirerData, data=relation_data) if relation_data else None
)
return LdapRequirerData(**relation_data) if relation_data else None


class LdapProviderEvents(ObjectEvents):
Expand Down Expand Up @@ -245,15 +274,21 @@ 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)

def update_relation_app_data(
self, /, relation_id: int, data: Optional[LdapProviderData]
def update_relations_app_data(
self, /, data: Optional[LdapProviderBaseData] = None, relation_id: Optional[int] = None
) -> None:
"""An API for the provider charm to provide the LDAP related information."""
if data is None:
return

relation = self.charm.model.get_relation(self._relation_name, relation_id)
_update_relation_app_databag(self.charm, relation, asdict(data))
if not (relations := self.charm.model.relations.get(self._relation_name)):
return

if relation_id is not None:
relations = [relation for relation in relations if relation.id == relation_id]

for relation in relations:
_update_relation_app_databag(self.charm, relation, data.model_dump())


class LdapRequirer(Object):
Expand Down Expand Up @@ -320,12 +355,4 @@ def consume_ldap_relation_data(
return None

provider_data = relation.data.get(relation.app)
return (
from_dict(
data_class=LdapProviderData,
data=provider_data,
config=Config(cast=[bool]),
)
if provider_data
else None
)
return LdapProviderData(**provider_data) if provider_data else None
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
cosl
dacite ~= 1.8.0
Jinja2
lightkube
lightkube-models
ops >= 2.2.0
psycopg[binary]
pydantic ~= 2.5.3
SQLAlchemy
tenacity ~= 8.2.3
9 changes: 6 additions & 3 deletions src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,9 @@ def _on_database_changed(self, event: DatabaseEndpointsChangedEvent) -> None:
def _on_config_changed(self, event: ConfigChangedEvent) -> None:
self.config_file.base_dn = self.config.get("base_dn")
self._handle_event_update(event)
self.ldap_provider.update_relations_app_data(
data=self._ldap_integration.provider_base_data
)

@validate_container_connectivity
def _on_pebble_ready(self, event: PebbleReadyEvent) -> None:
Expand All @@ -207,9 +210,9 @@ def _on_ldap_requested(self, event: LdapRequestedEvent) -> None:
return

self._ldap_integration.load_bind_account(requirer_data.user, requirer_data.group)
self.ldap_provider.update_relation_app_data(
event.relation.id,
self._ldap_integration.provider_data,
self.ldap_provider.update_relations_app_data(
relation_id=event.relation.id,
data=self._ldap_integration.provider_data,
)

def _on_promtail_error(self, event: PromtailDigestError) -> None:
Expand Down
23 changes: 19 additions & 4 deletions src/integrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from secrets import token_bytes
from typing import Optional

from charms.glauth_k8s.v0.ldap import LdapProviderData
from charms.glauth_k8s.v0.ldap import LdapProviderBaseData, LdapProviderData
from configs import DatabaseConfig
from constants import DEFAULT_GID, DEFAULT_UID, GLAUTH_LDAP_PORT
from database import Capability, Group, Operation, User
Expand Down Expand Up @@ -53,15 +53,30 @@ def load_bind_account(self, user: str, group: str) -> None:
database_config = DatabaseConfig.load(self._charm.database_requirer)
self._bind_account = _create_bind_account(database_config.dsn, user, group)

@property
def ldap_url(self) -> str:
return f"ldap://{self._charm.config.get('hostname')}:{GLAUTH_LDAP_PORT}"

@property
def base_dn(self) -> str:
return self._charm.config.get("base_dn")

@property
def provider_base_data(self) -> LdapProviderBaseData:
return LdapProviderBaseData(
url=self.ldap_url,
base_dn=self.base_dn,
)

@property
def provider_data(self) -> Optional[LdapProviderData]:
if not self._bind_account:
return None

return LdapProviderData(
url=f"ldap://{self._charm.config.get('hostname')}:{GLAUTH_LDAP_PORT}",
base_dn=self._charm.config.get("base_dn"),
bind_dn=f"cn={self._bind_account.cn},ou={self._bind_account.ou},{self._charm.config.get('base_dn')}",
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 "",
auth_method="simple",
starttls=True,
Expand Down

0 comments on commit 324c06a

Please sign in to comment.