Drizzled Public API Documentation

auth_ldap.cc
00001 /* -*- mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; -*-
00002  *  vim:expandtab:shiftwidth=2:tabstop=2:smarttab:
00003  *
00004  *  Copyright (C) 2010 Eric Day
00005  *
00006  *  This program is free software; you can redistribute it and/or modify
00007  *  it under the terms of the GNU General Public License as published by
00008  *  the Free Software Foundation; version 2 of the License.
00009  *
00010  *  This program is distributed in the hope that it will be useful,
00011  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
00012  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
00013  *  GNU General Public License for more details.
00014  *
00015  *  You should have received a copy of the GNU General Public License
00016  *  along with this program; if not, write to the Free Software
00017  *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
00018  */
00019 
00020 #include <config.h>
00021 
00022 /* This is needed for simple auth, we're not ready for SASL yet. */
00023 #define LDAP_DEPRECATED 1
00024 
00025 #include <ldap.h>
00026 #include <pthread.h>
00027 #include <sys/time.h>
00028 #include <map>
00029 #include <string>
00030 
00031 #include <drizzled/plugin/authentication.h>
00032 #include <drizzled/identifier.h>
00033 #include <drizzled/util/convert.h>
00034 #include <drizzled/algorithm/sha1.h>
00035 
00036 #include <drizzled/module/option_map.h>
00037 #include <boost/program_options.hpp>
00038 
00039 namespace po= boost::program_options;
00040 using namespace std;
00041 using namespace drizzled;
00042 
00043 namespace auth_ldap
00044 {
00045 
00046 std::string uri;
00047 const std::string DEFAULT_URI= "ldap://127.0.0.1/";
00048 std::string bind_dn;
00049 std::string bind_password;
00050 std::string base_dn;
00051 std::string password_attribute;
00052 std::string DEFAULT_PASSWORD_ATTRIBUTE= "userPassword";
00053 std::string mysql_password_attribute;
00054 const std::string DEFAULT_MYSQL_PASSWORD_ATTRIBUTE= "mysqlUserPassword";
00055 static const int DEFAULT_CACHE_TIMEOUT= 600;
00056 typedef constrained_check<int, DEFAULT_CACHE_TIMEOUT, 0, 2147483647> cachetimeout_constraint;
00057 static cachetimeout_constraint cache_timeout= 0;
00058 
00059 
00060 class AuthLDAP: public plugin::Authentication
00061 {
00062 public:
00063 
00064   AuthLDAP(string name_arg);
00065   ~AuthLDAP();
00066 
00072   bool initialize(void);
00073 
00079   bool connect(void);
00080 
00084   string& getError(void);
00085 
00086 private:
00087 
00088   typedef enum
00089   {
00090     NOT_FOUND,
00091     PLAIN_TEXT,
00092     MYSQL_HASH
00093   } PasswordType;
00094 
00095   typedef std::pair<PasswordType, std::string> PasswordEntry;
00096   typedef std::pair<std::string, PasswordEntry> UserEntry;
00097   typedef std::map<std::string, PasswordEntry> UserCache;
00098 
00102   bool authenticate(const identifier::User &sctx, const string &password);
00103 
00109   void lookupUser(const string& user);
00110 
00122   bool verifyMySQLHash(const PasswordEntry &password,
00123                        const string &scramble_bytes,
00124                        const string &scrambled_password);
00125 
00126   time_t next_cache_expiration;
00127   LDAP *ldap;
00128   string error;
00129   UserCache users;
00130   pthread_rwlock_t lock;
00131 };
00132 
00133 AuthLDAP::AuthLDAP(string name_arg):
00134   plugin::Authentication(name_arg),
00135   next_cache_expiration(),
00136   ldap(),
00137   error(),
00138   users()
00139 {
00140 }
00141 
00142 AuthLDAP::~AuthLDAP()
00143 {
00144   pthread_rwlock_destroy(&lock);
00145   if (ldap != NULL)
00146     ldap_unbind(ldap);
00147 }
00148 
00149 bool AuthLDAP::initialize(void)
00150 {
00151   int return_code= pthread_rwlock_init(&lock, NULL);
00152   if (return_code != 0)
00153   {
00154     error= "pthread_rwlock_init failed";
00155     return false;
00156   }
00157 
00158   return connect();
00159 }
00160 
00161 bool AuthLDAP::connect(void)
00162 {
00163   int return_code= ldap_initialize(&ldap, (char *)uri.c_str());
00164   if (return_code != LDAP_SUCCESS)
00165   {
00166     error= "ldap_initialize failed: ";
00167     error+= ldap_err2string(return_code);
00168     return false;
00169   }
00170 
00171   int version= 3;
00172   return_code= ldap_set_option(ldap, LDAP_OPT_PROTOCOL_VERSION, &version);
00173   if (return_code != LDAP_SUCCESS)
00174   {
00175     ldap_unbind(ldap);
00176     ldap= NULL;
00177     error= "ldap_set_option failed: ";
00178     error+= ldap_err2string(return_code);
00179     return false;
00180   }
00181 
00182   if (not bind_dn.empty())
00183   {
00184     return_code= ldap_simple_bind_s(ldap, (char *)bind_dn.c_str(), (char *)bind_password.c_str());
00185     if (return_code != LDAP_SUCCESS)
00186     {
00187       ldap_unbind(ldap);
00188       ldap= NULL;
00189       error= "ldap_simple_bind_s failed: ";
00190       error+= ldap_err2string(return_code);
00191       return false;
00192     }
00193   }
00194 
00195   return true;
00196 }
00197 
00198 string& AuthLDAP::getError(void)
00199 {
00200   return error;
00201 }
00202 
00203 bool AuthLDAP::authenticate(const identifier::User &sctx, const string &password)
00204 {
00205   /* See if cache should be emptied. */
00206   if (cache_timeout > 0)
00207   {
00208     struct timeval current_time;
00209     gettimeofday(&current_time, NULL);
00210     if (current_time.tv_sec > next_cache_expiration)
00211     {
00212       pthread_rwlock_wrlock(&lock);
00213       /* Make sure another thread didn't already clear it. */
00214       if (current_time.tv_sec > next_cache_expiration)
00215       {
00216         users.clear();
00217         next_cache_expiration= current_time.tv_sec + cache_timeout;
00218       }
00219       pthread_rwlock_unlock(&lock);
00220     }
00221   }
00222 
00223   pthread_rwlock_rdlock(&lock);
00224 
00225   AuthLDAP::UserCache::const_iterator user= users.find(sctx.username());
00226   if (user == users.end())
00227   {
00228     pthread_rwlock_unlock(&lock);
00229 
00230     pthread_rwlock_wrlock(&lock);
00231 
00232     /* Make sure the user was not added while we unlocked. */
00233     user= users.find(sctx.username());
00234     if (user == users.end())
00235       lookupUser(sctx.username());
00236 
00237     pthread_rwlock_unlock(&lock);
00238 
00239     pthread_rwlock_rdlock(&lock);
00240 
00241     /* Get user again because map may have changed while unlocked. */
00242     user= users.find(sctx.username());
00243     if (user == users.end())
00244     {
00245       pthread_rwlock_unlock(&lock);
00246       return false;
00247     }
00248   }
00249 
00250   if (user->second.first == NOT_FOUND)
00251   {
00252     pthread_rwlock_unlock(&lock);
00253     return false;
00254   }
00255 
00256   if (sctx.getPasswordType() == identifier::User::MYSQL_HASH)
00257   {
00258     bool allow= verifyMySQLHash(user->second, sctx.getPasswordContext(), password);
00259     pthread_rwlock_unlock(&lock);
00260     return allow;
00261   }
00262 
00263   if (user->second.first == PLAIN_TEXT && password == user->second.second)
00264   {
00265     pthread_rwlock_unlock(&lock);
00266     return true;
00267   }
00268 
00269   pthread_rwlock_unlock(&lock);
00270   return false;
00271 }
00272 
00273 void AuthLDAP::lookupUser(const string& user)
00274 {
00275   string filter("(cn=" + user + ")");
00276   const char *attributes[3]=
00277   {
00278     (char *)password_attribute.c_str(),
00279     (char *)mysql_password_attribute.c_str(),
00280     NULL
00281   };
00282   LDAPMessage *result;
00283   bool try_reconnect= true;
00284 
00285   while (true)
00286   {
00287     if (ldap == NULL)
00288     {
00289       if (! connect())
00290       {
00291         errmsg_printf(error::ERROR, _("Reconnect failed: %s\n"),
00292                       getError().c_str());
00293         return;
00294       }
00295     }
00296 
00297     int return_code= ldap_search_ext_s(ldap,
00298                                        (char *)base_dn.c_str(),
00299                                        LDAP_SCOPE_ONELEVEL,
00300                                        filter.c_str(),
00301                                        const_cast<char **>(attributes),
00302                                        0,
00303                                        NULL,
00304                                        NULL,
00305                                        NULL,
00306                                        1,
00307                                        &result);
00308     if (return_code != LDAP_SUCCESS)
00309     {
00310       errmsg_printf(error::ERROR, _("ldap_search_ext_s failed: %s\n"),
00311                     ldap_err2string(return_code));
00312 
00313       /* Only try one reconnect per request. */
00314       if (try_reconnect)
00315       {
00316         try_reconnect= false;
00317         ldap_unbind(ldap);
00318         ldap= NULL;
00319         continue;
00320       }
00321 
00322       return;
00323     }
00324 
00325     break;
00326   }
00327 
00328   LDAPMessage *entry= ldap_first_entry(ldap, result);
00329   AuthLDAP::PasswordEntry new_password;
00330   if (entry == NULL)
00331     new_password= AuthLDAP::PasswordEntry(NOT_FOUND, "");
00332   else
00333   {
00334     char **values= ldap_get_values(ldap, entry, (char *)mysql_password_attribute.c_str());
00335     if (values == NULL)
00336     {
00337       values= ldap_get_values(ldap, entry, (char *)password_attribute.c_str());
00338       if (values == NULL)
00339         new_password= AuthLDAP::PasswordEntry(NOT_FOUND, "");
00340       else
00341       {
00342         new_password= AuthLDAP::PasswordEntry(PLAIN_TEXT, values[0]);
00343         ldap_value_free(values);
00344       }
00345     }
00346     else
00347     {
00348       new_password= AuthLDAP::PasswordEntry(MYSQL_HASH, values[0]);
00349       ldap_value_free(values);
00350     }
00351   }
00352 
00353   users.insert(AuthLDAP::UserEntry(user, new_password));
00354 }
00355 
00356 bool AuthLDAP::verifyMySQLHash(const PasswordEntry &password,
00357                                const string &scramble_bytes,
00358                                const string &scrambled_password)
00359 {
00360   if (scramble_bytes.size() != SHA1_DIGEST_LENGTH ||
00361       scrambled_password.size() != SHA1_DIGEST_LENGTH)
00362   {
00363     return false;
00364   }
00365 
00366   SHA1_CTX ctx;
00367   uint8_t local_scrambled_password[SHA1_DIGEST_LENGTH];
00368   uint8_t temp_hash[SHA1_DIGEST_LENGTH];
00369   uint8_t scrambled_password_check[SHA1_DIGEST_LENGTH];
00370 
00371   if (password.first == MYSQL_HASH)
00372   {
00373     /* Get the double-hashed password from the given hex string. */
00374     drizzled_hex_to_string(reinterpret_cast<char*>(local_scrambled_password),
00375                            password.second.c_str(), SHA1_DIGEST_LENGTH * 2);
00376   }
00377   else
00378   {
00379     /* Generate the double SHA1 hash for the password stored locally first. */
00380     SHA1Init(&ctx);
00381     SHA1Update(&ctx, reinterpret_cast<const uint8_t *>(password.second.c_str()),
00382                password.second.size());
00383     SHA1Final(temp_hash, &ctx);
00384 
00385     SHA1Init(&ctx);
00386     SHA1Update(&ctx, temp_hash, SHA1_DIGEST_LENGTH);
00387     SHA1Final(local_scrambled_password, &ctx);
00388   }
00389 
00390   /* Hash the scramble that was sent to client with the local password. */
00391   SHA1Init(&ctx);
00392   SHA1Update(&ctx, reinterpret_cast<const uint8_t*>(scramble_bytes.c_str()),
00393              SHA1_DIGEST_LENGTH);
00394   SHA1Update(&ctx, local_scrambled_password, SHA1_DIGEST_LENGTH);
00395   SHA1Final(temp_hash, &ctx);
00396 
00397   /* Next, XOR the result with what the client sent to get the original
00398      single-hashed password. */
00399   for (int x= 0; x < SHA1_DIGEST_LENGTH; x++)
00400     temp_hash[x]= temp_hash[x] ^ scrambled_password[x];
00401 
00402   /* Hash this result once more to get the double-hashed password again. */
00403   SHA1Init(&ctx);
00404   SHA1Update(&ctx, temp_hash, SHA1_DIGEST_LENGTH);
00405   SHA1Final(scrambled_password_check, &ctx);
00406 
00407   /* These should match for a successful auth. */
00408   return memcmp(local_scrambled_password, scrambled_password_check, SHA1_DIGEST_LENGTH) == 0;
00409 }
00410 
00411 static int init(module::Context &context)
00412 {
00413   AuthLDAP *auth_ldap= new AuthLDAP("auth_ldap");
00414   if (! auth_ldap->initialize())
00415   {
00416     errmsg_printf(error::ERROR, _("Could not load auth ldap: %s\n"),
00417                   auth_ldap->getError().c_str());
00418     delete auth_ldap;
00419     return 1;
00420   }
00421 
00422   context.registerVariable(new sys_var_const_string_val("uri", uri));
00423   context.registerVariable(new sys_var_const_string_val("bind-dn", bind_dn));
00424   context.registerVariable(new sys_var_const_string_val("bind-password", bind_password));
00425   context.registerVariable(new sys_var_const_string_val("base-dn", base_dn));
00426   context.registerVariable(new sys_var_const_string_val("password-attribute",password_attribute));
00427   context.registerVariable(new sys_var_const_string_val("mysql-password-attribute", mysql_password_attribute));
00428   context.registerVariable(new sys_var_constrained_value_readonly<int>("cache-timeout", cache_timeout));
00429 
00430   context.add(auth_ldap);
00431   return 0;
00432 }
00433 
00434 static void init_options(drizzled::module::option_context &context)
00435 {
00436   context("uri", po::value<string>(&uri)->default_value(DEFAULT_URI),
00437           N_("URI of the LDAP server to contact"));
00438   context("bind-db", po::value<string>(&bind_dn)->default_value(""),
00439           N_("DN to use when binding to the LDAP server"));
00440   context("bind-password", po::value<string>(&bind_password)->default_value(""),
00441           N_("Password to use when binding the DN"));
00442   context("base-dn", po::value<string>(&base_dn)->default_value(""),
00443           N_("DN to use when searching"));
00444   context("password-attribute", po::value<string>(&password_attribute)->default_value(DEFAULT_PASSWORD_ATTRIBUTE),
00445           N_("Attribute in LDAP with plain text password"));
00446   context("mysql-password-attribute", po::value<string>(&mysql_password_attribute)->default_value(DEFAULT_MYSQL_PASSWORD_ATTRIBUTE),
00447           N_("Attribute in LDAP with MySQL hashed password"));
00448   context("cache-timeout", po::value<cachetimeout_constraint>(&cache_timeout)->default_value(DEFAULT_CACHE_TIMEOUT),
00449           N_("How often to empty the users cache, 0 to disable"));
00450 }
00451 
00452 } /* namespace auth_ldap */
00453 
00454 DRIZZLE_PLUGIN(auth_ldap::init, NULL, auth_ldap::init_options);