mirror of
https://github.com/hanzoai/datastore.git
synced 2026-09-26 13:55:07 +00:00
228 lines
7.9 KiB
Python
228 lines
7.9 KiB
Python
import logging
|
|
import os
|
|
import time
|
|
|
|
import pytest
|
|
|
|
from helpers.cluster import ClickHouseCluster, get_docker_compose_path, run_and_check
|
|
from helpers.test_tools import TSV
|
|
|
|
LDAP_ADMIN_BIND_DN = "cn=admin,dc=example,dc=org"
|
|
LDAP_ADMIN_PASSWORD = "clickhouse"
|
|
|
|
DOCKER_COMPOSE_PATH = get_docker_compose_path()
|
|
|
|
cluster = ClickHouseCluster(__file__)
|
|
|
|
# Instance with follow_referrals=true: role mapping should work via referral chasing.
|
|
# ClickHouse connects to openldap2 for bind (users live in dc=referral,dc=org),
|
|
# then role mapping search targets dc=example,dc=org which is NOT served by openldap2,
|
|
# so openldap2 returns a default referral to openldap where the groups actually exist.
|
|
instance_follow = cluster.add_instance(
|
|
"instance_follow",
|
|
main_configs=["configs/ldap_follow_referrals_true.xml"],
|
|
user_configs=["configs/users.xml"],
|
|
stay_alive=True,
|
|
with_ldap=True,
|
|
)
|
|
|
|
# Instance with follow_referrals=false: role mapping should NOT work (referral ignored)
|
|
instance_no_follow = cluster.add_instance(
|
|
"instance_no_follow",
|
|
main_configs=["configs/ldap_follow_referrals_false.xml"],
|
|
user_configs=["configs/users.xml"],
|
|
stay_alive=True,
|
|
)
|
|
|
|
|
|
def wait_openldap2_ready(timeout=180):
|
|
openldap2_docker_id = cluster.get_instance_docker_id("openldap2")
|
|
start = time.time()
|
|
attempts = 0
|
|
logging.info("Waiting for openldap2 readiness")
|
|
while time.time() - start < timeout:
|
|
attempts += 1
|
|
try:
|
|
cluster.exec_in_container(
|
|
openldap2_docker_id,
|
|
[
|
|
"bash",
|
|
"-c",
|
|
"test -f /tmp/.openldap-initialized"
|
|
" && /opt/bitnami/openldap/bin/ldapsearch -x -H ldap://localhost:1389"
|
|
" -D cn=admin,dc=referral,dc=org -w clickhouse -b dc=referral,dc=org"
|
|
" | grep -c '^dn: cn=janedoe,ou=users,dc=referral,dc=org$'"
|
|
" | grep 1 >> /dev/null",
|
|
],
|
|
user="root",
|
|
)
|
|
logging.info("openldap2 is ready")
|
|
return
|
|
except Exception as ex:
|
|
if attempts % 10 == 0:
|
|
logging.info("openldap2 not ready after %s attempts: %s", attempts, str(ex))
|
|
else:
|
|
logging.debug("openldap2 not ready yet: %s", str(ex))
|
|
time.sleep(1)
|
|
raise Exception("Timed out waiting for openldap2")
|
|
|
|
|
|
def add_ldap_group(group_cn, member_cn):
|
|
"""Add a group to the primary LDAP server (openldap).
|
|
|
|
The member DN uses dc=referral,dc=org because that is the bind_dn
|
|
that ClickHouse substitutes into the role mapping search filter
|
|
({bind_dn} = cn=<user>,ou=users,dc=referral,dc=org).
|
|
"""
|
|
code, (stdout, stderr) = cluster.ldap_container.exec_run(
|
|
[
|
|
"sh",
|
|
"-c",
|
|
"""echo "dn: cn={group_cn},dc=example,dc=org
|
|
objectClass: top
|
|
objectClass: groupOfNames
|
|
cn: {group_cn}
|
|
member: cn={member_cn},ou=users,dc=referral,dc=org" | \
|
|
ldapadd -H ldap://{host}:{port} -D "{admin_bind_dn}" -x -w {admin_password}
|
|
""".format(
|
|
host=cluster.ldap_host,
|
|
port=cluster.ldap_port,
|
|
admin_bind_dn=LDAP_ADMIN_BIND_DN,
|
|
admin_password=LDAP_ADMIN_PASSWORD,
|
|
group_cn=group_cn,
|
|
member_cn=member_cn,
|
|
),
|
|
],
|
|
demux=True,
|
|
)
|
|
logging.debug(
|
|
f"add_ldap_group code:{code} stdout:{stdout}, stderr:{stderr}"
|
|
)
|
|
assert code == 0, f"ldapadd failed: code={code}, stdout={stdout!r}, stderr={stderr!r}"
|
|
|
|
|
|
def delete_ldap_group(group_cn):
|
|
code, (stdout, stderr) = cluster.ldap_container.exec_run(
|
|
[
|
|
"sh",
|
|
"-c",
|
|
"""ldapdelete -r 'cn={group_cn},dc=example,dc=org' \
|
|
-H ldap://{host}:{port} -D "{admin_bind_dn}" -x -w {admin_password}
|
|
""".format(
|
|
host=cluster.ldap_host,
|
|
port=cluster.ldap_port,
|
|
admin_bind_dn=LDAP_ADMIN_BIND_DN,
|
|
admin_password=LDAP_ADMIN_PASSWORD,
|
|
group_cn=group_cn,
|
|
),
|
|
],
|
|
demux=True,
|
|
)
|
|
logging.debug(
|
|
f"delete_ldap_group code:{code} stdout:{stdout}, stderr:{stderr}"
|
|
)
|
|
assert code == 0, f"ldapdelete failed: code={code}, stdout={stdout!r}, stderr={stderr!r}"
|
|
|
|
|
|
@pytest.fixture(scope="module", autouse=True)
|
|
def ldap_cluster():
|
|
docker_compose_ldap2 = os.path.join(
|
|
DOCKER_COMPOSE_PATH, "docker_compose_ldap2.yml"
|
|
)
|
|
try:
|
|
cluster.start()
|
|
|
|
# Start the second LDAP server (referral server).
|
|
# openldap2 serves dc=referral,dc=org and has a default referral
|
|
# pointing to openldap:1389 for any DN it cannot resolve locally.
|
|
run_and_check(
|
|
cluster.compose_cmd(
|
|
"-f",
|
|
docker_compose_ldap2,
|
|
"up",
|
|
"--force-recreate",
|
|
"-d",
|
|
"--no-build",
|
|
)
|
|
)
|
|
wait_openldap2_ready()
|
|
|
|
yield cluster
|
|
finally:
|
|
# Tear down openldap2 explicitly since it was started outside of
|
|
# cluster.start() and cluster.shutdown() does not know about it.
|
|
run_and_check(
|
|
cluster.compose_cmd(
|
|
"-f",
|
|
docker_compose_ldap2,
|
|
"down",
|
|
"--volumes",
|
|
),
|
|
nothrow=True,
|
|
)
|
|
cluster.shutdown()
|
|
|
|
|
|
def test_follow_referrals_true(ldap_cluster):
|
|
"""With follow_referrals=true, the role mapping search targets dc=example,dc=org
|
|
on openldap2 which does not serve that suffix. openldap2 returns a default referral
|
|
to openldap:1389. The LDAP library chases the referral and finds the group,
|
|
so the user authenticates and gets the mapped role."""
|
|
instance_follow.query(
|
|
"DROP ROLE IF EXISTS role_ref", user="common_user", password="qwerty"
|
|
)
|
|
try:
|
|
instance_follow.query(
|
|
"CREATE ROLE role_ref", user="common_user", password="qwerty"
|
|
)
|
|
add_ldap_group(group_cn="clickhouse-role_ref", member_cn="johndoe")
|
|
|
|
assert instance_follow.query(
|
|
"SELECT currentUser()", user="johndoe", password="qwertz"
|
|
) == TSV([["johndoe"]])
|
|
|
|
result = instance_follow.query(
|
|
"SELECT role_name FROM system.current_roles ORDER BY role_name",
|
|
user="johndoe",
|
|
password="qwertz",
|
|
)
|
|
assert result == TSV([["role_ref"]])
|
|
finally:
|
|
instance_follow.query(
|
|
"DROP ROLE IF EXISTS role_ref", user="common_user", password="qwerty"
|
|
)
|
|
try:
|
|
delete_ldap_group(group_cn="clickhouse-role_ref")
|
|
except AssertionError:
|
|
logging.warning("Could not delete LDAP group clickhouse-role_ref")
|
|
|
|
|
|
def test_follow_referrals_false(ldap_cluster):
|
|
"""With follow_referrals=false, the role mapping search targets dc=example,dc=org
|
|
on openldap2 which returns a referral. The LDAP library does not chase it,
|
|
so the search fails and authentication is rejected."""
|
|
instance_no_follow.query(
|
|
"DROP ROLE IF EXISTS role_noref", user="common_user", password="qwerty"
|
|
)
|
|
try:
|
|
instance_no_follow.query(
|
|
"CREATE ROLE role_noref", user="common_user", password="qwerty"
|
|
)
|
|
add_ldap_group(group_cn="clickhouse-role_noref", member_cn="johndoe")
|
|
|
|
error = instance_no_follow.query_and_get_error(
|
|
"SELECT currentUser()", user="johndoe", password="qwertz"
|
|
)
|
|
err = error.lower()
|
|
assert "johndoe" in err and "authentication failed" in err, (
|
|
f"Unexpected error message: {error}"
|
|
)
|
|
finally:
|
|
instance_no_follow.query(
|
|
"DROP ROLE IF EXISTS role_noref", user="common_user", password="qwerty"
|
|
)
|
|
try:
|
|
delete_ldap_group(group_cn="clickhouse-role_noref")
|
|
except AssertionError:
|
|
logging.warning("Could not delete LDAP group clickhouse-role_noref")
|