Files
scylladb/test/ldap/ldap_connection_test.cc
Piotr Smaron 288f9b2b15 Introduce LDAP role manager & saslauthd authenticator
This PR extends authentication with 2 mechanisms:
- a new role_manager subclass, which allows managing users via
LDAP server,
- a new authenticator, which delegates plaintext authentication
to a running saslauthd daemon.

The features have been ported from the enterprise repository
with their test.py tests and the documentation as part of
changing license to source available.

Fixes: scylladb/scylla-enterprise#5000
Fixes: scylladb/scylla-enterprise#5001

Closes scylladb/scylladb#22030
2025-01-12 14:50:29 +02:00

294 lines
12 KiB
C++

/*
* Copyright (C) 2019 ScyllaDB
*/
/*
* SPDX-License-Identifier: LicenseRef-ScyllaDB-Source-Available-1.0
*/
#define LDAP_DEPRECATED 1
#include <seastar/testing/thread_test_case.hh>
#include <cassert>
#include <exception>
#include <seastar/core/seastar.hh>
#include <seastar/core/when_all.hh>
#include <seastar/util/log.hh>
#include <seastar/util/defer.hh>
#include <set>
#include <string>
#include "ent/ldap/ldap_connection.hh"
#include "test/lib/exception_utils.hh"
#include "ldap_common.hh"
#include "seastarx.hh"
extern "C" {
#include <ldap.h>
}
namespace {
logger mylog{"ldap_connection_test"}; // `log` is taken by math.
void set_defbase() {
ldap_set_option(nullptr, LDAP_OPT_DEFBASE, base_dn);
}
const std::set<std::string> results_expected_from_search_base_dn{
"dc=example,dc=com",
"cn=root,dc=example,dc=com",
"ou=People,dc=example,dc=com",
"uid=cassandra,ou=People,dc=example,dc=com",
"uid=jsmith,ou=People,dc=example,dc=com",
"uid=jdoe,ou=People,dc=example,dc=com",
"cn=role1,dc=example,dc=com",
"cn=role2,dc=example,dc=com",
"cn=role3,dc=example,dc=com",
};
/// Ignores exceptions from LDAP operations. Useful for failure-injection tests, which must
/// tolerate aborted communication.
ldap_msg_ptr ignore(std::exception_ptr) {
return ldap_msg_ptr();
}
/// Extracts entries from an ldap_search result.
std::set<std::string> entries(LDAP* ld, LDAPMessage* res) {
BOOST_REQUIRE_EQUAL(LDAP_RES_SEARCH_ENTRY, ldap_msgtype(res));
std::set<std::string> entry_set;
for (auto e = ldap_first_entry(ld, res); e; e = ldap_next_entry(ld, e)) {
char* dn = ldap_get_dn(ld, e);
entry_set.insert(dn);
ldap_memfree(dn);
}
return entry_set;
}
future<ldap_msg_ptr> search(ldap_connection& conn, const char* term) {
return conn.search(
const_cast<char*>(term),
LDAP_SCOPE_SUBTREE,
/*filter=*/nullptr,
/*attrs=*/nullptr,
/*attrsonly=*/0,
/*serverctrls=*/nullptr,
/*clientctrls=*/nullptr,
/*timeout=*/nullptr,
/*sizelimit=*/0);
}
future<ldap_msg_ptr> bind(ldap_connection& conn) {
return conn.simple_bind(manager_dn, manager_password);
}
/// Creates an ldap_connection, invokes a function on it, then waits for its closing. Must be
/// invoked from Seastar thread.
void with_ldap_connection(seastar::connected_socket&& socket, std::function<void(ldap_connection&)> f) {
mylog.trace("with_ldap_connection");
ldap_connection c(std::move(socket));
auto do_close = defer([&] { c.close().get(); });
mylog.trace("with_ldap_connection: invoking f");
f(c);
mylog.trace("with_ldap_connection done");
}
/// Connects to an address, then invokes with_ldap_connection on the resulting socket. Must be
/// invoked from Seastar thread.
void with_ldap_connection(const seastar::socket_address& a, std::function<void(ldap_connection&)> f) {
with_ldap_connection(connect(a).get(), f);
}
} // anonymous namespace
// Tests default (non-custom) libber networking. Failure here indicates a likely bug in test.py's
// LDAP setup.
SEASTAR_THREAD_TEST_CASE(bind_with_default_io) {
set_defbase();
const auto server_uri = "ldap://localhost:" + ldap_port;
LDAP *manager_client_state{nullptr};
BOOST_REQUIRE_EQUAL(LDAP_SUCCESS, ldap_initialize(&manager_client_state, server_uri.c_str()));
static constexpr int v3 = LDAP_VERSION3;
BOOST_REQUIRE_EQUAL(LDAP_OPT_SUCCESS, ldap_set_option(manager_client_state, LDAP_OPT_PROTOCOL_VERSION, &v3));
// Retry on EINTR, rather than return error:
BOOST_REQUIRE_EQUAL(LDAP_OPT_SUCCESS, ldap_set_option(manager_client_state, LDAP_OPT_RESTART, LDAP_OPT_ON));
BOOST_REQUIRE_EQUAL(LDAP_SUCCESS, ldap_simple_bind_s(manager_client_state, manager_dn, manager_password));
BOOST_REQUIRE_EQUAL(LDAP_SUCCESS, ldap_unbind(manager_client_state));
}
SEASTAR_THREAD_TEST_CASE(bind_with_custom_sockbuf_io) {
set_defbase();
mylog.trace("bind_with_custom_sockbuf_io");
with_ldap_connection(local_ldap_address, [] (ldap_connection& c) {
mylog.trace("bind_with_custom_sockbuf_io invoking bind");
const auto res = bind(c).get();
BOOST_REQUIRE_EQUAL(LDAP_RES_BIND, ldap_msgtype(res.get()));
});
mylog.trace("bind_with_custom_sockbuf_io done");
}
SEASTAR_THREAD_TEST_CASE(search_with_custom_sockbuf_io) {
set_defbase();
mylog.trace("search_with_custom_sockbuf_io");
with_ldap_connection(local_ldap_address, [] (ldap_connection& c) {
mylog.trace("search_with_custom_sockbuf_io: invoking search");
const auto res = search(c, base_dn).get();
mylog.trace("search_with_custom_sockbuf_io: got result");
const auto actual = entries(c.get_ldap(), res.get());
const std::set<std::string>& expected = results_expected_from_search_base_dn;
BOOST_REQUIRE_EQUAL_COLLECTIONS(actual.cbegin(), actual.cend(), expected.cbegin(), expected.cend());
});
mylog.trace("search_with_custom_sockbuf_io done");
}
SEASTAR_THREAD_TEST_CASE(multiple_outstanding_operations) {
set_defbase();
mylog.trace("multiple_outstanding_operations");
with_ldap_connection(local_ldap_address, [] (ldap_connection& c) {
mylog.trace("multiple_outstanding_operations: bind");
BOOST_REQUIRE_EQUAL(LDAP_RES_BIND, ldap_msgtype(bind(c).get().get()));
std::vector<future<ldap_msg_ptr>> results_base;
for (size_t i = 0; i < 30; ++i) {
mylog.trace("multiple_outstanding_operations: invoking search base #{}", i);
results_base.push_back(search(c, base_dn));
mylog.trace("multiple_outstanding_operations: search base #{} got future {}", i, static_cast<const void*>(&results_base.back()));
}
std::vector<future<ldap_msg_ptr>> results_jsmith;
for (size_t i = 0; i < 30; ++i) {
mylog.trace("multiple_outstanding_operations: invoking search jsmith #{}", i);
results_jsmith.push_back(search(c, "uid=jsmith,ou=People,dc=example,dc=com"));
mylog.trace("multiple_outstanding_operations: search jsmith #{} got future {}", i, static_cast<const void*>(&results_jsmith.back()));
}
using boost::test_tools::per_element;
mylog.trace("multiple_outstanding_operations: check base results");
future<> base_result_fut = parallel_for_each(results_base, [&c] (future<ldap_msg_ptr>& res) {
const auto actual_base = entries(c.get_ldap(), res.get().get());
BOOST_TEST_INFO("result for " << &res);
BOOST_TEST(actual_base == results_expected_from_search_base_dn, per_element());
return make_ready_future();
});
mylog.trace("multiple_outstanding_operations: check jsmith result");
static const std::set<std::string> expected_jsmith{"uid=jsmith,ou=People,dc=example,dc=com"};
future<> jsmith_result_fut = parallel_for_each(results_jsmith, [&c] (future<ldap_msg_ptr>& res) {
const auto actual_jsmith = entries(c.get_ldap(), res.get().get());
BOOST_TEST_INFO("result for " << &res);
BOOST_TEST(actual_jsmith == expected_jsmith, per_element());
return make_ready_future();
});
when_all(std::move(base_result_fut), std::move(jsmith_result_fut)).get();
});
mylog.trace("multiple_outstanding_operations done");
}
SEASTAR_THREAD_TEST_CASE(early_shutdown) {
set_defbase();
mylog.trace("early_shutdown: noop");
with_ldap_connection(local_ldap_address, [] (ldap_connection&) {});
mylog.trace("early_shutdown: bind");
with_ldap_connection(local_ldap_address, [] (ldap_connection& c) { bind(c).handle_exception(&ignore).get(); });
mylog.trace("early_shutdown: search");
with_ldap_connection(
local_ldap_address, [] (ldap_connection& c) { search(c, base_dn).handle_exception(&ignore).get(); });
}
SEASTAR_THREAD_TEST_CASE(bind_after_fail) {
set_defbase();
mylog.trace("bind_after_fail: wonky connection");
with_ldap_connection(local_fail_inject_address, [] (ldap_connection& wonky_conn) {
bind(wonky_conn).handle_exception(&ignore).get();
});
mylog.trace("bind_after_fail: solid connection");
with_ldap_connection(local_ldap_address, [] (ldap_connection& c) {
const auto res = bind(c).get();
BOOST_REQUIRE_EQUAL(LDAP_RES_BIND, ldap_msgtype(res.get()));
});
mylog.trace("bind_after_fail done");
}
SEASTAR_THREAD_TEST_CASE(search_after_fail) {
set_defbase();
mylog.trace("search_after_fail: wonky connection");
with_ldap_connection(local_fail_inject_address, [] (ldap_connection& wonky_conn) {
search(wonky_conn, base_dn).handle_exception(&ignore).get();
});
mylog.trace("search_after_fail: solid connection");
with_ldap_connection(local_ldap_address, [] (ldap_connection& c) {
const auto res = search(c, base_dn).get();
mylog.trace("search_after_fail: got search result");
const auto actual = entries(c.get_ldap(), res.get());
BOOST_REQUIRE_EQUAL_COLLECTIONS(
actual.cbegin(), actual.cend(),
results_expected_from_search_base_dn.cbegin(), results_expected_from_search_base_dn.cend());
});
mylog.trace("search_after_fail done");
}
SEASTAR_THREAD_TEST_CASE(multiple_outstanding_operations_on_failing_connection) {
set_defbase();
mylog.trace("multiple_outstanding_operations_on_failing_connection");
with_ldap_connection(local_fail_inject_address, [] (ldap_connection& c) {
mylog.trace("multiple_outstanding_operations_on_failing_connection: invoking bind");
bind(c).handle_exception(&ignore).get();;
std::vector<future<ldap_msg_ptr>> results_base;
for (size_t i = 0; i < 10; ++i) {
mylog.trace("multiple_outstanding_operations_on_failing_connection: invoking search base");
results_base.push_back(search(c, base_dn).handle_exception(&ignore));
}
std::vector<future<ldap_msg_ptr>> results_jsmith;
for (size_t i = 0; i < 10; ++i) {
mylog.trace("multiple_outstanding_operations_on_failing_connection: invoking search jsmith");
results_jsmith.push_back(search(c, "uid=jsmith,ou=People,dc=example,dc=com").handle_exception(&ignore));
}
mylog.trace("multiple_outstanding_operations_on_failing_connection: getting base results");
when_all_succeed(results_base.begin(), results_base.end()).get();
mylog.trace("multiple_outstanding_operations_on_failing_connection: getting jsmith results");
when_all_succeed(results_jsmith.begin(), results_jsmith.end()).get();
});
mylog.trace("multiple_outstanding_operations_on_failing_connection done");
}
using exception_predicate::message_contains;
SEASTAR_THREAD_TEST_CASE(bind_after_close) {
set_defbase();
ldap_connection c(connect(local_ldap_address).get());
c.close().get();
BOOST_REQUIRE_EXCEPTION(bind(c).get(), std::runtime_error, message_contains("ldap_connection"));
}
SEASTAR_THREAD_TEST_CASE(search_after_close) {
set_defbase();
ldap_connection c(connect(local_ldap_address).get());
c.close().get();
BOOST_REQUIRE_EXCEPTION(search(c, base_dn).get(), std::runtime_error, message_contains("ldap_connection"));
}
SEASTAR_THREAD_TEST_CASE(close_after_close) {
set_defbase();
ldap_connection c(connect(local_ldap_address).get());
c.close().get();
BOOST_REQUIRE_EXCEPTION(c.close().get(), std::runtime_error, message_contains("ldap_connection"));
}
SEASTAR_THREAD_TEST_CASE(severed_connection_yields_exceptional_future) {
set_defbase();
with_ldap_connection(local_fail_inject_address, [] (ldap_connection& c) {
int up = 1;
while (up) {
search(c, base_dn)
.handle_exception_type([&] (std::runtime_error&) { up = 0; return ldap_msg_ptr(); })
.get();
}
});
}