charmlibs-interfaces-ldap 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- charmlibs/interfaces/ldap/__init__.py +158 -0
- charmlibs/interfaces/ldap/_ldap.py +411 -0
- charmlibs/interfaces/ldap/_version.py +15 -0
- charmlibs/interfaces/ldap/py.typed +0 -0
- charmlibs_interfaces_ldap-1.0.0.dist-info/METADATA +30 -0
- charmlibs_interfaces_ldap-1.0.0.dist-info/RECORD +7 -0
- charmlibs_interfaces_ldap-1.0.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# Copyright 2026 Canonical Ltd.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""The charmlibs.interfaces.ldap package.
|
|
16
|
+
|
|
17
|
+
Juju Charm Library for the `ldap` Juju Interface
|
|
18
|
+
================================================
|
|
19
|
+
|
|
20
|
+
This Juju charm library contains the Provider and Requirer classes for handling
|
|
21
|
+
the ``ldap`` interface.
|
|
22
|
+
|
|
23
|
+
Requirer Charm
|
|
24
|
+
--------------
|
|
25
|
+
|
|
26
|
+
The requirer charm is expected to:
|
|
27
|
+
|
|
28
|
+
- Provide information for the provider charm to deliver LDAP related
|
|
29
|
+
information in the juju integration, in order to communicate with the LDAP
|
|
30
|
+
server and authenticate LDAP operations
|
|
31
|
+
- Listen to the custom juju event ``LdapReadyEvent`` to obtain the LDAP
|
|
32
|
+
related information from the integration
|
|
33
|
+
- Listen to the custom juju event ``LdapUnavailableEvent`` to handle the
|
|
34
|
+
situation when the LDAP integration is broken
|
|
35
|
+
|
|
36
|
+
.. code-block:: python
|
|
37
|
+
|
|
38
|
+
from charmlibs.interfaces.ldap import (
|
|
39
|
+
LdapRequirer,
|
|
40
|
+
LdapReadyEvent,
|
|
41
|
+
LdapUnavailableEvent,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
class RequirerCharm(CharmBase):
|
|
45
|
+
# LDAP requirer charm that integrates with an LDAP provider charm.
|
|
46
|
+
|
|
47
|
+
def __init__(self, *args):
|
|
48
|
+
super().__init__(*args)
|
|
49
|
+
|
|
50
|
+
self.ldap_requirer = LdapRequirer(self)
|
|
51
|
+
self.framework.observe(
|
|
52
|
+
self.ldap_requirer.on.ldap_ready,
|
|
53
|
+
self._on_ldap_ready,
|
|
54
|
+
)
|
|
55
|
+
self.framework.observe(
|
|
56
|
+
self.ldap_requirer.on.ldap_unavailable,
|
|
57
|
+
self._on_ldap_unavailable,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
def _on_ldap_ready(self, event: LdapReadyEvent) -> None:
|
|
61
|
+
# Consume the LDAP related information
|
|
62
|
+
ldap_data = self.ldap_requirer.consume_ldap_relation_data(
|
|
63
|
+
relation=event.relation,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# Configure the LDAP requirer charm
|
|
67
|
+
...
|
|
68
|
+
|
|
69
|
+
def _on_ldap_unavailable(self, event: LdapUnavailableEvent) -> None:
|
|
70
|
+
# Handle the situation where the LDAP integration is broken
|
|
71
|
+
...
|
|
72
|
+
|
|
73
|
+
As shown above, the library offers custom juju events to handle specific
|
|
74
|
+
situations, which are listed below:
|
|
75
|
+
|
|
76
|
+
- ``ldap_ready``: event emitted when the LDAP related information is ready for
|
|
77
|
+
requirer charm to use.
|
|
78
|
+
- ``ldap_unavailable``: event emitted when the LDAP integration is broken.
|
|
79
|
+
|
|
80
|
+
Additionally, the requirer charmed operator needs to declare the ``ldap``
|
|
81
|
+
interface in the ``metadata.yaml``:
|
|
82
|
+
|
|
83
|
+
.. code-block:: yaml
|
|
84
|
+
|
|
85
|
+
requires:
|
|
86
|
+
ldap:
|
|
87
|
+
interface: ldap
|
|
88
|
+
|
|
89
|
+
Provider Charm
|
|
90
|
+
--------------
|
|
91
|
+
|
|
92
|
+
The provider charm is expected to:
|
|
93
|
+
|
|
94
|
+
- Use the information provided by the requirer charm to provide LDAP related
|
|
95
|
+
information for the requirer charm to connect and authenticate to the LDAP
|
|
96
|
+
server
|
|
97
|
+
- Listen to the custom juju event ``LdapRequestedEvent`` to offer LDAP related
|
|
98
|
+
information in the integration
|
|
99
|
+
|
|
100
|
+
.. code-block:: python
|
|
101
|
+
|
|
102
|
+
from charmlibs.interfaces.ldap import (
|
|
103
|
+
LdapProvider,
|
|
104
|
+
LdapRequestedEvent,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
class ProviderCharm(CharmBase):
|
|
108
|
+
# LDAP provider charm.
|
|
109
|
+
|
|
110
|
+
def __init__(self, *args):
|
|
111
|
+
super().__init__(*args)
|
|
112
|
+
|
|
113
|
+
self.ldap_provider = LdapProvider(self)
|
|
114
|
+
self.framework.observe(
|
|
115
|
+
self.ldap_provider.on.ldap_requested,
|
|
116
|
+
self._on_ldap_requested,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
def _on_ldap_requested(self, event: LdapRequestedEvent) -> None:
|
|
120
|
+
# Consume the information provided by the requirer charm
|
|
121
|
+
requirer_data = event.data
|
|
122
|
+
|
|
123
|
+
# Prepare the LDAP related information using the requirer's data
|
|
124
|
+
ldap_data = ...
|
|
125
|
+
|
|
126
|
+
# Update the integration data
|
|
127
|
+
self.ldap_provider.update_relations_app_data(
|
|
128
|
+
relation.id,
|
|
129
|
+
ldap_data,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
As shown above, the library offers custom juju events to handle specific
|
|
133
|
+
situations, which are listed below:
|
|
134
|
+
|
|
135
|
+
- ``ldap_requested``: event emitted when the requirer charm is requesting the
|
|
136
|
+
LDAP related information in order to connect and authenticate to the LDAP server.
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
from ._ldap import (
|
|
140
|
+
LdapProvider,
|
|
141
|
+
LdapProviderData,
|
|
142
|
+
LdapReadyEvent,
|
|
143
|
+
LdapRequestedEvent,
|
|
144
|
+
LdapRequirer,
|
|
145
|
+
LdapRequirerData,
|
|
146
|
+
LdapUnavailableEvent,
|
|
147
|
+
)
|
|
148
|
+
from ._version import __version__ as __version__
|
|
149
|
+
|
|
150
|
+
__all__ = [
|
|
151
|
+
'LdapProvider',
|
|
152
|
+
'LdapProviderData',
|
|
153
|
+
'LdapReadyEvent',
|
|
154
|
+
'LdapRequestedEvent',
|
|
155
|
+
'LdapRequirer',
|
|
156
|
+
'LdapRequirerData',
|
|
157
|
+
'LdapUnavailableEvent',
|
|
158
|
+
]
|
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
# Copyright 2026 Canonical Ltd.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
"""LDAP interface implementation.
|
|
17
|
+
|
|
18
|
+
Migrated from charms.glauth_k8s.v0.ldap (v0.13).
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import json
|
|
22
|
+
from collections.abc import Callable
|
|
23
|
+
from functools import wraps
|
|
24
|
+
from string import Template
|
|
25
|
+
from typing import Any, Literal, Optional, Union
|
|
26
|
+
|
|
27
|
+
import ops
|
|
28
|
+
from ops.charm import (
|
|
29
|
+
CharmBase,
|
|
30
|
+
RelationBrokenEvent,
|
|
31
|
+
RelationChangedEvent,
|
|
32
|
+
RelationCreatedEvent,
|
|
33
|
+
RelationEvent,
|
|
34
|
+
)
|
|
35
|
+
from ops.framework import EventSource, Handle, Object, ObjectEvents
|
|
36
|
+
from ops.model import Relation, SecretNotFoundError
|
|
37
|
+
from pydantic import (
|
|
38
|
+
BaseModel,
|
|
39
|
+
Field,
|
|
40
|
+
StrictBool,
|
|
41
|
+
ValidationError,
|
|
42
|
+
field_serializer,
|
|
43
|
+
field_validator,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
DEFAULT_RELATION_NAME = 'ldap'
|
|
47
|
+
BIND_ACCOUNT_SECRET_LABEL_TEMPLATE = Template('relation-$relation_id-bind-account-secret')
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def leader_unit(func: Callable) -> Callable:
|
|
51
|
+
@wraps(func)
|
|
52
|
+
def wrapper(
|
|
53
|
+
obj: Union['LdapProvider', 'LdapRequirer'], *args: Any, **kwargs: Any
|
|
54
|
+
) -> Any | None:
|
|
55
|
+
if not obj.unit.is_leader():
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
return func(obj, *args, **kwargs)
|
|
59
|
+
|
|
60
|
+
return wrapper
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@leader_unit
|
|
64
|
+
def _update_relation_app_databag(
|
|
65
|
+
ldap: Union['LdapProvider', 'LdapRequirer'], relation: Relation, data: dict
|
|
66
|
+
) -> None:
|
|
67
|
+
if relation is None:
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
data = {k: str(v) if v else '' for k, v in data.items()}
|
|
71
|
+
relation.data[ldap.app].update(data)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class Secret:
|
|
75
|
+
def __init__(self, secret: ops.Secret = None) -> None:
|
|
76
|
+
self._secret: ops.Secret = secret
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def uri(self) -> str:
|
|
80
|
+
return self._secret.id if self._secret else ''
|
|
81
|
+
|
|
82
|
+
@classmethod
|
|
83
|
+
def load(
|
|
84
|
+
cls,
|
|
85
|
+
charm: CharmBase,
|
|
86
|
+
label: str,
|
|
87
|
+
) -> Optional['Secret']:
|
|
88
|
+
try:
|
|
89
|
+
secret = charm.model.get_secret(label=label)
|
|
90
|
+
except SecretNotFoundError:
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
return Secret(secret)
|
|
94
|
+
|
|
95
|
+
@classmethod
|
|
96
|
+
def create_or_update(cls, charm: CharmBase, label: str, content: dict[str, str]) -> 'Secret':
|
|
97
|
+
try:
|
|
98
|
+
secret = charm.model.get_secret(label=label)
|
|
99
|
+
secret.set_content(content=content)
|
|
100
|
+
except SecretNotFoundError:
|
|
101
|
+
secret = charm.app.add_secret(label=label, content=content)
|
|
102
|
+
|
|
103
|
+
return Secret(secret)
|
|
104
|
+
|
|
105
|
+
def grant(self, relation: Relation) -> None:
|
|
106
|
+
self._secret.grant(relation)
|
|
107
|
+
|
|
108
|
+
def remove(self) -> None:
|
|
109
|
+
self._secret.remove_all_revisions()
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class LdapProviderBaseData(BaseModel):
|
|
113
|
+
urls: list[str] = Field(frozen=True)
|
|
114
|
+
ldaps_urls: list[str] = Field(frozen=True)
|
|
115
|
+
base_dn: str = Field(frozen=True)
|
|
116
|
+
starttls: StrictBool = Field(frozen=True)
|
|
117
|
+
|
|
118
|
+
@field_validator('urls', mode='before')
|
|
119
|
+
@classmethod
|
|
120
|
+
def validate_ldap_urls(cls, vs: list[str] | str) -> list[str]:
|
|
121
|
+
if isinstance(vs, str):
|
|
122
|
+
vs = json.loads(vs)
|
|
123
|
+
if isinstance(vs, str):
|
|
124
|
+
vs = [vs]
|
|
125
|
+
|
|
126
|
+
for v in vs:
|
|
127
|
+
if not v.startswith('ldap://'):
|
|
128
|
+
raise ValidationError.from_exception_data('Invalid LDAP URL scheme.')
|
|
129
|
+
|
|
130
|
+
return vs
|
|
131
|
+
|
|
132
|
+
@field_validator('ldaps_urls', mode='before')
|
|
133
|
+
@classmethod
|
|
134
|
+
def validate_ldaps_urls(cls, vs: list[str] | str) -> list[str]:
|
|
135
|
+
if isinstance(vs, str):
|
|
136
|
+
vs = json.loads(vs)
|
|
137
|
+
if isinstance(vs, str):
|
|
138
|
+
vs = [vs]
|
|
139
|
+
|
|
140
|
+
for v in vs:
|
|
141
|
+
if not v.startswith('ldaps://'):
|
|
142
|
+
raise ValidationError.from_exception_data('Invalid LDAPS URL scheme.')
|
|
143
|
+
|
|
144
|
+
return vs
|
|
145
|
+
|
|
146
|
+
@field_serializer('urls', 'ldaps_urls')
|
|
147
|
+
def serialize_list(self, urls: list[str]) -> str:
|
|
148
|
+
return str(json.dumps(urls))
|
|
149
|
+
|
|
150
|
+
@field_validator('starttls', mode='before')
|
|
151
|
+
@classmethod
|
|
152
|
+
def deserialize_bool(cls, v: str | bool) -> bool:
|
|
153
|
+
if isinstance(v, str):
|
|
154
|
+
return v.casefold() == 'true'
|
|
155
|
+
|
|
156
|
+
return v
|
|
157
|
+
|
|
158
|
+
@field_serializer('starttls')
|
|
159
|
+
def serialize_bool(self, starttls: bool) -> str:
|
|
160
|
+
return str(starttls)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class LdapProviderData(LdapProviderBaseData):
|
|
164
|
+
bind_dn: str = Field(frozen=True)
|
|
165
|
+
bind_password: str = Field(exclude=True)
|
|
166
|
+
bind_password_secret: str | None = None
|
|
167
|
+
auth_method: Literal['simple'] = Field(frozen=True)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class LdapRequirerData(BaseModel):
|
|
171
|
+
user: str = Field(frozen=True)
|
|
172
|
+
group: str = Field(frozen=True)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class LdapRequestedEvent(RelationEvent):
|
|
176
|
+
"""An event emitted when the LDAP integration is built."""
|
|
177
|
+
|
|
178
|
+
def __init__(self, handle: Handle, relation: Relation) -> None:
|
|
179
|
+
super().__init__(handle, relation, relation.app)
|
|
180
|
+
|
|
181
|
+
@property
|
|
182
|
+
def data(self) -> LdapRequirerData | None:
|
|
183
|
+
relation_data = self.relation.data.get(self.relation.app)
|
|
184
|
+
return LdapRequirerData(**relation_data) if relation_data else None
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class LdapProviderEvents(ObjectEvents):
|
|
188
|
+
ldap_requested = EventSource(LdapRequestedEvent)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class LdapReadyEvent(RelationEvent):
|
|
192
|
+
"""An event when the LDAP related information is ready."""
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class LdapUnavailableEvent(RelationEvent):
|
|
196
|
+
"""An event when the LDAP integration is unavailable."""
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class LdapRequirerEvents(ObjectEvents):
|
|
200
|
+
ldap_ready = EventSource(LdapReadyEvent)
|
|
201
|
+
ldap_unavailable = EventSource(LdapUnavailableEvent)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class _LdapInterface(Object):
|
|
205
|
+
def __init__(self, charm: CharmBase, relation_name: str = DEFAULT_RELATION_NAME) -> None:
|
|
206
|
+
super().__init__(charm, relation_name)
|
|
207
|
+
|
|
208
|
+
self.charm = charm
|
|
209
|
+
self.app = charm.app
|
|
210
|
+
self.unit = charm.unit
|
|
211
|
+
self._relation_name = relation_name
|
|
212
|
+
|
|
213
|
+
@property
|
|
214
|
+
def relations(self) -> list[Relation]:
|
|
215
|
+
"""The list of Relation instances associated with this relation_name."""
|
|
216
|
+
return [
|
|
217
|
+
relation
|
|
218
|
+
for relation in self.charm.model.relations[self._relation_name]
|
|
219
|
+
if self._is_relation_active(relation)
|
|
220
|
+
]
|
|
221
|
+
|
|
222
|
+
@staticmethod
|
|
223
|
+
def _is_relation_active(relation: Relation) -> bool:
|
|
224
|
+
"""Whether the relation is active based on contained data."""
|
|
225
|
+
try:
|
|
226
|
+
_ = repr(relation.data)
|
|
227
|
+
return True
|
|
228
|
+
except (RuntimeError, ops.ModelError):
|
|
229
|
+
return False
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
class LdapProvider(_LdapInterface):
|
|
233
|
+
on = LdapProviderEvents()
|
|
234
|
+
|
|
235
|
+
def __init__(
|
|
236
|
+
self,
|
|
237
|
+
charm: CharmBase,
|
|
238
|
+
relation_name: str = DEFAULT_RELATION_NAME,
|
|
239
|
+
) -> None:
|
|
240
|
+
super().__init__(charm, relation_name)
|
|
241
|
+
|
|
242
|
+
self.framework.observe(
|
|
243
|
+
self.charm.on[self._relation_name].relation_changed,
|
|
244
|
+
self._on_relation_changed,
|
|
245
|
+
)
|
|
246
|
+
self.framework.observe(
|
|
247
|
+
self.charm.on[self._relation_name].relation_broken,
|
|
248
|
+
self._on_relation_broken,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
@leader_unit
|
|
252
|
+
def _on_relation_changed(self, event: RelationChangedEvent) -> None:
|
|
253
|
+
"""Handle the event emitted when the requirer charm provides the necessary data."""
|
|
254
|
+
self.on.ldap_requested.emit(event.relation)
|
|
255
|
+
|
|
256
|
+
@leader_unit
|
|
257
|
+
def _on_relation_broken(self, event: RelationBrokenEvent) -> None:
|
|
258
|
+
"""Handle the event emitted when the LDAP integration is broken."""
|
|
259
|
+
secret = Secret.load(
|
|
260
|
+
self.charm,
|
|
261
|
+
label=BIND_ACCOUNT_SECRET_LABEL_TEMPLATE.substitute(relation_id=event.relation.id),
|
|
262
|
+
)
|
|
263
|
+
if secret:
|
|
264
|
+
secret.remove()
|
|
265
|
+
|
|
266
|
+
def get_bind_password(self, relation_id: int) -> str | None:
|
|
267
|
+
"""Retrieve the bind account password for a given integration."""
|
|
268
|
+
try:
|
|
269
|
+
secret = self.charm.model.get_secret(
|
|
270
|
+
label=BIND_ACCOUNT_SECRET_LABEL_TEMPLATE.substitute(relation_id=relation_id)
|
|
271
|
+
)
|
|
272
|
+
except SecretNotFoundError:
|
|
273
|
+
return None
|
|
274
|
+
return secret.get_content().get('password')
|
|
275
|
+
|
|
276
|
+
def update_relations_app_data(
|
|
277
|
+
self,
|
|
278
|
+
data: LdapProviderBaseData | LdapProviderData,
|
|
279
|
+
/,
|
|
280
|
+
relation_id: int | None = None,
|
|
281
|
+
) -> None:
|
|
282
|
+
"""An API for the provider charm to provide the LDAP related information."""
|
|
283
|
+
if not (relations := self.charm.model.relations.get(self._relation_name)):
|
|
284
|
+
return
|
|
285
|
+
|
|
286
|
+
if relation_id is not None and isinstance(data, LdapProviderData):
|
|
287
|
+
relations = [relation for relation in relations if relation.id == relation_id]
|
|
288
|
+
secret = Secret.create_or_update(
|
|
289
|
+
self.charm,
|
|
290
|
+
BIND_ACCOUNT_SECRET_LABEL_TEMPLATE.substitute(relation_id=relation_id),
|
|
291
|
+
{'password': data.bind_password},
|
|
292
|
+
)
|
|
293
|
+
secret.grant(relations[0])
|
|
294
|
+
data.bind_password_secret = secret.uri
|
|
295
|
+
|
|
296
|
+
for relation in relations:
|
|
297
|
+
_update_relation_app_databag(self.charm, relation, data.model_dump())
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
class LdapRequirer(_LdapInterface):
|
|
301
|
+
"""An LDAP requirer to consume data delivered by an LDAP provider charm."""
|
|
302
|
+
|
|
303
|
+
on = LdapRequirerEvents()
|
|
304
|
+
|
|
305
|
+
def __init__(
|
|
306
|
+
self,
|
|
307
|
+
charm: CharmBase,
|
|
308
|
+
relation_name: str = DEFAULT_RELATION_NAME,
|
|
309
|
+
*,
|
|
310
|
+
data: LdapRequirerData | None = None,
|
|
311
|
+
) -> None:
|
|
312
|
+
super().__init__(charm, relation_name)
|
|
313
|
+
|
|
314
|
+
self._data = data
|
|
315
|
+
|
|
316
|
+
self.framework.observe(
|
|
317
|
+
self.charm.on[self._relation_name].relation_created,
|
|
318
|
+
self._on_ldap_relation_created,
|
|
319
|
+
)
|
|
320
|
+
self.framework.observe(
|
|
321
|
+
self.charm.on[self._relation_name].relation_changed,
|
|
322
|
+
self._on_ldap_relation_changed,
|
|
323
|
+
)
|
|
324
|
+
self.framework.observe(
|
|
325
|
+
self.charm.on[self._relation_name].relation_broken,
|
|
326
|
+
self._on_ldap_relation_broken,
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
def _on_ldap_relation_created(self, event: RelationCreatedEvent) -> None:
|
|
330
|
+
"""Handle the event emitted when an LDAP integration is created."""
|
|
331
|
+
user = self._data.user if self._data else self.app.name
|
|
332
|
+
group = self._data.group if self._data else self.model.name
|
|
333
|
+
_update_relation_app_databag(self.charm, event.relation, {'user': user, 'group': group})
|
|
334
|
+
|
|
335
|
+
def _on_ldap_relation_changed(self, event: RelationChangedEvent) -> None:
|
|
336
|
+
"""Handle the event emitted when the LDAP related information is ready."""
|
|
337
|
+
provider_app = event.relation.app
|
|
338
|
+
|
|
339
|
+
if not (provider_data := event.relation.data.get(provider_app)):
|
|
340
|
+
return
|
|
341
|
+
|
|
342
|
+
provider_data = dict(provider_data)
|
|
343
|
+
if self._load_provider_data(provider_data):
|
|
344
|
+
self.on.ldap_ready.emit(event.relation)
|
|
345
|
+
|
|
346
|
+
def _on_ldap_relation_broken(self, event: RelationBrokenEvent) -> None:
|
|
347
|
+
"""Handle the event emitted when the LDAP integration is broken."""
|
|
348
|
+
self.on.ldap_unavailable.emit(event.relation)
|
|
349
|
+
|
|
350
|
+
def _load_provider_data(self, provider_data: dict) -> LdapProviderData | None:
|
|
351
|
+
try:
|
|
352
|
+
secret_id = provider_data.get('bind_password_secret')
|
|
353
|
+
secret = self.charm.model.get_secret(id=secret_id)
|
|
354
|
+
provider_data['bind_password'] = secret.get_content().get('password')
|
|
355
|
+
return LdapProviderData(**provider_data)
|
|
356
|
+
except (ops.ModelError, ops.SecretNotFoundError, TypeError, ValidationError):
|
|
357
|
+
return None
|
|
358
|
+
|
|
359
|
+
def consume_ldap_relation_data(
|
|
360
|
+
self,
|
|
361
|
+
/,
|
|
362
|
+
relation: Relation | None = None,
|
|
363
|
+
relation_id: int | None = None,
|
|
364
|
+
) -> LdapProviderData | None:
|
|
365
|
+
"""Consume the LDAP related information from the application databag."""
|
|
366
|
+
if not relation:
|
|
367
|
+
relation = self.charm.model.get_relation(self._relation_name, relation_id)
|
|
368
|
+
|
|
369
|
+
if not relation:
|
|
370
|
+
return None
|
|
371
|
+
|
|
372
|
+
provider_data = dict(relation.data.get(relation.app))
|
|
373
|
+
if not provider_data:
|
|
374
|
+
return None
|
|
375
|
+
|
|
376
|
+
return self._load_provider_data(provider_data)
|
|
377
|
+
|
|
378
|
+
def _ready_for_relation(self, relation: Relation) -> bool:
|
|
379
|
+
if not relation.app:
|
|
380
|
+
return False
|
|
381
|
+
|
|
382
|
+
return 'urls' in relation.data[relation.app] and 'bind_dn' in relation.data[relation.app]
|
|
383
|
+
|
|
384
|
+
def ready(self, relation_id: int | None = None) -> bool:
|
|
385
|
+
"""Check if the resource has been created.
|
|
386
|
+
|
|
387
|
+
This function can be used to check if the Provider answered with data in the charm code
|
|
388
|
+
when outside an event callback.
|
|
389
|
+
|
|
390
|
+
Args:
|
|
391
|
+
relation_id (int, optional): When provided the check is done only for the relation id
|
|
392
|
+
provided, otherwise the check is done for all relations
|
|
393
|
+
|
|
394
|
+
Returns:
|
|
395
|
+
True or False
|
|
396
|
+
|
|
397
|
+
Raises:
|
|
398
|
+
IndexError: If relation_id is provided but that relation does not exist
|
|
399
|
+
"""
|
|
400
|
+
if relation_id is None:
|
|
401
|
+
return (
|
|
402
|
+
all(self._ready_for_relation(relation) for relation in self.relations)
|
|
403
|
+
if self.relations
|
|
404
|
+
else False
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
try:
|
|
408
|
+
relation = next(relation for relation in self.relations if relation.id == relation_id)
|
|
409
|
+
return self._ready_for_relation(relation)
|
|
410
|
+
except StopIteration:
|
|
411
|
+
raise IndexError(f'relation id {relation_id} cannot be accessed') from None
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Copyright 2026 Canonical Ltd.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
__version__ = '1.0.0'
|
|
File without changes
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: charmlibs-interfaces-ldap
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: The charmlibs.interfaces.ldap package.
|
|
5
|
+
Project-URL: Documentation, https://canonical.com/juju/docs/charmlibs/reference/charmlibs/interfaces/ldap
|
|
6
|
+
Project-URL: Repository, https://github.com/canonical/charmlibs/tree/main/interfaces/ldap
|
|
7
|
+
Project-URL: Issues, https://github.com/canonical/charmlibs/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/canonical/charmlibs/blob/main/interfaces/ldap/CHANGELOG.md
|
|
9
|
+
Author: Identity Team at Canonical
|
|
10
|
+
License-Expression: Apache-2.0
|
|
11
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Requires-Dist: ops<4,>=2.23.1
|
|
17
|
+
Requires-Dist: pydantic>=2.13.4
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# charmlibs.interfaces.ldap
|
|
21
|
+
|
|
22
|
+
The `ldap` interface library.
|
|
23
|
+
|
|
24
|
+
To install, add `charmlibs-interfaces-ldap` to your Python dependencies. Then in your Python code, import as:
|
|
25
|
+
|
|
26
|
+
```py
|
|
27
|
+
from charmlibs.interfaces import ldap
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
See the [reference documentation](https://canonical.com/juju/docs/charmlibs/reference/charmlibs/interfaces/ldap) for more.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
charmlibs/interfaces/ldap/__init__.py,sha256=1dUTfbVaS6zwrwD7dqSGzWx8fn4JQJqy9mE3A3MOV7M,4879
|
|
2
|
+
charmlibs/interfaces/ldap/_ldap.py,sha256=QN65iaceZFNi0FLu9sWyjZLlY8x2--xOxiRaHBivsd4,13416
|
|
3
|
+
charmlibs/interfaces/ldap/_version.py,sha256=mimKCOrPlb1vtVJVsxk3us5vTy0K8n3WoU3GISe6wFE,597
|
|
4
|
+
charmlibs/interfaces/ldap/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
charmlibs_interfaces_ldap-1.0.0.dist-info/METADATA,sha256=tN_wEViigEQ_z9nH58uql3oeK1hVP31izZofQV8C4Nk,1213
|
|
6
|
+
charmlibs_interfaces_ldap-1.0.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
7
|
+
charmlibs_interfaces_ldap-1.0.0.dist-info/RECORD,,
|