cutelyst 5.1.0
A C++ Web Framework built on top of Qt, using the simple approach of Catalyst (Perl) framework.
storeldap.cpp
1/*
2 * SPDX-FileCopyrightText: (C) 2026 Daniel Nicoletti <dantti12@gmail.com>
3 * SPDX-License-Identifier: BSD-3-Clause
4 */
5#include "storeldap.h"
6
7#include "common.h"
8
9#include <QLoggingCategory>
10#include <QVariantList>
11
12#ifdef CUTELYST_PLUGIN_AUTHENTICATION_HAS_LDAP
13# include <ldap.h>
14#endif
15
16using namespace Cutelyst;
17using namespace Qt::StringLiterals;
18
19Q_LOGGING_CATEGORY(C_AUTH_LDAP, "cutelyst.plugin.authentication.ldap", QtWarningMsg)
20
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)
26 , m_startTls(false)
27{
28}
29
33
35{
36 Q_UNUSED(c)
37 if (m_userField.isEmpty()) {
38 qCWarning(C_AUTH_LDAP) << "User field is empty";
39 return {};
40 }
41
42 return findUserByAttribute(c, m_userField, userInfo.value(m_userField));
43}
44
46{
47 Q_UNUSED(c)
48 // Store the full user data in the session for restoration without LDAP
49 return user.data();
50}
51
53{
54 // Try to restore from full user data if available
55 if (frozenUser.canConvert<QVariantMap>()) {
57 user.setData(frozenUser.toMap());
58 return user;
59 }
60 // Fallback to old behavior (lookup by id)
61 return findUserByAttribute(c, m_idAttribute, frozenUser.toString());
62}
63
65 const AuthenticationUser &user,
66 const QString &password) const
67{
68 Q_UNUSED(c)
69
70 if (password.isEmpty()) {
71 return false;
72 }
73
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";
77 return false;
78 }
79
80#ifdef CUTELYST_PLUGIN_AUTHENTICATION_HAS_LDAP
81 if (m_serverUris.isEmpty()) {
82 qCWarning(C_AUTH_LDAP) << "No LDAP server URI configured";
83 return false;
84 }
85
86 const QByteArray uri = m_serverUris.join(u" "_s).toUtf8();
87
88 LDAP *ld = nullptr;
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);
92 return false;
93 }
94
95 int ldapVersion = LDAP_VERSION3;
96 ldap_set_option(ld, LDAP_OPT_PROTOCOL_VERSION, &ldapVersion);
97
98 if (m_startTls) {
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);
103 return false;
104 }
105 }
106
107 const QByteArray passwordUtf8 = password.toUtf8();
108 berval bindCred;
109 bindCred.bv_val = const_cast<char *>(passwordUtf8.constData());
110 bindCred.bv_len = static_cast<ber_len_t>(passwordUtf8.size());
111
112 rc = ldap_sasl_bind_s(
113 ld, userDn.toUtf8().constData(), LDAP_SASL_SIMPLE, &bindCred, nullptr, nullptr, nullptr);
114
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;
119 }
120
121 ldap_unbind_ext_s(ld, nullptr, nullptr);
122
123 return rc == LDAP_SUCCESS;
124#else
125 qCWarning(C_AUTH_LDAP) << "StoreLDAP requested but plugin was built without LDAP support";
126 return false;
127#endif
128}
129
131{
132 m_serverUris = serverUris;
133}
134
136{
137 return m_serverUris;
138}
139
140void StoreLDAP::setBindDn(const QString &bindDn)
141{
142 m_bindDn = bindDn;
143}
144
146{
147 return m_bindDn;
148}
149
150void StoreLDAP::setBindPassword(const QString &bindPassword)
151{
152 m_bindPassword = bindPassword;
153}
154
156{
157 m_userBaseDn = baseDn;
158}
159
161{
162 return m_userBaseDn;
163}
164
165void StoreLDAP::setUserField(const QString &userField)
166{
167 m_userField = userField;
168}
169
171{
172 return m_userField;
173}
174
175void StoreLDAP::setIdAttribute(const QString &idAttribute)
176{
177 m_idAttribute = idAttribute;
178}
179
181{
182 return m_idAttribute;
183}
184
185void StoreLDAP::setUserFilter(const QString &userFilter)
186{
187 m_userFilter = userFilter;
188}
189
191{
192 return m_userFilter;
193}
194
196{
197 m_userScope = scope;
198}
199
201{
202 return m_userScope;
203}
204
206{
207 m_attributes = attributes;
208}
209
211{
212 return m_attributes;
213}
214
215void StoreLDAP::setStartTls(bool startTls)
216{
217 m_startTls = startTls;
218}
219
221{
222 return m_startTls;
223}
224
225#ifdef CUTELYST_PLUGIN_AUTHENTICATION_HAS_LDAP
226namespace {
227int toLdapScope(StoreLDAP::SearchScope scope)
228{
229 switch (scope) {
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:
235 default:
236 return LDAP_SCOPE_SUBTREE;
237 }
238}
239
240QString escapeFilterValue(QStringView value)
241{
242 QString escaped;
243 escaped.reserve(value.size());
244
245 for (const QChar ch : value) {
246 switch (ch.unicode()) {
247 case '*':
248 escaped += u"\\2a"_s;
249 break;
250 case '(':
251 escaped += u"\\28"_s;
252 break;
253 case ')':
254 escaped += u"\\29"_s;
255 break;
256 case '\\':
257 escaped += u"\\5c"_s;
258 break;
259 case '\0':
260 escaped += u"\\00"_s;
261 break;
262 default:
263 escaped += ch;
264 break;
265 }
266 }
267
268 return escaped;
269}
270
271QStringList valuesToStrings(struct berval **values)
272{
273 QStringList out;
274 if (!values) {
275 return out;
276 }
277
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));
281 }
282
283 return out;
284}
285} // namespace
286#endif
287
289 StoreLDAP::findUserByAttribute(Context *c, const QString &attribute, const QString &value)
290{
291 Q_UNUSED(c)
292
294
295 if (value.isEmpty()) {
296 return user;
297 }
298
299#ifdef CUTELYST_PLUGIN_AUTHENTICATION_HAS_LDAP
300 if (m_serverUris.isEmpty()) {
301 qCWarning(C_AUTH_LDAP) << "No LDAP server URI configured";
302 return user;
303 }
304
305 const QByteArray uri = m_serverUris.join(u" "_s).toUtf8();
306
307 LDAP *ld = nullptr;
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);
311 return user;
312 }
313
314 int ldapVersion = LDAP_VERSION3;
315 ldap_set_option(ld, LDAP_OPT_PROTOCOL_VERSION, &ldapVersion);
316
317 if (m_startTls) {
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);
322 return user;
323 }
324 }
325
326 // Only bind when a service account is configured.
327 // If both are empty, keep the connection anonymous and proceed with search.
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);
332 return user;
333 }
334
335 const QByteArray bindPassword = m_bindPassword.toUtf8();
336 const QByteArray bindDnUtf8 = m_bindDn.toUtf8();
337 berval bindCred;
338 bindCred.bv_val = const_cast<char *>(bindPassword.constData());
339 bindCred.bv_len = static_cast<ber_len_t>(bindPassword.size());
340
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;
348 } else {
349 qCWarning(C_AUTH_LDAP) << "LDAP bind failed:" << ldap_err2string(rc);
350 }
351 ldap_unbind_ext_s(ld, nullptr, nullptr);
352 return user;
353 }
354 }
355
356 QString filter = m_userFilter;
357 const QString escapedValue = escapeFilterValue(value);
358 if (filter.isEmpty()) {
359 filter = u"(%1=%2)"_s.arg(attribute, escapedValue);
360 } else {
361 filter = filter.arg(escapedValue);
362 }
363
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());
369 }
370 for (QByteArray &attr : attrStorage) {
371 attrs.push_back(attr.data());
372 }
373 attrs.push_back(nullptr);
374
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(),
381 0,
382 nullptr,
383 nullptr,
384 nullptr,
385 0,
386 &result);
387
388 if (rc != LDAP_SUCCESS) {
389 qCWarning(C_AUTH_LDAP) << "LDAP search failed:" << ldap_err2string(rc);
390 ldap_unbind_ext_s(ld, nullptr, nullptr);
391 return user;
392 }
393
394 LDAPMessage *entry = ldap_first_entry(ld, result);
395 if (!entry) {
396 ldap_msgfree(result);
397 ldap_unbind_ext_s(ld, nullptr, nullptr);
398 return user;
399 }
400
401 char *dnRaw = ldap_get_dn(ld, entry);
402 if (dnRaw) {
403 const QString dn = QString::fromUtf8(dnRaw);
404 user.insert(u"dn"_s, dn);
405 ldap_memfree(dnRaw);
406 }
407
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);
412
413 berval **values = ldap_get_values_len(ld, entry, attr);
414 const QStringList stringValues = valuesToStrings(values);
415
416 if (stringValues.size() == 1) {
417 user.insert(attrName, stringValues.constFirst());
418 } else if (!stringValues.isEmpty()) {
419 QVariantList list;
420 list.reserve(stringValues.size());
421 for (const QString &item : stringValues) {
422 list.push_back(item);
423 }
424 user.insert(attrName, list);
425 }
426
427 if (values) {
428 ldap_value_free_len(values);
429 }
430 ldap_memfree(attr);
431 }
432
433 if (ber) {
434 ber_free(ber, 0);
435 }
436
437 const QVariant idValue =
438 m_idAttribute == u"dn"_s ? user.value(u"dn"_s) : user.value(m_idAttribute);
439 if (!idValue.isNull()) {
440 user.setId(idValue);
441 } else {
442 user = AuthenticationUser();
443 }
444
445 ldap_msgfree(result);
446 ldap_unbind_ext_s(ld, nullptr, nullptr);
447#else
448 Q_UNUSED(attribute)
449 qCWarning(C_AUTH_LDAP) << "StoreLDAP requested but plugin was built without LDAP support";
450#endif
451
452 return user;
453}
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)
The Cutelyst Context.
Definition context.h:42
Authentication store backed by an LDAP directory.
Definition storeldap.h:27
AuthenticationUser fromSession(Context *c, const QVariant &frozenUser) override final
Definition storeldap.cpp:52
void setBindDn(const QString &bindDn)
void setUserBaseDn(const QString &baseDn)
QString userField() const
QVariant forSession(Context *c, const AuthenticationUser &user) override final
Definition storeldap.cpp:45
void setBindPassword(const QString &bindPassword)
QString userBaseDn() const
void setUserFilter(const QString &userFilter)
QString bindDn() const
QStringList attributes() const
SearchScope userScope() const
void setIdAttribute(const QString &idAttribute)
bool startTls() const
~StoreLDAP() override
Definition storeldap.cpp:30
void setStartTls(bool startTls)
QString idAttribute() const
void setServerUris(const QStringList &serverUris)
bool validatePassword(Context *c, const AuthenticationUser &user, const QString &password) const
Definition storeldap.cpp:64
void setUserScope(SearchScope scope)
QString userFilter() const
AuthenticationUser findUser(Context *c, const ParamsMultiMap &userInfo) override final
Definition storeldap.cpp:34
void setUserField(const QString &userField)
QStringList serverUris() const
void setAttributes(const QStringList &attributes)
The Cutelyst namespace holds all public Cutelyst API.
const char * constData() const const
qsizetype size() const const
const T & constFirst() const const
bool isEmpty() const const
void push_back(QList::parameter_type value)
void reserve(qsizetype size)
qsizetype size() const const
T value(const Key &key, const T &defaultValue) const const
QString arg(Args &&... args) const const
QString fromUtf8(QByteArrayView str)
bool isEmpty() const const
void reserve(qsizetype size)
QByteArray toUtf8() const const
QString join(QChar separator) const const
bool canConvert() const const
bool isNull() const const
QMap< QString, QVariant > toMap() const const
QString toString() const const