~drizzle-trunk/drizzle/development

« back to all changes in this revision

Viewing changes to plugin/auth_ldap/auth_ldap.cc

  • Committer: Eric Day
  • Date: 2010-04-23 21:44:46 UTC
  • mto: This revision was merged to the branch mainline in revision 1519.
  • Revision ID: eday@oddments.org-20100423214446-4sztquyo1hyjnsg2
Initial auth_ldap plugin. There are no test cases since this would require a pre-configured LDAP server running. Testing will instead be done on one of the build hosts that has been configured with LDAP and a separate test script.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
/* -*- mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; -*-
 
2
 *  vim:expandtab:shiftwidth=2:tabstop=2:smarttab:
 
3
 *
 
4
 *  Copyright (C) 2010 Eric Day
 
5
 *
 
6
 *  This program is free software; you can redistribute it and/or modify
 
7
 *  it under the terms of the GNU General Public License as published by
 
8
 *  the Free Software Foundation; version 2 of the License.
 
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 Free Software
 
17
 *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 
18
 */
 
19
 
 
20
#include "config.h"
 
21
 
 
22
/* This is needed for simple auth, we're not ready for SASL yet. */
 
23
#define LDAP_DEPRECATED 1
 
24
 
 
25
#include <ldap.h>
 
26
#include <pthread.h>
 
27
#include <sys/time.h>
 
28
#include <map>
 
29
#include <string>
 
30
 
 
31
#include "drizzled/plugin/authentication.h"
 
32
#include "drizzled/security_context.h"
 
33
#include "drizzled/util/convert.h"
 
34
#include "drizzled/algorithm/sha1.h"
 
35
 
 
36
using namespace std;
 
37
using namespace drizzled;
 
38
 
 
39
namespace auth_ldap
 
40
{
 
41
 
 
42
static char *uri= NULL;
 
43
static const char DEFAULT_URI[]= "ldap://127.0.0.1/";
 
44
static char *bind_dn= NULL;
 
45
static char *bind_password= NULL;
 
46
static char *base_dn= NULL;
 
47
static char *password_attribute= NULL;
 
48
static const char DEFAULT_PASSWORD_ATTRIBUTE[]= "userPassword";
 
49
static char *mysql_password_attribute= NULL;
 
50
static const char DEFAULT_MYSQL_PASSWORD_ATTRIBUTE[]= "mysqlUserPassword";
 
51
static int cache_timeout= 0;
 
52
static const int DEFAULT_CACHE_TIMEOUT= 600;
 
53
 
 
54
class AuthLDAP: public plugin::Authentication
 
55
{
 
56
public:
 
57
 
 
58
  AuthLDAP(string name_arg);
 
59
  ~AuthLDAP();
 
60
 
 
61
  /**
 
62
   * Initialize the LDAP connection.
 
63
   *
 
64
   * @return True on success, false otherwise.
 
65
   */
 
66
  bool initialize(void);
 
67
 
 
68
  /**
 
69
   * Connect to the LDAP server.
 
70
   *
 
71
   * @return True on success, false otherwise.
 
72
   */
 
73
  bool connect(void);
 
74
 
 
75
  /**
 
76
   * Retrieve the last error encountered in the class.
 
77
   */
 
78
  string& getError(void);
 
79
 
 
80
private:
 
81
 
 
82
  typedef enum
 
83
  {
 
84
    NOT_FOUND,
 
85
    PLAIN_TEXT,
 
86
    MYSQL_HASH
 
87
  } PasswordType;
 
88
 
 
89
  typedef pair<PasswordType, string> PasswordEntry;
 
90
  typedef pair<string, PasswordEntry> UserEntry;
 
91
  typedef map<string, PasswordEntry> UserCache;
 
92
 
 
93
  /**
 
94
   * Base class method to check authentication for a user.
 
95
   */
 
96
  bool authenticate(const SecurityContext &sctx, const string &password);
 
97
 
 
98
  /**
 
99
   * Lookup a user in LDAP.
 
100
   *
 
101
   * @param[in] Username to lookup.
 
102
   */
 
103
  void lookupUser(const string& user);
 
104
 
 
105
  /**
 
106
   * Verify the local and remote scrambled password match using the MySQL
 
107
   * hashing algorithm.
 
108
   *
 
109
   * @param[in] password Plain text password that is stored locally.
 
110
   * @param[in] scramble_bytes The random bytes that the server sent to the
 
111
   *  client for scrambling the password.
 
112
   * @param[in] scrambled_password The result of the client scrambling the
 
113
   *  password remotely.
 
114
   * @return True if the password matched, false if not.
 
115
   */
 
116
  bool verifyMySQLHash(const PasswordEntry &password,
 
117
                       const string &scramble_bytes,
 
118
                       const string &scrambled_password);
 
119
 
 
120
  time_t next_cache_expiration;
 
121
  LDAP *ldap;
 
122
  string error;
 
123
  UserCache users;
 
124
  pthread_rwlock_t lock;
 
125
};
 
126
 
 
127
AuthLDAP::AuthLDAP(string name_arg):
 
128
  plugin::Authentication(name_arg),
 
129
  next_cache_expiration(),
 
130
  ldap(),
 
131
  error(),
 
132
  users()
 
133
{
 
134
}
 
135
 
 
136
AuthLDAP::~AuthLDAP()
 
137
{
 
138
  pthread_rwlock_destroy(&lock);
 
139
  if (ldap != NULL)
 
140
    ldap_unbind(ldap);
 
141
}
 
142
 
 
143
bool AuthLDAP::initialize(void)
 
144
{
 
145
  int return_code= pthread_rwlock_init(&lock, NULL);
 
146
  if (return_code != 0)
 
147
  {
 
148
    error= "pthread_rwlock_init failed";
 
149
    return false;
 
150
  }
 
151
 
 
152
  return connect();
 
153
}
 
154
 
 
155
bool AuthLDAP::connect(void)
 
156
{
 
157
  int return_code= ldap_initialize(&ldap, uri);
 
158
  if (return_code != LDAP_SUCCESS)
 
159
  {
 
160
    error= "ldap_initialize failed: ";
 
161
    error+= ldap_err2string(return_code);
 
162
    return false;
 
163
  }
 
164
 
 
165
  int version= 3;
 
166
  return_code= ldap_set_option(ldap, LDAP_OPT_PROTOCOL_VERSION, &version);
 
167
  if (return_code != LDAP_SUCCESS)
 
168
  {
 
169
    ldap_unbind(ldap);
 
170
    ldap= NULL;
 
171
    error= "ldap_set_option failed: ";
 
172
    error+= ldap_err2string(return_code);
 
173
    return false;
 
174
  }
 
175
 
 
176
  if (bind_dn != NULL)
 
177
  {
 
178
    return_code= ldap_simple_bind_s(ldap, bind_dn, bind_password);
 
179
    if (return_code != LDAP_SUCCESS)
 
180
    {
 
181
      ldap_unbind(ldap);
 
182
      ldap= NULL;
 
183
      error= "ldap_simple_bind_s failed: ";
 
184
      error+= ldap_err2string(return_code);
 
185
      return false;
 
186
    }
 
187
  }
 
188
 
 
189
  return true;
 
190
}
 
191
 
 
192
string& AuthLDAP::getError(void)
 
193
{
 
194
  return error;
 
195
}
 
196
 
 
197
bool AuthLDAP::authenticate(const SecurityContext &sctx, const string &password)
 
198
{
 
199
  /* See if cache should be emptied. */
 
200
  if (cache_timeout > 0)
 
201
  {
 
202
    struct timeval current_time;
 
203
    gettimeofday(&current_time, NULL);
 
204
    if (current_time.tv_sec > next_cache_expiration)
 
205
    {
 
206
      pthread_rwlock_wrlock(&lock);
 
207
      /* Make sure another thread didn't already clear it. */
 
208
      if (current_time.tv_sec > next_cache_expiration)
 
209
      {
 
210
        users.clear();
 
211
        next_cache_expiration= current_time.tv_sec + cache_timeout;
 
212
      }
 
213
      pthread_rwlock_unlock(&lock);
 
214
    }
 
215
  }
 
216
 
 
217
  pthread_rwlock_rdlock(&lock);
 
218
 
 
219
  AuthLDAP::UserCache::const_iterator user= users.find(sctx.getUser());
 
220
  if (user == users.end())
 
221
  {
 
222
    pthread_rwlock_unlock(&lock);
 
223
 
 
224
    pthread_rwlock_wrlock(&lock);
 
225
 
 
226
    /* Make sure the user was not added while we unlocked. */
 
227
    user= users.find(sctx.getUser());
 
228
    if (user == users.end())
 
229
      lookupUser(sctx.getUser());
 
230
 
 
231
    pthread_rwlock_unlock(&lock);
 
232
 
 
233
    pthread_rwlock_rdlock(&lock);
 
234
 
 
235
    /* Get user again because map may have changed while unlocked. */
 
236
    user= users.find(sctx.getUser());
 
237
    if (user == users.end())
 
238
    {
 
239
      pthread_rwlock_unlock(&lock);
 
240
      return false;
 
241
    }
 
242
  }
 
243
 
 
244
  if (user->second.first == NOT_FOUND)
 
245
  {
 
246
    pthread_rwlock_unlock(&lock);
 
247
    return false;
 
248
  }
 
249
 
 
250
  if (sctx.getPasswordType() == SecurityContext::MYSQL_HASH)
 
251
  {
 
252
    bool allow= verifyMySQLHash(user->second, sctx.getPasswordContext(), password);
 
253
    pthread_rwlock_unlock(&lock);
 
254
    return allow;
 
255
  }
 
256
 
 
257
  if (user->second.first == PLAIN_TEXT && password == user->second.second)
 
258
  {
 
259
    pthread_rwlock_unlock(&lock);
 
260
    return true;
 
261
  }
 
262
 
 
263
  pthread_rwlock_unlock(&lock);
 
264
  return false;
 
265
}
 
266
 
 
267
void AuthLDAP::lookupUser(const string& user)
 
268
{
 
269
  string filter("(cn=" + user + ")");
 
270
  const char *attributes[3]=
 
271
  {
 
272
    password_attribute,
 
273
    mysql_password_attribute,
 
274
    NULL
 
275
  };
 
276
  LDAPMessage *result;
 
277
  bool try_reconnect= true;
 
278
 
 
279
  while (true)
 
280
  {
 
281
    if (ldap == NULL)
 
282
    {
 
283
      if (! connect())
 
284
      {
 
285
        errmsg_printf(ERRMSG_LVL_ERROR, _("Reconnect failed: %s\n"),
 
286
                      getError().c_str());
 
287
        return;
 
288
      }
 
289
    }
 
290
 
 
291
    int return_code= ldap_search_ext_s(ldap,
 
292
                                       base_dn,
 
293
                                       LDAP_SCOPE_ONELEVEL,
 
294
                                       filter.c_str(),
 
295
                                       const_cast<char **>(attributes),
 
296
                                       0,
 
297
                                       NULL,
 
298
                                       NULL,
 
299
                                       NULL,
 
300
                                       1,
 
301
                                       &result);
 
302
    if (return_code != LDAP_SUCCESS)
 
303
    {
 
304
      errmsg_printf(ERRMSG_LVL_ERROR, _("ldap_search_ext_s failed: %s\n"),
 
305
                    ldap_err2string(return_code));
 
306
 
 
307
      /* Only try one reconnect per request. */
 
308
      if (try_reconnect)
 
309
      {
 
310
        try_reconnect= false;
 
311
        ldap_unbind(ldap);
 
312
        ldap= NULL;
 
313
        continue;
 
314
      }
 
315
 
 
316
      return;
 
317
    }
 
318
 
 
319
    break;
 
320
  }
 
321
 
 
322
  LDAPMessage *entry= ldap_first_entry(ldap, result);
 
323
  AuthLDAP::PasswordEntry new_password;
 
324
  if (entry == NULL)
 
325
    new_password= AuthLDAP::PasswordEntry(NOT_FOUND, "");
 
326
  else
 
327
  {
 
328
    char **values= ldap_get_values(ldap, entry, mysql_password_attribute);
 
329
    if (values == NULL)
 
330
    {
 
331
      values= ldap_get_values(ldap, entry, password_attribute);
 
332
      if (values == NULL)
 
333
        new_password= AuthLDAP::PasswordEntry(NOT_FOUND, "");
 
334
      else
 
335
      {
 
336
        new_password= AuthLDAP::PasswordEntry(PLAIN_TEXT, values[0]);
 
337
        ldap_value_free(values);
 
338
      }
 
339
    }
 
340
    else
 
341
    {
 
342
      new_password= AuthLDAP::PasswordEntry(MYSQL_HASH, values[0]);
 
343
      ldap_value_free(values);
 
344
    }
 
345
  }
 
346
 
 
347
  users.insert(AuthLDAP::UserEntry(user, new_password));
 
348
}
 
349
 
 
350
bool AuthLDAP::verifyMySQLHash(const PasswordEntry &password,
 
351
                               const string &scramble_bytes,
 
352
                               const string &scrambled_password)
 
353
{
 
354
  if (scramble_bytes.size() != SHA1_DIGEST_LENGTH ||
 
355
      scrambled_password.size() != SHA1_DIGEST_LENGTH)
 
356
  {
 
357
    return false;
 
358
  }
 
359
 
 
360
  SHA1_CTX ctx;
 
361
  uint8_t local_scrambled_password[SHA1_DIGEST_LENGTH];
 
362
  uint8_t temp_hash[SHA1_DIGEST_LENGTH];
 
363
  uint8_t scrambled_password_check[SHA1_DIGEST_LENGTH];
 
364
 
 
365
  if (password.first == MYSQL_HASH)
 
366
  {
 
367
    /* Get the double-hashed password from the given hex string. */
 
368
    drizzled_hex_to_string(reinterpret_cast<char*>(local_scrambled_password),
 
369
                           password.second.c_str(), SHA1_DIGEST_LENGTH * 2);
 
370
  }
 
371
  else
 
372
  {
 
373
    /* Generate the double SHA1 hash for the password stored locally first. */
 
374
    SHA1Init(&ctx);
 
375
    SHA1Update(&ctx, reinterpret_cast<const uint8_t *>(password.second.c_str()),
 
376
               password.second.size());
 
377
    SHA1Final(temp_hash, &ctx);
 
378
 
 
379
    SHA1Init(&ctx);
 
380
    SHA1Update(&ctx, temp_hash, SHA1_DIGEST_LENGTH);
 
381
    SHA1Final(local_scrambled_password, &ctx);
 
382
  }
 
383
 
 
384
  /* Hash the scramble that was sent to client with the local password. */
 
385
  SHA1Init(&ctx);
 
386
  SHA1Update(&ctx, reinterpret_cast<const uint8_t*>(scramble_bytes.c_str()),
 
387
             SHA1_DIGEST_LENGTH);
 
388
  SHA1Update(&ctx, local_scrambled_password, SHA1_DIGEST_LENGTH);
 
389
  SHA1Final(temp_hash, &ctx);
 
390
 
 
391
  /* Next, XOR the result with what the client sent to get the original
 
392
     single-hashed password. */
 
393
  for (int x= 0; x < SHA1_DIGEST_LENGTH; x++)
 
394
    temp_hash[x]= temp_hash[x] ^ scrambled_password[x];
 
395
 
 
396
  /* Hash this result once more to get the double-hashed password again. */
 
397
  SHA1Init(&ctx);
 
398
  SHA1Update(&ctx, temp_hash, SHA1_DIGEST_LENGTH);
 
399
  SHA1Final(scrambled_password_check, &ctx);
 
400
 
 
401
  /* These should match for a successful auth. */
 
402
  return memcmp(local_scrambled_password, scrambled_password_check, SHA1_DIGEST_LENGTH) == 0;
 
403
}
 
404
 
 
405
static int init(plugin::Context &context)
 
406
{
 
407
  AuthLDAP *auth_ldap= new AuthLDAP("auth_ldap");
 
408
  if (! auth_ldap->initialize())
 
409
  {
 
410
    errmsg_printf(ERRMSG_LVL_ERROR, _("Could not load auth ldap: %s\n"),
 
411
                  auth_ldap->getError().c_str());
 
412
    delete auth_ldap;
 
413
    return 1;
 
414
  }
 
415
 
 
416
  context.add(auth_ldap);
 
417
  return 0;
 
418
}
 
419
 
 
420
static DRIZZLE_SYSVAR_STR(uri,
 
421
                          uri,
 
422
                          PLUGIN_VAR_READONLY,
 
423
                          N_("URI of the LDAP server to contact"),
 
424
                          NULL, /* check func */
 
425
                          NULL, /* update func*/
 
426
                          DEFAULT_URI);
 
427
 
 
428
static DRIZZLE_SYSVAR_STR(bind_dn,
 
429
                          bind_dn,
 
430
                          PLUGIN_VAR_READONLY,
 
431
                          N_("DN to use when binding to the LDAP server"),
 
432
                          NULL, /* check func */
 
433
                          NULL, /* update func*/
 
434
                          NULL); /* default value */
 
435
 
 
436
static DRIZZLE_SYSVAR_STR(bind_password,
 
437
                          bind_password,
 
438
                          PLUGIN_VAR_READONLY,
 
439
                          N_("Password to use when binding the DN"),
 
440
                          NULL, /* check func */
 
441
                          NULL, /* update func*/
 
442
                          NULL); /* default value */
 
443
 
 
444
static DRIZZLE_SYSVAR_STR(base_dn,
 
445
                          base_dn,
 
446
                          PLUGIN_VAR_READONLY,
 
447
                          N_("DN to use when searching"),
 
448
                          NULL, /* check func */
 
449
                          NULL, /* update func*/
 
450
                          NULL); /* default value */
 
451
 
 
452
static DRIZZLE_SYSVAR_STR(password_attribute,
 
453
                          password_attribute,
 
454
                          PLUGIN_VAR_READONLY,
 
455
                          N_("Attribute in LDAP with plain text password"),
 
456
                          NULL, /* check func */
 
457
                          NULL, /* update func*/
 
458
                          DEFAULT_PASSWORD_ATTRIBUTE);
 
459
 
 
460
static DRIZZLE_SYSVAR_STR(mysql_password_attribute,
 
461
                          mysql_password_attribute,
 
462
                          PLUGIN_VAR_READONLY,
 
463
                          N_("Attribute in LDAP with MySQL hashed password"),
 
464
                          NULL, /* check func */
 
465
                          NULL, /* update func*/
 
466
                          DEFAULT_MYSQL_PASSWORD_ATTRIBUTE);
 
467
 
 
468
static DRIZZLE_SYSVAR_INT(cache_timeout,
 
469
                          cache_timeout,
 
470
                          PLUGIN_VAR_READONLY,
 
471
                          N_("How often to empty the users cache, 0 to disable"),
 
472
                          NULL, /* check func */
 
473
                          NULL, /* update func */
 
474
                          DEFAULT_CACHE_TIMEOUT,
 
475
                          0,
 
476
                          2147483647,
 
477
                          0);
 
478
 
 
479
static drizzle_sys_var* sys_variables[]=
 
480
{
 
481
  DRIZZLE_SYSVAR(uri),
 
482
  DRIZZLE_SYSVAR(bind_dn),
 
483
  DRIZZLE_SYSVAR(bind_password),
 
484
  DRIZZLE_SYSVAR(base_dn),
 
485
  DRIZZLE_SYSVAR(password_attribute),
 
486
  DRIZZLE_SYSVAR(mysql_password_attribute),
 
487
  DRIZZLE_SYSVAR(cache_timeout),
 
488
  NULL
 
489
};
 
490
 
 
491
} /* namespace auth_ldap */
 
492
 
 
493
DRIZZLE_PLUGIN(auth_ldap::init, auth_ldap::sys_variables);