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 
16 using namespace Cutelyst;
17 using namespace Qt::StringLiterals;
18 
19 Q_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 
31 {
32 }
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>()) {
56  AuthenticationUser user;
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 
130 void StoreLDAP::setServerUris(const QStringList &serverUris)
131 {
132  m_serverUris = serverUris;
133 }
134 
136 {
137  return m_serverUris;
138 }
139 
140 void StoreLDAP::setBindDn(const QString &bindDn)
141 {
142  m_bindDn = bindDn;
143 }
144 
146 {
147  return m_bindDn;
148 }
149 
150 void 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 
165 void StoreLDAP::setUserField(const QString &userField)
166 {
167  m_userField = userField;
168 }
169 
171 {
172  return m_userField;
173 }
174 
175 void StoreLDAP::setIdAttribute(const QString &idAttribute)
176 {
177  m_idAttribute = idAttribute;
178 }
179 
181 {
182  return m_idAttribute;
183 }
184 
185 void 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 
205 void StoreLDAP::setAttributes(const QStringList &attributes)
206 {
207  m_attributes = attributes;
208 }
209 
211 {
212  return m_attributes;
213 }
214 
215 void 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
226 namespace {
227 int 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 
240 QString 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 
271 QStringList 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 
293  AuthenticationUser user;
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 }
void insert(const QString &key, const QVariant &value)
~StoreLDAP() override
Definition: storeldap.cpp:30
void setId(const QVariant &id)
void setUserField(const QString &userField)
Definition: storeldap.cpp:165
void setServerUris(const QStringList &serverUris)
Definition: storeldap.cpp:130
void push_back(parameter_type value)
SearchScope userScope() const
Definition: storeldap.cpp:200
void reserve(qsizetype size)
qsizetype size() const const
const T & constFirst() const const
bool startTls() const
Definition: storeldap.cpp:220
void setBindPassword(const QString &bindPassword)
Definition: storeldap.cpp:150
QString fromUtf8(QByteArrayView str)
void setAttributes(const QStringList &attributes)
Definition: storeldap.cpp:205
AuthenticationUser findUser(Context *c, const ParamsMultiMap &userInfo) override final
Definition: storeldap.cpp:34
Authentication store backed by an LDAP directory.
Definition: storeldap.h:27
qsizetype size() const const
QString userField() const
Definition: storeldap.cpp:170
void setData(const QVariantMap &data)
The Cutelyst Context.
Definition: context.h:42
bool canConvert() const const
bool isNull() const const
QString userFilter() const
Definition: storeldap.cpp:190
bool isEmpty() const const
bool isEmpty() const const
const char * constData() const const
pointer data()
The Cutelyst namespace holds all public Cutelyst API.
void setUserBaseDn(const QString &baseDn)
Definition: storeldap.cpp:155
void setUserScope(SearchScope scope)
Definition: storeldap.cpp:195
void setUserFilter(const QString &userFilter)
Definition: storeldap.cpp:185
AuthenticationUser fromSession(Context *c, const QVariant &frozenUser) override final
Definition: storeldap.cpp:52
Container for user data retrieved from an AuthenticationStore.
QStringList serverUris() const
Definition: storeldap.cpp:135
QMap< QString, QVariant > toMap() const const
QStringList attributes() const
Definition: storeldap.cpp:210
QVariant value(const QString &key, const QVariant &defaultValue=QVariant()) const
QString idAttribute() const
Definition: storeldap.cpp:180
void reserve(qsizetype size)
void setIdAttribute(const QString &idAttribute)
Definition: storeldap.cpp:175
QString bindDn() const
Definition: storeldap.cpp:145
bool validatePassword(Context *c, const AuthenticationUser &user, const QString &password) const override
Definition: storeldap.cpp:64
qsizetype size() const const
void setStartTls(bool startTls)
Definition: storeldap.cpp:215
void setBindDn(const QString &bindDn)
Definition: storeldap.cpp:140
QString arg(Args &&... args) const const
QString toString() const const
QVariant forSession(Context *c, const AuthenticationUser &user) override final
Definition: storeldap.cpp:45
QString userBaseDn() const
Definition: storeldap.cpp:160
T value(const Key &key, const T &defaultValue) const const
QByteArray toUtf8() const const