Authenticator code cleanup as per review
[quassel.git] / src / core / ldapauthenticator.cpp
1 /***************************************************************************
2  *   Copyright (C) 2005-2015 by the Quassel Project                        *
3  *   devel@quassel-irc.org                                                 *
4  *                                                                         *
5  *   This program is free software; you can redistribute it and/or modify  *
6  *   it under the terms of the GNU General Public License as published by  *
7  *   the Free Software Foundation; either version 2 of the License, or     *
8  *   (at your option) version 3.                                           *
9  *                                                                         *
10  *   This program is distributed in the hope that it will be useful,       *
11  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
12  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
13  *   GNU General Public License for more details.                          *
14  *                                                                         *
15  *   You should have received a copy of the GNU General Public License     *
16  *   along with this program; if not, write to the                         *
17  *   Free Software Foundation, Inc.,                                       *
18  *   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.         *
19  ***************************************************************************/
20
21 /* This file contains an implementation of an LDAP Authenticator, as an example
22  * of what a custom external auth provider could do.
23  *
24  * It's based off of this pull request for quassel by abustany:
25  * https://github.com/quassel/quassel/pull/4/
26  *
27  */
28
29 #include "ldapauthenticator.h"
30
31 #include "logger.h"
32 #include "network.h"
33 #include "quassel.h"
34
35 /* We should use openldap on windows if at all possible, rather than trying to
36  * write some kind of compatiblity routine.
37 #ifdef Q_CC_MSVC
38 #include <windows.h>
39 #include <winldap.h>
40 #else*/
41 #include <ldap.h>
42 //#endif
43
44 LdapAuthenticator::LdapAuthenticator(QObject *parent)
45     : Authenticator(parent),
46     _connection(0)
47 {
48 }
49
50
51 LdapAuthenticator::~LdapAuthenticator()
52 {
53     if (_connection != 0)
54     {
55         ldap_unbind_ext(_connection, 0, 0);
56     }
57 }
58
59
60 bool LdapAuthenticator::isAvailable() const
61 {
62     // FIXME: probably this should test if we can speak to the LDAP server.
63     return true;
64 }
65
66 QString LdapAuthenticator::backendId() const
67 {
68     // We identify the backend to use for the monolithic core by its displayname.
69     // so only change this string if you _really_ have to and make sure the core
70     // setup for the mono client still works ;)
71     return QString("LDAP");
72 }
73
74 QString LdapAuthenticator::description() const
75 {
76     return tr("Authenticate users using an LDAP server.");
77 }
78
79 QStringList LdapAuthenticator::setupKeys() const
80 {
81     // The parameters needed for LDAP.
82     QStringList keys;
83     keys << "Hostname"
84          << "Port"
85          << "Bind DN"
86          << "Bind Password"
87          << "Base DN"
88          << "Filter"
89          << "UID Attribute";
90     return keys;
91 }
92
93 QVariantMap LdapAuthenticator::setupDefaults() const
94 {
95     QVariantMap map;
96     map["Hostname"] = QVariant(QString("ldap://localhost"));
97     map["Port"] = QVariant(DEFAULT_LDAP_PORT);
98     map["UID Attribute"] = QVariant(QString("uid"));
99     return map;
100 }
101
102 void LdapAuthenticator::setConnectionProperties(const QVariantMap &properties)
103 {
104     _hostName = properties["Hostname"].toString();
105     _port = properties["Port"].toInt();
106     _baseDN = properties["Base DN"].toString();
107     _filter = properties["Filter"].toString();
108     _bindDN = properties["Bind DN"].toString();
109     _bindPassword = properties["Bind Password"].toString();
110     _uidAttribute = properties["UID Attribute"].toString();
111 }
112
113 // TODO: this code is sufficiently general that in the future, perhaps an abstract
114 // class should be created implementing it.
115 // i.e. a provider that does its own thing and then pokes at the current storage
116 // through the default core method.
117 UserId LdapAuthenticator::validateUser(const QString &username, const QString &password)
118 {
119     bool result = ldapAuth(username, password);
120     if (!result)
121     {
122         return UserId();
123     }
124
125     // If auth succeeds, but the user has not logged into quassel previously, make
126     // a new user for them and return that ID.
127     // Users created via LDAP have empty passwords, but authenticator column = LDAP.
128     // On the other hand, if auth succeeds and the user already exists, do a final
129     // cross-check to confirm we're using the right auth provider.
130     UserId quasselID = Core::validateUser(username, QString());
131     if (!quasselID.isValid())
132     {
133         return Core::addUser(username, QString(), backendId());
134     }
135     else if (!(Core::checkAuthProvider(quasselID, backendId())))
136     {
137         return 0;
138     }
139     return quasselID;
140 }
141
142 bool LdapAuthenticator::setup(const QVariantMap &settings)
143 {
144     setConnectionProperties(settings);
145     bool status = ldapConnect();
146     return status;
147 }
148
149 Authenticator::State LdapAuthenticator::init(const QVariantMap &settings)
150 {
151     setConnectionProperties(settings);
152
153     bool status = ldapConnect();
154     if (!status)
155     {
156         quInfo() << qPrintable(backendId()) << "Authenticator cannot connect.";
157         return NotAvailable;
158     }
159
160     quInfo() << qPrintable(backendId()) << "Authenticator is ready.";
161     return IsReady;
162 }
163
164 // Method based on abustany LDAP quassel patch.
165 bool LdapAuthenticator::ldapConnect()
166 {
167     if (_connection != 0) {
168         ldapDisconnect();
169     }
170
171     int res, v = LDAP_VERSION3;
172
173     QString serverURI;
174     QByteArray serverURIArray;
175
176     // Convert info to hostname:port.
177     serverURI = _hostName + ":" + QString::number(_port);
178     serverURIArray = serverURI.toLocal8Bit();
179     res = ldap_initialize(&_connection, serverURIArray);
180
181     if (res != LDAP_SUCCESS) {
182         qWarning() << "Could not connect to LDAP server:" << ldap_err2string(res);
183         return false;
184     }
185
186     res = ldap_set_option(_connection, LDAP_OPT_PROTOCOL_VERSION, (void*)&v);
187
188     if (res != LDAP_SUCCESS) {
189         qWarning() << "Could not set LDAP protocol version to v3:" << ldap_err2string(res);
190         ldap_unbind_ext(_connection, 0, 0);
191         _connection = 0;
192         return false;
193     }
194
195     return true;
196 }
197
198 void LdapAuthenticator::ldapDisconnect()
199 {
200     if (_connection == 0) {
201         return;
202     }
203
204     ldap_unbind_ext(_connection, 0, 0);
205     _connection = 0;
206 }
207
208 bool LdapAuthenticator::ldapAuth(const QString &username, const QString &password)
209 {
210     if (password.isEmpty()) {
211         return false;
212     }
213
214     int res;
215
216     // Attempt to establish a connection.
217     if (_connection == 0) {
218         if (not ldapConnect()) {
219             return false;
220         }
221     }
222
223     struct berval cred;
224
225     // Convert some things to byte arrays as needed.
226     QByteArray bindPassword = _bindPassword.toLocal8Bit();
227     QByteArray bindDN = _bindDN.toLocal8Bit();
228     QByteArray baseDN = _baseDN.toLocal8Bit();
229     QByteArray uidAttribute = _uidAttribute.toLocal8Bit();
230
231     cred.bv_val = const_cast<char*>(bindPassword.size() > 0 ? bindPassword.constData() : NULL);
232     cred.bv_len = bindPassword.size();
233
234     res = ldap_sasl_bind_s(_connection, bindDN.size() > 0 ? bindDN.constData() : 0, LDAP_SASL_SIMPLE, &cred, 0, 0, 0);
235
236     if (res != LDAP_SUCCESS) {
237         qWarning() << "Refusing connection from" << username << "(LDAP bind failed:" << ldap_err2string(res) << ")";
238         ldapDisconnect();
239         return false;
240     }
241
242     LDAPMessage *msg = NULL, *entry = NULL;
243
244     const QByteArray ldapQuery = "(&(" + uidAttribute + '=' + username.toLocal8Bit() + ")" + _filter.toLocal8Bit() + ")";
245
246     res = ldap_search_ext_s(_connection, baseDN.constData(), LDAP_SCOPE_SUBTREE, ldapQuery.constData(), 0, 0, 0, 0, 0, 0, &msg);
247
248     if (res != LDAP_SUCCESS) {
249         qWarning() << "Refusing connection from" << username << "(LDAP search failed:" << ldap_err2string(res) << ")";
250         return false;
251     }
252
253     if (ldap_count_entries(_connection, msg) > 1) {
254         qWarning() << "Refusing connection from" << username << "(LDAP search returned more than one result)";
255         ldap_msgfree(msg);
256         return false;
257     }
258
259     entry = ldap_first_entry(_connection, msg);
260
261     if (entry == 0) {
262         qWarning() << "Refusing connection from" << username << "(LDAP search returned no results)";
263         ldap_msgfree(msg);
264         return false;
265     }
266
267     const QByteArray passwordArray = password.toLocal8Bit();
268     cred.bv_val = const_cast<char*>(passwordArray.constData());
269     cred.bv_len = password.size();
270
271     char *userDN = ldap_get_dn(_connection, entry);
272
273     res = ldap_sasl_bind_s(_connection, userDN, LDAP_SASL_SIMPLE, &cred, 0, 0, 0);
274
275     if (res != LDAP_SUCCESS) {
276         qWarning() << "Refusing connection from" << username << "(LDAP authentication failed)";
277         ldap_memfree(userDN);
278         ldap_msgfree(msg);
279         return false;
280     }
281
282     // The original implementation had requiredAttributes. I have not included this code
283     // but it would be easy to re-add if someone wants this feature.
284     // Ben Rosser <bjr@acm.jhu.edu> (12/23/15).
285
286     ldap_memfree(userDN);
287     ldap_msgfree(msg);
288     return true;
289 }