9#include <QLoggingCategory>
10#include <QVariantList>
12#ifdef CUTELYST_PLUGIN_AUTHENTICATION_HAS_LDAP
17using namespace Qt::StringLiterals;
19Q_LOGGING_CATEGORY(C_AUTH_LDAP,
"cutelyst.plugin.authentication.ldap", QtWarningMsg)
22 : m_serverUris({u
"ldap://127.0.0.1:389"_s})
23 , m_userField(u
"id"_s)
24 , m_idAttribute(u
"id"_s)
25 , m_userScope(SearchScope::SubTree)
37 if (m_userField.isEmpty()) {
38 qCWarning(C_AUTH_LDAP) <<
"User field is empty";
42 return findUserByAttribute(c, m_userField, userInfo.value(m_userField));
55 if (frozenUser.canConvert<QVariantMap>()) {
57 user.
setData(frozenUser.toMap());
61 return findUserByAttribute(c, m_idAttribute, frozenUser.toString());
66 const QString &password)
const
70 if (password.isEmpty()) {
74 const QString userDn = user.
value(u
"dn"_s).toString();
75 if (userDn.isEmpty()) {
76 qCWarning(C_AUTH_LDAP) <<
"LDAP self-check failed, user has no DN value";
80#ifdef CUTELYST_PLUGIN_AUTHENTICATION_HAS_LDAP
81 if (m_serverUris.isEmpty()) {
82 qCWarning(C_AUTH_LDAP) <<
"No LDAP server URI configured";
86 const QByteArray uri = m_serverUris.join(u
" "_s).toUtf8();
89 int rc = ldap_initialize(&ld, uri.constData());
90 if (rc != LDAP_SUCCESS || !ld) {
91 qCWarning(C_AUTH_LDAP) <<
"Failed to initialize LDAP connection:" << ldap_err2string(rc);
95 int ldapVersion = LDAP_VERSION3;
96 ldap_set_option(ld, LDAP_OPT_PROTOCOL_VERSION, &ldapVersion);
99 rc = ldap_start_tls_s(ld,
nullptr,
nullptr);
100 if (rc != LDAP_SUCCESS) {
101 qCWarning(C_AUTH_LDAP) <<
"Failed to start TLS:" << ldap_err2string(rc);
102 ldap_unbind_ext_s(ld,
nullptr,
nullptr);
107 const QByteArray passwordUtf8 = password.toUtf8();
109 bindCred.bv_val =
const_cast<char *
>(passwordUtf8.constData());
110 bindCred.bv_len =
static_cast<ber_len_t
>(passwordUtf8.size());
112 rc = ldap_sasl_bind_s(
113 ld, userDn.toUtf8().constData(), LDAP_SASL_SIMPLE, &bindCred,
nullptr,
nullptr,
nullptr);
115 if (rc == LDAP_INAPPROPRIATE_AUTH) {
116 qCWarning(C_AUTH_LDAP) <<
"LDAP user bind failed with Inappropriate authentication. "
117 "Server may require StartTLS/LDAPS or stronger auth."
118 <<
"startTls=" << m_startTls <<
"dn=" << userDn;
121 ldap_unbind_ext_s(ld,
nullptr,
nullptr);
123 return rc == LDAP_SUCCESS;
125 qCWarning(C_AUTH_LDAP) <<
"StoreLDAP requested but plugin was built without LDAP support";
152 m_bindPassword = bindPassword;
157 m_userBaseDn = baseDn;
182 return m_idAttribute;
225#ifdef CUTELYST_PLUGIN_AUTHENTICATION_HAS_LDAP
230 case StoreLDAP::SearchScope::Base:
231 return LDAP_SCOPE_BASE;
232 case StoreLDAP::SearchScope::OneLevel:
233 return LDAP_SCOPE_ONELEVEL;
234 case StoreLDAP::SearchScope::SubTree:
236 return LDAP_SCOPE_SUBTREE;
240QString escapeFilterValue(QStringView value)
243 escaped.reserve(value.size());
245 for (
const QChar ch : value) {
246 switch (ch.unicode()) {
248 escaped += u
"\\2a"_s;
251 escaped += u
"\\28"_s;
254 escaped += u
"\\29"_s;
257 escaped += u
"\\5c"_s;
260 escaped += u
"\\00"_s;
271QStringList valuesToStrings(
struct berval **values)
278 for (
int i = 0; values[i] !=
nullptr; ++i) {
279 const QByteArray value(values[i]->bv_val, values[i]->bv_len);
280 out.push_back(QString::fromUtf8(value));
289 StoreLDAP::findUserByAttribute(
Context *c,
const QString &attribute,
const QString &value)
293 AuthenticationUser user;
295 if (value.isEmpty()) {
299#ifdef CUTELYST_PLUGIN_AUTHENTICATION_HAS_LDAP
300 if (m_serverUris.isEmpty()) {
301 qCWarning(C_AUTH_LDAP) <<
"No LDAP server URI configured";
305 const QByteArray uri = m_serverUris.join(u
" "_s).toUtf8();
308 int rc = ldap_initialize(&ld, uri.constData());
309 if (rc != LDAP_SUCCESS || !ld) {
310 qCWarning(C_AUTH_LDAP) <<
"Failed to initialize LDAP connection:" << ldap_err2string(rc);
314 int ldapVersion = LDAP_VERSION3;
315 ldap_set_option(ld, LDAP_OPT_PROTOCOL_VERSION, &ldapVersion);
318 rc = ldap_start_tls_s(ld,
nullptr,
nullptr);
319 if (rc != LDAP_SUCCESS) {
320 qCWarning(C_AUTH_LDAP) <<
"Failed to start TLS:" << ldap_err2string(rc);
321 ldap_unbind_ext_s(ld,
nullptr,
nullptr);
328 if (!m_bindDn.isEmpty() || !m_bindPassword.isEmpty()) {
329 if (m_bindDn.isEmpty()) {
330 qCWarning(C_AUTH_LDAP) <<
"LDAP bind password is set but bind DN is empty";
331 ldap_unbind_ext_s(ld,
nullptr,
nullptr);
335 const QByteArray bindPassword = m_bindPassword.toUtf8();
336 const QByteArray bindDnUtf8 = m_bindDn.toUtf8();
338 bindCred.bv_val =
const_cast<char *
>(bindPassword.constData());
339 bindCred.bv_len =
static_cast<ber_len_t
>(bindPassword.size());
341 rc = ldap_sasl_bind_s(
342 ld, bindDnUtf8.constData(), LDAP_SASL_SIMPLE, &bindCred,
nullptr,
nullptr,
nullptr);
343 if (rc != LDAP_SUCCESS) {
344 if (rc == LDAP_INAPPROPRIATE_AUTH) {
345 qCWarning(C_AUTH_LDAP) <<
"LDAP bind failed with Inappropriate authentication. "
346 "Server may require StartTLS/LDAPS or stronger auth."
347 <<
"startTls=" << m_startTls <<
"bindDn=" << m_bindDn;
349 qCWarning(C_AUTH_LDAP) <<
"LDAP bind failed:" << ldap_err2string(rc);
351 ldap_unbind_ext_s(ld,
nullptr,
nullptr);
356 QString filter = m_userFilter;
357 const QString escapedValue = escapeFilterValue(value);
358 if (filter.isEmpty()) {
359 filter = u
"(%1=%2)"_s.arg(attribute, escapedValue);
361 filter = filter.arg(escapedValue);
364 QByteArrayList attrStorage;
365 QVector<char *> attrs;
366 attrs.reserve(m_attributes.size() + 1);
367 for (
const QString &attr : m_attributes) {
368 attrStorage.push_back(attr.toUtf8());
370 for (QByteArray &attr : attrStorage) {
371 attrs.push_back(attr.data());
373 attrs.push_back(
nullptr);
375 LDAPMessage *result =
nullptr;
376 rc = ldap_search_ext_s(ld,
377 m_userBaseDn.isEmpty() ?
nullptr : m_userBaseDn.toUtf8().constData(),
378 toLdapScope(m_userScope),
379 filter.toUtf8().constData(),
380 m_attributes.isEmpty() ?
nullptr : attrs.data(),
388 if (rc != LDAP_SUCCESS) {
389 qCWarning(C_AUTH_LDAP) <<
"LDAP search failed:" << ldap_err2string(rc);
390 ldap_unbind_ext_s(ld,
nullptr,
nullptr);
394 LDAPMessage *entry = ldap_first_entry(ld, result);
396 ldap_msgfree(result);
397 ldap_unbind_ext_s(ld,
nullptr,
nullptr);
401 char *dnRaw = ldap_get_dn(ld, entry);
403 const QString dn = QString::fromUtf8(dnRaw);
408 BerElement *ber =
nullptr;
409 for (
char *attr = ldap_first_attribute(ld, entry, &ber); attr !=
nullptr;
410 attr = ldap_next_attribute(ld, entry, ber)) {
411 const QString attrName = QString::fromUtf8(attr);
413 berval **values = ldap_get_values_len(ld, entry, attr);
414 const QStringList stringValues = valuesToStrings(values);
416 if (stringValues.size() == 1) {
417 user.
insert(attrName, stringValues.constFirst());
418 }
else if (!stringValues.isEmpty()) {
420 list.reserve(stringValues.size());
421 for (
const QString &item : stringValues) {
422 list.push_back(item);
424 user.
insert(attrName, list);
428 ldap_value_free_len(values);
437 const QVariant idValue =
438 m_idAttribute == u
"dn"_s ? user.
value(u
"dn"_s) : user.
value(m_idAttribute);
439 if (!idValue.isNull()) {
442 user = AuthenticationUser();
445 ldap_msgfree(result);
446 ldap_unbind_ext_s(ld,
nullptr,
nullptr);
449 qCWarning(C_AUTH_LDAP) <<
"StoreLDAP requested but plugin was built without LDAP support";
Container for user data retrieved from an AuthenticationStore.
QVariant value(const QString &key, const QVariant &defaultValue=QVariant()) const
void setData(const QVariantMap &data)
void setId(const QVariant &id)
void insert(const QString &key, const QVariant &value)
AuthenticationUser fromSession(Context *c, const QVariant &frozenUser) override final
void setBindDn(const QString &bindDn)
void setUserBaseDn(const QString &baseDn)
QString userField() const
QVariant forSession(Context *c, const AuthenticationUser &user) override final
void setBindPassword(const QString &bindPassword)
QString userBaseDn() const
void setUserFilter(const QString &userFilter)
QStringList attributes() const
SearchScope userScope() const
void setIdAttribute(const QString &idAttribute)
void setStartTls(bool startTls)
QString idAttribute() const
void setServerUris(const QStringList &serverUris)
void setUserScope(SearchScope scope)
QString userFilter() const
AuthenticationUser findUser(Context *c, const ParamsMultiMap &userInfo) override final
void setUserField(const QString &userField)
bool validatePassword(Context *c, const AuthenticationUser &user, const QString &password) const override
QStringList serverUris() const
void setAttributes(const QStringList &attributes)
QMultiMap< QString, QString > ParamsMultiMap
The Cutelyst namespace holds all public Cutelyst API.