From 7eedf50c1207da393225b41631b12de81161e95e Mon Sep 17 00:00:00 2001 From: Marcin Maliszkiewicz Date: Mon, 12 Jan 2026 19:01:05 +0100 Subject: [PATCH] auth: ldap: add permissions reload to unified cache The LDAP server may change role-chain assignments without notifying Scylla. As a result, effective permissions can change, so some form of polling is required. Currently, this is handled via cache expiration. However, the unified cache is designed to be consistent and does not support expiration. To provide an equivalent mechanism for LDAP, we will periodically reload the permissions portion of the new cache at intervals matching the previously configured expiration time. --- auth/cache.cc | 18 ++++++++++++++++++ auth/cache.hh | 1 + auth/ldap_role_manager.cc | 32 ++++++++++++++++++++++++++++++-- auth/ldap_role_manager.hh | 9 +++++++++ test/ldap/role_manager_test.cc | 1 + 5 files changed, 59 insertions(+), 2 deletions(-) diff --git a/auth/cache.cc b/auth/cache.cc index f3b5ca791d..e75a6e7164 100644 --- a/auth/cache.cc +++ b/auth/cache.cc @@ -8,6 +8,7 @@ #include "auth/cache.hh" #include "auth/common.hh" +#include "auth/role_or_anonymous.hh" #include "auth/roles-metadata.hh" #include "cql3/query_processor.hh" #include "cql3/untyped_result_set.hh" @@ -97,6 +98,23 @@ future<> cache::prune(const resource& r) { } } +future<> cache::reload_all_permissions() noexcept { + SCYLLA_ASSERT(_permission_loader); + auto units = co_await get_units(_loading_sem, 1, _as); + const role_or_anonymous anon; + for (auto& [res, perms] : _anonymous_permissions) { + perms = co_await _permission_loader(anon, res); + } + for (auto& [role, entry] : _roles) { + auto& perms_cache = entry->cached_permissions; + auto r = role_or_anonymous(role); + for (auto& [res, perms] : perms_cache) { + perms = co_await _permission_loader(r, res); + } + } + logger.debug("Reloaded auth cache with {} entries", _roles.size()); +} + future> cache::fetch_role(const role_name_t& role) const { auto rec = make_lw_shared(); rec->version = _current_version; diff --git a/auth/cache.hh b/auth/cache.hh index e245463924..f411245421 100644 --- a/auth/cache.hh +++ b/auth/cache.hh @@ -55,6 +55,7 @@ public: void set_permission_loader(permission_loader_func loader); future get_permissions(const role_or_anonymous& role, const resource& r); future<> prune(const resource& r); + future<> reload_all_permissions() noexcept; future<> load_all(); future<> load_roles(std::unordered_set roles); static bool includes_table(const table_id&) noexcept; diff --git a/auth/ldap_role_manager.cc b/auth/ldap_role_manager.cc index c01e830214..b667e8e6c5 100644 --- a/auth/ldap_role_manager.cc +++ b/auth/ldap_role_manager.cc @@ -88,10 +88,16 @@ static const class_registrator< ldap_role_manager::ldap_role_manager( std::string_view query_template, std::string_view target_attr, std::string_view bind_name, std::string_view bind_password, + uint32_t permissions_update_interval_in_ms, + utils::observer permissions_update_interval_in_ms_observer, cql3::query_processor& qp, ::service::raft_group0_client& rg0c, ::service::migration_manager& mm, cache& cache) : _std_mgr(qp, rg0c, mm, cache), _group0_client(rg0c), _query_template(query_template), _target_attr(target_attr), _bind_name(bind_name) , _bind_password(bind_password) - , _connection_factory(bind(std::mem_fn(&ldap_role_manager::reconnect), std::ref(*this))) { + , _permissions_update_interval_in_ms(permissions_update_interval_in_ms) + , _permissions_update_interval_in_ms_observer(std::move(permissions_update_interval_in_ms_observer)) + , _connection_factory(bind(std::mem_fn(&ldap_role_manager::reconnect), std::ref(*this))) + , _cache(cache) + , _cache_pruner(make_ready_future<>()) { } ldap_role_manager::ldap_role_manager(cql3::query_processor& qp, ::service::raft_group0_client& rg0c, ::service::migration_manager& mm, cache& cache) @@ -100,6 +106,8 @@ ldap_role_manager::ldap_role_manager(cql3::query_processor& qp, ::service::raft_ qp.db().get_config().ldap_attr_role(), qp.db().get_config().ldap_bind_dn(), qp.db().get_config().ldap_bind_passwd(), + qp.db().get_config().permissions_update_interval_in_ms(), + qp.db().get_config().permissions_update_interval_in_ms.observe([this] (const uint32_t& v) { _permissions_update_interval_in_ms = v; }), qp, rg0c, mm, @@ -119,6 +127,22 @@ future<> ldap_role_manager::start() { return make_exception_future( std::runtime_error(fmt::format("error getting LDAP server address from template {}", _query_template))); } + _cache_pruner = futurize_invoke([this] () -> future<> { + while (true) { + try { + co_await seastar::sleep_abortable(std::chrono::milliseconds(_permissions_update_interval_in_ms), _as); + } catch (const seastar::sleep_aborted&) { + co_return; // ignore + } + co_await _cache.container().invoke_on_all([] (cache& c) -> future<> { + try { + co_await c.reload_all_permissions(); + } catch (...) { + mylog.warn("Cache reload all permissions failed: {}", std::current_exception()); + } + }); + } + }); return _std_mgr.start(); } @@ -175,7 +199,11 @@ future ldap_role_manager::reconnect() { future<> ldap_role_manager::stop() { _as.request_abort(); - return _std_mgr.stop().then([this] { return _connection_factory.stop(); }); + return std::move(_cache_pruner).then([this] { + return _std_mgr.stop(); + }).then([this] { + return _connection_factory.stop(); + }); } future<> ldap_role_manager::create(std::string_view name, const role_config& config, ::service::group0_batch& mc) { diff --git a/auth/ldap_role_manager.hh b/auth/ldap_role_manager.hh index a68c9b96dc..e2f27d8fad 100644 --- a/auth/ldap_role_manager.hh +++ b/auth/ldap_role_manager.hh @@ -10,6 +10,7 @@ #pragma once #include +#include #include #include "ent/ldap/ldap_connection.hh" @@ -34,14 +35,22 @@ class ldap_role_manager : public role_manager { seastar::sstring _target_attr; ///< LDAP entry attribute containing the Scylla role name. seastar::sstring _bind_name; ///< Username for LDAP simple bind. seastar::sstring _bind_password; ///< Password for LDAP simple bind. + + uint32_t _permissions_update_interval_in_ms; + utils::observer _permissions_update_interval_in_ms_observer; + mutable ldap_reuser _connection_factory; // Potentially modified by query_granted(). seastar::abort_source _as; + cache& _cache; + seastar::future<> _cache_pruner; public: ldap_role_manager( std::string_view query_template, ///< LDAP query template as described in Scylla documentation. std::string_view target_attr, ///< LDAP entry attribute containing the Scylla role name. std::string_view bind_name, ///< LDAP bind credentials. std::string_view bind_password, ///< LDAP bind credentials. + uint32_t permissions_update_interval_in_ms, + utils::observer permissions_update_interval_in_ms_observer, cql3::query_processor& qp, ///< Passed to standard_role_manager. ::service::raft_group0_client& rg0c, ///< Passed to standard_role_manager. ::service::migration_manager& mm, ///< Passed to standard_role_manager. diff --git a/test/ldap/role_manager_test.cc b/test/ldap/role_manager_test.cc index bd2f51ab87..61ded11bc1 100644 --- a/test/ldap/role_manager_test.cc +++ b/test/ldap/role_manager_test.cc @@ -284,6 +284,7 @@ auto make_ldap_manager(cql_test_env& env, sstring query_template = default_query }; return std::unique_ptr( new auth::ldap_role_manager(query_template, /*target_attr=*/"cn", manager_dn, manager_password, + env.db_config().permissions_update_interval_in_ms(), env.db_config().permissions_update_interval_in_ms.observe([] (const uint32_t& v) {}), env.local_qp(), env.get_raft_group0_client(), env.migration_manager().local(), env.auth_cache().local()), std::move(stop_role_manager)); }