Skip to content

Commit 138095b

Browse files
[DPE-6345] LDAP V: Define mapping option (#849)
1 parent 327d491 commit 138095b

File tree

5 files changed

+94
-1
lines changed

5 files changed

+94
-1
lines changed

config.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,12 @@ options:
6969
Enable synchronized sequential scans.
7070
type: boolean
7171
default: true
72+
ldap_map:
73+
description: |
74+
List of mapped LDAP group names to PostgreSQL group names, separated by commas.
75+
The map is used to assign LDAP synchronized users to PostgreSQL authorization groups.
76+
Example: <ldap_group_1>=<psql_group_1>,<ldap_group_2>=<psql_group_2>
77+
type: string
7278
ldap_search_filter:
7379
description: |
7480
The LDAP search filter to match users with.

lib/charms/postgresql_k8s/v0/postgresql.py

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535

3636
# Increment this PATCH version before using `charmcraft publish-lib` or reset
3737
# to 0 if you are raising the major API version
38-
LIBPATCH = 47
38+
LIBPATCH = 49
3939

4040
# Groups to distinguish HBA access
4141
ACCESS_GROUP_IDENTITY = "identity_access"
@@ -776,6 +776,42 @@ def is_restart_pending(self) -> bool:
776776
if connection:
777777
connection.close()
778778

779+
@staticmethod
780+
def build_postgresql_group_map(group_map: Optional[str]) -> List[Tuple]:
781+
"""Build the PostgreSQL authorization group-map.
782+
783+
Args:
784+
group_map: serialized group-map with the following format:
785+
<ldap_group_1>=<psql_group_1>,
786+
<ldap_group_2>=<psql_group_2>,
787+
...
788+
789+
Returns:
790+
List of LDAP group to PostgreSQL group tuples.
791+
"""
792+
if group_map is None:
793+
return []
794+
795+
group_mappings = group_map.split(",")
796+
group_mappings = (mapping.strip() for mapping in group_mappings)
797+
group_map_list = []
798+
799+
for mapping in group_mappings:
800+
mapping_parts = mapping.split("=")
801+
if len(mapping_parts) != 2:
802+
raise ValueError("The group-map must contain value pairs split by commas")
803+
804+
ldap_group = mapping_parts[0]
805+
psql_group = mapping_parts[1]
806+
807+
if psql_group in [*ACCESS_GROUPS, PERMISSIONS_GROUP_ADMIN]:
808+
logger.warning(f"Tried to assign LDAP users to forbidden group: {psql_group}")
809+
continue
810+
811+
group_map_list.append((ldap_group, psql_group))
812+
813+
return group_map_list
814+
779815
@staticmethod
780816
def build_postgresql_parameters(
781817
config_options: dict, available_memory: int, limit_memory: Optional[int] = None
@@ -855,3 +891,34 @@ def validate_date_style(self, date_style: str) -> bool:
855891
return True
856892
except psycopg2.Error:
857893
return False
894+
895+
def validate_group_map(self, group_map: Optional[str]) -> bool:
896+
"""Validate the PostgreSQL authorization group-map.
897+
898+
Args:
899+
group_map: serialized group-map with the following format:
900+
<ldap_group_1>=<psql_group_1>,
901+
<ldap_group_2>=<psql_group_2>,
902+
...
903+
904+
Returns:
905+
Whether the group-map is valid.
906+
"""
907+
if group_map is None:
908+
return True
909+
910+
try:
911+
group_map = self.build_postgresql_group_map(group_map)
912+
except ValueError:
913+
return False
914+
915+
for _, psql_group in group_map:
916+
with self._connect_to_database() as connection, connection.cursor() as cursor:
917+
query = SQL("SELECT TRUE FROM pg_roles WHERE rolname={};")
918+
query = query.format(Literal(psql_group))
919+
cursor.execute(query)
920+
921+
if cursor.fetchone() is None:
922+
return False
923+
924+
return True

src/charm.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from charms.grafana_agent.v0.cos_agent import COSAgentProvider, charm_tracing_config
2525
from charms.operator_libs_linux.v2 import snap
2626
from charms.postgresql_k8s.v0.postgresql import (
27+
ACCESS_GROUP_IDENTITY,
2728
ACCESS_GROUPS,
2829
REQUIRED_PLUGINS,
2930
PostgreSQL,
@@ -1381,13 +1382,16 @@ def _setup_ldap_sync(self, postgres_snap: snap.Snap | None = None) -> None:
13811382
ldap_base_dn = ldap_params["ldapbasedn"]
13821383
ldap_bind_username = ldap_params["ldapbinddn"]
13831384
ldap_bind_password = ldap_params["ldapbindpasswd"]
1385+
ldap_group_mappings = self.postgresql.build_postgresql_group_map(self.config.ldap_map)
13841386

13851387
postgres_snap.set({
13861388
"ldap-sync.ldap_host": ldap_host,
13871389
"ldap-sync.ldap_port": ldap_port,
13881390
"ldap-sync.ldap_base_dn": ldap_base_dn,
13891391
"ldap-sync.ldap_bind_username": ldap_bind_username,
13901392
"ldap-sync.ldap_bind_password": ldap_bind_password,
1393+
"ldap-sync.ldap_group_identity": json.dumps(ACCESS_GROUP_IDENTITY),
1394+
"ldap-sync.ldap_group_mappings": json.dumps(ldap_group_mappings),
13911395
"ldap-sync.postgres_host": "127.0.0.1",
13921396
"ldap-sync.postgres_port": DATABASE_PORT,
13931397
"ldap-sync.postgres_database": DATABASE_DEFAULT_NAME,
@@ -2067,6 +2071,9 @@ def _validate_config_options(self) -> None:
20672071
"instance_default_text_search_config config option has an invalid value"
20682072
)
20692073

2074+
if not self.postgresql.validate_group_map(self.config.ldap_map):
2075+
raise ValueError("ldap_map config option has an invalid value")
2076+
20702077
if not self.postgresql.validate_date_style(self.config.request_date_style):
20712078
raise ValueError("request_date_style config option has an invalid value")
20722079

src/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ class CharmConfig(BaseConfigModel):
2929
instance_max_locks_per_transaction: int | None
3030
instance_password_encryption: str | None
3131
instance_synchronize_seqscans: bool | None
32+
ldap_map: str | None
3233
ldap_search_filter: str | None
3334
logging_client_min_messages: str | None
3435
logging_log_connections: bool | None

tests/unit/test_charm.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1445,6 +1445,7 @@ def test_validate_config_options(harness):
14451445
):
14461446
_charm_lib.return_value.get_postgresql_text_search_configs.return_value = []
14471447
_charm_lib.return_value.validate_date_style.return_value = False
1448+
_charm_lib.return_value.validate_group_map.return_value = False
14481449
_charm_lib.return_value.get_postgresql_timezones.return_value = []
14491450

14501451
# Test instance_default_text_search_config exception
@@ -1463,6 +1464,17 @@ def test_validate_config_options(harness):
14631464
"pg_catalog.test"
14641465
]
14651466

1467+
# Test ldap_map exception
1468+
with harness.hooks_disabled():
1469+
harness.update_config({"ldap_map": "ldap_group="})
1470+
1471+
with pytest.raises(ValueError) as e:
1472+
harness.charm._validate_config_options()
1473+
assert str(e.value) == "ldap_map config option has an invalid value"
1474+
1475+
_charm_lib.return_value.validate_group_map.assert_called_once_with("ldap_group=")
1476+
_charm_lib.return_value.validate_group_map.return_value = True
1477+
14661478
# Test request_date_style exception
14671479
with harness.hooks_disabled():
14681480
harness.update_config({"request_date_style": "ISO, TEST"})

0 commit comments

Comments
 (0)