Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions interfaces/ldap/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## 1.1.0 - 11 September 2026

- Add `ldaps_enabled` convenience property to `LdapProviderBaseData` (derived from `ldaps_urls`).

## 1.0.0

Initial release. Migrated from `charms.glauth_k8s.v0.ldap` (v0.13).
5 changes: 3 additions & 2 deletions interfaces/ldap/interface/v0/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ be able to provide or consume the LDAP authentication configuration data.
```mermaid
flowchart TD
Requirer -- user, \ngroup --> Provider
Provider -- urls, \nbase_dn, \nbind_dn, \nbind_password_secret, \nauth_method, \nstarttls --> Requirer
Provider -- urls, \nldaps_urls, \nbase_dn, \nbind_dn, \nbind_password_secret, \nauth_method, \nstarttls --> Requirer
```

## Behavior
Expand Down Expand Up @@ -69,12 +69,13 @@ It should be placed in the **application** databag.
related-endpoint: ldap
application-data:
urls: [ldap://ldap.canonical.com:3893, ldap://ldap.ubuntu.com:3893]
ldaps_urls: [ldaps://ldap.canonical.com:3894, ldaps://ldap.ubuntu.com:3894]
base_dn: dc=canonical,dc=com
bind_dn: cn=app,ou=model,dc=canonical,dc=com
bind_password_secret: secret://59060ecc-0495-4a80-8006-5f1fc13fd783/cjqub6vubg2s77p3nio0
auth_method: simple
starttls: true
``````
```

### Requirer

Expand Down
6 changes: 6 additions & 0 deletions interfaces/ldap/interface/v0/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ class LdapProviderData(BaseModel):
title='LDAP URLs',
example=['ldap://ldap.canonical.com:3893', 'ldap://ldap.ubuntu.com:3893'],
)
ldaps_urls: list[AnyUrl] = Field(
default=[],
description='List of LDAPS URLs',
title='LDAPS URLs',
example=['ldaps://ldap.canonical.com:3894', 'ldaps://ldap.ubuntu.com:3894'],
)
base_dn: str = Field(
description='The base entry as the starting point for LDAP search operation',
title='Base DN',
Expand Down
2 changes: 2 additions & 0 deletions interfaces/ldap/src/charmlibs/interfaces/ldap/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ def _on_ldap_requested(self, event: LdapRequestedEvent) -> None:

from ._ldap import (
LdapProvider,
LdapProviderBaseData,
LdapProviderData,
LdapReadyEvent,
LdapRequestedEvent,
Expand All @@ -149,6 +150,7 @@ def _on_ldap_requested(self, event: LdapRequestedEvent) -> None:

__all__ = [
'LdapProvider',
'LdapProviderBaseData',
'LdapProviderData',
'LdapReadyEvent',
'LdapRequestedEvent',
Expand Down
5 changes: 5 additions & 0 deletions interfaces/ldap/src/charmlibs/interfaces/ldap/_ldap.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,11 @@ class LdapProviderBaseData(BaseModel):
base_dn: str = Field(frozen=True)
starttls: StrictBool = Field(frozen=True)

@property
def ldaps_enabled(self) -> bool:
"""Whether LDAPS is enabled based on the presence of LDAPS URLs."""
return bool(self.ldaps_urls)

@field_validator('urls', mode='before')
@classmethod
def validate_ldap_urls(cls, vs: list[str] | str) -> list[str]:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@
# See the License for the specific language governing permissions and
# limitations under the License.

__version__ = '1.0.0'
__version__ = '1.1.0'
99 changes: 99 additions & 0 deletions interfaces/ldap/tests/unit/test_ldap_provider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Copyright 2026 Canonical Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import json
from typing import Any

import pytest
import yaml
from ops import CharmBase
from ops.testing import Context, Relation, State

from charmlibs.interfaces.ldap import (
LdapProvider,
LdapProviderBaseData,
LdapProviderData,
)

PROVIDER_METADATA = """
name: provider-tester
provides:
ldap:
interface: ldap
"""


class LdapProviderCharm(CharmBase):
"""Test charm that wraps LdapProvider."""

def __init__(self, *args: Any) -> None:
super().__init__(*args)
self.ldap_provider = LdapProvider(self)


@pytest.fixture
def provider_context() -> Context:
"""ops.testing Context for the test LdapProviderCharm."""
return Context(LdapProviderCharm, meta=yaml.safe_load(PROVIDER_METADATA), juju_version='3.2.1')


@pytest.mark.parametrize(
'ldaps_urls, expected_enabled',
[
(['ldaps://path.to.glauth:3894'], True),
([], False),
],
)
def test_update_relations_app_data_ldaps_urls_and_property(
provider_context: Context, ldaps_urls: list[str], expected_enabled: bool
) -> None:
relation = Relation('ldap')
state = State(leader=True, relations=[relation])

with provider_context(provider_context.on.update_status(), state) as mgr:
data = LdapProviderData(
urls=['ldap://path.to.glauth:3893'],
ldaps_urls=ldaps_urls,
base_dn='dc=glauth,dc=com',
bind_dn='cn=serviceuser,ou=svcaccts,dc=glauth,dc=com',
bind_password='password',
auth_method='simple',
starttls=True,
)
assert data.ldaps_enabled is expected_enabled

mgr.charm.ldap_provider.update_relations_app_data(data, relation_id=relation.id)
state_out = mgr.run()

app_data = state_out.get_relation(relation.id).local_app_data
assert app_data['ldaps_urls'] == json.dumps(ldaps_urls)
assert 'ldaps_enabled' not in app_data


def test_base_data_ldaps_enabled_property() -> None:
data_with_ldaps = LdapProviderBaseData(
urls=['ldap://path.to.glauth:3893'],
ldaps_urls=['ldaps://path.to.glauth:3894'],
base_dn='dc=glauth,dc=com',
starttls=True,
)
assert data_with_ldaps.ldaps_enabled is True

data_without_ldaps = LdapProviderBaseData(
urls=['ldap://path.to.glauth:3893'],
ldaps_urls=[],
base_dn='dc=glauth,dc=com',
starttls=True,
)
assert data_without_ldaps.ldaps_enabled is False
26 changes: 26 additions & 0 deletions interfaces/ldap/tests/unit/test_ldap_requirer.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import json

import pytest
from ops.testing import Context, Model, Relation, Secret, State

from charmlibs.interfaces.ldap import LdapReadyEvent, LdapUnavailableEvent
Expand Down Expand Up @@ -79,6 +80,31 @@ def test_consume_ldap_relation_data(context: Context, provider_data: dict[str, s
assert result.bind_dn == provider_data['bind_dn']
assert result.bind_password == password
assert result.bind_password_secret == secret.id
assert result.ldaps_enabled is True


@pytest.mark.parametrize(
'ldaps_urls_json, expected',
[
('["ldaps://path.to.glauth:3894"]', True),
('[]', False),
],
)
def test_consume_ldap_relation_data_ldaps_enabled(
context: Context, provider_data: dict[str, str], ldaps_urls_json: str, expected: bool
) -> None:
password = 'p4ssw0rd'
secret = Secret(id='secret:bind-0003', tracked_content={'password': password})
data = {**provider_data, 'bind_password_secret': secret.id, 'ldaps_urls': ldaps_urls_json}
relation = Relation('ldap', remote_app_data=data)
state = create_state(leader=True, relations=[relation], secrets=[secret], containers=[])

with context(context.on.relation_changed(relation), state) as mgr:
mgr.run()
result = mgr.charm.ldap_requirer.consume_ldap_relation_data()

assert result is not None
assert result.ldaps_enabled is expected


def test_consume_ldap_relation_data_inaccessible_secret(
Expand Down
Loading