postgresql-charms-single-kernel 16.3.6__tar.gz → 16.3.8__tar.gz
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.
- {postgresql_charms_single_kernel-16.3.6 → postgresql_charms_single_kernel-16.3.8}/PKG-INFO +1 -1
- {postgresql_charms_single_kernel-16.3.6 → postgresql_charms_single_kernel-16.3.8}/pyproject.toml +1 -1
- {postgresql_charms_single_kernel-16.3.6 → postgresql_charms_single_kernel-16.3.8}/pyproject.toml.orig +1 -1
- {postgresql_charms_single_kernel-16.3.6 → postgresql_charms_single_kernel-16.3.8}/single_kernel_postgresql/charms/abstract_charm.py +7 -1
- {postgresql_charms_single_kernel-16.3.6 → postgresql_charms_single_kernel-16.3.8}/single_kernel_postgresql/config/literals.py +3 -0
- postgresql_charms_single_kernel-16.3.8/single_kernel_postgresql/events/ldap.py +98 -0
- postgresql_charms_single_kernel-16.3.8/single_kernel_postgresql/lib/charms/glauth_k8s/v0/ldap.py +583 -0
- {postgresql_charms_single_kernel-16.3.6 → postgresql_charms_single_kernel-16.3.8}/single_kernel_postgresql/managers/config.py +4 -3
- {postgresql_charms_single_kernel-16.3.6 → postgresql_charms_single_kernel-16.3.8}/single_kernel_postgresql/managers/database.py +10 -2
- {postgresql_charms_single_kernel-16.3.6 → postgresql_charms_single_kernel-16.3.8}/LICENSE +0 -0
- {postgresql_charms_single_kernel-16.3.6 → postgresql_charms_single_kernel-16.3.8}/README.md +0 -0
- {postgresql_charms_single_kernel-16.3.6 → postgresql_charms_single_kernel-16.3.8}/single_kernel_postgresql/__init__.py +0 -0
- {postgresql_charms_single_kernel-16.3.6 → postgresql_charms_single_kernel-16.3.8}/single_kernel_postgresql/charmcraft.yaml +0 -0
- {postgresql_charms_single_kernel-16.3.6 → postgresql_charms_single_kernel-16.3.8}/single_kernel_postgresql/charms/__init__.py +0 -0
- {postgresql_charms_single_kernel-16.3.6 → postgresql_charms_single_kernel-16.3.8}/single_kernel_postgresql/charms/k8s_charm.py +0 -0
- {postgresql_charms_single_kernel-16.3.6 → postgresql_charms_single_kernel-16.3.8}/single_kernel_postgresql/charms/vm_charm.py +0 -0
- {postgresql_charms_single_kernel-16.3.6 → postgresql_charms_single_kernel-16.3.8}/single_kernel_postgresql/compat/__init__.py +0 -0
- {postgresql_charms_single_kernel-16.3.6 → postgresql_charms_single_kernel-16.3.8}/single_kernel_postgresql/compat/postgresql.py +0 -0
- {postgresql_charms_single_kernel-16.3.6 → postgresql_charms_single_kernel-16.3.8}/single_kernel_postgresql/compat/pyproject.toml +0 -0
- {postgresql_charms_single_kernel-16.3.6 → postgresql_charms_single_kernel-16.3.8}/single_kernel_postgresql/config/__init__.py +0 -0
- {postgresql_charms_single_kernel-16.3.6 → postgresql_charms_single_kernel-16.3.8}/single_kernel_postgresql/config/enums.py +0 -0
- {postgresql_charms_single_kernel-16.3.6 → postgresql_charms_single_kernel-16.3.8}/single_kernel_postgresql/config/exceptions.py +0 -0
- {postgresql_charms_single_kernel-16.3.6 → postgresql_charms_single_kernel-16.3.8}/single_kernel_postgresql/config/locales.py +0 -0
- {postgresql_charms_single_kernel-16.3.6 → postgresql_charms_single_kernel-16.3.8}/single_kernel_postgresql/config/pyproject.toml +0 -0
- {postgresql_charms_single_kernel-16.3.6 → postgresql_charms_single_kernel-16.3.8}/single_kernel_postgresql/config/statuses.py +0 -0
- {postgresql_charms_single_kernel-16.3.6 → postgresql_charms_single_kernel-16.3.8}/single_kernel_postgresql/core/__init__.py +0 -0
- {postgresql_charms_single_kernel-16.3.6 → postgresql_charms_single_kernel-16.3.8}/single_kernel_postgresql/core/config.py +0 -0
- {postgresql_charms_single_kernel-16.3.6 → postgresql_charms_single_kernel-16.3.8}/single_kernel_postgresql/core/peer_relation.py +0 -0
- {postgresql_charms_single_kernel-16.3.6 → postgresql_charms_single_kernel-16.3.8}/single_kernel_postgresql/core/relation_state.py +0 -0
- {postgresql_charms_single_kernel-16.3.6 → postgresql_charms_single_kernel-16.3.8}/single_kernel_postgresql/core/state.py +0 -0
- {postgresql_charms_single_kernel-16.3.6 → postgresql_charms_single_kernel-16.3.8}/single_kernel_postgresql/events/__init__.py +0 -0
- {postgresql_charms_single_kernel-16.3.6 → postgresql_charms_single_kernel-16.3.8}/single_kernel_postgresql/events/database.py +0 -0
- {postgresql_charms_single_kernel-16.3.6 → postgresql_charms_single_kernel-16.3.8}/single_kernel_postgresql/events/postgresql.py +0 -0
- {postgresql_charms_single_kernel-16.3.6 → postgresql_charms_single_kernel-16.3.8}/single_kernel_postgresql/events/tls.py +0 -0
- {postgresql_charms_single_kernel-16.3.6 → postgresql_charms_single_kernel-16.3.8}/single_kernel_postgresql/events/tls_transfer.py +0 -0
- {postgresql_charms_single_kernel-16.3.6 → postgresql_charms_single_kernel-16.3.8}/single_kernel_postgresql/lib/charms/data_platform_libs/v0/data_interfaces.py +0 -0
- {postgresql_charms_single_kernel-16.3.6 → postgresql_charms_single_kernel-16.3.8}/single_kernel_postgresql/lib/charms/data_platform_libs/v1/data_models.py +0 -0
- {postgresql_charms_single_kernel-16.3.6 → postgresql_charms_single_kernel-16.3.8}/single_kernel_postgresql/managers/__init__.py +0 -0
- {postgresql_charms_single_kernel-16.3.6 → postgresql_charms_single_kernel-16.3.8}/single_kernel_postgresql/managers/base.py +0 -0
- {postgresql_charms_single_kernel-16.3.6 → postgresql_charms_single_kernel-16.3.8}/single_kernel_postgresql/managers/cluster.py +0 -0
- {postgresql_charms_single_kernel-16.3.6 → postgresql_charms_single_kernel-16.3.8}/single_kernel_postgresql/managers/k8s.py +0 -0
- {postgresql_charms_single_kernel-16.3.6 → postgresql_charms_single_kernel-16.3.8}/single_kernel_postgresql/managers/patroni.py +0 -0
- {postgresql_charms_single_kernel-16.3.6 → postgresql_charms_single_kernel-16.3.8}/single_kernel_postgresql/managers/tls.py +0 -0
- {postgresql_charms_single_kernel-16.3.6 → postgresql_charms_single_kernel-16.3.8}/single_kernel_postgresql/templates/__init__.py +0 -0
- {postgresql_charms_single_kernel-16.3.6 → postgresql_charms_single_kernel-16.3.8}/single_kernel_postgresql/templates/patroni.yml.j2 +0 -0
- {postgresql_charms_single_kernel-16.3.6 → postgresql_charms_single_kernel-16.3.8}/single_kernel_postgresql/utils/__init__.py +0 -0
- {postgresql_charms_single_kernel-16.3.6 → postgresql_charms_single_kernel-16.3.8}/single_kernel_postgresql/utils/arch.py +0 -0
- {postgresql_charms_single_kernel-16.3.6 → postgresql_charms_single_kernel-16.3.8}/single_kernel_postgresql/utils/filesystem.py +0 -0
- {postgresql_charms_single_kernel-16.3.6 → postgresql_charms_single_kernel-16.3.8}/single_kernel_postgresql/utils/postgresql.py +0 -0
- {postgresql_charms_single_kernel-16.3.6 → postgresql_charms_single_kernel-16.3.8}/single_kernel_postgresql/utils/secret.py +0 -0
- {postgresql_charms_single_kernel-16.3.6 → postgresql_charms_single_kernel-16.3.8}/single_kernel_postgresql/utils/status.py +0 -0
- {postgresql_charms_single_kernel-16.3.6 → postgresql_charms_single_kernel-16.3.8}/single_kernel_postgresql/workload/__init__.py +0 -0
- {postgresql_charms_single_kernel-16.3.6 → postgresql_charms_single_kernel-16.3.8}/single_kernel_postgresql/workload/base.py +0 -0
- {postgresql_charms_single_kernel-16.3.6 → postgresql_charms_single_kernel-16.3.8}/single_kernel_postgresql/workload/k8s.py +0 -0
- {postgresql_charms_single_kernel-16.3.6 → postgresql_charms_single_kernel-16.3.8}/single_kernel_postgresql/workload/paths/__init__.py +0 -0
- {postgresql_charms_single_kernel-16.3.6 → postgresql_charms_single_kernel-16.3.8}/single_kernel_postgresql/workload/paths/base.py +0 -0
- {postgresql_charms_single_kernel-16.3.6 → postgresql_charms_single_kernel-16.3.8}/single_kernel_postgresql/workload/paths/k8s.py +0 -0
- {postgresql_charms_single_kernel-16.3.6 → postgresql_charms_single_kernel-16.3.8}/single_kernel_postgresql/workload/paths/vm.py +0 -0
- {postgresql_charms_single_kernel-16.3.6 → postgresql_charms_single_kernel-16.3.8}/single_kernel_postgresql/workload/vm.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: postgresql-charms-single-kernel
|
|
3
|
-
Version: 16.3.
|
|
3
|
+
Version: 16.3.8
|
|
4
4
|
Summary: Shared and reusable code for PostgreSQL-related charms
|
|
5
5
|
Author: Canonical Data Platform
|
|
6
6
|
Author-email: Canonical Data Platform <data-platform@lists.launchpad.net>
|
|
@@ -10,6 +10,7 @@ from ops.charm import CharmBase
|
|
|
10
10
|
|
|
11
11
|
from single_kernel_postgresql.core.state import CharmState
|
|
12
12
|
from single_kernel_postgresql.events.database import DatabaseEventsHandler
|
|
13
|
+
from single_kernel_postgresql.events.ldap import LDAP
|
|
13
14
|
from single_kernel_postgresql.events.postgresql import PostgreSQLEventsHandler
|
|
14
15
|
from single_kernel_postgresql.events.tls import TLS
|
|
15
16
|
from single_kernel_postgresql.lib.charms.data_platform_libs.v0.data_interfaces import (
|
|
@@ -40,6 +41,10 @@ class AbstractPostgreSQLCharm(CharmBase, ABC):
|
|
|
40
41
|
# TLS manager so the manager can constructor-inject them for its live-fetch getters.
|
|
41
42
|
self.tls = TLS(self, self.state)
|
|
42
43
|
|
|
44
|
+
# LDAP events handler owns the ldap requirer relation and the auth parameters
|
|
45
|
+
# the config manager renders into the Patroni hba section.
|
|
46
|
+
self.ldap = LDAP(self, self.state)
|
|
47
|
+
|
|
43
48
|
# Managers
|
|
44
49
|
self.tls_manager = TLSManager(
|
|
45
50
|
state=self.state,
|
|
@@ -68,6 +73,7 @@ class AbstractPostgreSQLCharm(CharmBase, ABC):
|
|
|
68
73
|
tls_manager=self.tls_manager,
|
|
69
74
|
patroni_manager=self.patroni_manager,
|
|
70
75
|
database_manager=self.database_manager,
|
|
76
|
+
ldap_handler=self.ldap,
|
|
71
77
|
resource_provider=self.get_resource_provider,
|
|
72
78
|
request_restart=self.request_restart,
|
|
73
79
|
restart_services=self.restart_services,
|
|
@@ -116,7 +122,7 @@ class AbstractPostgreSQLCharm(CharmBase, ABC):
|
|
|
116
122
|
|
|
117
123
|
# Charm-side bridges the lib calls back into. request_restart/restart_services are
|
|
118
124
|
# substrate-tangled and stay until their own migration phases; update_config still
|
|
119
|
-
# supplies the
|
|
125
|
+
# supplies the async/watcher values those phases own; primary_endpoint is the
|
|
120
126
|
# VM's Patroni-derived primary lookup. set_unit_status routes status writes through
|
|
121
127
|
# the charm_refresh priority gate and stays until the refresh logic itself migrates
|
|
122
128
|
# into the library, at which point the managers own their status writes.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Copyright 2025 Canonical Ltd.
|
|
3
|
+
# See LICENSE file for licensing details.
|
|
4
|
+
|
|
5
|
+
"""LDAP events handler — owns the ``ldap`` requirer relation and the LDAP auth parameters.
|
|
6
|
+
|
|
7
|
+
Ported from the PostgreSQL VM and K8s charms' LDAP module. The substrate-specific
|
|
8
|
+
LDAP-sync sidecar restart stays behind the charm's ``restart_services`` bridge.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from ops import Relation
|
|
15
|
+
from ops.framework import Object
|
|
16
|
+
from ops.model import ActiveStatus
|
|
17
|
+
|
|
18
|
+
from single_kernel_postgresql.config.literals import LDAP_RELATION
|
|
19
|
+
from single_kernel_postgresql.core.state import CharmState
|
|
20
|
+
from single_kernel_postgresql.lib.charms.glauth_k8s.v0.ldap import (
|
|
21
|
+
LdapProviderData,
|
|
22
|
+
LdapReadyEvent,
|
|
23
|
+
LdapRequirer,
|
|
24
|
+
LdapUnavailableEvent,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class LDAP(Object):
|
|
31
|
+
"""In this class, we manage PostgreSQL LDAP access."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, charm, state: CharmState):
|
|
34
|
+
super().__init__(charm, "ldap")
|
|
35
|
+
self.charm = charm
|
|
36
|
+
self.state = state
|
|
37
|
+
|
|
38
|
+
# LDAP relation handles the config options for LDAP access
|
|
39
|
+
self.ldap = LdapRequirer(self.charm, LDAP_RELATION)
|
|
40
|
+
self.framework.observe(self.ldap.on.ldap_ready, self._on_ldap_ready)
|
|
41
|
+
self.framework.observe(self.ldap.on.ldap_unavailable, self._on_ldap_unavailable)
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def _relation(self) -> Relation | None:
|
|
45
|
+
"""Return the relation object."""
|
|
46
|
+
return self.model.get_relation(LDAP_RELATION)
|
|
47
|
+
|
|
48
|
+
def _on_ldap_ready(self, _: LdapReadyEvent) -> None:
|
|
49
|
+
"""Handler for the LDAP ready event."""
|
|
50
|
+
logger.debug("Enabling LDAP connection")
|
|
51
|
+
if self.charm.unit.is_leader():
|
|
52
|
+
self.state.application.data.update({"ldap_enabled": "True"})
|
|
53
|
+
|
|
54
|
+
self.charm.update_config()
|
|
55
|
+
self.charm.set_unit_status(ActiveStatus())
|
|
56
|
+
|
|
57
|
+
def _on_ldap_unavailable(self, _: LdapUnavailableEvent) -> None:
|
|
58
|
+
"""Handler for the LDAP unavailable event."""
|
|
59
|
+
logger.debug("Disabling LDAP connection")
|
|
60
|
+
if self.charm.unit.is_leader():
|
|
61
|
+
self.state.application.data.update({"ldap_enabled": "False"})
|
|
62
|
+
|
|
63
|
+
self.charm.update_config()
|
|
64
|
+
|
|
65
|
+
def get_relation_data(self) -> LdapProviderData | None:
|
|
66
|
+
"""Get the LDAP info from the LDAP Provider class."""
|
|
67
|
+
data = self.ldap.consume_ldap_relation_data(relation=self._relation)
|
|
68
|
+
if data is None:
|
|
69
|
+
logger.warning("LDAP relation is not ready")
|
|
70
|
+
|
|
71
|
+
if not self.state.peer.is_connectivity_enabled:
|
|
72
|
+
logger.warning("LDAP server will not be accessible")
|
|
73
|
+
|
|
74
|
+
return data
|
|
75
|
+
|
|
76
|
+
def get_ldap_parameters(self) -> dict[str, Any]:
|
|
77
|
+
"""Returns the LDAP configuration to use."""
|
|
78
|
+
if not self.state.application.is_cluster_initialised:
|
|
79
|
+
return {}
|
|
80
|
+
if not self.state.application.is_ldap_charm_related:
|
|
81
|
+
logger.debug("LDAP is not enabled")
|
|
82
|
+
return {}
|
|
83
|
+
|
|
84
|
+
relation_data = self.get_relation_data()
|
|
85
|
+
if relation_data is None:
|
|
86
|
+
return {}
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
"ldapbasedn": relation_data.base_dn,
|
|
90
|
+
"ldapbinddn": relation_data.bind_dn,
|
|
91
|
+
"ldapbindpasswd": relation_data.bind_password,
|
|
92
|
+
"ldaptls": relation_data.starttls,
|
|
93
|
+
"ldapurl": relation_data.urls[0],
|
|
94
|
+
# LDAP authentication parameters that are exclusive to
|
|
95
|
+
# one of the two supported modes (simple bind or search+bind)
|
|
96
|
+
# must be put at the very end of the parameters string
|
|
97
|
+
"ldapsearchfilter": self.state.config.ldap_search_filter,
|
|
98
|
+
}
|
postgresql_charms_single_kernel-16.3.8/single_kernel_postgresql/lib/charms/glauth_k8s/v0/ldap.py
ADDED
|
@@ -0,0 +1,583 @@
|
|
|
1
|
+
# Copyright 2023 Canonical Ltd.
|
|
2
|
+
# See LICENSE file for licensing details.
|
|
3
|
+
|
|
4
|
+
"""# Juju Charm Library for the `ldap` Juju Interface.
|
|
5
|
+
|
|
6
|
+
This juju charm library contains the Provider and Requirer classes for handling
|
|
7
|
+
the `ldap` interface.
|
|
8
|
+
|
|
9
|
+
## Requirer Charm
|
|
10
|
+
|
|
11
|
+
The requirer charm is expected to:
|
|
12
|
+
|
|
13
|
+
- Provide information for the provider charm to deliver LDAP related
|
|
14
|
+
information in the juju integration, in order to communicate with the LDAP
|
|
15
|
+
server and authenticate LDAP operations
|
|
16
|
+
- Listen to the custom juju event `LdapReadyEvent` to obtain the LDAP
|
|
17
|
+
related information from the integration
|
|
18
|
+
- Listen to the custom juju event `LdapUnavailableEvent` to handle the
|
|
19
|
+
situation when the LDAP integration is broken
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
|
|
23
|
+
from charms.glauth_k8s.v0.ldap import (
|
|
24
|
+
LdapRequirer,
|
|
25
|
+
LdapReadyEvent,
|
|
26
|
+
LdapUnavailableEvent,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
class RequirerCharm(CharmBase):
|
|
30
|
+
# LDAP requirer charm that integrates with an LDAP provider charm.
|
|
31
|
+
|
|
32
|
+
def __init__(self, *args):
|
|
33
|
+
super().__init__(*args)
|
|
34
|
+
|
|
35
|
+
self.ldap_requirer = LdapRequirer(self)
|
|
36
|
+
self.framework.observe(
|
|
37
|
+
self.ldap_requirer.on.ldap_ready,
|
|
38
|
+
self._on_ldap_ready,
|
|
39
|
+
)
|
|
40
|
+
self.framework.observe(
|
|
41
|
+
self.ldap_requirer.on.ldap_unavailable,
|
|
42
|
+
self._on_ldap_unavailable,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
def _on_ldap_ready(self, event: LdapReadyEvent) -> None:
|
|
46
|
+
# Consume the LDAP related information
|
|
47
|
+
ldap_data = self.ldap_requirer.consume_ldap_relation_data(
|
|
48
|
+
relation=event.relation,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# Configure the LDAP requirer charm
|
|
52
|
+
...
|
|
53
|
+
|
|
54
|
+
def _on_ldap_unavailable(self, event: LdapUnavailableEvent) -> None:
|
|
55
|
+
# Handle the situation where the LDAP integration is broken
|
|
56
|
+
...
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
As shown above, the library offers custom juju events to handle specific
|
|
60
|
+
situations, which are listed below:
|
|
61
|
+
|
|
62
|
+
- ldap_ready: event emitted when the LDAP related information is ready for
|
|
63
|
+
requirer charm to use.
|
|
64
|
+
- ldap_unavailable: event emitted when the LDAP integration is broken.
|
|
65
|
+
|
|
66
|
+
Additionally, the requirer charmed operator needs to declare the `ldap`
|
|
67
|
+
interface in the `metadata.yaml`:
|
|
68
|
+
|
|
69
|
+
```yaml
|
|
70
|
+
requires:
|
|
71
|
+
ldap:
|
|
72
|
+
interface: ldap
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Provider Charm
|
|
76
|
+
|
|
77
|
+
The provider charm is expected to:
|
|
78
|
+
|
|
79
|
+
- Use the information provided by the requirer charm to provide LDAP related
|
|
80
|
+
information for the requirer charm to connect and authenticate to the LDAP
|
|
81
|
+
server
|
|
82
|
+
- Listen to the custom juju event `LdapRequestedEvent` to offer LDAP related
|
|
83
|
+
information in the integration
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
|
|
87
|
+
from charms.glauth_k8s.v0.ldap import (
|
|
88
|
+
LdapProvider,
|
|
89
|
+
LdapRequestedEvent,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
class ProviderCharm(CharmBase):
|
|
93
|
+
# LDAP provider charm.
|
|
94
|
+
|
|
95
|
+
def __init__(self, *args):
|
|
96
|
+
super().__init__(*args)
|
|
97
|
+
|
|
98
|
+
self.ldap_provider = LdapProvider(self)
|
|
99
|
+
self.framework.observe(
|
|
100
|
+
self.ldap_provider.on.ldap_requested,
|
|
101
|
+
self._on_ldap_requested,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
def _on_ldap_requested(self, event: LdapRequestedEvent) -> None:
|
|
105
|
+
# Consume the information provided by the requirer charm
|
|
106
|
+
requirer_data = event.data
|
|
107
|
+
|
|
108
|
+
# Prepare the LDAP related information using the requirer's data
|
|
109
|
+
ldap_data = ...
|
|
110
|
+
|
|
111
|
+
# Update the integration data
|
|
112
|
+
self.ldap_provider.update_relations_app_data(
|
|
113
|
+
relation.id,
|
|
114
|
+
ldap_data,
|
|
115
|
+
)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
As shown above, the library offers custom juju events to handle specific
|
|
119
|
+
situations, which are listed below:
|
|
120
|
+
|
|
121
|
+
- ldap_requested: event emitted when the requirer charm is requesting the
|
|
122
|
+
LDAP related information in order to connect and authenticate to the LDAP server
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
import json
|
|
126
|
+
from functools import wraps
|
|
127
|
+
from string import Template
|
|
128
|
+
from typing import Any, Callable, Dict, List, Literal, Optional, Tuple, Union
|
|
129
|
+
|
|
130
|
+
import ops
|
|
131
|
+
from ops.charm import (
|
|
132
|
+
CharmBase,
|
|
133
|
+
RelationBrokenEvent,
|
|
134
|
+
RelationChangedEvent,
|
|
135
|
+
RelationCreatedEvent,
|
|
136
|
+
RelationEvent,
|
|
137
|
+
)
|
|
138
|
+
from ops.framework import EventSource, Handle, Object, ObjectEvents
|
|
139
|
+
from ops.model import Relation, SecretNotFoundError
|
|
140
|
+
from pydantic import StrictBool, ValidationError, version
|
|
141
|
+
|
|
142
|
+
# The unique CharmHub library identifier, never change it
|
|
143
|
+
LIBID = "5a535b3c4d0b40da98e29867128e57b9"
|
|
144
|
+
|
|
145
|
+
# Increment this major API version when introducing breaking changes
|
|
146
|
+
LIBAPI = 0
|
|
147
|
+
|
|
148
|
+
# Increment this PATCH version before using `charmcraft publish-lib` or reset
|
|
149
|
+
# to 0 if you are raising the major API version
|
|
150
|
+
LIBPATCH = 13
|
|
151
|
+
|
|
152
|
+
PYDEPS = ["pydantic"]
|
|
153
|
+
|
|
154
|
+
DEFAULT_RELATION_NAME = "ldap"
|
|
155
|
+
BIND_ACCOUNT_SECRET_LABEL_TEMPLATE = Template("relation-$relation_id-bind-account-secret")
|
|
156
|
+
|
|
157
|
+
PYDANTIC_IS_V1 = int(version.VERSION.split(".")[0]) < 2
|
|
158
|
+
if PYDANTIC_IS_V1:
|
|
159
|
+
# Pydantic v1 backwards compatibility logic,
|
|
160
|
+
# see https://docs.pydantic.dev/latest/migration/ for more info.
|
|
161
|
+
# This does not offer complete backwards compatibility
|
|
162
|
+
|
|
163
|
+
from pydantic import BaseModel as BaseModelV1
|
|
164
|
+
from pydantic import Field as FieldV1
|
|
165
|
+
from pydantic import validator
|
|
166
|
+
from pydantic.main import ModelMetaclass
|
|
167
|
+
|
|
168
|
+
def Field(*args: Any, **kwargs: Any) -> FieldV1: # noqa N802
|
|
169
|
+
if frozen := kwargs.pop("frozen", None):
|
|
170
|
+
kwargs["allow_mutations"] = not frozen
|
|
171
|
+
return FieldV1(*args, **kwargs)
|
|
172
|
+
|
|
173
|
+
def field_validator(*args: Any, **kwargs: Any) -> Callable:
|
|
174
|
+
if kwargs.get("mode") == "before":
|
|
175
|
+
kwargs.pop("mode")
|
|
176
|
+
kwargs["pre"] = True
|
|
177
|
+
return validator(*args, **kwargs)
|
|
178
|
+
|
|
179
|
+
encoders_config = {}
|
|
180
|
+
|
|
181
|
+
def field_serializer(*fields: str, mode: Optional[str] = None) -> Callable:
|
|
182
|
+
def _field_serializer(f: Callable, *args: Any, **kwargs: Any) -> Callable:
|
|
183
|
+
@wraps(f)
|
|
184
|
+
def wrapper(self: object, *args: Any, **kwargs: Any) -> Any:
|
|
185
|
+
return f(self, *args, **kwargs)
|
|
186
|
+
|
|
187
|
+
encoders_config[wrapper] = fields
|
|
188
|
+
return wrapper
|
|
189
|
+
|
|
190
|
+
return _field_serializer
|
|
191
|
+
|
|
192
|
+
class ModelCompatibilityMeta(ModelMetaclass):
|
|
193
|
+
def __init__(self, name: str, bases: Tuple[object], attrs: Dict) -> None:
|
|
194
|
+
if not hasattr(self, "_encoders"):
|
|
195
|
+
self._encoders = {}
|
|
196
|
+
|
|
197
|
+
self._encoders.update({
|
|
198
|
+
encoder: func
|
|
199
|
+
for func in attrs.values()
|
|
200
|
+
if callable(func) and func in encoders_config
|
|
201
|
+
for encoder in encoders_config[func]
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
super().__init__(name, bases, attrs)
|
|
205
|
+
|
|
206
|
+
class BaseModel(BaseModelV1, metaclass=ModelCompatibilityMeta):
|
|
207
|
+
def model_dump(self, *args: Any, **kwargs: Any) -> Dict:
|
|
208
|
+
d = self.dict(*args, **kwargs)
|
|
209
|
+
for name, f in self._encoders.items():
|
|
210
|
+
d[name] = f(self, d[name])
|
|
211
|
+
return d
|
|
212
|
+
|
|
213
|
+
else:
|
|
214
|
+
from pydantic import ( # type: ignore[no-redef]
|
|
215
|
+
BaseModel,
|
|
216
|
+
Field,
|
|
217
|
+
field_serializer,
|
|
218
|
+
field_validator,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def leader_unit(func: Callable) -> Callable:
|
|
223
|
+
@wraps(func)
|
|
224
|
+
def wrapper(
|
|
225
|
+
obj: Union["LdapProvider", "LdapRequirer"], *args: Any, **kwargs: Any
|
|
226
|
+
) -> Optional[Any]:
|
|
227
|
+
if not obj.unit.is_leader():
|
|
228
|
+
return None
|
|
229
|
+
|
|
230
|
+
return func(obj, *args, **kwargs)
|
|
231
|
+
|
|
232
|
+
return wrapper
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
@leader_unit
|
|
236
|
+
def _update_relation_app_databag(
|
|
237
|
+
ldap: Union["LdapProvider", "LdapRequirer"], relation: Relation, data: dict
|
|
238
|
+
) -> None:
|
|
239
|
+
if relation is None:
|
|
240
|
+
return
|
|
241
|
+
|
|
242
|
+
data = {k: str(v) if v else "" for k, v in data.items()}
|
|
243
|
+
relation.data[ldap.app].update(data)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
class Secret:
|
|
247
|
+
def __init__(self, secret: ops.Secret = None) -> None:
|
|
248
|
+
self._secret: ops.Secret = secret
|
|
249
|
+
|
|
250
|
+
@property
|
|
251
|
+
def uri(self) -> str:
|
|
252
|
+
return self._secret.id if self._secret else ""
|
|
253
|
+
|
|
254
|
+
@classmethod
|
|
255
|
+
def load(
|
|
256
|
+
cls,
|
|
257
|
+
charm: CharmBase,
|
|
258
|
+
label: str,
|
|
259
|
+
) -> Optional["Secret"]:
|
|
260
|
+
try:
|
|
261
|
+
secret = charm.model.get_secret(label=label)
|
|
262
|
+
except SecretNotFoundError:
|
|
263
|
+
return None
|
|
264
|
+
|
|
265
|
+
return Secret(secret)
|
|
266
|
+
|
|
267
|
+
@classmethod
|
|
268
|
+
def create_or_update(cls, charm: CharmBase, label: str, content: dict[str, str]) -> "Secret":
|
|
269
|
+
try:
|
|
270
|
+
secret = charm.model.get_secret(label=label)
|
|
271
|
+
secret.set_content(content=content)
|
|
272
|
+
except SecretNotFoundError:
|
|
273
|
+
secret = charm.app.add_secret(label=label, content=content)
|
|
274
|
+
|
|
275
|
+
return Secret(secret)
|
|
276
|
+
|
|
277
|
+
def grant(self, relation: Relation) -> None:
|
|
278
|
+
self._secret.grant(relation)
|
|
279
|
+
|
|
280
|
+
def remove(self) -> None:
|
|
281
|
+
self._secret.remove_all_revisions()
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
class LdapProviderBaseData(BaseModel):
|
|
285
|
+
urls: List[str] = Field(frozen=True)
|
|
286
|
+
ldaps_urls: List[str] = Field(frozen=True)
|
|
287
|
+
base_dn: str = Field(frozen=True)
|
|
288
|
+
starttls: StrictBool = Field(frozen=True)
|
|
289
|
+
|
|
290
|
+
@field_validator("urls", mode="before")
|
|
291
|
+
@classmethod
|
|
292
|
+
def validate_ldap_urls(cls, vs: List[str] | str) -> List[str]:
|
|
293
|
+
if isinstance(vs, str):
|
|
294
|
+
vs = json.loads(vs)
|
|
295
|
+
if isinstance(vs, str):
|
|
296
|
+
vs = [vs]
|
|
297
|
+
|
|
298
|
+
for v in vs:
|
|
299
|
+
if not v.startswith("ldap://"):
|
|
300
|
+
raise ValidationError.from_exception_data("Invalid LDAP URL scheme.")
|
|
301
|
+
|
|
302
|
+
return vs
|
|
303
|
+
|
|
304
|
+
@field_validator("ldaps_urls", mode="before")
|
|
305
|
+
@classmethod
|
|
306
|
+
def validate_ldaps_urls(cls, vs: List[str] | str) -> List[str]:
|
|
307
|
+
if isinstance(vs, str):
|
|
308
|
+
vs = json.loads(vs)
|
|
309
|
+
if isinstance(vs, str):
|
|
310
|
+
vs = [vs]
|
|
311
|
+
|
|
312
|
+
for v in vs:
|
|
313
|
+
if not v.startswith("ldaps://"):
|
|
314
|
+
raise ValidationError.from_exception_data("Invalid LDAPS URL scheme.")
|
|
315
|
+
|
|
316
|
+
return vs
|
|
317
|
+
|
|
318
|
+
@field_serializer("urls", "ldaps_urls")
|
|
319
|
+
def serialize_list(self, urls: List[str]) -> str:
|
|
320
|
+
return str(json.dumps(urls))
|
|
321
|
+
|
|
322
|
+
@field_validator("starttls", mode="before")
|
|
323
|
+
@classmethod
|
|
324
|
+
def deserialize_bool(cls, v: str | bool) -> bool:
|
|
325
|
+
if isinstance(v, str):
|
|
326
|
+
return True if v.casefold() == "true" else False
|
|
327
|
+
|
|
328
|
+
return v
|
|
329
|
+
|
|
330
|
+
@field_serializer("starttls")
|
|
331
|
+
def serialize_bool(self, starttls: bool) -> str:
|
|
332
|
+
return str(starttls)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
class LdapProviderData(LdapProviderBaseData):
|
|
336
|
+
bind_dn: str = Field(frozen=True)
|
|
337
|
+
bind_password: str = Field(exclude=True)
|
|
338
|
+
bind_password_secret: Optional[str] = None
|
|
339
|
+
auth_method: Literal["simple"] = Field(frozen=True)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
class LdapRequirerData(BaseModel):
|
|
343
|
+
user: str = Field(frozen=True)
|
|
344
|
+
group: str = Field(frozen=True)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
class LdapRequestedEvent(RelationEvent):
|
|
348
|
+
"""An event emitted when the LDAP integration is built."""
|
|
349
|
+
|
|
350
|
+
def __init__(self, handle: Handle, relation: Relation) -> None:
|
|
351
|
+
super().__init__(handle, relation, relation.app)
|
|
352
|
+
|
|
353
|
+
@property
|
|
354
|
+
def data(self) -> Optional[LdapRequirerData]:
|
|
355
|
+
relation_data = self.relation.data.get(self.relation.app)
|
|
356
|
+
return LdapRequirerData(**relation_data) if relation_data else None
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
class LdapProviderEvents(ObjectEvents):
|
|
360
|
+
ldap_requested = EventSource(LdapRequestedEvent)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
class LdapReadyEvent(RelationEvent):
|
|
364
|
+
"""An event when the LDAP related information is ready."""
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
class LdapUnavailableEvent(RelationEvent):
|
|
368
|
+
"""An event when the LDAP integration is unavailable."""
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
class LdapRequirerEvents(ObjectEvents):
|
|
372
|
+
ldap_ready = EventSource(LdapReadyEvent)
|
|
373
|
+
ldap_unavailable = EventSource(LdapUnavailableEvent)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
class _LdapInterface(Object):
|
|
377
|
+
def __init__(self, charm: CharmBase, relation_name: str = DEFAULT_RELATION_NAME) -> None:
|
|
378
|
+
super().__init__(charm, relation_name)
|
|
379
|
+
|
|
380
|
+
self.charm = charm
|
|
381
|
+
self.app = charm.app
|
|
382
|
+
self.unit = charm.unit
|
|
383
|
+
self._relation_name = relation_name
|
|
384
|
+
|
|
385
|
+
@property
|
|
386
|
+
def relations(self) -> List[Relation]:
|
|
387
|
+
"""The list of Relation instances associated with this relation_name."""
|
|
388
|
+
return [
|
|
389
|
+
relation
|
|
390
|
+
for relation in self.charm.model.relations[self._relation_name]
|
|
391
|
+
if self._is_relation_active(relation)
|
|
392
|
+
]
|
|
393
|
+
|
|
394
|
+
@staticmethod
|
|
395
|
+
def _is_relation_active(relation: Relation) -> bool:
|
|
396
|
+
"""Whether the relation is active based on contained data."""
|
|
397
|
+
try:
|
|
398
|
+
_ = repr(relation.data)
|
|
399
|
+
return True
|
|
400
|
+
except (RuntimeError, ops.ModelError):
|
|
401
|
+
return False
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
class LdapProvider(_LdapInterface):
|
|
405
|
+
on = LdapProviderEvents()
|
|
406
|
+
|
|
407
|
+
def __init__(
|
|
408
|
+
self,
|
|
409
|
+
charm: CharmBase,
|
|
410
|
+
relation_name: str = DEFAULT_RELATION_NAME,
|
|
411
|
+
) -> None:
|
|
412
|
+
super().__init__(charm, relation_name)
|
|
413
|
+
|
|
414
|
+
self.framework.observe(
|
|
415
|
+
self.charm.on[self._relation_name].relation_changed,
|
|
416
|
+
self._on_relation_changed,
|
|
417
|
+
)
|
|
418
|
+
self.framework.observe(
|
|
419
|
+
self.charm.on[self._relation_name].relation_broken,
|
|
420
|
+
self._on_relation_broken,
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
@leader_unit
|
|
424
|
+
def _on_relation_changed(self, event: RelationChangedEvent) -> None:
|
|
425
|
+
"""Handle the event emitted when the requirer charm provides the necessary data."""
|
|
426
|
+
self.on.ldap_requested.emit(event.relation)
|
|
427
|
+
|
|
428
|
+
@leader_unit
|
|
429
|
+
def _on_relation_broken(self, event: RelationBrokenEvent) -> None:
|
|
430
|
+
"""Handle the event emitted when the LDAP integration is broken."""
|
|
431
|
+
secret = Secret.load(
|
|
432
|
+
self.charm,
|
|
433
|
+
label=BIND_ACCOUNT_SECRET_LABEL_TEMPLATE.substitute(relation_id=event.relation.id),
|
|
434
|
+
)
|
|
435
|
+
if secret:
|
|
436
|
+
secret.remove()
|
|
437
|
+
|
|
438
|
+
def get_bind_password(self, relation_id: int) -> Optional[str]:
|
|
439
|
+
"""Retrieve the bind account password for a given integration."""
|
|
440
|
+
try:
|
|
441
|
+
secret = self.charm.model.get_secret(
|
|
442
|
+
label=BIND_ACCOUNT_SECRET_LABEL_TEMPLATE.substitute(relation_id=relation_id)
|
|
443
|
+
)
|
|
444
|
+
except SecretNotFoundError:
|
|
445
|
+
return None
|
|
446
|
+
return secret.get_content().get("password")
|
|
447
|
+
|
|
448
|
+
def update_relations_app_data(
|
|
449
|
+
self,
|
|
450
|
+
data: Union[LdapProviderBaseData, LdapProviderData],
|
|
451
|
+
/,
|
|
452
|
+
relation_id: Optional[int] = None,
|
|
453
|
+
) -> None:
|
|
454
|
+
"""An API for the provider charm to provide the LDAP related information."""
|
|
455
|
+
if not (relations := self.charm.model.relations.get(self._relation_name)):
|
|
456
|
+
return
|
|
457
|
+
|
|
458
|
+
if relation_id is not None and isinstance(data, LdapProviderData):
|
|
459
|
+
relations = [relation for relation in relations if relation.id == relation_id]
|
|
460
|
+
secret = Secret.create_or_update(
|
|
461
|
+
self.charm,
|
|
462
|
+
BIND_ACCOUNT_SECRET_LABEL_TEMPLATE.substitute(relation_id=relation_id),
|
|
463
|
+
{"password": data.bind_password},
|
|
464
|
+
)
|
|
465
|
+
secret.grant(relations[0])
|
|
466
|
+
data.bind_password_secret = secret.uri
|
|
467
|
+
|
|
468
|
+
for relation in relations:
|
|
469
|
+
_update_relation_app_databag(self.charm, relation, data.model_dump())
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
class LdapRequirer(_LdapInterface):
|
|
473
|
+
"""An LDAP requirer to consume data delivered by an LDAP provider charm."""
|
|
474
|
+
|
|
475
|
+
on = LdapRequirerEvents()
|
|
476
|
+
|
|
477
|
+
def __init__(
|
|
478
|
+
self,
|
|
479
|
+
charm: CharmBase,
|
|
480
|
+
relation_name: str = DEFAULT_RELATION_NAME,
|
|
481
|
+
*,
|
|
482
|
+
data: Optional[LdapRequirerData] = None,
|
|
483
|
+
) -> None:
|
|
484
|
+
super().__init__(charm, relation_name)
|
|
485
|
+
|
|
486
|
+
self._data = data
|
|
487
|
+
|
|
488
|
+
self.framework.observe(
|
|
489
|
+
self.charm.on[self._relation_name].relation_created,
|
|
490
|
+
self._on_ldap_relation_created,
|
|
491
|
+
)
|
|
492
|
+
self.framework.observe(
|
|
493
|
+
self.charm.on[self._relation_name].relation_changed,
|
|
494
|
+
self._on_ldap_relation_changed,
|
|
495
|
+
)
|
|
496
|
+
self.framework.observe(
|
|
497
|
+
self.charm.on[self._relation_name].relation_broken,
|
|
498
|
+
self._on_ldap_relation_broken,
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
def _on_ldap_relation_created(self, event: RelationCreatedEvent) -> None:
|
|
502
|
+
"""Handle the event emitted when an LDAP integration is created."""
|
|
503
|
+
user = self._data.user if self._data else self.app.name
|
|
504
|
+
group = self._data.group if self._data else self.model.name
|
|
505
|
+
_update_relation_app_databag(self.charm, event.relation, {"user": user, "group": group})
|
|
506
|
+
|
|
507
|
+
def _on_ldap_relation_changed(self, event: RelationChangedEvent) -> None:
|
|
508
|
+
"""Handle the event emitted when the LDAP related information is ready."""
|
|
509
|
+
provider_app = event.relation.app
|
|
510
|
+
|
|
511
|
+
if not (provider_data := event.relation.data.get(provider_app)):
|
|
512
|
+
return
|
|
513
|
+
|
|
514
|
+
provider_data = dict(provider_data)
|
|
515
|
+
if self._load_provider_data(provider_data):
|
|
516
|
+
self.on.ldap_ready.emit(event.relation)
|
|
517
|
+
|
|
518
|
+
def _on_ldap_relation_broken(self, event: RelationBrokenEvent) -> None:
|
|
519
|
+
"""Handle the event emitted when the LDAP integration is broken."""
|
|
520
|
+
self.on.ldap_unavailable.emit(event.relation)
|
|
521
|
+
|
|
522
|
+
def _load_provider_data(self, provider_data: dict) -> Optional[LdapProviderData]:
|
|
523
|
+
try:
|
|
524
|
+
secret_id = provider_data.get("bind_password_secret")
|
|
525
|
+
secret = self.charm.model.get_secret(id=secret_id)
|
|
526
|
+
provider_data["bind_password"] = secret.get_content().get("password")
|
|
527
|
+
return LdapProviderData(**provider_data)
|
|
528
|
+
except (ops.ModelError, ops.SecretNotFoundError, TypeError, ValidationError):
|
|
529
|
+
return None
|
|
530
|
+
|
|
531
|
+
def consume_ldap_relation_data(
|
|
532
|
+
self,
|
|
533
|
+
/,
|
|
534
|
+
relation: Optional[Relation] = None,
|
|
535
|
+
relation_id: Optional[int] = None,
|
|
536
|
+
) -> Optional[LdapProviderData]:
|
|
537
|
+
"""An API for the requirer charm to consume the LDAP related information in the application databag."""
|
|
538
|
+
if not relation:
|
|
539
|
+
relation = self.charm.model.get_relation(self._relation_name, relation_id)
|
|
540
|
+
|
|
541
|
+
if not relation:
|
|
542
|
+
return None
|
|
543
|
+
|
|
544
|
+
provider_data = dict(relation.data.get(relation.app))
|
|
545
|
+
if not provider_data:
|
|
546
|
+
return None
|
|
547
|
+
|
|
548
|
+
return self._load_provider_data(provider_data)
|
|
549
|
+
|
|
550
|
+
def _ready_for_relation(self, relation: Relation) -> bool:
|
|
551
|
+
if not relation.app:
|
|
552
|
+
return False
|
|
553
|
+
|
|
554
|
+
return "urls" in relation.data[relation.app] and "bind_dn" in relation.data[relation.app]
|
|
555
|
+
|
|
556
|
+
def ready(self, relation_id: Optional[int] = None) -> bool:
|
|
557
|
+
"""Check if the resource has been created.
|
|
558
|
+
|
|
559
|
+
This function can be used to check if the Provider answered with data in the charm code
|
|
560
|
+
when outside an event callback.
|
|
561
|
+
|
|
562
|
+
Args:
|
|
563
|
+
relation_id (int, optional): When provided the check is done only for the relation id
|
|
564
|
+
provided, otherwise the check is done for all relations
|
|
565
|
+
|
|
566
|
+
Returns:
|
|
567
|
+
True or False
|
|
568
|
+
|
|
569
|
+
Raises:
|
|
570
|
+
IndexError: If relation_id is provided but that relation does not exist
|
|
571
|
+
"""
|
|
572
|
+
if relation_id is None:
|
|
573
|
+
return (
|
|
574
|
+
all(self._ready_for_relation(relation) for relation in self.relations)
|
|
575
|
+
if self.relations
|
|
576
|
+
else False
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
try:
|
|
580
|
+
relation = [relation for relation in self.relations if relation.id == relation_id][0]
|
|
581
|
+
return self._ready_for_relation(relation)
|
|
582
|
+
except IndexError:
|
|
583
|
+
raise IndexError(f"relation id {relation_id} cannot be accessed")
|
|
@@ -40,6 +40,7 @@ from single_kernel_postgresql.workload.base import BaseWorkload, ResourceProvide
|
|
|
40
40
|
|
|
41
41
|
if TYPE_CHECKING:
|
|
42
42
|
# Import-time only: the VM workload pulls the snap charm lib, which K8s does not ship.
|
|
43
|
+
from single_kernel_postgresql.events.ldap import LDAP
|
|
43
44
|
from single_kernel_postgresql.managers.database import DatabaseManager
|
|
44
45
|
from single_kernel_postgresql.workload.vm import VMWorkload
|
|
45
46
|
|
|
@@ -59,6 +60,7 @@ class ConfigManager(BaseManager):
|
|
|
59
60
|
tls_manager: TLSManager,
|
|
60
61
|
patroni_manager: PatroniManager,
|
|
61
62
|
database_manager: "DatabaseManager",
|
|
63
|
+
ldap_handler: "LDAP",
|
|
62
64
|
resource_provider: Callable[[], ResourceProvider],
|
|
63
65
|
request_restart: Callable[[], None],
|
|
64
66
|
restart_services: Callable[[], None],
|
|
@@ -66,6 +68,7 @@ class ConfigManager(BaseManager):
|
|
|
66
68
|
super().__init__(state, workload, "config_manager")
|
|
67
69
|
self.tls_manager = tls_manager
|
|
68
70
|
self.patroni_manager = patroni_manager
|
|
71
|
+
self.ldap_handler = ldap_handler
|
|
69
72
|
self.database_manager = database_manager
|
|
70
73
|
# Resolved on use, not at construction: the K8s manager that provides it is built
|
|
71
74
|
# after this manager in the charm's __init__.
|
|
@@ -470,8 +473,6 @@ class ConfigManager(BaseManager):
|
|
|
470
473
|
# TODO add rel handler
|
|
471
474
|
relations_user_databases_map: dict[str, Any] | None = None,
|
|
472
475
|
# TODO add rel handler
|
|
473
|
-
ldap_parameters: dict[str, Any] | None = None,
|
|
474
|
-
# TODO add rel handler
|
|
475
476
|
async_primary_cluster_endpoint: str | None = None,
|
|
476
477
|
async_partner_addresses: list[str] | None = None,
|
|
477
478
|
async_standby_endpoints: list[str] | None = None,
|
|
@@ -520,7 +521,7 @@ class ConfigManager(BaseManager):
|
|
|
520
521
|
parameters=pg_parameters,
|
|
521
522
|
user_databases_map=relations_user_databases_map,
|
|
522
523
|
slots=replication_slots,
|
|
523
|
-
ldap_parameters=
|
|
524
|
+
ldap_parameters=self.ldap_handler.get_ldap_parameters(),
|
|
524
525
|
async_primary_cluster_endpoint=async_primary_cluster_endpoint,
|
|
525
526
|
async_partner_addresses=async_partner_addresses,
|
|
526
527
|
async_standby_endpoints=async_standby_endpoints,
|
|
@@ -570,7 +570,11 @@ class DatabaseManager(BaseManager):
|
|
|
570
570
|
for relation in self.state.model.relations.get(self.relation_name, []):
|
|
571
571
|
if relation.id == relation_id:
|
|
572
572
|
continue
|
|
573
|
-
for data in relation.data.
|
|
573
|
+
for entity, data in relation.data.items():
|
|
574
|
+
# Requested data only lives in remote databags; a non-leader unit
|
|
575
|
+
# cannot read its own application databag (ops forbids that read).
|
|
576
|
+
if entity in (self.state.model.app, self.state.model.unit):
|
|
577
|
+
continue
|
|
574
578
|
for extra_user_role in self.sanitize_extra_roles(data.get("extra-user-roles")):
|
|
575
579
|
if (
|
|
576
580
|
extra_user_role not in valid_privileges
|
|
@@ -589,7 +593,11 @@ class DatabaseManager(BaseManager):
|
|
|
589
593
|
for relation in self.state.model.relations.get(self.relation_name, []):
|
|
590
594
|
if relation.id == relation_id:
|
|
591
595
|
continue
|
|
592
|
-
for data in relation.data.
|
|
596
|
+
for entity, data in relation.data.items():
|
|
597
|
+
# Requested data only lives in remote databags; a non-leader unit
|
|
598
|
+
# cannot read its own application databag (ops forbids that read).
|
|
599
|
+
if entity in (self.state.model.app, self.state.model.unit):
|
|
600
|
+
continue
|
|
593
601
|
database = data.get("database")
|
|
594
602
|
if database is not None and (
|
|
595
603
|
len(database) > 49 or database in INVALID_DATABASE_NAMES
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|