File 0001-LDAP-auth-support.patch of Package mpd
From e3e558ba7c615f997708baad77c1b1954ec3caf4 Mon Sep 17 00:00:00 2001
From: Mia Herkt <mia@0x0.st>
Date: Fri, 28 Mar 2025 01:44:13 +0100
Subject: [PATCH] LDAP auth support
---
meson.build | 6 ++
src/Permission.cxx | 144 ++++++++++++++++++++++++++++++++++++++-
src/config/Option.hxx | 6 ++
src/config/Templates.cxx | 6 ++
4 files changed, 160 insertions(+), 2 deletions(-)
diff --git a/meson.build b/meson.build
index a3193254..6445fc7e 100644
--- a/meson.build
+++ b/meson.build
@@ -609,6 +609,11 @@ else
install_dir = get_option('bindir')
endif
+ldap_deps = [
+ c_compiler.find_library('ldap'),
+ c_compiler.find_library('lber'),
+]
+
mpd = build_target(
target_name,
sources,
@@ -641,6 +646,7 @@ mpd = build_target(
more_deps,
chromaprint_dep,
fmt_dep,
+ ldap_deps,
],
link_args: link_args,
build_by_default: not get_option('fuzzer'),
diff --git a/src/Permission.cxx b/src/Permission.cxx
index d7d54718..7b248e0d 100644
--- a/src/Permission.cxx
+++ b/src/Permission.cxx
@@ -34,6 +34,15 @@ static constexpr struct {
};
static std::map<std::string, unsigned, std::less<>> permission_passwords;
+static struct LDAPDirectory {
+ const char
+ * uri,
+ * dn,
+ * pw,
+ * authmeth,
+ * base,
+ * scope;
+} directory;
static unsigned permission_default;
@@ -124,6 +133,18 @@ initPermissions(const ConfigData &config)
});
}
#endif
+
+ auto getp = [&](ConfigOption c) -> const char* {
+ const ConfigParam* cp = config.GetParam(c);
+ if (cp == nullptr) return nullptr;
+ return cp->value.c_str();
+ };
+ directory.uri = getp(ConfigOption::LDAP_BIND_URI);
+ directory.dn = getp(ConfigOption::LDAP_BIND_DN);
+ directory.pw = getp(ConfigOption::LDAP_BIND_PW);
+ directory.authmeth= getp(ConfigOption::LDAP_BIND_METH);
+ directory.base = getp(ConfigOption::LDAP_Q_BASE);
+ directory.scope = getp(ConfigOption::LDAP_Q_SCOPE);
}
#ifdef HAVE_TCP
@@ -140,12 +161,131 @@ GetPermissionsFromAddress(SocketAddress address) noexcept
#endif
+namespace ldap { extern "C" {
+# include <ldap.h>
+ char** ldap_get_values (LDAP*, LDAPMessage*, char*);
+ int ldap_count_values (char**);
+ void ldap_value_free (char**);
+ int ldap_unbind_s (LDAP*);
+ int ldap_simple_bind_s(LDAP*, const char*, const char*);
+}}
+
+static ldap::LDAP*
+openLDAPConnection() {
+ using namespace ldap;
+ LDAP* ctx;
+ ldap_initialize(&ctx, directory.uri);
+ {int v=3; ldap_set_option(ctx, LDAP_OPT_PROTOCOL_VERSION, &v);}
+ int res;
+ if (directory.authmeth == nullptr) {
+ res = ldap_simple_bind_s(ctx, directory.dn, directory.pw);
+ } else {
+ res = ldap_sasl_interactive_bind_s(ctx, directory.dn, directory.authmeth, nullptr, nullptr, LDAP_SASL_QUIET, nullptr, nullptr);
+ }
+ if (res != 0) {
+ ldap_unbind_s(ctx);
+ throw std::runtime_error(ldap_err2string(res));
+ // return nullptr;
+ }
+
+ return ctx;
+}
+
+static int
+getPermissionFromLDAPLookup(const char* password, unsigned* permission) {
+ using namespace ldap;
+ typedef char const* str;
+
+ LDAP* l = openLDAPConnection();
+ if (l == nullptr) return -1;
+
+ str const filter = "(&(preSharedKey=*)(authorizedService=music*))";
+ str const attrs[] = {
+ "authorizedService",
+ "preSharedKey",
+ // "cn",
+ // "aRecord",
+ // "aAAARecord",
+ // "associatedDomain",
+ nullptr,
+ };
+
+ LDAPMessage* m;
+ int e = ldap_search_ext_s(l, directory.base, LDAP_SCOPE_SUBTREE, filter, const_cast<char**>(attrs),
+ /* attrs only*/ 0,
+ /*server controls*/ nullptr,
+ /*client controls*/ nullptr,
+ /* timeout*/ nullptr,
+ /* sizelimit*/ 0, &m);
+
+ bool found = false;
+
+ if (e != LDAP_SUCCESS) {
+ throw std::runtime_error(ldap_err2string(e));
+ //goto fail;
+ }
+
+ int permset = 0;
+ for (LDAPMessage* ent = ldap_first_entry(l, m); ent != nullptr;
+ ent = ldap_next_entry(l, ent)) {
+ char* dn = ldap_get_dn(l, ent);
+ char** keys = ldap_get_values(l, ent, const_cast<char*>("preSharedKey"));
+ char** svcs = ldap_get_values(l, ent, const_cast<char*>("authorizedService"));
+
+ size_t nkeys = ldap_count_values(keys);
+ size_t nsvcs = ldap_count_values(svcs);
+
+ bool keyMatch = false;
+ for (size_t i = 0; i < nkeys; ++i) {
+ if (strcmp(password, keys[i]) == 0) {
+ keyMatch = true;
+ break;
+ }
+ }
+
+ if (keyMatch) for (size_t i = 0; i < nsvcs; ++i) {
+ auto svc = svcs[i];
+ if (strncmp(svc, "music", 5) == 0) {
+ if (svc[5] == 0) /* all privileges */ {
+ found = true;
+ permset |= (unsigned)-1;
+ } else if(svc[5] == '=') /* defined privs */ {
+ found = true;
+ char* priv = svc + 6, *s;
+ for (char* p = strtok_r(priv, ",", &s); p != nullptr;
+ p = strtok_r(nullptr, ",", &s)) {
+ permset |= ParsePermission(p);
+ }
+ }
+ }
+ }
+
+ ldap_value_free(keys);
+ ldap_value_free(svcs);
+
+ ldap_memfree(dn);
+ if (found) break;
+ }
+
+ if(found) {
+ *permission = permset;
+ }
+
+ /*fail:*/ ldap_msgfree(m);
+ ldap_unbind_s(l);
+ return found ? 0 : -1;
+}
+
std::optional<unsigned>
GetPermissionFromPassword(const char *password) noexcept
{
auto i = permission_passwords.find(password);
- if (i == permission_passwords.end())
- return std::nullopt;
+ if (i == permission_passwords.end()) {
+ unsigned perm;
+ if (getPermissionFromLDAPLookup(password, &perm) != 0)
+ return std::nullopt;
+ return perm;
+ }
return i->second;
}
diff --git a/src/config/Option.hxx b/src/config/Option.hxx
index 31353788..9bed17ba 100644
--- a/src/config/Option.hxx
+++ b/src/config/Option.hxx
@@ -59,6 +59,12 @@ enum class ConfigOption {
GAPLESS_MP3_PLAYBACK,
AUTO_UPDATE,
AUTO_UPDATE_DEPTH,
+ LDAP_BIND_URI,
+ LDAP_BIND_DN,
+ LDAP_BIND_PW,
+ LDAP_BIND_METH,
+ LDAP_Q_BASE,
+ LDAP_Q_SCOPE,
MIXRAMP_ANALYZER,
diff --git a/src/config/Templates.cxx b/src/config/Templates.cxx
index e3794e00..0df87bd3 100644
--- a/src/config/Templates.cxx
+++ b/src/config/Templates.cxx
@@ -58,6 +58,12 @@ const ConfigTemplate config_param_templates[] = {
{ "auto_update" },
{ "auto_update_depth" },
{ "mixramp_analyzer" },
+ { "ldap_bind_uri" },
+ { "ldap_bind_dn" },
+ { "ldap_bind_pw" },
+ { "ldap_bind_meth" },
+ { "ldap_q_base" },
+ { "ldap_q_scope" },
};
static constexpr unsigned n_config_param_templates =
--
2.49.0