feat: add directory adapter
LDAPS directory adapter with certificate verification, allow-listed reads and writes, escaped filters, password verification and change, password policy and account state, plus an in-memory fake. Adds the LDAP_* configuration, python-ldap with its image libraries, the CA file mount and an opt-in live test for the test OU. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N3t4WL7bd9bdHHNjEsxzqC
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
"""Opt-in check against the real test OU. Skipped unless PORTAL_LIVE_LDAP=1.
|
||||
|
||||
Set LIVE_LDAP_SERVER, LIVE_LDAP_USER_OU_DN, LIVE_LDAP_BIND_DN, LIVE_LDAP_BIND_PASSWORD,
|
||||
LIVE_LDAP_CA_FILE, LIVE_LDAP_PUBLISHER_GROUP_DN, LIVE_LDAP_ADMIN_GROUP_DN and
|
||||
LIVE_LDAP_TEST_PASSWORD (the current password of ptest-user1). Needs python-ldap (Linux).
|
||||
Only ever point this at the test OU. It restores what it writes and never changes a password.
|
||||
"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from portal.adapters.directory import Forbidden, LdapDirectory
|
||||
from portal.config.env import DirectoryConfig
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
os.environ.get("PORTAL_LIVE_LDAP") != "1", reason="set PORTAL_LIVE_LDAP=1 to run"
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def live():
|
||||
pytest.importorskip("ldap")
|
||||
env = os.environ
|
||||
config = DirectoryConfig(
|
||||
server=env["LIVE_LDAP_SERVER"],
|
||||
user_ou_dn=env["LIVE_LDAP_USER_OU_DN"],
|
||||
bind_dn=env["LIVE_LDAP_BIND_DN"],
|
||||
bind_password=env["LIVE_LDAP_BIND_PASSWORD"],
|
||||
ca_file=Path(env["LIVE_LDAP_CA_FILE"]),
|
||||
publisher_group_dn=env["LIVE_LDAP_PUBLISHER_GROUP_DN"],
|
||||
admin_group_dn=env["LIVE_LDAP_ADMIN_GROUP_DN"],
|
||||
)
|
||||
return LdapDirectory(config)
|
||||
|
||||
|
||||
def test_reads_state_and_roles(live):
|
||||
user = live.find_user("ptest-user1")
|
||||
assert user is not None and user.enabled and not user.is_publisher
|
||||
assert live.find_user("ptest-publisher").is_publisher
|
||||
assert live.find_user("ptest-admin").is_admin
|
||||
assert live.find_user("ptest-disabled").enabled is False
|
||||
assert live.find_user("ptest-expired").must_change_password is True
|
||||
assert live.find_user("does-not-exist") is None
|
||||
|
||||
|
||||
def test_users_outside_the_ou_are_not_found(live):
|
||||
assert live.find_user("Administrator") is None
|
||||
|
||||
|
||||
def test_allowed_attributes_are_written_and_restored(live):
|
||||
before = live.find_user("ptest-user1")
|
||||
try:
|
||||
live.update_attributes("ptest-user1", {"mobile": "+49 000 0000", "displayName": "Test X"})
|
||||
after = live.find_user("ptest-user1")
|
||||
assert (after.mobile, after.display_name) == ("+49 000 0000", "Test X")
|
||||
finally:
|
||||
live.update_attributes(
|
||||
"ptest-user1", {"mobile": before.mobile, "displayName": before.display_name}
|
||||
)
|
||||
assert live.find_user("ptest-user1").mobile == before.mobile
|
||||
|
||||
|
||||
def test_other_attributes_are_refused(live):
|
||||
with pytest.raises(Forbidden):
|
||||
live.update_attributes("ptest-user1", {"description": "x"})
|
||||
|
||||
|
||||
def test_password_policy_is_returned(live):
|
||||
policy = live.password_policy("ptest-user1")
|
||||
assert policy.min_length >= 0
|
||||
|
||||
|
||||
def test_verify_password(live):
|
||||
assert live.verify_password("ptest-user1", os.environ["LIVE_LDAP_TEST_PASSWORD"]) is True
|
||||
assert live.verify_password("ptest-user1", "definitely-not-the-password") is False
|
||||
assert live.verify_password("ptest-user1", "") is False
|
||||
Reference in New Issue
Block a user