From: Michael M Slusarz Date: Tue, 7 Jul 2009 01:08:53 +0000 (-0600) Subject: Auth:: (CVS) -> Horde_Auth:: X-Git-Url: https://git.internetallee.de/?a=commitdiff_plain;h=880314e236f67912136bd1494d4dc177ade28846;p=horde.git Auth:: (CVS) -> Horde_Auth:: --- diff --git a/framework/Auth/lib/Horde/Auth.php b/framework/Auth/lib/Horde/Auth.php new file mode 100644 index 000000000..4e4edce17 --- /dev/null +++ b/framework/Auth/lib/Horde/Auth.php @@ -0,0 +1,1001 @@ + + * @package Horde_Auth + */ +class Horde_Auth +{ + /** + * The parameter name for the logout reason. + */ + const REASON_PARAM = 'logout_reason'; + + /** + * The parameter name for the logout message used with type + * REASON_MESSAGE. + */ + const REASON_MSG_PARAM = 'logout_msg'; + + /** + * The 'badlogin' reason. + * + * The following 'reasons' for the logout screen are recognized: + *
+     * REASON_BADLOGIN - Bad username and/or password
+     * REASON_BROWSER - A browser change was detected
+     * REASON_FAILED - Login failed
+     * REASON_EXPIRED - Password has expired
+     * REASON_LOGOUT - Logout due to user request
+     * REASON_MESSAGE - Logout with custom message in REASON_MSG_PARAM
+     * REASON_SESSION - Logout due to session expiration
+     * REASON_SESSIONIP - Logout due to change of IP address during session
+     * 
+ */ + const REASON_BADLOGIN = 1; + const REASON_BROWSER = 2; + const REASON_FAILED = 3; + const REASON_EXPIRED = 4; + const REASON_LOGOUT = 5; + const REASON_MESSAGE = 6; + const REASON_SESSION = 7; + const REASON_SESSIONIP = 8; + + /** + * 64 characters that are valid for APRMD5 passwords. + */ + const APRMD5_VALID = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; + + /** + * Characters used when generating a password. + */ + const VOWELS = 'aeiouy'; + const CONSONANTS = 'bcdfghjklmnpqrstvwxz'; + const NUMBERS = '0123456789'; + + /** + * Singleton instances. + * + * @var array + */ + static protected $_instances = array(); + + /** + * Attempts to return a concrete Horde_Auth_Driver instance based on + * $driver. + * + * @param mixed $driver The type of concrete Horde_Auth_Driver subclass + * to return. If $driver is an array, then look + * in $driver[0]/lib/Auth/ for the subclass + * implementation named $driver[1].php. + * @param array $params A hash containing any additional configuration or + * parameters a subclass might need. + * + * @return Horde_Auth_Driver The newly created concrete instance. + * @throws Horde_Exception + */ + static public function factory($driver, $params = null) + { + if (is_array($driver)) { + list($app, $driv_name) = $driver; + $driver = basename($driv_name); + } else { + $driver = basename($driver); + } + + /* Return a base Horde_Auth_Driver object if no driver is + * specified. */ + if (empty($driver) || (strcasecmp($driver, 'none') == 0)) { + return new Horde_Auth_Driver(); + } + + if (empty($params)) { + $params = Horde::getDriverConfig('auth', $driver); + } + + $class = (empty($app) ? 'Horde' : $app) . '_Auth_' . ucfirst($driver); + + if (class_exists($class)) { + return new $class($params); + } + + throw new Horde_Exception('Class definition of ' . $class . ' not found.'); + } + + /** + * Attempts to return a reference to a concrete instance based on $driver. + * It will only create a new instance if no instance with the same + * parameters currently exists. + * + * This method must be invoked as: $var = Horde_Auth::singleton() + * + * @param mixed $driver The type of concrete Horde_Auth_Driver subclass + * to return. If $driver is an array, then look + * in $driver[0]/lib/Auth/ for the subclass + * implementation named $driver[1].php. + * @param array $params A hash containing any additional configuration or + * connection parameters a subclass might need. + * + * @return Horde_Auth_Driver The concrete reference. + * @throws Horde_Exception + */ + static public function singleton($driver, $params = array()) + { + ksort($params); + $signature = hash('md5', serialize(array($driver, $params))); + + if (!isset(self::$_instances[$signature])) { + self::$_instances[$signature] = Horde_Auth::factory($driver, $params); + } + + return self::$_instances[$signature]; + } + + /** + * Formats a password using the current encryption. + * + * @param string $plaintext The plaintext password to encrypt. + * @param string $salt The salt to use to encrypt the password. + * If not present, a new salt will be + * generated. + * @param string $encryption The kind of pasword encryption to use. + * Defaults to md5-hex. + * @param boolean $show_encrypt Some password systems prepend the kind of + * encryption to the crypted password ({SHA}, + * etc). Defaults to false. + * + * @return string The encrypted password. + */ + static public function getCryptedPassword($plaintext, $salt = '', + $encryption = 'md5-hex', + $show_encrypt = false) + { + /* Get the salt to use. */ + $salt = self::getSalt($encryption, $salt, $plaintext); + + /* Encrypt the password. */ + switch ($encryption) { + case 'plain': + return $plaintext; + + case 'msad': + return Horde_String::convertCharset('"' . $plaintext . '"', 'ISO-8859-1', 'UTF-16LE'); + + case 'sha': + $encrypted = base64_encode(pack('H*', hash('sha1', $plaintext))); + return $show_encrypt ? '{SHA}' . $encrypted : $encrypted; + + case 'crypt': + case 'crypt-des': + case 'crypt-md5': + case 'crypt-blowfish': + return ($show_encrypt ? '{crypt}' : '') . crypt($plaintext, $salt); + + case 'md5-base64': + $encrypted = base64_encode(pack('H*', hash('md5', $plaintext))); + return $show_encrypt ? '{MD5}' . $encrypted : $encrypted; + + case 'ssha': + $encrypted = base64_encode(pack('H*', hash('sha1', $plaintext . $salt)) . $salt); + return $show_encrypt ? '{SSHA}' . $encrypted : $encrypted; + + case 'smd5': + $encrypted = base64_encode(pack('H*', hash('md5', $plaintext . $salt)) . $salt); + return $show_encrypt ? '{SMD5}' . $encrypted : $encrypted; + + case 'aprmd5': + $length = strlen($plaintext); + $context = $plaintext . '$apr1$' . $salt; + $binary = pack('H*', hash('md5', $plaintext . $salt . $plaintext)); + + for ($i = $length; $i > 0; $i -= 16) { + $context .= substr($binary, 0, ($i > 16 ? 16 : $i)); + } + for ($i = $length; $i > 0; $i >>= 1) { + $context .= ($i & 1) ? chr(0) : $plaintext[0]; + } + + $binary = pack('H*', hash('md5', $context)); + + for ($i = 0; $i < 1000; ++$i) { + $new = ($i & 1) ? $plaintext : substr($binary, 0, 16); + if ($i % 3) { + $new .= $salt; + } + if ($i % 7) { + $new .= $plaintext; + } + $new .= ($i & 1) ? substr($binary, 0, 16) : $plaintext; + $binary = pack('H*', hash('md5', $new)); + } + + $p = array(); + for ($i = 0; $i < 5; $i++) { + $k = $i + 6; + $j = $i + 12; + if ($j == 16) { + $j = 5; + } + $p[] = self::_toAPRMD5((ord($binary[$i]) << 16) | + (ord($binary[$k]) << 8) | + (ord($binary[$j])), + 5); + } + + return '$apr1$' . $salt . '$' . implode('', $p) . self::_toAPRMD5(ord($binary[11]), 3); + + case 'md5-hex': + default: + return ($show_encrypt) ? '{MD5}' . hash('md5', $plaintext) : hash('md5', $plaintext); + } + } + + /** + * Returns a salt for the appropriate kind of password encryption. + * Optionally takes a seed and a plaintext password, to extract the seed + * of an existing password, or for encryption types that use the plaintext + * in the generation of the salt. + * + * @param string $encryption The kind of pasword encryption to use. + * Defaults to md5-hex. + * @param string $seed The seed to get the salt from (probably a + * previously generated password). Defaults to + * generating a new seed. + * @param string $plaintext The plaintext password that we're generating + * a salt for. Defaults to none. + * + * @return string The generated or extracted salt. + */ + static public function getSalt($encryption = 'md5-hex', $seed = '', + $plaintext = '') + { + switch ($encryption) { + case 'crypt': + case 'crypt-des': + return $seed + ? substr(preg_replace('|^{crypt}|i', '', $seed), 0, 2) + : substr(hash('md5', mt_rand()), 0, 2); + + case 'crypt-md5': + return $seed + ? substr(preg_replace('|^{crypt}|i', '', $seed), 0, 12) + : '$1$' . substr(hash('md5', mt_rand()), 0, 8) . '$'; + + case 'crypt-blowfish': + return $seed + ? substr(preg_replace('|^{crypt}|i', '', $seed), 0, 16) + : '$2$' . substr(hash('md5', mt_rand()), 0, 12) . '$'; + + case 'ssha': + return $seed + ? substr(base64_decode(preg_replace('|^{SSHA}|i', '', $seed)), 20) + : substr(pack('H*', sha1(substr(pack('h*', hash('md5', mt_rand())), 0, 8) . $plaintext)), 0, 4); + + case 'smd5': + return $seed + ? substr(base64_decode(preg_replace('|^{SMD5}|i', '', $seed)), 16) + : substr(pack('H*', hash('md5', substr(pack('h*', hash('md5', mt_rand())), 0, 8) . $plaintext)), 0, 4); + + case 'aprmd5': + if ($seed) { + return substr(preg_replace('/^\$apr1\$(.{8}).*/', '\\1', $seed), 0, 8); + } else { + $salt = ''; + $valid = self::APRMD5_VALID; + for ($i = 0; $i < 8; ++$i) { + $salt .= $valid[mt_rand(0, 63)]; + } + return $salt; + } + + default: + return ''; + } + } + + /** + * Generates a random, hopefully pronounceable, password. This can be used + * when resetting automatically a user's password. + * + * @return string A random password + */ + static public function genRandomPassword() + { + /* Alternate consonant and vowel random chars with two random numbers + * at the end. This should produce a fairly pronounceable password. */ + return substr(self::CONSONANTS, mt_rand(0, strlen(self::CONSONANTS) - 1), 1) . + substr(self::VOWELS, mt_rand(0, strlen(self::VOWELS) - 1), 1) . + substr(self::CONSONANTS, mt_rand(0, strlen(self::CONSONANTS) - 1), 1) . + substr(self::VOWELS, mt_rand(0, strlen(self::VOWELS) - 1), 1) . + substr(self::CONSONANTS, mt_rand(0, strlen(self::CONSONANTS) - 1), 1) . + substr(self::NUMBERS, mt_rand(0, strlen(self::NUMBERS) - 1), 1) . + substr(self::NUMBERS, mt_rand(0, strlen(self::NUMBERS) - 1), 1); + } + + /** + * Calls all applications' removeUser API methods. + * + * @param string $userId The userId to delete. + * + * @throws Horde_Exception + */ + static public function removeUserData($userId) + { + $errApps = array(); + + foreach ($GLOBALS['registry']->listApps(array('notoolbar', 'hidden', 'active', 'admin')) as $app) { + if ($GLOBALS['registry']->hasMethod('removeUserData', $app) && + is_a($result = $GLOBALS['registry']->callByPackage($app, 'removeUserData', array($userId)), 'PEAR_Error')) { + Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR); + $errApps[] = $app; + } + } + + if (count($errApps)) { + throw new Horde_Exception(sprintf(_("The following applications encountered errors removing user data: %s"), implode(', ', $errApps))); + } + } + + /** + * Checks if there is a session with valid auth information. for the + * specified user. If there isn't, but the configured Auth driver supports + * transparent authentication, then we try that. + * + * @param string $realm The authentication realm to check. + * + * @return boolean Whether or not the user is authenticated. + */ + static public function isAuthenticated($realm = null) + { + if (isset($_SESSION['horde_auth']) && + !empty($_SESSION['horde_auth']['authenticated']) && + !empty($_SESSION['horde_auth']['userId']) && + ($_SESSION['horde_auth']['realm'] == $realm)) { + if (!self::checkSessionIP()) { + self::setAuthError(self::REASON_SESSIONIP); + return false; + } elseif (!self::checkBrowserString()) { + self::setAuthError(self::REASON_BROWSER); + return false; + } + return true; + } + + // Try transparent authentication now. + $auth = self::singleton($GLOBALS['conf']['auth']['driver']); + if ($auth->hasCapability('transparent') && $auth->transparent()) { + return self::isAuthenticated($realm); + } + + return false; + } + + /** + * Returns the currently logged in user, if there is one. + * + * @return mixed The userId of the current user, or false if no user is + * logged in. + */ + static public function getAuth() + { + if (isset($_SESSION['horde_auth'])) { + if (!empty($_SESSION['horde_auth']['authenticated']) && + !empty($_SESSION['horde_auth']['userId'])) { + return $_SESSION['horde_auth']['userId']; + } + } + + return false; + } + + /** + * Return whether the authentication backend requested a password change. + * + * @return boolean Whether the backend requested a password change. + */ + static public function isPasswordChangeRequested() + { + return (isset($_SESSION['horde_auth']) && + !empty($_SESSION['horde_auth']['authenticated']) && + !empty($_SESSION['horde_auth']['changeRequested'])); + } + + /** + * Returns the curently logged-in user without any domain information + * (e.g., bob@example.com would be returned as 'bob'). + * + * @return mixed The user ID of the current user, or false if no user + * is logged in. + */ + static public function getBareAuth() + { + $user = self::getAuth(); + if ($user) { + $pos = strpos($user, '@'); + if ($pos !== false) { + $user = substr($user, 0, $pos); + } + } + + return $user; + } + + /** + * Returns the domain of currently logged-in user (e.g., bob@example.com + * would be returned as 'example.com'). + * + * @return mixed The domain suffix of the current user, or false. + */ + static public function getAuthDomain() + { + if ($user = self::getAuth()) { + $pos = strpos($user, '@'); + if ($pos !== false) { + return substr($user, $pos + 1); + } + } + + return false; + } + + /** + * Returns the requested credential for the currently logged in user, if + * present. + * + * @param string $credential The credential to retrieve. + * + * @return mixed The requested credential, or false if no user is + * logged in. + */ + static public function getCredential($credential) + { + if (empty($_SESSION['horde_auth']) || + empty($_SESSION['horde_auth']['authenticated'])) { + return false; + } + + $credentials = Horde_Secret::read(Horde_Secret::getKey('auth'), $_SESSION['horde_auth']['credentials']); + $credentials = @unserialize($credentials); + + if (is_array($credentials) && + isset($credentials[$credential])) { + return $credentials[$credential]; + } + + return false; + } + + /** + * Sets the requested credential for the currently logged in user. + * + * @param string $credential The credential to set. + * @param string $value The value to set the credential to. + */ + static public function setCredential($credential, $value) + { + if (!empty($_SESSION['horde_auth']) && + !empty($_SESSION['horde_auth']['authenticated'])) { + $credentials = @unserialize(Horde_Secret::read(Horde_Secret::getKey('auth'), $_SESSION['horde_auth']['credentials'])); + if (is_array($credentials)) { + $credentials[$credential] = $value; + } else { + $credentials = array($credential => $value); + } + $_SESSION['horde_auth']['credentials'] = Horde_Secret::write(Horde_Secret::getKey('auth'), serialize($credentials)); + } + } + + /** + * Sets a variable in the session saying that authorization has succeeded, + * note which userId was authorized, and note when the login took place. + * + * If a user name hook was defined in the configuration, it gets applied + * to the $userId at this point. + * + * @param string $userId The userId who has been authorized. + * @param array $credentials The credentials of the user. + * @param string $realm The authentication realm to use. + * @param boolean $change Whether to request that the user change + * their password. + */ + static public function setAuth($userId, $credentials, $realm = null, + $change = false) + { + $userId = self::addHook(trim($userId)); + + if (!empty($GLOBALS['conf']['hooks']['postauthenticate']) && + !Horde::callHook('_horde_hook_postauthenticate', array($userId, $credentials, $realm), 'horde', false)) { + if (self::getAuthError() != self::REASON_MESSAGE) { + self::setAuthError(self::REASON_FAILED); + } + return false; + } + + /* If we're already set with this userId, don't continue. */ + if (isset($_SESSION['horde_auth']['userId']) && + ($_SESSION['horde_auth']['userId'] == $userId)) { + return true; + } + + /* Clear any existing info. */ + self::clearAuth($realm); + + $credentials = Horde_Secret::write(Horde_Secret::getKey('auth'), serialize($credentials)); + + if (!empty($realm)) { + $userId .= '@' . $realm; + } + + $browser = Horde_Browser::singleton(); + + $_SESSION['horde_auth'] = array( + 'authenticated' => true, + 'browser' => $browser->getAgentString(), + 'changeRequested' => $change, + 'credentials' => $credentials, + 'realm' => $realm, + 'remote_addr' => isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : null, + 'timestamp' => time(), + 'userId' => $userId + ); + + /* Reload preferences for the new user. */ + $GLOBALS['registry']->loadPrefs(); + NLS::setLang($GLOBALS['prefs']->getValue('language')); + + /* Fetch the user's last login time. */ + $old_login = @unserialize($GLOBALS['prefs']->getValue('last_login')); + + /* Display it, if we have a notification object and the + * show_last_login preference is active. */ + if (isset($GLOBALS['notification']) && + $GLOBALS['prefs']->getValue('show_last_login')) { + if (empty($old_login['time'])) { + $GLOBALS['notification']->push(_("Last login: Never"), 'horde.message'); + } else { + if (empty($old_login['host'])) { + $GLOBALS['notification']->push(sprintf(_("Last login: %s"), strftime('%c', $old_login['time'])), 'horde.message'); + } else { + $GLOBALS['notification']->push(sprintf(_("Last login: %s from %s"), strftime('%c', $old_login['time']), $old_login['host']), 'horde.message'); + } + } + } + + /* Set the user's last_login information. */ + $host = empty($_SERVER['HTTP_X_FORWARDED_FOR']) + ? $_SERVER['REMOTE_ADDR'] + : $_SERVER['HTTP_X_FORWARDED_FOR']; + + if (class_exists('Net_DNS')) { + $resolver = new Net_DNS_Resolver(); + $resolver->retry = isset($GLOBALS['conf']['dns']['retry']) ? $GLOBALS['conf']['dns']['retry'] : 1; + $resolver->retrans = isset($GLOBALS['conf']['dns']['retrans']) ? $GLOBALS['conf']['dns']['retrans'] : 1; + $response = $resolver->query($host, 'PTR'); + $ptrdname = $response ? $response->answer[0]->ptrdname : $host; + } else { + $ptrdname = @gethostbyaddr($host); + } + + $last_login = array('time' => time(), 'host' => $ptrdname); + $GLOBALS['prefs']->setValue('last_login', serialize($last_login)); + + if ($change) { + $GLOBALS['notification']->push(_("Your password has expired."), + 'horde.message'); + + $auth = self::singleton($GLOBALS['conf']['auth']['driver']); + if ($auth->hasCapability('update')) { + /* A bit of a kludge. URL is set from the login screen, but + * we aren't completely certain we got here from the login + * screen. So any screen which calls setAuth() which has a + * url will end up going there. Should be OK. */ + $url_param = Horde_Util::getFormData('url'); + + if ($url_param) { + $url = Horde::url(Horde_Util::removeParameter($url_param, session_name()), true); + $return_to = $GLOBALS['registry']->get('webroot', 'horde') . '/index.php'; + $return_to = Horde_Util::addParameter($return_to, 'url', $url); + } else { + $return_to = Horde::url($GLOBALS['registry']->get('webroot', 'horde') . '/index.php'); + } + + $url = Horde::applicationUrl('services/changepassword.php'); + $url = Horde_Util::addParameter($url, array('return_to' => $return_to), null, false); + + header('Location: ' . $url); + exit; + } + } + + return true; + } + + /** + * Clears any authentication tokens in the current session. + * + * @param string $realm The authentication realm to clear. + */ + static public function clearAuth($realm = null) + { + if (!empty($realm) && isset($_SESSION['horde_auth'][$realm])) { + $_SESSION['horde_auth'][$realm] = array('authenticated' => false); + } elseif (isset($_SESSION['horde_auth'])) { + $_SESSION['horde_auth'] = array('authenticated' => false); + } + + /* Remove the user's cached preferences if they are present. */ + if (isset($GLOBALS['registry'])) { + $GLOBALS['registry']->unloadPrefs(); + } + } + + /** + * Is the current user an administrator? + * + * @param string $permission Allow users with this permission admin access + * in the current context. + * @param integer $permlevel The level of permissions to check for + * (PERMS_EDIT, PERMS_DELETE, etc). Defaults + * to PERMS_EDIT. + * @param string $user The user to check. Defaults to + * self::getAuth(). + * + * @return boolean Whether or not this is an admin user. + */ + static public function isAdmin($permission = null, $permlevel = null, + $user = null) + { + if (is_null($user)) { + $user = self::getAuth(); + } + + if ($user && + @is_array($GLOBALS['conf']['auth']['admins']) && + in_array($user, $GLOBALS['conf']['auth']['admins'])) { + return true; + } + + if (!is_null($permission)) { + if (is_null($permlevel)) { + $permlevel = PERMS_EDIT; + } + return $GLOBALS['perms']->hasPermission($permission, $user, $permlevel); + } + + return false; + } + + /** + * Applies a hook defined by the function _username_hook_frombackend() to + * the given user name if this function exists and user hooks are enabled. + * + * This method should be called if a authentication backend's user name + * needs to be converted to a (unique) Horde user name. The backend's user + * name is what the user sees and uses, but internally we use the Horde + * user name. + * + * @param string $userId The authentication backend's user name. + * + * @return string The internal Horde user name. + */ + static public function addHook($userId) + { + if (!empty($GLOBALS['conf']['hooks']['username'])) { + $newId = Horde::callHook('_username_hook_frombackend', array($userId)); + if (!is_a($newId, 'PEAR_Error')) { + return $newId; + } + } + + return $userId; + } + + /** + * Applies a hook defined by the function _username_hook_tobackend() to + * the given user name if this function exists and user hooks are enabled. + * + * This method should be called if a Horde user name needs to be converted + * to an authentication backend's user name or displayed to the user. The + * backend's user name is what the user sees and uses, but internally we + * use the Horde user name. + * + * @param string $userId The internal Horde user name. + * + * @return string The authentication backend's user name. + */ + static public function removeHook($userId) + { + if (!empty($GLOBALS['conf']['hooks']['username'])) { + $newId = Horde::callHook('_username_hook_tobackend', array($userId)); + if (!is_a($newId, 'PEAR_Error')) { + return $newId; + } + } + + return $userId; + } + + /** + * Returns the name of the authentication provider. + * + * @param string $driver Used by recursive calls when untangling composite + * auth. + * @param array $params Used by recursive calls when untangling composite + * auth. + * + * @return string The name of the driver currently providing + * authentication. + */ + static public function getProvider($driver = null, $params = null) + { + if (is_null($driver)) { + $driver = $GLOBALS['conf']['auth']['driver']; + } + + if (is_null($params)) { + $params = Horde::getDriverConfig('auth', is_array($driver) ? $driver[1] : $driver); + } + + if ($driver == 'application') { + return isset($params['app']) ? $params['app'] : 'application'; + } elseif ($driver == 'composite') { + if (($login_driver = self::getDriverByParam('loginscreen_switch', $params)) && + !empty($params['drivers'][$login_driver])) { + return self::getProvider($params['drivers'][$login_driver]['driver'], + isset($params['drivers'][$login_driver]['params']) ? $params['drivers'][$login_driver]['params'] : null); + } + return 'composite'; + } else { + return $driver; + } + } + + /** + * Returns the logout reason. + * + * @return string One of the logout reasons (see the AUTH_LOGOUT_* + * constants for the valid reasons). Returns null if there + * is no logout reason present. + */ + static public function getLogoutReason() + { + return isset($GLOBALS['horde_auth']['logout']['type']) + ? $GLOBALS['horde_auth']['logout']['type'] + : Horde_Util::getFormData(self::REASON_PARAM); + } + + /** + * Returns the status string to use for logout messages. + * + * @return string The logout reason string. + */ + static public function getLogoutReasonString() + { + switch (self::getLogoutReason()) { + case self::REASON_SESSION: + return sprintf(_("Your %s session has expired. Please login again."), $GLOBALS['registry']->get('name')); + + case self::REASON_SESSIONIP: + return sprintf(_("Your Internet Address has changed since the beginning of your %s session. To protect your security, you must login again."), $GLOBALS['registry']->get('name')); + + case self::REASON_BROWSER: + return sprintf(_("Your browser appears to have changed since the beginning of your %s session. To protect your security, you must login again."), $GLOBALS['registry']->get('name')); + + case self::REASON_LOGOUT: + return _("You have been logged out."); + + case self::REASON_FAILED: + return _("Login failed."); + break; + + case self::REASON_BADLOGIN: + return _("Login failed because your username or password was entered incorrectly."); + break; + + case self::REASON_EXPIRED: + return _("Your login has expired."); + break; + + case self::REASON_MESSAGE: + return isset($GLOBALS['horde_auth']['logout']['msg']) + ? $GLOBALS['horde_auth']['logout']['msg'] + : Horde_Util::getFormData(self::REASON_MSG_PARAM); + + default: + return ''; + } + } + + /** + * Generates the correct parameters to pass to the given logout URL. + * + * If no reason/msg is passed in, use the current global authentication + * error message. + * + * @param string $url The URL to redirect to. + * @param string $reason The reason for logout. + * @param string $msg If reason is self::REASON_MESSAGE, the message to + * display to the user. + * + * @return string The formatted URL + */ + static public function addLogoutParameters($url, $reason = null, + $msg = null) + { + $params = array('horde_logout_token' => Horde::getRequestToken('horde.logout')); + + if (isset($GLOBALS['registry'])) { + $params['app'] = $GLOBALS['registry']->getApp(); + } + + if (is_null($reason)) { + $reason = self::getLogoutReason(); + } + + if ($reason) { + $params[self::REASON_PARAM] = $reason; + if ($reason == self::REASON_MESSAGE) { + if (is_null($msg)) { + $msg = self::getLogoutReasonString(); + } + $params[self::REASON_MSG_PARAM] = $msg; + } + } + + return Horde_Util::addParameter($url, $params, null, false); + } + + /** + * Reads session data to determine if it contains Horde authentication + * credentials. + * + * @param string $session_data The session data. + * @param boolean $info Return session information. The following + * information is returned: userid, realm, + * timestamp, remote_addr, browser. + * + * @return array An array of the user's sesion information if + * authenticated or false. The following information is + * returned: userid, realm, timestamp, remote_addr, browser. + */ + static public function readSessionData($session_data) + { + if (empty($session_data)) { + return false; + } + + $pos = strpos($session_data, 'horde_auth|'); + if ($pos === false) { + return false; + } + + $endpos = $pos + 7; + $old_error = error_reporting(0); + + while ($endpos !== false) { + $endpos = strpos($session_data, '|', $endpos); + $data = unserialize(substr($session_data, $pos + 7, $endpos)); + if (is_array($data)) { + error_reporting($old_error); + if (empty($data['authenticated'])) { + return false; + } + return array( + 'browser' => $data['browser'], + 'realm' => $data['realm'], + 'remote_addr' => $data['remote_addr'], + 'timestamp' => $data['timestamp'], + 'userid' => $data['userId'] + ); + } + ++$endpos; + } + + return false; + } + + /** + * Sets the error message for an invalid authentication. + * + * @param string $type The type of error (self::REASON_* constant). + * @param string $msg The error message/reason for invalid + * authentication. + */ + public function setAuthError($type, $msg = null) + { + $GLOBALS['horde_auth']['logout'] = array( + 'msg' => $msg, + 'type' => $type + ); + } + + /** + * Returns the error type for an invalid authentication or false on error. + * + * @return mixed Error type or false on error. + */ + public function getAuthError() + { + return isset($GLOBALS['horde_auth']['logout']['type']) + ? $GLOBALS['horde_auth']['logout']['type'] + : false; + } + + /** + * Returns the appropriate authentication driver, if any, selecting by the + * specified parameter. + * + * @param string $name The parameter name. + * @param array $params The parameter list. + * @param string $driverparams A list of parameters to pass to the driver. + * + * @return mixed Return value or called user func or null if unavailable + */ + public function getDriverByParam($name, $params, + $driverparams = array()) + { + if (isset($params[$name]) && + function_exists($params[$name])) { + return call_user_func_array($params[$name], $driverparams); + } + + return null; + } + + /** + * Performs check on session to see if IP Address has changed since the + * last access. + * + * @return boolean True if IP Address is the same (or the check is + * disabled), false if the address has changed. + */ + static public function checkSessionIP() + { + return (empty($GLOBALS['conf']['auth']['checkip']) || + (isset($_SESSION['horde_auth']['remote_addr']) && + ($_SESSION['horde_auth']['remote_addr'] == $_SERVER['REMOTE_ADDR']))); + } + + /** + * Performs check on session to see if browser string has changed since + * the last access. + * + * @return boolean True if browser string is the same, false if the + * string has changed. + */ + static public function checkBrowserString() + { + return (empty($GLOBALS['conf']['auth']['checkbrowser']) || + ($_SESSION['horde_auth']['browser'] == $GLOBALS['browser']->getAgentString())); + } + + /** + * Converts to allowed 64 characters for APRMD5 passwords. + * + * @param string $value TODO + * @param integer $count TODO + * + * @return string $value converted to the 64 MD5 characters. + */ + static protected function _toAPRMD5($value, $count) + { + $aprmd5 = ''; + $count = abs($count); + $valid = self::APRMD5_VALID; + + while (--$count) { + $aprmd5 .= $valid[$value & 0x3f]; + $value >>= 6; + } + + return $aprmd5; + } + +} diff --git a/framework/Auth/lib/Horde/Auth/Application.php b/framework/Auth/lib/Horde/Auth/Application.php new file mode 100644 index 000000000..392426274 --- /dev/null +++ b/framework/Auth/lib/Horde/Auth/Application.php @@ -0,0 +1,199 @@ + + * 'app' - (string) The application which is providing authentication. + * + * + * Copyright 2002-2009 The Horde Project (http://www.horde.org/) + * + * See the enclosed file COPYING for license information (LGPL). If you did + * not receive this file, see http://opensource.org/licenses/lgpl-2.1.php + * + * @author Chuck Hagenbuch + * @package Horde_Auth + */ +class Horde_Auth_Application extends Horde_Auth_Driver +{ + /** + * Cache for hasCapability(). + * + * @var array + */ + protected $_loaded = array(); + + /** + * Constructor. + * + * @param array $params A hash containing connection parameters. + */ + public function __construct($params = array()) + { + Horde::assertDriverConfig($params, 'auth', array('app'), 'authentication application'); + + parent::__construct($params); + } + + /** + * Queries the current Auth object to find out if it supports the given + * capability. + * + * @param string $capability The capability to test for. + * + * @return boolean Whether or not the capability is supported. + */ + public function hasCapability($capability) + { + $capability = strtolower($capability); + + $methods = array( + 'add' => 'addUser', + 'exists' => 'userExists', + 'list' => 'userList', + 'remove' => 'removeUser', + 'update' => 'updateUser' + ); + + if (!in_array($capability, $this->_loaded) && + isset($methods[$capability])) { + $this->_capabilities[$capability] = $GLOBALS['registry']->hasMethod($methods[$capability], $this->_params['app']); + $this->_loaded[] = $capability; + } + + return parent::hasCapability($capability); + } + + /** + * Find out if a set of login credentials are valid. + * + * @param string $userId The userId to check. + * @param array $credentials The credentials to use. + * + * @throws Horde_Exception + */ + protected function _authenticate($userId, $credentials) + { + if (!$GLOBALS['registry']->hasMethod('authenticate', $this->_params['app'])) { + throw new Horde_Exception($this->_params['app'] . ' does not provide an authenticate() method.'); + } + + if (!$GLOBALS['registry']->callByPackage($this->_params['app'], 'authenticate', array('userId' => $userId, 'credentials' => $credentials, 'params' => $this->_params))) { + throw new Horde_Exception('', Horde_Auth::REASON_BADLOGIN); + } + + // Horrific hack. Avert your eyes. Since an application may already + // set the authentication information necessary, we don't want to + // overwrite that info. Coming into this function, we know that + // the authentication has not yet been set in this session. So after + // calling the app-specific auth handler, if authentication + // information has suddenly appeared, it follows that the information + // has been stored already in the session and we shouldn't overwrite. + // So grab the authentication ID set and stick it in $_authCredentials + // this will eventually cause setAuth() in authenticate() to exit + // before re-setting the auth info values. + if ($ret && ($authid = Horde_Auth::getAuth())) { + $this->_authCredentials['userId'] = $authid; + } + } + + /** + * Return the URI of the login screen for this authentication method. + * + * @param string $app The application to use. + * @param string $url The URL to redirect to after login. + * + * @return string The login screen URI. + */ + public function getLoginScreen($app = 'horde', $url = '') + { + return parent::getLoginScreen($this->_params['app'], $url); + } + + /** + * List all users in the system. + * + * @return array The array of userIds. + * @throws Horde_Exception + */ + public function listUsers() + { + return $this->hasCapability('list') + ? $GLOBALS['registry']->callByPackage($this->_params['app'], 'userList') + : parent::listUsers(); + } + + /** + * Checks if $userId exists in the system. + * + * @param string $userId User ID to check. + * + * @return boolean Whether or not $userId already exists. + */ + public function exists($userId) + { + return $this->hasCapability('exists') + ? $GLOBALS['registry']->callByPackage($this->_params['app'], 'userExists', array($userId)) + : parent::exists($userId); + } + + /** + * Add a set of authentication credentials. + * + * @param string $userId The userId to add. + * @param array $credentials The credentials to use. + * + * @throws Horde_Exception + */ + public function addUser($userId, $credentials) + { + if ($this->hasCapability('add')) { + $GLOBALS['registry']->callByPackage($this->_params['app'], 'addUser', array($userId, $credentials)); + } else { + parent::addUser($userId, $credentials); + } + } + + /** + * Update a set of authentication credentials. + * + * @param string $oldID The old userId. + * @param string $newID The new userId. + * @param array $credentials The new credentials + * + * @throws Horde_Exception + */ + public function updateUser($oldID, $newID, $credentials) + { + if ($this->hasCapability('update')) { + $GLOBALS['registry']->callByPackage($this->_params['app'], 'updateUser', array($oldID, $newID, $credentials)); + } else { + parent::addUser($userId, $credentials); + } + } + + /** + * Delete a set of authentication credentials. + * + * @param string $userId The userId to delete. + * + * @throws Horde_Exception + */ + public function removeUser($userId) + { + if ($this->hasCapability('remove')) { + $res = $GLOBALS['registry']->callByPackage($this->_params['app'], 'removeUser', array($userId)); + if (is_a($res, 'PEAR_Error')) { + throw new Horde_Exception($result); + } + + Horde_Auth::removeUserData($userId); + } else { + parent::removeUser($userId); + } + } + +} diff --git a/framework/Auth/lib/Horde/Auth/Auto.php b/framework/Auth/lib/Horde/Auth/Auto.php new file mode 100644 index 000000000..3fac9b3fb --- /dev/null +++ b/framework/Auth/lib/Horde/Auth/Auto.php @@ -0,0 +1,70 @@ + + * 'password' - (string) The password to record in the user's credentials. + * DEFAULT: none + * 'requestuser' - (boolean) If true, allow username to be passed by GET, POST + * or cookie. + * DEFAULT: No + * 'username' - (string) The username to authenticate everyone as. + * DEFAULT: 'horde_user' + * + * + * Copyright 1999-2009 The Horde Project (http://www.horde.org/) + * + * See the enclosed file COPYING for license information (LGPL). If you did + * not receive this file, see http://opensource.org/licenses/lgpl-2.1.php + * + * @author Chuck Hagenbuch + * @package Horde_Auth + */ +class Horde_Auth_Auto extends Horde_Auth_Driver +{ + /** + * An array of capabilities, so that the driver can report which + * operations it supports and which it doesn't. + * + * @var array + */ + protected $_capabilities = array( + 'transparent' => true + ); + + /** + * Constructor. + * + * @param array $params A hash containing parameters. + */ + public function __construct($params = array()) + { + parent::__construct($params); + + if (!isset($this->_params['username'])) { + $this->_params['username'] = 'horde_user'; + } + } + + /** + * Automatic authentication: Set the user allowed IP block. + * + * @return boolean Whether or not the client is allowed. + */ + protected function _transparent() + { + $username = (!empty($this->_params['requestuser']) && isset($_REQUEST['username'])) + ? $_REQUEST['username'] + : $this->_params['username']; + + return Horde_Auth::setAuth($username, array( + 'transparent' => 1, + 'password' => isset($this->_params['password']) ? $this->_params['password'] : null + )); + } + +} diff --git a/framework/Auth/lib/Horde/Auth/Composite.php b/framework/Auth/lib/Horde/Auth/Composite.php new file mode 100644 index 000000000..5dd17ab65 --- /dev/null +++ b/framework/Auth/lib/Horde/Auth/Composite.php @@ -0,0 +1,254 @@ + + * @package Horde_Auth + */ +class Horde_Auth_Composite extends Horde_Auth_Driver +{ + /** + * Hash containing any instantiated drivers. + * + * @var array + */ + protected $_drivers = array(); + + /** + * Return the named parameter for the current auth driver. + * + * @param string $param The parameter to fetch. + * + * @return string The parameter's value. + */ + public function getParam($param) + { + if (($login_driver = Horde_Auth::getDriverByParam('loginscreen_switch', $this->_params)) && + $this->_loadDriver($login_driver)) { + return $this->_drivers[$login_driver]->getParam($param); + } + + return null; + } + + /** + * Find out if a set of login credentials are valid. + * + * @param string $userId The userId to check. + * @param array $credentials The credentials to use. + * + * @throws Horde_Exception + */ + protected function _authenticate($userId, $credentials) + { + if (($auth_driver = Horde_Auth::getDriverByParam('loginscreen_switch', $this->_params)) && + $this->_loadDriver($auth_driver)) { + $this->_drivers[$auth_driver]->authenticate($userId, $credentials); + return; + } + + if (($auth_driver = Horde_Auth::getDriverByParam('username_switch', $this->_params, array($userId))) && + $this->_loadDriver($auth_driver)) { + $this->_drivers[$auth_driver]->hasCapability('transparent'); + return; + } + + throw new Horde_Exception('', Horde_Auth::REASON_FAILED); + } + + /** + * Query the current Auth object to find out if it supports the given + * capability. + * + * @param string $capability The capability to test for. + * + * @return boolean Whether or not the capability is supported. + */ + public function hasCapability($capability) + { + switch ($capability) { + case 'add': + case 'update': + case 'remove': + case 'list': + if (!empty($this->_params['admin_driver']) && + $this->_loadDriver($this->_params['admin_driver'])) { + return $this->_drivers[$this->_params['admin_driver']]->hasCapability($capability); + } else { + return false; + } + break; + + case 'transparent': + if (($login_driver = Horde_Auth::getDriverByParam('loginscreen_switch', $this->_params)) && + $this->_loadDriver($login_driver)) { + return $this->_drivers[$login_driver]->hasCapability('transparent'); + } + return false; + break; + + default: + return false; + } + } + + /** + * Automatic authentication: Find out if the client matches an allowed IP + * block. + * + * @return boolean Whether or not the client is allowed. + */ + protected function _transparent() + { + if (($login_driver = Horde_Auth::getDriverByParam('loginscreen_switch', $this->_params)) && + $this->_loadDriver($login_driver)) { + return $this->_drivers[$login_driver]->transparent(); + } + + return false; + } + + /** + * Return the URI of the login screen for this authentication object. + * + * @param string $app The application to use. + * @param string $url The URL to redirect to after login. + * + * @return string The login screen URI. + */ + public function getLoginScreen($app = 'horde', $url = '') + { + if (($login_driver = Horde_Auth::getDriverByParam('loginscreen_switch', $this->_params)) && + $this->_loadDriver($login_driver)) { + return $this->_drivers[$login_driver]->getLoginScreen($app, $url); + } + + return parent::getLoginScreen($app, $url); + } + + /** + * Add a set of authentication credentials. + * + * @param string $userId The userId to add. + * @param array $credentials The credentials to use. + * + * @throws Horde_Exception + */ + public function addUser($userId, $credentials) + { + if (!empty($this->_params['admin_driver']) && + $this->_loadDriver($this->_params['admin_driver'])) { + $this->_drivers[$this->_params['admin_driver']]->addUser($userId, $credentials); + } else { + parent::addUser($userId, $credentials); + } + } + + /** + * Update a set of authentication credentials. + * + * @param string $oldID The old userId. + * @param string $newID The new userId. + * @param array $credentials The new credentials + * + * @throws Horde_Exception + */ + public function updateUser($oldID, $newID, $credentials) + { + if (!empty($this->_params['admin_driver']) && + $this->_loadDriver($this->_params['admin_driver'])) { + $this->_drivers[$this->_params['admin_driver']]->updateUser($oldID, $newID, $credentials); + } else { + parent::updateUser($oldID, $newID, $credentials); + } + } + + /** + * Delete a set of authentication credentials. + * + * @param string $userId The userId to delete. + * + * @throws Horde_Exception + */ + public function removeUser($userId) + { + if (!empty($this->_params['admin_driver']) && + $this->_loadDriver($this->_params['admin_driver'])) { + $this->_drivers[$this->_params['admin_driver']]->removeUser($userId); + } else { + parent::removeUser($userId); + } + } + + /** + * List all users in the system. + * + * @return array The array of userIds. + * @throws Horde_Exception + */ + public function listUsers() + { + if (!empty($this->_params['admin_driver']) && + $this->_loadDriver($this->_params['admin_driver'])) { + return $this->_drivers[$this->_params['admin_driver']]->listUsers(); + } + + return parent::listUsers(); + } + + /** + * Checks if a userId exists in the system. + * + * @param string $userId User ID to check + * + * @return boolean Whether or not the userId already exists. + */ + public function exists($userId) + { + if (!empty($this->_params['admin_driver']) && + $this->_loadDriver($this->_params['admin_driver'])) { + return $this->_drivers[$this->_params['admin_driver']]->exists($userId); + } + + return parent::exists($userId); + } + + /** + * Loads one of the drivers in our configuration array, if it isn't already + * loaded. + * + * @param string $driver The name of the driver to load. + * + * @return boolean True if driver successfully initializes. + */ + protected function _loadDriver($driver) + { + if (empty($this->_drivers[$driver])) { + // This is a bit specialized for Horde::getDriverConfig(), + // so localize it here: + global $conf; + if (!empty($this->_params['drivers'][$driver]['params'])) { + $params = $this->_params['drivers'][$driver]['params']; + if (isset($conf[$this->_params['drivers'][$driver]['driver']])) { + $params = array_merge($conf[$this->_params['drivers'][$driver]['driver']], $params); + } + } elseif (!empty($conf[$driver])) { + $params = $conf[$driver]; + } else { + $params = null; + } + + $this->_drivers[$driver] = Horde_Auth::singleton($this->_params['drivers'][$driver]['driver'], $params); + } + + return isset($this->_drivers[$driver]); + } + +} diff --git a/framework/Auth/lib/Horde/Auth/Customsql.php b/framework/Auth/lib/Horde/Auth/Customsql.php new file mode 100644 index 000000000..243e92e81 --- /dev/null +++ b/framework/Auth/lib/Horde/Auth/Customsql.php @@ -0,0 +1,297 @@ + + * Some special tokens can be used in the sql query. They are replaced + * at the query stage: + * + * - '\L' will be replaced by the user's login + * - '\P' will be replaced by the user's password. + * - '\O' will be replaced by the old user's login (required for update) + * + * Eg: "SELECT * FROM users WHERE uid = \L + * AND passwd = \P + * AND billing = 'paid' + * + * 'query_auth' Authenticate the user. '\L' & '\P' + * 'query_add' Add user. '\L' & '\P' + * 'query_getpw' Get one user's password. '\L' + * 'query_update' Update user. '\O', '\L' & '\P' + * 'query_resetpassword' Reset password. '\L', & '\P' + * 'query_remove' Remove user. '\L' + * 'query_list' List user. + * 'query_exists' Check for existance of user. '\L' + * + * + * Optional parameters: See Horde_Auth_Sql driver. + * + * Copyright 2002 Ronnie Garcia + * + * See the enclosed file COPYING for license information (LGPL). If you did + * not receive this file, see http://opensource.org/licenses/lgpl-2.1.php + * + * @author Ronnie Garcia + * @author Chuck Hagenbuch + * @author Joel Vandal + * @package Horde_Auth + */ +class Horde_Auth_Customsql extends Horde_Auth_Sql +{ + /** + * An array of capabilities, so that the driver can report which + * operations it supports and which it doesn't. + * + * @var array + */ + protected $_capabilities = array( + 'add' => true, + 'list' => true, + 'remove' => true, + 'resetpassword' => true, + 'update' => true + ); + + /** + * Constructor. + * + * @param array $params A hash containing connection parameters. + */ + public function __construct($params = array()) + { + Horde::assertDriverConfig($params, 'auth', + array('query_auth'), + 'authentication custom SQL'); + + parent::__construct($params); + } + + /** + * Find out if a set of login credentials are valid. + * + * @param string $userId The userId to check. + * @param array $credentials The credentials to use. + * + * @throws Horde_Exception + */ + protected function _authenticate($userId, $credentials) + { + try { + $this->_connect(); + } catch (Horde_Exception $e) { + throw new Horde_Exception('', Horde_Auth::REASON_FAILED); + } + + /* Build a custom query, based on the config file. */ + $query = $this->_params['query_auth']; + $query = str_replace('\L', $this->_db->quote($userId), $query); + $query = str_replace('\P', $this->_db->quote(Horde_Auth::getCryptedPassword( + $credentials['password'], + $this->_getPassword($userId), + $this->_params['encryption'], + $this->_params['show_encryption'])), $query); + + $result = $this->_db->query($query); + if ($result instanceof PEAR_Error) { + throw new Horde_Exception('', Horde_Auth::REASON_FAILED); + } + + $row = $result->fetchRow(DB_GETMODE_ASSOC); + + /* If we have at least one returned row, then the user is valid. */ + if (is_array($row)) { + $result->free(); + return; + } + + $result->free(); + throw new Horde_Exception('', Horde_Auth::REASON_BADLOGIN); + } + + /** + * Add a set of authentication credentials. + * + * @param string $userId The userId to add. + * @param array $credentials The credentials to add. + * + * @throws Horde_Exception + */ + public function addUser($userId, $credentials) + { + $this->_connect(); + + /* Build a custom query, based on the config file. */ + $query = $this->_params['query_add']; + $query = str_replace('\L', $this->_db->quote($userId), $query); + $query = str_replace('\P', $this->_db->quote(Horde_Auth::getCryptedPassword( + $credentials['password'], '', + $this->_params['encryption'], + $this->_params['show_encryption'])), $query); + + $result = $this->_db->query($query); + if ($result instanceof PEAR_Error) { + throw new Horde_Exception($result); + } + } + + /** + * Update a set of authentication credentials. + * + * @param string $oldId The old userId. + * @param string $newId The new userId. + * @param array $credentials The new credentials + * + * @throws Horde_Exception + */ + function updateUser($oldId, $newId, $credentials) + { + $this->_connect(); + + /* Build a custom query, based on the config file. */ + $query = $this->_params['query_update']; + $query = str_replace('\O', $this->_db->quote($oldId), $query); + $query = str_replace('\L', $this->_db->quote($newId), $query); + $query = str_replace('\P', $this->_db->quote(Horde_Auth::getCryptedPassword( + $credentials['password'], + $this->_getPassword($oldId), + $this->_params['encryption'], + $this->_params['show_encryption'])), $query); + + $result = $this->_db->query($query); + if ($result instanceof PEAR_Error) { + throw new Horde_Exception($result); + } + } + + /** + * Resets a user's password. Used for example when the user does not + * remember the existing password. + * + * @param string $userId The user id for which to reset the password. + * + * @return string The new password on success. + * @throws Horde_Exception + */ + public function resetPassword($userId) + { + $this->_connect(); + + /* Get a new random password. */ + $password = Horde_Auth::genRandomPassword(); + + /* Build the SQL query. */ + $query = $this->_params['query_resetpassword']; + $query = str_replace('\L', $this->_db->quote($userId), $query); + $query = str_replace('\P', $this->_db->quote(Horde_Auth::getCryptedPassword($password, + '', + $this->_params['encryption'], + $this->_params['show_encryption'])), $query); + + $result = $this->_db->query($query); + if ($result instanceof PEAR_Error) { + throw new Horde_Exception($result); + } + + return $password; + } + + /** + * Delete a set of authentication credentials. + * + * @param string $userId The userId to delete. + * + * @throws Horde_Exception + */ + public function removeUser($userId) + { + $this->_connect(); + + /* Build a custom query, based on the config file. */ + $query = $this->_params['query_remove']; + $query = str_replace('\L', $this->_db->quote($userId), $query); + + $result = $this->_db->query($query); + if ($result instanceof PEAR_Error) { + throw new Horde_Exception($result); + } + + $this->removeUserData($userId); + } + + /** + * List all users in the system. + * + * @return array The array of userIds. + * @throws Horde_Exception + */ + public function listUsers() + { + $this->_connect(); + + /* Build a custom query, based on the config file. */ + $query = $this->_params['query_list']; + $query = str_replace('\L', $this->_db->quote(Horde_Auth::getAuth()), $query); + + $result = $this->_db->getAll($query, null, DB_FETCHMODE_ORDERED); + if ($result instanceof PEAR_Error) { + throw new Horde_Exception($result); + } + + /* Loop through and build return array. */ + $users = array(); + foreach ($result as $ar) { + $users[] = $ar[0]; + } + + return $users; + } + + /** + * Checks if a userId exists in the system. + * + * @return boolean Whether or not the userId already exists. + */ + public function exists($userId) + { + try { + $this->_connect(); + } catch (Horde_Exception $e) { + return false; + } + + /* Build a custom query, based on the config file. */ + $query = $this->_params['query_exists']; + $query = str_replace('\L', $this->_db->quote($userId), $query); + + $result = $this->_db->getOne($query); + + return ($result instanceof PEAR_Error) + ? false + : (bool)$result; + } + + /** + * Fetch $userId's current password - needed for the salt with some + * encryption schemes when doing authentication or updates. + * + * @param string $userId TODO + * + * @return string $userId's current password. + */ + protected function _getPassword($userId) + { + /* Retrieve the old password in case we need the salt. */ + $query = $this->_params['query_getpw']; + $query = str_replace('\L', $this->_db->quote($userId), $query); + $pw = $this->_db->getOne($query); + if ($pw instanceof PEAR_Error) { + Horde::logMessage($pw, __FILE__, __LINE__, PEAR_LOG_ERR); + return ''; + } + + return $pw; + } + +} diff --git a/framework/Auth/lib/Horde/Auth/Cyrsql.php b/framework/Auth/lib/Horde/Auth/Cyrsql.php new file mode 100644 index 000000000..a44d7f015 --- /dev/null +++ b/framework/Auth/lib/Horde/Auth/Cyrsql.php @@ -0,0 +1,497 @@ + + * 'cyradmin' The username of the cyrus administrator. + * 'cyrpass' The password for the cyrus administrator. + * 'hostspec' The hostname or IP address of the server. + * DEFAULT: 'localhost' + * 'port' The server port to which we will connect. + * IMAP is generally 143, while IMAP-SSL is generally 993. + * DEFAULT: Encryption port default + * 'secure' The encryption to use. Either 'none', 'ssl', or 'tls'. + * DEFAULT: 'none' + * + * + * Optional parameters: See Horde_Auth_Sql driver. + *
+ * 'domain_field'    If set to anything other than 'none' this is used as
+ *                   field name where domain is stored.
+ *                   DEFAULT: 'domain_name'
+ * 'hidden_accounts' An array of system accounts to hide from the user
+ *                   interface.
+ * 'folders'         An array of folders to create under username.
+ *                   DEFAULT: NONE
+ * 'quota'           The quota (in kilobytes) to grant on the mailbox.
+ *                   DEFAULT: NONE
+ * 'unixhier'        The value of imapd.conf's unixhierarchysep setting.
+ *                   Set this to true if the value is true in imapd.conf.
+ * 
+ * + * The table structure for the auth system is as follows: + *
+ * CREATE TABLE accountuser (
+ *     username    VARCHAR(255) BINARY NOT NULL DEFAULT '',
+ *     password    VARCHAR(32) BINARY NOT NULL DEFAULT '',
+ *     prefix      VARCHAR(50) NOT NULL DEFAULT '',
+ *     domain_name VARCHAR(255) NOT NULL DEFAULT '',
+ *     UNIQUE KEY username (username)
+ * );
+ *
+ * CREATE TABLE adminuser (
+ *     username    VARCHAR(50) BINARY NOT NULL DEFAULT '',
+ *     password    VARCHAR(50) BINARY NOT NULL DEFAULT '',
+ *     type        INT(11) NOT NULL DEFAULT '0',
+ *     SID         VARCHAR(255) NOT NULL DEFAULT '',
+ *     home        VARCHAR(255) NOT NULL DEFAULT '',
+ *     PRIMARY KEY (username)
+ * );
+ *
+ * CREATE TABLE alias (
+ *     alias       VARCHAR(255) NOT NULL DEFAULT '',
+ *     dest        LONGTEXT,
+ *     username    VARCHAR(50) NOT NULL DEFAULT '',
+ *     status      INT(11) NOT NULL DEFAULT '1',
+ *     PRIMARY KEY (alias)
+ * );
+ *
+ * CREATE TABLE domain (
+ *     domain_name VARCHAR(255) NOT NULL DEFAULT '',
+ *     prefix      VARCHAR(50) NOT NULL DEFAULT '',
+ *     maxaccounts INT(11) NOT NULL DEFAULT '20',
+ *     quota       INT(10) NOT NULL DEFAULT '20000',
+ *     transport   VARCHAR(255) NOT NULL DEFAULT 'cyrus',
+ *     freenames   ENUM('YES','NO') NOT NULL DEFAULT 'NO',
+ *     freeaddress ENUM('YES','NO') NOT NULL DEFAULT 'NO',
+ *     PRIMARY KEY (domain_name),
+ *     UNIQUE KEY prefix (prefix)
+ * );
+ *
+ * CREATE TABLE domainadmin (
+ *     domain_name VARCHAR(255) NOT NULL DEFAULT '',
+ *     adminuser   VARCHAR(255) NOT NULL DEFAULT ''
+ * );
+ *
+ * CREATE TABLE search (
+ *     search_id   VARCHAR(255) NOT NULL DEFAULT '',
+ *     search_sql  TEXT NOT NULL,
+ *     perpage     INT(11) NOT NULL DEFAULT '0',
+ *     timestamp   TIMESTAMP(14) NOT NULL,
+ *     PRIMARY KEY (search_id),
+ *     KEY search_id (search_id)
+ * );
+ *
+ * CREATE TABLE virtual (
+ *     alias       VARCHAR(255) NOT NULL DEFAULT '',
+ *     dest        LONGTEXT,
+ *     username    VARCHAR(50) NOT NULL DEFAULT '',
+ *     status      INT(11) NOT NULL DEFAULT '1',
+ *     KEY alias (alias)
+ * );
+ *
+ * CREATE TABLE log (
+ *     id          INT(11) NOT NULL AUTO_INCREMENT,
+ *     msg         TEXT NOT NULL,
+ *     user        VARCHAR(255) NOT NULL DEFAULT '',
+ *     host        VARCHAR(255) NOT NULL DEFAULT '',
+ *     time        DATETIME NOT NULL DEFAULT '2000-00-00 00:00:00',
+ *     pid         VARCHAR(255) NOT NULL DEFAULT '',
+ *     PRIMARY KEY (id)
+ * );
+ * 
+ * + * Copyright 2002-2009 The Horde Project (http://www.horde.org/) + * + * See the enclosed file COPYING for license information (LGPL). If you did + * not receive this file, see http://opensource.org/licenses/lgpl-2.1.php + * + * @author Ilya Krel + * @author Jan Schneider + * @package Horde_Auth + */ +class Horde_Auth_Cyrsql extends Horde_Auth_Sql +{ + /** + * Horde_Imap_Client object. + * + * @var Horde_Imap_Client_Base + */ + protected $_ob; + + /** + * Hierarchy separator to use (e.g., is it user/mailbox or user.mailbox) + * + * @var string + */ + protected $_separator = '.'; + + /** + * Constructor. + * + * @param array $params A hash containing connection parameters. + */ + public function __construct($params = array()) + { + parent::__construct($params); + + $admin_params = array( + 'admin_user' => $params['cyradmin'], + 'admin_password' => $params['cyrpass'], + 'dsn' => $params['imap_dsn'] + ); + + if (!empty($this->_params['unixhier'])) { + $admin_params['userhierarchy'] = 'user/'; + } + + if (!empty($this->_params['unixhier'])) { + $this->_separator = '/'; + } + } + + /** + * Find out if a set of login credentials are valid. + * + * @param string $userId The userId to check. + * @param array $credentials The credentials to use. + * + * @throws Horde_Exception + */ + protected function _authenticate($userId, $credentials) + { + try { + $this->_connect(); + } catch (Horde_Exception $e) { + Horde::logMessage($e, __FILE__, __LINE__, PEAR_LOG_ERR); + throw new Horde_Exception('', Horde_Auth::REASON_FAILED); + } + + if (!empty($this->_params['domain_field']) && + ($this->_params['domain_field'] != 'none')) { + /* Build the SQL query with domain. */ + $query = sprintf('SELECT * FROM %s WHERE %s = ? AND %s = ?', + $this->_params['table'], + $this->_params['username_field'], + $this->_params['domain_field']); + $values = explode('@', $userId); + } else { + /* Build the SQL query without domain. */ + $query = sprintf('SELECT * FROM %s WHERE %s = ?', + $this->_params['table'], + $this->_params['username_field']); + $values = array($userId); + } + + Horde::logMessage('SQL Query by Horde_Auth_Cyrsql::_authenticate(): ' . $query, __FILE__, __LINE__, PEAR_LOG_DEBUG); + + $result = $this->_db->query($query, $values); + if ($result instanceof PEAR_Error) { + Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR); + throw new Horde_Exception('', Horde_Auth::REASON_FAILED); + } + + $row = $result->fetchRow(DB_GETMODE_ASSOC); + if (is_array($row)) { + $result->free(); + } else { + throw new Horde_Exception('', Horde_Auth::REASON_BADLOGIN); + } + + if (!$this->_comparePasswords($row[$this->_params['password_field']], + $credentials['password'])) { + throw new Horde_Exception('', Horde_Auth::REASON_BADLOGIN); + } + + $now = time(); + if (!empty($this->_params['hard_expiration_field']) && + !empty($row[$this->_params['hard_expiration_field']]) && + ($now > $row[$this->_params['hard_expiration_field']])) { + throw new Horde_Exception('', Horde_Auth::REASON_EXPIRED); + } + + if (!empty($this->_params['soft_expiration_field']) && + !empty($row[$this->_params['soft_expiration_field']]) && + ($now > $row[$this->_params['soft_expiration_field']])) { + $this->_authCredentials['changeRequested'] = true; + } + } + + /** + * Add a set of authentication credentials. + * + * @param string $userId The userId to add. + * @param array $credentials The credentials to add. + * + * @throw Horde_Exception + */ + public function addUser($userId, $credentials) + { + $this->_connect(); + + if (!empty($this->_params['domain_field']) && + ($this->_params['domain_field'] != 'none')) { + list($name, $domain) = explode('@', $userId); + /* Build the SQL query. */ + $query = sprintf('INSERT INTO %s (%s, %s, %s) VALUES (?, ?, ?)', + $this->_params['table'], + $this->_params['username_field'], + $this->_params['domain_field'], + $this->_params['password_field']); + $values = array($name, + $domain, + Horde_Auth::getCryptedPassword($credentials['password'], + '', + $this->_params['encryption'], + $this->_params['show_encryption'])); + + Horde::logMessage('SQL Query by Horde_Auth_Cyrsql::addUser(): ' . $query, __FILE__, __LINE__, PEAR_LOG_DEBUG); + + $dbresult = $this->_db->query($query, $values); + $query = 'INSERT INTO virtual (alias, dest, username, status) VALUES (?, ?, ?, 1)'; + $values = array($userId, $userId, $name); + + Horde::logMessage('SQL Query by Horde_Auth_Cyrsql::addUser(): ' . $query, __FILE__, __LINE__, PEAR_LOG_DEBUG); + + $dbresult2 = $this->_db->query($query, $values); + if ($dbresult2 instanceof PEAR_Error) { + throw new Horde_Exception($dbresult2); + } + } else { + parent::addUser($userId, $credentials); + } + + try { + $mailbox = Horde_String::convertCharset($this->_params['userhierarchy'] . $userId, NLS::getCharset(), 'utf7-imap'); + $ob->createMailbox($mailbox); + $ob->setACL($mailbox, $this->_params['cyradm'], 'lrswipcda'); + } catch (Horde_Imap_Client_Exception $e) { + throw new Horde_Exception($e); + } + + foreach ($this->_params['folders'] as $folders) { + if (!empty($this->_params['domain_field']) && + ($this->_params['domain_field'] != 'none')) { + list($userName, $domain) = explode('@', $userName); + $tmp = $userName . $this->_separator . $value . '@' . $domain; +Horde_String::convertCharset($userName . $this->_separator . $value . '@' . $domain, NLS::getCharset(), 'utf7-imap'); + } else { + $tmp = $userName . $this->_separator . $value; + } + + $tmp = Horde_String::convertCharset($tmp, NLS::getCharset(), 'utf7-imap'); + $ob->createMailbox($tmp); + $ob->setACL($tmp, $this->_params['cyradm'], 'lrswipcda'); + } + + if (isset($this->_params['quota']) && + ($this->_params['quota'] >= 0)) { + try { + $this->_ob->setQuota($mailbox, array('storage' => $this->_params['quota'])); + } catch (Horde_Imap_Client_Exception $e) { + throw new Horde_Exception($e); + } + } + + if (isset($this->_params['quota']) && + ($this->_params['quota'] >= 0) && + !@imap_set_quota($this->_imapStream, 'user' . $this->_separator . $userId, $this->_params['quota'])) { + throw new Horde_Exception(sprintf(_("IMAP mailbox quota creation failed: %s"), imap_last_error())); + } + } + + /** + * Delete a set of authentication credentials. + * + * @param string $userId The userId to delete. + * + * @throws Horde_Exception + */ + function removeUser($userId) + { + $this->_connect(); + + if (!empty($this->_params['domain_field']) && + ($this->_params['domain_field'] != 'none')) { + list($name, $domain) = explode('@', $userId); + /* Build the SQL query. */ + $query = sprintf('DELETE FROM %s WHERE %s = ? and %s = ?', + $this->_params['table'], + $this->_params['username_field'], + $this->_params['domain_field']); + $values = array($name, $domain); + + Horde::logMessage('SQL Query by Horde_Auth_Cyrsql::removeUser(): ' . $query, __FILE__, __LINE__, PEAR_LOG_DEBUG); + + $dbresult = $this->_db->query($query, $values); + $query = 'DELETE FROM virtual WHERE dest = ?'; + $values = array($userId); + + Horde::logMessage('SQL Query by Horde_Auth_Cyrsql::removeUser(): ' . $query, __FILE__, __LINE__, PEAR_LOG_DEBUG); + + $dbresult2 = $this->_db->query($query, $values); + if ($dbresult2 instanceof PEAR_Error) { + return $dbresult2; + } + } else { + parent::removeUser($userId); + } + + /* Set ACL for mailbox deletion. */ + list($admin) = explode('@', $this->_params['cyradmin']); + + try { + $this->_ob->setACL($mailbox, $admin, array('rights' => 'lrswipcda')); + $this->_ob->deleteMailbox($mailbox); + } catch (Horde_Imap_Client_Exception $e) { + throw new Horde_Exception($e); + } + + Horde_Auth::removeUserData($userId); + } + + /** + * List all users in the system. + * + * @return mixed The array of userIds. + * @throws Horde_Exception + */ + public function listUsers() + { + $this->_connect(); + + if (!empty($this->_params['domain_field']) && + ($this->_params['domain_field'] != 'none')) { + /* Build the SQL query with domain. */ + $query = sprintf('SELECT %s, %s FROM %s ORDER BY %s', + $this->_params['username_field'], + $this->_params['domain_field'], + $this->_params['table'], + $this->_params['username_field']); + } else { + /* Build the SQL query without domain. */ + $query = sprintf('SELECT %s FROM %s ORDER BY %s', + $this->_params['username_field'], + $this->_params['table'], + $this->_params['username_field']); + } + + Horde::logMessage('SQL Query by Horde_Auth_Cyrsql::listUsers(): ' . $query, __FILE__, __LINE__, PEAR_LOG_DEBUG); + + $result = $this->_db->getAll($query, null, DB_FETCHMODE_ORDERED); + if ($result instanceof PEAR_Error) { + throw new Horde_Exception($result); + } + + /* Loop through and build return array. */ + $users = array(); + if (!empty($this->_params['domain_field']) + && ($this->_params['domain_field'] != 'none')) { + foreach ($result as $ar) { + if (!in_array($ar[0], $this->_params['hidden_accounts'])) { + $users[] = $ar[0] . '@' . $ar[1]; + } + } + } else { + foreach ($result as $ar) { + if (!in_array($ar[0], $this->_params['hidden_accounts'])) { + $users[] = $ar[0]; + } + } + } + + return $users; + } + + /** + * Update a set of authentication credentials. + * + * @param string $oldID The old userId. + * @param string $newID The new userId. + * @param array $credentials The new credentials + * + * @throws Horde_Exception + */ + public function updateUser($oldID, $newID, $credentials) + { + $this->_connect(); + + if (!empty($this->_params['domain_field']) && + ($this->_params['domain_field'] != 'none')) { + list($name, $domain) = explode('@', $oldID); + /* Build the SQL query with domain. */ + $query = sprintf('UPDATE %s SET %s = ? WHERE %s = ? and %s = ?', + $this->_params['table'], + $this->_params['password_field'], + $this->_params['username_field'], + $this->_params['domain_field']); + $values = array(Horde_Auth::getCryptedPassword($credentials['password'], + '', + $this->_params['encryption'], + $this->_params['show_encryption']), + $name, $domain); + } else { + /* Build the SQL query. */ + $query = sprintf('UPDATE %s SET %s = ? WHERE %s = ?', + $this->_params['table'], + $this->_params['password_field'], + $this->_params['username_field']); + $values = array(Horde_Auth::getCryptedPassword($credentials['password'], + '', + $this->_params['encryption'], + $this->_params['show_encryption']), + $oldID); + } + + Horde::logMessage('SQL Query by Horde_Auth_Cyrsql::updateUser(): ' . $query, __FILE__, __LINE__, PEAR_LOG_DEBUG); + + $res = $this->_db->query($query, $values); + if ($res instanceof PEAR_Error) { + throw new Horde_Exception($res); + } + } + + /** + * Attempts to open connections to the SQL and IMAP servers. + * + * @throws Horde_Exception + */ + protected function _connect() + { + if ($this->_connected) { + return; + } + + parent::_connect(); + + if (!isset($this->_params['hidden_accounts'])) { + $this->_params['hidden_accounts'] = array('cyrus'); + } + + // Reset the $_connected flag; we haven't yet successfully + // opened everything. + $this->_connected = false; + + $imap_config = array( + 'hostspec' => empty($this->_params['hostspec']) ? null : $this->_params['hostspec'], + 'password' => $this->_params['cyrpass'], + 'port' => empty($this->_params['port']) ? null : $this->_params['port'], + 'secure' => ($this->_params['secure'] == 'none') ? null : $this->_params['secure'], + 'username' => $this->_params['cyradmin'] + ); + + try { + $this->_ob = Horde_Imap_Client::getInstance('Socket', $imap_config); + $this->_ob->login(); + } catch (Horde_Imap_Client_Exception $e) { + throw new Horde_Exception($e); + } + + $this->_connected = true; + } + +} diff --git a/framework/Auth/lib/Horde/Auth/Cyrus.php b/framework/Auth/lib/Horde/Auth/Cyrus.php new file mode 100644 index 000000000..ad6e76772 --- /dev/null +++ b/framework/Auth/lib/Horde/Auth/Cyrus.php @@ -0,0 +1,302 @@ + + * 'backend' The complete hash for the Auth_* driver that cyrus + * authenticates against (eg SQL, LDAP). + * 'cyradmin' The username of the cyrus administrator + * 'cyrpass' The password for the cyrus administrator + * 'hostspec' The hostname or IP address of the server. + * DEFAULT: 'localhost' + * 'port' The server port to which we will connect. + * IMAP is generally 143, while IMAP-SSL is generally 993. + * DEFAULT: Encryption port default + * 'secure' The encryption to use. Either 'none', 'ssl', or 'tls'. + * DEFAULT: 'none' + * + * + * Optional values: + *
+ * 'folders'    An array of folders to create under username.
+ *              Doesn't create subfolders by default.
+ * 'quota'      The quota (in kilobytes) to grant on the mailbox.
+ *              Does not establish quota by default.
+ * 'separator'  Hierarchy separator to use (e.g., is it user/mailbox or
+ *              user.mailbox)
+ * 'unixhier'   The value of imapd.conf's unixhierarchysep setting.
+ *              Set this to 'true' if the value is true in imapd.conf
+ * 
+ * + * Example Usage: + *
+ * $conf['auth']['driver'] = 'composite';
+ * $conf['auth']['params']['loginscreen_switch'] = '_horde_select_loginscreen';
+ * $conf['auth']['params']['admin_driver'] = 'cyrus';
+ * $conf['auth']['params']['drivers']['imp'] = array(
+ *     'driver' => 'application',
+ *     'params' => array('app' => 'imp')
+ * );
+ * $conf['auth']['params']['drivers']['cyrus'] = array(
+ *    'driver' => 'cyrus',
+ *    'params' => array(
+ *        'cyradmin' => 'cyrus',
+ *        'cyrpass' => 'password',
+ *        'hostspec' => 'imap.example.com',
+ *        'secure' => 'none'
+ *        'separator' => '.'
+ *    )
+ * );
+ * $conf['auth']['params']['drivers']['cyrus']['params']['backend'] = array(
+ *     'driver' => 'sql',
+ *     'params' => array(
+ *         'phptype' => 'mysql',
+ *         'hostspec' => 'database.example.com',
+ *         'protocol' => 'tcp',
+ *         'username' => 'username',
+ *         'password' => 'password',
+ *         'database' => 'mail',
+ *         'table' => 'accountuser',
+ *         'encryption' => 'md5-hex',
+ *         'username_field' => 'username',
+ *         'password_field' => 'password'
+ *     )
+ * );
+ *
+ * if (!function_exists('_horde_select_loginscreen')) {
+ *     function _horde_select_loginscreen()
+ *     {
+ *         return 'imp';
+ *     }
+ * }
+ * 
+ * + * Copyright 2002-2009 The Horde Project (http://www.horde.org/) + * + * See the enclosed file COPYING for license information (LGPL). If you did + * not receive this file, see http://opensource.org/licenses/lgpl-2.1.php + * + * @author Ilya Krel + * @author Mike Cochrane + * @package Horde_Auth + */ +class Horde_Auth_Cyrus extends Horde_Auth_Driver +{ + /** + * Horde_Imap_Client object. + * + * @var Horde_Imap_Client_Base + */ + protected $_ob; + + /** + * Pointer to another backend that Cyrus authenticates against. + * + * @var Horde_Auth_Driver + */ + protected $_backend; + + /** + * An array of capabilities, so that the driver can report which + * operations it supports and which it doesn't. + * + * @var array + */ + protected $_capabilities = array( + 'add' => true, + 'remove' => true, + 'update' => true + ); + + /** + * Constructor. + * + * @param array $params A hash containing connection parameters. + */ + public function __construct($params = array()) + { + parent::__construct($params); + + if (!isset($this->_params['separator'])) { + $this->_params['separator'] = '.'; + } + + if (isset($this->_params['unixhier']) && + $this->_params['unixhier'] == true) { + $this->_params['separator'] = '/'; + } + + // Create backend instance. + $this->_backend = Horde_Auth::singleton($this->_params['backend']['driver'], $this->_params['backend']['params']); + + // Check the capabilities of the backend. + if (!$this->_backend->hasCapability('add') || + !$this->_backend->hasCapability('update') || + !$this->_backend->hasCapability('remove')) { + Horde::fatal('Horde_Auth_Cyrus: Backend does not have required capabilites.', __FILE__, __LINE__); + } + + $this->_capabilities['list'] = $this->_backend->hasCapability('list'); + $this->_capabilities['groups'] = $this->_backend->hasCapability('groups'); + $this->_capabilities['transparent'] = $this->_backend->hasCapability('transparent'); + } + + /** + * Add a set of authentication credentials. + * + * @param string $userId The userId to add. + * @param array $credentials The credentials to add. + * + * @throws Horde_Exception + */ + public function addUser($userId, $credentials) + { + $this->_connect(); + + $this->_backend->addUser($userId, $credentials); + + $mailbox = Horde_String::convertCharset('user' . $this->_params['separator'] . $userId, NLS::getCharset(), 'utf7-imap'); + + try { + $this->_ob->createMailbox($mailbox); + } catch (Horde_Imap_Client_Exception $e) { + throw new Horde_Exception($e); + } + + if (isset($this->_params['folders']) && + is_array($this->_params['folders'])) { + foreach ($this->_params['folders'] as $folder) { + try { + $this->_ob->createMailbox($mailbox . Horde_String::convertCharset($this->_params['separator'] . $folder, NLS::getCharset(), 'utf7-imap')); + } catch (Horde_Imap_Client_Exception $e) {} + } + } + + if (isset($this->_params['quota']) && + ($this->_params['quota'] >= 0)) { + try { + $this->_ob->setQuota($mailbox, array('storage' => $this->_params['quota'])); + } catch (Horde_Imap_Client_Exception $e) { + throw new Horde_Exception($e); + } + } + } + + /** + * Delete a set of authentication credentials. + * + * @param string $userId The userId to delete. + * + * @throws Horde_Exception + */ + public function removeUser($userId) + { + $this->_connect(); + + $this->_backend->removeUser($userId); + + $mailbox = Horde_String::convertCharset('user' . $this->_params['separator'] . $userId, NLS::getCharset(), 'utf7-imap'); + + /* Set ACL for mailbox deletion. */ + list($admin) = explode('@', $this->_params['cyradmin']); + + try { + $this->_ob->setACL($mailbox, $admin, array('rights' => 'lrswipcda')); + $this->_ob->deleteMailbox($mailbox); + } catch (Horde_Imap_Client_Exception $e) { + throw new Horde_Exception($e); + } + + Horde_Auth::removeUserData($userId); + } + + /** + * Attempts to open connections to the IMAP servers. + * + * @throws Horde_Exception + */ + protected function _connect() + { + if ($this->_ob) { + return; + } + + $imap_config = array( + 'hostspec' => empty($this->_params['hostspec']) ? null : $this->_params['hostspec'], + 'password' => $pass, + 'port' => empty($this->_params['port']) ? null : $this->_params['port'], + 'secure' => ($this->_params['secure'] == 'none') ? null : $this->_params['secure'], + 'username' => $user + ); + + try { + $this->_ob = Horde_Imap_Client::getInstance('Socket', $imap_config); + $this->_ob->login(); + } catch (Horde_Imap_Client_Exception $e) { + throw new Horde_Exception($e); + } + } + + /** + * List all users in the system. + * + * @return array The array of userIds. + * @throws Horde_Exception + */ + public function listUsers() + { + return $this->_backend->listUsers(); + } + + /** + * Update a set of authentication credentials. + * + * @param string $oldID The old userId. + * @param string $newID The new userId. + * @param array $credentials The new credentials + * + * @throws Horde_Exception + */ + public function updateUser($oldID, $newID, $credentials) + { + $this->_backend->updateUser($oldID, $newID, $credentials); + } + + /** + * Return the URI of the login screen for this authentication method. + * + * @param string $app The application to use. + * @param string $url The URL to redirect to after login. + * + * @return string The login screen URI. + */ + public function getLoginScreen($app = 'horde', $url = '') + { + return $this->_backend->getLoginScreen($app, $url); + } + + /** + * Checks if a userId exists in the system. + * + * @return boolean Whether or not the userId already exists. + */ + public function exists($userId) + { + return $this->_backend->exists($userId); + } + + /** + * Automatic authentication: Find out if the client matches an allowed IP + * block. + * + * @return boolean Whether or not the client is allowed. + */ + protected function _transparent() + { + return $this->_backend->transparent(); + } + +} diff --git a/framework/Auth/lib/Horde/Auth/Driver.php b/framework/Auth/lib/Horde/Auth/Driver.php new file mode 100644 index 000000000..0fa3edc9d --- /dev/null +++ b/framework/Auth/lib/Horde/Auth/Driver.php @@ -0,0 +1,304 @@ + + * @author Michael Slusarz + * @package Horde_Auth + */ +class Horde_Auth_Driver +{ + /** + * An array of capabilities, so that the driver can report which + * operations it supports and which it doesn't. + * + * @var array + */ + protected $_capabilities = array( + 'add' => false, + 'groups' => false, + 'list' => false, + 'resetpassword' => false, + 'remove' => false, + 'transparent' => false, + 'update' => false + ); + + /** + * Hash containing parameters needed for the drivers. + * + * @var array + */ + protected $_params = array(); + + /** + * The credentials currently being authenticated. + * + * @var array + */ + protected $_authCredentials = array(); + + /** + * Constructor. + * + * @param array $params A hash containing parameters. + */ + public function __construct($params = array()) + { + $this->_params = $params; + } + + /** + * Finds out if a set of login credentials are valid, and if requested, + * mark the user as logged in in the current session. + * + * @param string $userId The userId to check. + * @param array $credentials The credentials to check. + * @param boolean $login Whether to log the user in. If false, we'll + * only test the credentials and won't modify + * the current session. Defaults to true. + * @param string $realm The authentication realm to check. + * + * @return boolean Whether or not the credentials are valid. + */ + public function authenticate($userId, $credentials, $login = true, + $realm = null) + { + $auth = false; + $userId = trim($userId); + + if (!empty($GLOBALS['conf']['hooks']['preauthenticate'])) { + if (!Horde::callHook('_horde_hook_preauthenticate', array($userId, $credentials, $realm), 'horde', false)) { + if (Horde_Auth::getAuthError() != Horde_Auth::REASON_MESSAGE) { + Horde_Auth::setAuthError(Horde_Auth::REASON_FAILED); + } + return false; + } + } + + /* Store the credentials being checked so that subclasses can modify + * them if necessary (like transparent auth does). */ + $this->_authCredentials = array( + 'changeRequested' => false, + 'credentials' => $credentials, + 'realm' => $realm, + 'userId' => $userId + ); + + try { + $this->_authenticate($userId, $credentials); + + if ($login) { + $auth = Horde_Auth::setAuth( + $this->_authCredentials['userId'], + $this->_authCredentials['credentials'], + $this->_authCredentials['realm'], + $this->_authCredentials['changeRequested'] + ); + } else { + if (!Horde_Auth::checkSessionIP()) { + Horde_Auth::setAuthError(self::REASON_SESSIONIP); + } elseif (!Horde_Auth::checkBrowserString()) { + Horde_Auth::setAuthError(self::REASON_BROWSER); + } else { + $auth = true; + } + } + } catch (Horde_Exception $e) { + Horde_Auth::setAuthError($e->getCode() || Horde_Auth::REASON_MESSAGE, $e->getMessage()); + } + + return $auth; + } + + /** + * Authentication stub. + * + * Horde_Exception should pass a message string (if any) in the message + * field, and the REASON_* constant in the code field (defaults to + * REASON_MESSAGE). + * + * @throws Horde_Exception + */ + protected function _authenticate() + { + } + + /** + * Adds a set of authentication credentials. + * + * @param string $userId The userId to add. + * @param array $credentials The credentials to use. + * + * @throws Horde_Exception + */ + public function addUser($userId, $credentials) + { + throw new Horde_Exception('unsupported'); + } + + /** + * Updates a set of authentication credentials. + * + * @param string $oldID The old userId. + * @param string $newID The new userId. + * @param array $credentials The new credentials + * + * @throws Horde_Exception + */ + public function updateUser($oldID, $newID, $credentials) + { + throw new Horde_Exception('unsupported'); + } + + /** + * Deletes a set of authentication credentials. + * + * @param string $userId The userId to delete. + * + * @throws Horde_Exception + */ + public function removeUser($userId) + { + throw new Horde_Exception('unsupported'); + } + + /** + * Lists all users in the system. + * + * @return mixed The array of userIds. + * @throws Horde_Exception + */ + public function listUsers() + { + throw new Horde_Exception('unsupported'); + } + + /** + * Checks if $userId exists in the system. + * + * @param string $userId User ID for which to check + * + * @return boolean Whether or not $userId already exists. + */ + public function exists($userId) + { + try { + $users = $this->listUsers(); + return in_array($userId, $users); + } catch (Horde_Exception $e) { + return false; + } + } + + /** + * Automatic authentication: Finds out if the client matches an allowed IP + * block. + * + * @return boolean Whether or not the client is allowed. + */ + public function transparent() + { + try { + return $this->_transparent(); + } catch (Horde_Exception $e) { + Horde_Auth::setAuthError($e->getCode() || Horde_Auth::REASON_MESSAGE, $e->getMessage()); + return false; + } + } + + /** + * Transparent authentication stub. + * + * If the auth error message is desired to be set, Horde_Exception should + * thrown instead of returning false. + * The Horde_Exception object should have a message string (if any) in the + * message field, and the REASON_* constant in the code field (defaults to + * REASON_MESSAGE). + * + * @return boolean Whether transparent login is supported. + * @throws Horde_Exception + */ + protected function _transparent() + { + return false; + } + + /** + * Reset a user's password. Used for example when the user does not + * remember the existing password. + * + * @param string $userId The user id for which to reset the password. + * + * @return string The new password on success. + * @throws Horde_Exception + */ + public function resetPassword($userId) + { + throw new Horde_Exception('unsupported'); + } + + /** + * Queries the current driver to find out if it supports the given + * capability. + * + * @param string $capability The capability to test for. + * + * @return boolean Whether or not the capability is supported. + */ + public function hasCapability($capability) + { + return !empty($this->_capabilities[$capability]); + } + + /** + * Returns the URI of the login screen for the current authentication + * method. + * + * @param string $app The application to use. + * @param string $url The URL to redirect to after login. + * + * @return string The login screen URI. + */ + public function getLoginScreen($app = 'horde', $url = '') + { + $login = Horde::url($GLOBALS['registry']->get('webroot', $app) . '/login.php', true); + if (!empty($url)) { + $login = Horde_Util::addParameter($login, 'url', $url); + } + return $login; + } + + /** + * Returns the named parameter for the current auth driver. + * + * @param string $param The parameter to fetch. + * + * @return string The parameter's value, or null if it doesn't exist. + */ + public function getParam($param) + { + return isset($this->_params[$param]) + ? $this->_params[$param] + : null; + } + + /** + * Driver-level admin check stub. + * + * @todo + * + * @return boolean False. + */ + public function isAdmin($permission = null, $permlevel = null, $user = null) + { + return false; + } + +} diff --git a/framework/Auth/lib/Horde/Auth/Ftp.php b/framework/Auth/lib/Horde/Auth/Ftp.php new file mode 100644 index 000000000..3003c0c47 --- /dev/null +++ b/framework/Auth/lib/Horde/Auth/Ftp.php @@ -0,0 +1,68 @@ + + * 'hostspec' - (string) The hostname or IP address of the FTP server. + * DEFAULT: 'localhost' + * 'port' - (integer) The server port to connect to. + * DEFAULT: 21 + * + * + * Copyright 1999-2009 The Horde Project (http://www.horde.org/) + * + * See the enclosed file COPYING for license information (LGPL). If you did + * not receive this file, see http://opensource.org/licenses/lgpl-2.1.php + * + * @author Chuck Hagenbuch + * @author Max Kalika + * @package Horde_Auth + */ +class Horde_Auth_Ftp extends Horde_Auth_Driver +{ + /** + * Constructor. + * + * @param array $params A hash containing connection parameters. + * + * @throws Horde_Exception + */ + public function __construct($params = array()) + { + if (!Horde_Util::extensionExists('ftp')) { + throw new Horde_Exception('Horde_Auth_Ftp: Required FTP extension not found. Compile PHP with the --enable-ftp switch.'); + return false; + } + + $params = array_merge(array( + 'hostspec' => 'localhost', + 'port' => 21 + ), $params); + + parent::__construct($params); + } + + /** + * Find out if a set of login credentials are valid. + * + * @param string $userId The userId to check. + * @param array $credentials An array of login credentials. For FTP, + * this must contain a password entry. + * + * @throws Horde_Exception + */ + protected function _authenticate($userId, $credentials) + { + $ftp = @ftp_connect($this->_params['hostspec'], $this->_params['port']); + + $res = $ftp && @ftp_login($ftp, $userId, $credentials['password']); + @ftp_quit($ftp); + + if ($res) { + throw new Horde_Exception('', Horde_Auth::REASON_BADLOGIN); + } + } + +} diff --git a/framework/Auth/lib/Horde/Auth/Http.php b/framework/Auth/lib/Horde/Auth/Http.php new file mode 100644 index 000000000..99693c5bf --- /dev/null +++ b/framework/Auth/lib/Horde/Auth/Http.php @@ -0,0 +1,140 @@ + + * @package Horde_Auth + */ +class Horde_Auth_Http extends Horde_Auth_Driver +{ + /** + * An array of capabilities, so that the driver can report which + * operations it supports and which it doesn't. + * + * @var array + */ + protected $_capabilities = array( + 'transparent' => true + ); + + /** + * Array of usernames and hashed passwords. + * + * @var array + */ + protected $_users = array(); + + /** + * Constructor. + * + * @param array $params A hash containing parameters. + */ + public function __construct($params = array()) + { + parent::__construct($params); + + // Default to DES passwords. + if (empty($this->_params['encryption'])) { + $this->_params['encryption'] = 'crypt-des'; + } + + if (!empty($this->_params['htpasswd_file'])) { + $users = file($this->_params['htpasswd_file']); + if (is_array($users)) { + // Enable the list users capability. + $this->_capabilities['list'] = true; + + // Put users into alphabetical order. + sort($users); + + foreach ($users as $line) { + list($user, $pass) = explode(':', $line, 2); + $this->_users[trim($user)] = trim($pass); + } + } + } + } + + /** + * Find out if a set of login credentials are valid. Only supports + * htpasswd files with DES passwords right now. + * + * @param string $userId The userId to check. + * @param array $credentials An array of login credentials. For IMAP, + * this must contain a password entry. + * + * @throws Horde_Exception + */ + protected function _authenticate($userId, $credentials) + { + if (empty($credentials['password']) || + empty($this->_users[$userId])) { + throw new Horde_Exception('', Horde_Auth::REASON_BADLOGIN); + } + + $hash = Horde_Auth::getCryptedPassword($credentials['password'], $this->_users[$userId], $this->_params['encryption'], !empty($this->_params['show_encryption'])); + + if ($hash != $this->_users[$userId]) { + throw new Horde_Exception('', Horde_Auth::REASON_BADLOGIN); + } + } + + /** + * Return the URI of the login screen for this authentication object. + * + * @param string $app The application to use. + * @param string $url The URL to redirect to after login. + * + * @return string The login screen URI. + */ + public function getLoginScreen($app = 'horde', $url = '') + { + if (!empty($this->_params['loginScreen'])) { + return $url + ? Horde_Util::addParameter($this->_params['loginScreen'], 'url', $url) + : $this->_params['loginScreen']; + } + + return parent::getLoginScreen($app, $url); + } + + /** + * List all users in the system. + * + * @return array The array of userIds. + */ + public function listUsers() + { + return array_keys($this->_users); + } + + /** + * Automatic authentication: Find out if the client has HTTP + * authentication info present. + * + * @return boolean Whether or not the client is allowed. + * @throws Horde_Exception + */ + protected function _transparent() + { + if (!empty($_SERVER['PHP_AUTH_USER']) && + !empty($_SERVER['PHP_AUTH_PW'])) { + return Horde_Auth::setAuth(Horde_Util::dispelMagicQuotes($_SERVER['PHP_AUTH_USER']), array('password' => Horde_Util::dispelMagicQuotes($_SERVER['PHP_AUTH_PW']), 'transparent' => 1)); + } + + throw new Horde_Exception(_("HTTP Authentication not found.")); + } + +} diff --git a/framework/Auth/lib/Horde/Auth/HttpRemote.php b/framework/Auth/lib/Horde/Auth/HttpRemote.php new file mode 100644 index 000000000..7b8aa31ad --- /dev/null +++ b/framework/Auth/lib/Horde/Auth/HttpRemote.php @@ -0,0 +1,46 @@ + + * @package Horde_Auth + */ +class Horde_Auth_HttpRemote extends Horde_Auth_Driver +{ + /** + * Find out if a set of login credentials are valid. + * + * @param string $userId The userId to check. + * @param array $credentials An array of login credentials. + * + * @throws Horde_Exception + */ + protected function _authenticate($userId, $credentials) + { + $options = array( + 'allowRedirects' => true, + 'method' => 'GET', + 'timeout' => 5 + ); + + if (!empty($GLOBALS['conf']['http']['proxy']['proxy_host'])) { + $options = array_merge($options, $GLOBALS['conf']['http']['proxy']); + } + + $request = new HTTP_Request($this->_params['url'], $options); + $request->setBasicAuth($userId, $credentials['password']); + + $request->sendRequest(); + + if ($request->getResponseCode() != 200) { + throw new Horde_Exception('', Horde_Auth::REASON_BADLOGIN); + } + } + +} diff --git a/framework/Auth/lib/Horde/Auth/Imap.php b/framework/Auth/lib/Horde/Auth/Imap.php new file mode 100644 index 000000000..e8e7d9f4f --- /dev/null +++ b/framework/Auth/lib/Horde/Auth/Imap.php @@ -0,0 +1,165 @@ + + * 'admin_password' The password of the adminstrator. + * DEFAULT: null + * 'admin_user' The name of a user with admin privileges. + * DEFAULT: null + * 'hostspec' The hostname or IP address of the server. + * DEFAULT: 'localhost' + * 'port' The server port to which we will connect. + * IMAP is generally 143, while IMAP-SSL is generally 993. + * DEFAULT: Encryption port default + * 'secure' The encryption to use. Either 'none', 'ssl', or 'tls'. + * DEFAULT: 'none' + * 'userhierarchy' The hierarchy where user mailboxes are stored. + * DEFAULT: 'user.' + * + * + * If setting up as Horde auth handler in conf.php, this is a sample entry: + *
+ * $conf['auth']['params']['hostspec'] = 'imap.example.com';
+ * $conf['auth']['params']['port'] = 143;
+ * $conf['auth']['params']['secure'] = 'none';
+ * 
+ * + * Copyright 1999-2009 The Horde Project (http://www.horde.org/) + * + * See the enclosed file COPYING for license information (LGPL). If you did + * not receive this file, see http://opensource.org/licenses/lgpl-2.1.php + * + * @author Chuck Hagenbuch + * @author Gaudenz Steinlin + * @author Jan Schneider + * @package Horde_Auth + */ +class Horde_Auth_Imap extends Horde_Auth_Driver +{ + /** + * Constructor. + * + * @param array $params A hash containing connection parameters. + */ + public function __construct($params = array()) + { + $params = array_merge(array( + 'admin_password' => null, + 'admin_user' => null, + 'hostspec' => '', + 'port' => '', + 'secure' => 'none', + 'userhierarchy' => 'user.' + ), $params); + + parent::__construct(array_merge($default_params, $params)); + + if (!empty($this->_params['admin_user'])) { + $this->_capabilities['add'] = true; + $this->_capabilities['remove'] = true; + $this->_capabilities['list'] = true; + } + } + + /** + * Find out if a set of login credentials are valid. + * + * @param string $userId The userId to check. + * @param array $credentials An array of login credentials. For IMAP, + * this must contain a password entry. + * + * @throws Horde_Exception + */ + protected function _authenticate($userId, $credentials) + { + try { + $ob->_getOb($userId, $credentials['password']); + $ob->login(); + $ob->logout(); + } catch (Horde_Imap_Client_Exception $e) { + throw new Horde_Exception('', Horde_Auth::REASON_BADLOGIN); + } + } + + /** + * Add a set of authentication credentials. + * + * @param string $userId The userId to add. + * @param array $credentials The credentials to use. + * + * @throws Horde_Exception + */ + public function addUser($userId, $credentials) + { + try { + $ob->_getOb($this->_params['admin_user'], $this->_params['admin_password']); + $mailbox = Horde_String::convertCharset($this->_params['userhierarchy'] . $userId, NLS::getCharset(), 'utf7-imap'); + $ob->createMailbox($mailbox); + $ob->setACL($mailbox, $this->_params['admin_user'], 'lrswipcda'); + } catch (Horde_Imap_Client_Exception $e) { + throw new Horde_Exception($e); + } + } + + /** + * Delete a set of authentication credentials. + * + * @param string $userId The userId to delete. + * + * @throws Horde_Exception + */ + public function removeUser($userId) + { + try { + $ob->_getOb($this->_params['admin_user'], $this->_params['admin_password']); + $ob->setACL($mailbox, $this->_params['admin_user'], 'lrswipcda'); + $ob->deleteMailbox(Horde_String::convertCharset($this->_params['userhierarchy'] . $userId, NLS::getCharset(), 'utf7-imap')); + } catch (Horde_Imap_Client_Exception $e) { + throw new Horde_Exception($e); + } + + Horde_Auth::removeUserData($userId); + } + + /** + * List all users in the system. + * + * @return array The array of userIds. + * @throws Horde_Exception + */ + public function listUsers() + { + try { + $ob->_getOb($this->_params['admin_user'], $this->_params['admin_password']); + $list = $ob->listMailboxes($this->_params['userhierarchy'] . '%', Horde_Imap_Client::MBOX_ALL, array('flat' => true)); + } catch (Horde_Imap_Client_Exception $e) { + throw new Horde_Exception($e); + } + + return empty($list) + ? array() + : preg_replace('/.*' . preg_quote($this->_params['userhierarchy'], '/') . '(.*)/', '\\1', $list); + } + + /** + * Get Horde_Imap_Client object. + * + * @return Horde_Imap_Client_Base IMAP client object. + */ + protected function _getOb($user, $pass) + { + $imap_config = array( + 'hostspec' => empty($this->_params['hostspec']) ? null : $this->_params['hostspec'], + 'password' => $pass, + 'port' => empty($this->_params['port']) ? null : $this->_params['port'], + 'secure' => ($this->_params['secure'] == 'none') ? null : $this->_params['secure'], + 'username' => $user + ); + + return Horde_Imap_Client::getInstance('Socket', $imap_config); + } + +} diff --git a/framework/Auth/lib/Horde/Auth/Imsp.php b/framework/Auth/lib/Horde/Auth/Imsp.php new file mode 100644 index 000000000..6004ac46d --- /dev/null +++ b/framework/Auth/lib/Horde/Auth/Imsp.php @@ -0,0 +1,42 @@ + + * @package Horde_Auth + */ +class Horde_Auth_imsp extends Horde_Auth_Driver +{ + /** + * Private authentication function. + * + * @param string $userID Username for IMSP server. + * @param array $credentials Hash containing 'password' element. + * + * @return boolean True on success / False on failure. + */ + protected function _authenticate($userID, $credentials) + { + $this->_params['username'] = $userID; + $this->_params['password'] = $credentials['password']; + + $imsp = Net_IMSP_Auth::singleton($this->_params['auth_method']); + if ($imsp instanceof PEAR_Error) { + throw new Horde_Exception($imsp->getMessage()); + } + + $result = $imsp->authenticate($this->_params, false); + if ($result instanceof PEAR_Error) { + throw new Horde_Exception('', Horde_Auth::REASON_BADLOGIN); + } + } + +} diff --git a/framework/Auth/lib/Horde/Auth/Ipbasic.php b/framework/Auth/lib/Horde/Auth/Ipbasic.php new file mode 100644 index 000000000..066adfb96 --- /dev/null +++ b/framework/Auth/lib/Horde/Auth/Ipbasic.php @@ -0,0 +1,90 @@ + + * 'blocks' - (array) CIDR masks which are allowed access. + * + * + * Copyright 1999-2009 The Horde Project (http://www.horde.org/) + * + * See the enclosed file COPYING for license information (LGPL). If you did + * not receive this file, see http://opensource.org/licenses/lgpl-2.1.php + * + * @author Chuck Hagenbuch + * @package Horde_Auth + */ +class Horde_Auth_Ipbasic extends Horde_Auth_Driver +{ + /** + * An array of capabilities, so that the driver can report which + * operations it supports and which it doesn't. + * + * @var array + */ + protected $_capabilities = array( + 'transparent' => true + ); + + /** + * Constructor. + * + * @param array $params A hash containing parameters. + */ + public function __construct($params = array()) + { + if (empty($params['blocks'])) { + $params['blocks'] = array(); + } elseif (!is_array($params['blocks'])) { + $params['blocks'] = array($params['blocks']); + } + + parent::__construct($params); + } + + /** + * Automatic authentication: Find out if the client matches an allowed IP + * block. + * + * @return boolean Whether or not the client is allowed. + * @throws Horde_Exception + */ + protected function _transparent() + { + if (!isset($_SERVER['REMOTE_ADDR'])) { + throw new Horde_Exception(_("IP Address not avaialble.")); + } + + $client = $_SERVER['REMOTE_ADDR']; + foreach ($this->_params['blocks'] as $cidr) { + if ($this->_addressWithinCIDR($client, $cidr)) { + return Horde_Auth::setAuth($cidr, array('transparent' => 1)); + } + } + + throw new Horde_Exception(_("IP Address not within allowed CIDR block.")); + } + + /** + * Determine if an IP address is within a CIDR block. + * + * @param string $address The IP address to check. + * @param string $cidr The block (e.g. 192.168.0.0/16) to test against. + * + * @return boolean Whether or not the address matches the mask. + */ + protected function _addressWithinCIDR($address, $cidr) + { + $address = ip2long($address); + list($quad, $bits) = explode('/', $cidr); + $bits = intval($bits); + $quad = ip2long($quad); + + return (($address >> (32 - $bits)) == ($quad >> (32 - $bits))); + } + +} diff --git a/framework/Auth/lib/Horde/Auth/Kolab.php b/framework/Auth/lib/Horde/Auth/Kolab.php new file mode 100644 index 000000000..d511467dd --- /dev/null +++ b/framework/Auth/lib/Horde/Auth/Kolab.php @@ -0,0 +1,208 @@ + + * Copyright 2008-2009 The Horde Project (http://www.horde.org/) + * + * See the enclosed file COPYING for license information (LGPL). If you did + * not receive this file, see http://opensource.org/licenses/lgpl-2.1.php + * + * @author Stuart Binge + * @author Gunnar Wrobel + * @package Horde_Auth + */ +class Horde_Auth_Kolab extends Horde_Auth_Driver +{ + /** + * An array of capabilities, so that the driver can report which + * operations it supports and which it doesn't. + * + * @var array + */ + protected $capabilities = array( + 'add' => true, + 'list' => true + ); + + /** + * Find out if a set of login credentials are valid. + * + * For Kolab this requires to identify the IMAP server the user should + * be authenticated against before the credentials can be checked using + * this server. The Kolab_Server module handles identification of the + * correct IMAP server. + * + * @param string $userId The userId to check. + * @param array $credentials An array of login credentials. For Kolab, + * this must contain a "password" entry. + * + * @throws Horde_Exception + */ + protected function _authenticate($userId, $credentials) + { + global $conf; + + $params = array(); + + if (class_exists('Horde_Kolab_Session')) { + try { + $session = Horde_Kolab_Session::singleton($userId, $credentials, true); + } catch (Horde_Kolab_Server_MissingObjectException $e) { + throw new Horde_Exception('', Horde_Auth::REASON_BADLOGIN); + } catch (Exception $e) { + Horde::logMessage($e, __FILE__, __LINE__, PEAR_LOG_ERR); + throw new Horde_Exception('', Horde_Auth::REASON_FAILED); + } + } else { + throw new Horde_Exception('The class Horde_Kolab_Session is required for the Kolab auth driver but it is missing!', Horde_Auth::REASON_MESSAGE); + } + + if (!isset($conf['auth']['params']) || + $conf['auth']['params']['login_block'] != 1) { + // Return if feature is disabled. + return $session->auth; + } + + if ($session->auth !== true && + class_exists('Horde_History')) { + $history = Horde_History::singleton(); + + $history_identifier = "$userId@logins.kolab"; + $history_log = $history->getHistory($history_identifier); + $history_list = array(); + + // Extract history list from log. + if ($history_log && !($history_log instanceof PEAR_Error)) { + $data = $history_log->getData(); + if (!empty($data)) { + $entry = array_shift($data); + $history_list = $entry['history_list']; + } + } + + // Calculate the time range. + $start_time = (time() - $conf['auth']['params']['login_block_time'] * 60); + + $new_history_list = array(); + $count = 0; + + // Copy and count all relevant timestamps. + foreach ($history_list as $entry) { + $timestamp = $entry[ 'timestamp' ]; + + if ($timestamp > $start_time) { + $new_history_list[] = $entry; + $count++; + } + } + + $max_count = $conf['auth']['params']['login_block_count']; + + if ($count > $max_count) { + // Add entry for current failed login. + $entry = array(); + $entry[ 'timestamp' ] = time(); + $new_history_list[] = $entry; + + // Write back history. + $history->log($history_identifier, + array('action' => 'add', 'who' => $userId, + 'history_list' => $new_history_list), true); + + if ($count > $max_count) { + throw new Horde_Exception(_("Too many invalid logins during the last minutes.")); + } + + throw new Horde_Exception('', Horde_Auth::REASON_BADLOGIN); + } + } + + return ($session->auth === true); + } + + /** + * Sets a variable in the session saying that authorization has succeeded, + * note which userId was authorized, and note when the login took place. + * + * The kolab driver rewrites UIDs into the correct mail addresses that + * need to be used to log into the IMAP server. + * + * @param string $userId The userId who has been authorized. + * @param array $credentials The credentials of the user. + * @param string $realm The authentication realm to use. + * @param boolean $changeRequested Whether to request that the user change + * their password. + */ + function setAuth($userId, $credentials, $realm = null, $changeRequested = false) + { + // TODO - setAuth doesn't exist in Horde_Auth_Driver + // This should probably use _username_hook_frombackend. + + if (class_exists('Horde_Kolab_Session')) { + $session = Horde_Kolab_Session::singleton($userId); + $userId = $session->user_mail; + } + + return parent::setAuth($userId, $credentials, $realm, $changeRequested); + } + + /** + * List Users + * + * @return array List of Users + * @throws Horde_Exception + */ + public function listUsers() + { + if (!class_exists('Horde_Kolab_Session')) { + throw new Horde_Exception('The Horde_Kolab_Session class is not available.'); + } + + $session = Horde_Kolab_Session::singleton(); + $server = $session->getServer(); + if ($server instanceof PEAR_Error) { + return $server; + } + $users = $server->listObjects(KOLAB_OBJECT_USER); + $mails = array(); + foreach ($users as $user) { + $mails[] = $user->get(KOLAB_ATTR_MAIL); + } + return $mails; + } + + /** + * Add a set of authentication credentials. + * + * @param string $userId The userId to add. + * @param array $credentials The credentials to be set. + * + * @throws Horde_Exception + */ + public function addUser($userId, $credentials) + { + if (!class_exists('Horde_Kolab_Session')) { + throw new Horde_Exception('The Horde_Kolab_Session class is not available.'); + } + + $session = Horde_Kolab_Session::singleton(); + $server = $session->getServer(); + if ($server instanceof PEAR_Error) { + return $server; + } + + $result = $server->store(KOLAB_OBJECT_USER, $userId, $credentials); + + if (is_a($result, KOLAB_OBJECT_USER)) { + return true; + } else if ($result instanceof PEAR_Error) { + return $result; + } else { + throw new Horde_Exception(sprintf('The new Kolab object is a %s rather than a ' . KOLAB_OBJECT_USER, get_class($result))); + } + } + +} diff --git a/framework/Auth/lib/Horde/Auth/Krb5.php b/framework/Auth/lib/Horde/Auth/Krb5.php new file mode 100644 index 000000000..8a248448a --- /dev/null +++ b/framework/Auth/lib/Horde/Auth/Krb5.php @@ -0,0 +1,72 @@ + + * @package Horde_Auth + */ +class Horde_Auth_Krb5 extends Horde_Auth_Driver +{ + /** + * Constructor. + * + * @param array $params Not used. + * + * @throws Horde_Exception + */ + public function __construct($params = array()) + { + if (!Horde_Util::extensionExists('krb5')) { + throw new Horde_Exception(_("Horde_Auth_Krb5: Required krb5 extension not found.")); + } + + parent::__construct($params); + } + + /** + * Find out if a set of login credentials are valid. + * + * @param string $userId The userId to check. + * @param array $credentials An array of login credentials. + * For kerberos, this must contain a password + * entry. + * + * @throws Horde_Exception + */ + protected function _authenticate($userId, $credentials) + { + if (empty($credentials['password'])) { + throw new Horde_Exception('', Horde_Auth::REASON_BADLOGIN); + } + + $result = krb5_login($userId, $credentials['password']); + + switch ($result) { + case KRB5_OK: + return; + + case KRB5_BAD_PASSWORD: + throw new Horde_Exception(_("Bad kerberos password.")); + + case KRB5_BAD_USER: + throw new Horde_Exception(_("Bad kerberos username.")); + + default: + throw new Horde_Exception(_("Kerberos server rejected authentication.")); + } + } + +} diff --git a/framework/Auth/lib/Horde/Auth/Ldap.php b/framework/Auth/lib/Horde/Auth/Ldap.php new file mode 100644 index 000000000..e2dd72311 --- /dev/null +++ b/framework/Auth/lib/Horde/Auth/Ldap.php @@ -0,0 +1,566 @@ + + * 'basedn' The base DN for the LDAP server. + * 'hostspec' The hostname of the LDAP server. + * 'uid' The username search key. + * 'filter' The LDAP formatted search filter to search for users. This + * setting overrides the 'objectclass' method below. + * 'objectclass' The objectclass filter used to search for users. Can be a + * single objectclass or an array. + * + * + * Optional parameters: + *
+ * 'binddn'       The DN used to bind to the LDAP server
+ * 'password'     The password used to bind to the LDAP server
+ * 'version'      The version of the LDAP protocol to use.
+ *                DEFAULT: NONE (system default will be used)
+ * 
+ * + * Copyright 1999-2009 The Horde Project (http://www.horde.org/) + * + * See the enclosed file COPYING for license information (LGPL). If you did + * not receive this file, see http://opensource.org/licenses/lgpl-2.1.php + * + * @author Jon Parise + * @package Horde_Auth + */ +class Horde_Auth_Ldap extends Horde_Auth_Driver +{ + /** + * An array of capabilities, so that the driver can report which + * operations it supports and which it doesn't. + * + * @var array + */ + protected $_capabilities = array( + 'add' => true, + 'update' => true, + 'remove' => true, + 'list' => true + ); + + /** + * LDAP connection handle. + * + * @var resource + */ + protected $_ds; + + /** + * Construct. + * + * @param array $params A hash containing connection parameters. + * + * @throws Horde_Exception + */ + public function __construct($params = array()) + { + if (!Horde_Util::extensionExists('ldap')) { + throw new Horde_Exception(_("Horde_Auth_Ldap: Required LDAP extension not found.")); + } + + /* Ensure we've been provided with all of the necessary parameters. */ + Horde::assertDriverConfig($params, 'auth', + array('hostspec', 'basedn', 'uid'), + 'authentication LDAP'); + + parent::__construct($params); + } + + /** + * Does an ldap connect and binds as the guest user or as the optional dn. + * + * @throws Horde_Exception + */ + protected function _connect() + { + /* Connect to the LDAP server. */ + $this->_ds = @ldap_connect($this->_params['hostspec']); + if (!$this->_ds) { + throw new Horde_Exception(_("Failed to connect to LDAP server.")); + } + + if (isset($this->_params['version'])) { + if (!ldap_set_option($this->_ds, LDAP_OPT_PROTOCOL_VERSION, + $this->_params['version'])) { + Horde::logMessage( + sprintf('Set LDAP protocol version to %d failed: [%d] %s', + $this->_params['version'], + @ldap_errno($this->_ds), + @ldap_error($this->_ds)), + __FILE__, __LINE__, PEAR_LOG_ERR); + } + } + + /* Start TLS if we're using it. */ + if (!empty($this->_params['tls'])) { + if (!@ldap_start_tls($this->_ds)) { + Horde::logMessage( + sprintf('STARTTLS failed: [%d] %s', + @ldap_errno($this->_ds), + @ldap_error($this->_ds)), + __FILE__, __LINE__, PEAR_LOG_ERR); + } + } + + /* Work around Active Directory quirk. */ + if (!empty($this->_params['ad'])) { + if (!ldap_set_option($this->_ds, LDAP_OPT_REFERRALS, false)) { + Horde::logMessage( + sprintf('Unable to disable directory referrals on this connection to Active Directory: [%d] %s', + @ldap_errno($this->_ds), + @ldap_error($this->_ds)), + __FILE__, __LINE__, PEAR_LOG_ERR); + } + } + + $bind = isset($this->_params['binddn']) + ? @ldap_bind($this->_ds, $this->_params['binddn'], $this->_params['password']) + : @ldap_bind($this->_ds); + + if (!$bind) { + throw new Horde_Exception(_("Could not bind to LDAP server.")); + } + } + + /** + * Find the user dn + * + * @param string $userId The userId to find. + * + * @return string The users full DN + * @throws Horde_Exception + */ + protected function _findDN($userId) + { + /* Search for the user's full DN. */ + $filter = $this->_getParamFilter(); + $filter = '(&(' . $this->_params['uid'] . '=' . $userId . ')' . + $filter . ')'; + + $func = ($this->_params['scope'] == 'one') + ? 'ldap_list' + : 'ldap_search'; + + $search = @$func($this->_ds, $this->_params['basedn'], $filter, + array($this->_params['uid'])); + if (!$search) { + Horde::logMessage(ldap_error($this->_ds), __FILE__, __LINE__, PEAR_LOG_ERR); + throw new Horde_Exception(_("Could not search the LDAP server.")); + } + + $result = @ldap_get_entries($this->_ds, $search); + if (is_array($result) && (count($result) > 1)) { + $dn = $result[0]['dn']; + } else { + throw new Horde_Exception(_("Empty result.")); + } + + return $dn; + } + + /** + * Checks for shadowLastChange and shadowMin/Max support and returns their + * values. We will also check for pwdLastSet if Active Directory is + * support is requested. For this check to succeed we need to be bound + * to the directory. + * + * @param string $dn The dn of the user. + * + * @return array Array with keys being "shadowlastchange", "shadowmin" + * "shadowmax", "shadowwarning" and containing their + * respective values or false for no support. + */ + protected function _lookupShadow($dn) + { + /* Init the return array. */ + $lookupshadow = array( + 'shadowlastchange' => false, + 'shadowmin' => false, + 'shadowmax' => false, + 'shadowwarning' => false + ); + + /* According to LDAP standard, to read operational attributes, you + * must request them explicitly. Attributes involved in password + * expiration policy: + * pwdlastset: Active Directory + * shadow*: shadowUser schema + * passwordexpirationtime: Sun and Fedora Directory Server */ + $result = @ldap_read($this->_ds, $dn, '(objectClass=*)', + array('pwdlastset', 'shadowmax', 'shadowmin', + 'shadowlastchange', 'shadowwarning', + 'passwordexpirationtime')); + if ($result) { + $information = @ldap_get_entries($this->_ds, $result); + + if ($this->_params['ad']) { + if (isset($information[0]['pwdlastset'][0])) { + /* Active Directory handles timestamps a bit differently. + * Convert the timestamp to a UNIX timestamp. */ + $lookupshadow['shadowlastchange'] = floor((($information[0]['pwdlastset'][0] / 10000000) - 11644406783) / 86400) - 1; + + /* Password expiry attributes are in a policy. We cannot + * read them so use the Horde config. */ + $lookupshadow['shadowwarning'] = $this->_params['warnage']; + $lookupshadow['shadowmin'] = $this->_params['minage']; + $lookupshadow['shadowmax'] = $this->_params['maxage']; + } + } elseif (isset($information[0]['passwordexpirationtime'][0])) { + /* Sun/Fedora Directory Server uses a special attribute + * passwordexpirationtime. It has precedence over shadow* + * because it actually locks the expired password at the LDAP + * server level. The correct way to check expiration should + * be using LDAP controls, unfortunately PHP doesn't support + * controls on bind() responses. */ + $ldaptimepattern = "/([0-9]{4})([0-9]{2})([0-9]{2})([0-9]{2})([0-9]{2})([0-9]{2})Z/"; + if (preg_match($ldaptimepattern, $information[0]['passwordexpirationtime'][0], $regs)) { + /* Sun/Fedora Directory Server return expiration time, not + * last change time. We emulate the behaviour taking it + * back to maxage. */ + $lookupshadow['shadowlastchange'] = floor(mktime($regs[4], $regs[5], $regs[6], $regs[2], $regs[3], $regs[1]) / 86400) - $this->_params['maxage']; + + /* Password expiry attributes are in not accessible policy + * entry. */ + $lookupshadow['shadowwarning'] = $this->_params['warnage']; + $lookupshadow['shadowmin'] = $this->_params['minage']; + $lookupshadow['shadowmax'] = $this->_params['maxage']; + } else { + Horde::logMessage('Wrong time format: ' . $information[0]['passwordexpirationtime'][0], __FILE__, __LINE__, PEAR_LOG_ERR); + } + } else { + if (isset($information[0]['shadowmax'][0])) { + $lookupshadow['shadowmax'] = + $information[0]['shadowmax'][0]; + } + if (isset($information[0]['shadowmin'][0])) { + $lookupshadow['shadowmin'] = + $information[0]['shadowmin'][0]; + } + if (isset($information[0]['shadowlastchange'][0])) { + $lookupshadow['shadowlastchange'] = + $information[0]['shadowlastchange'][0]; + } + if (isset($information[0]['shadowwarning'][0])) { + $lookupshadow['shadowwarning'] = + $information[0]['shadowwarning'][0]; + } + } + } + + return $lookupshadow; + } + + /** + * Find out if the given set of login credentials are valid. + * + * @param string $userId The userId to check. + * @param array $credentials An array of login credentials. + * + * @throws Horde_Exception + */ + protected function _authenticate($userId, $credentials) + { + /* Connect to the LDAP server. */ + $this->_connect(); + + /* Search for the user's full DN. */ + $dn = $this->_findDN($userId); + + /* Attempt to bind to the LDAP server as the user. */ + $bind = @ldap_bind($this->_ds, $dn, $credentials['password']); + if ($bind == false) { + @ldap_close($this->_ds); + throw new Horde_Exception('', Horde_Auth::REASON_FAILED); + } + + if ($this->_params['password_expiration'] == 'yes') { + $shadow = $this->_lookupShadow($dn); + if ($shadow['shadowmax'] && $shadow['shadowlastchange'] && + $shadow['shadowwarning']) { + $today = floor(time() / 86400); + $warnday = $shadow['shadowlastchange'] + + $shadow['shadowmax'] - $shadow['shadowwarning']; + $toexpire = $shadow['shadowlastchange'] + + $shadow['shadowmax'] - $today; + + if ($today >= $warnday) { + $GLOBALS['notification']->push(sprintf(ngettext("%d day until your password expires.", "%d days until your password expires.", $toexpire), $toexpire), 'horde.warning'); + } + + if ($toexpire == 0) { + $this->_authCredentials['changeRequested'] = true; + } elseif ($toexpire < 0) { + throw new Horde_Exception('', Horde_Auth::REASON_EXPIRED); + } + } + } + + @ldap_close($this->_ds); + } + + /** + * Add a set of authentication credentials. + * + * @param string $userId The userId to add. + * @param array $credentials The credentials to be set. + * + * @throws Horde_Exception + */ + public function addUser($userId, $credentials) + { + if ($this->_params['ad']) { + throw new Horde_Exception(_("Horde_Auth_Ldap: Adding users is not supported for Active Directory")); + } + + /* Connect to the LDAP server. */ + $this->_connect(); + + global $conf; + if (!empty($conf['hooks']['authldap'])) { + $entry = Horde::callHook('_horde_hook_authldap', array($userId, $credentials)); + if ($entry instanceof PEAR_Error) { + throw new Horde_Exception($entry); + } + $dn = $entry['dn']; + /* Remove the dn entry from the array. */ + unset($entry['dn']); + } else { + /* Try this simple default and hope it works. */ + $dn = $this->_params['uid'] . '=' . $userId . ',' + . $this->_params['basedn']; + $entry['cn'] = $userId; + $entry['sn'] = $userId; + $entry[$this->_params['uid']] = $userId; + $entry['objectclass'] = array_merge( + array('top'), + $this->_params['newuser_objectclass']); + $entry['userPassword'] = Horde_Auth::getCryptedPassword( + $credentials['password'], '', + $this->_params['encryption'], + 'true'); + + if ($this->_params['password_expiration'] == 'yes') { + $entry['shadowMin'] = $this->_params['minage']; + $entry['shadowMax'] = $this->_params['maxage']; + $entry['shadowWarning'] = $this->_params['warnage']; + $entry['shadowLastChange'] = floor(time() / 86400); + } + } + $result = @ldap_add($this->_ds, $dn, $entry); + + if (!$result) { + throw new Horde_Exception(sprintf(_("Horde_Auth_Ldap: Unable to add user \"%s\". This is what the server said: "), $userId) . @ldap_error($this->_ds)); + } + + @ldap_close($this->_ds); + } + + /** + * Remove a set of authentication credentials. + * + * @param string $userId The userId to add. + * + * @throws Horde_Exception + */ + public function removeUser($userId) + { + if ($this->_params['ad']) { + throw new Horde_Exception(_("Horde_Auth_Ldap: Removing users is not supported for Active Directory")); + } + + /* Connect to the LDAP server. */ + $this->_connect(); + + if (!empty($GLOBALS['conf']['hooks']['authldap'])) { + $entry = Horde::callHook('_horde_hook_authldap', array($userId)); + if ($entry instanceof PEAR_Error) { + throw new Horde_Exception($entry); + } + $dn = $entry['dn']; + } else { + /* Search for the user's full DN. */ + $dn = $this->_findDN($userId); + } + + $result = @ldap_delete($this->_ds, $dn); + if (!$result) { + throw new Horde_Exception(sprintf(_("Auth_ldap: Unable to remove user \"%s\""), $userId)); + } + + @ldap_close($this->_ds); + + Horde_Auth::removeUserData($userId); + } + + /** + * Update a set of authentication credentials. + * + * @param string $oldID The old userId. + * @param string $newID The new userId. + * @param array $credentials The new credentials + * + * @throws Horde_Exception + */ + public function updateUser($oldID, $newID, $credentials) + { + if ($this->_params['ad']) { + throw new Horde_Exception(_("Horde_Auth_Ldap: Updating users is not supported for Active Directory.")); + } + + /* Connect to the LDAP server. */ + $this->_connect(); + + if (!empty($GLOBLS['conf']['hooks']['authldap'])) { + $entry = Horde::callHook('_horde_hook_authldap', array($oldID, $credentials)); + if ($entry instanceof PEAR_Error) { + throw new Horde_Exception($entry); + } + $olddn = $entry['dn']; + $entry = Horde::callHook('_horde_hook_authldap', array($newID, $credentials)); + $newdn = $entry['dn']; + unset($entry['dn']); + } else { + /* Search for the user's full DN. */ + $dn = $this->_findDN($oldID); + + $olddn = $dn; + $newdn = preg_replace('/uid=.*?,/', 'uid=' . $newID . ',', $dn, 1); + $shadow = $this->_lookupShadow($dn); + + /* If shadowmin hasn't yet expired only change when we are + administrator */ + if ($shadow['shadowlastchange'] && + $shadow['shadowmin'] && + ($shadow['shadowlastchange'] + $shadow['shadowmin'] > (time() / 86400))) { + throw new Horde_Exception(_("Minimum password age has not yet expired")); + } + + /* Set the lastchange field */ + if ($shadow['shadowlastchange']) { + $entry['shadowlastchange'] = floor(time() / 86400); + } + + /* Encrypt the new password */ + $entry['userpassword'] = Horde_Auth::getCryptedPassword( + $credentials['password'], '', + $this->_params['encryption'], + 'true'); + } + + if ($oldID != $newID) { + if (LDAP_OPT_PROTOCOL_VERSION == 3) { + ldap_rename($this->_ds, $olddn, $newdn, + $this->_params['basedn'], true); + + $result = ldap_modify($this->_ds, $newdn, $entry); + } else { + /* Get the complete old record first */ + $result = @ldap_read($this->_ds, $olddn, 'objectClass=*'); + + if ($result) { + $information = @ldap_get_entries($this->_ds, $result); + + /* Remove the count elements from the array */ + $counter = 0; + $newrecord = array(); + while (isset($information[0][$counter])) { + if ($information[0][$information[0][$counter]]['count'] == 1) { + $newrecord[$information[0][$counter]] = $information[0][$information[0][$counter]][0]; + } else { + $newrecord[$information[0][$counter]] = $information[0][$information[0][$counter]]; + unset($newrecord[$information[0][$counter]]['count']); + } + $counter++; + } + + /* Adjust the changed parameters */ + unset($newrecord['dn']); + $newrecord[$this->_params['uid']] = $newID; + $newrecord['userpassword'] = $entry['userpassword']; + if (isset($entry['shadowlastchange'])) { + $newrecord['shadowlastchange'] = $entry['shadowlastchange']; + } + + $result = ldap_add($this->_ds, $newdn, $newrecord); + if ($result) { + $result = @ldap_delete($this->_ds, $olddn); + } + } + } + } else { + $result = @ldap_modify($this->_ds, $olddn, $entry); + } + + if (!$result) { + throw new Horde_Exception(sprintf(_("Horde_Auth_Ldap: Unable to update user \"%s\""), $newID)); + } + + @ldap_close($this->_ds); + } + + /** + * List Users + * + * @return array List of Users + * @throws Horde_Exception + */ + public function listUsers() + { + /* Connect to the LDAP server. */ + $this->_connect(); + + $filter = $this->_getParamFilter(); + + $func = ($this->_params['scope'] == 'one') + ? 'ldap_list' + : 'ldap_search'; + + /* Add a sizelimit, if specified. Default is 0, which means no limit. + * Note: You cannot override a server-side limit with this. */ + $sizelimit = isset($this->_params['sizelimit']) ? $this->_params['sizelimit'] : 0; + $search = @$func($this->_ds, $this->_params['basedn'], $filter, + array($this->_params['uid']), 0, $sizelimit); + + $entries = @ldap_get_entries($this->_ds, $search); + $userlist = array(); + $uid = Horde_String::lower($this->_params['uid']); + for ($i = 0; $i < $entries['count']; $i++) { + $userlist[$i] = $entries[$i][$uid][0]; + } + + return $userlist; + } + + /** + * Return a formatted LDAP filter as configured within the parameters. + * + * @return string LDAP search filter + */ + protected function _getParamFilter() + { + if (!empty($this->_params['filter'])) { + $filter = $this->_params['filter']; + } elseif (!is_array($this->_params['objectclass'])) { + $filter = 'objectclass=' . $this->_params['objectclass']; + } else { + $filter = ''; + if (count($this->_params['objectclass']) > 1) { + $filter = '(&' . $filter; + foreach ($this->_params['objectclass'] as $objectclass) { + $filter .= '(objectclass=' . $objectclass . ')'; + } + $filter .= ')'; + } elseif (count($this->_params['objectclass']) == 1) { + $filter = '(objectClass=' . $this->_params['objectclass'][0] . ')'; + } + } + return $filter; + } +} diff --git a/framework/Auth/lib/Horde/Auth/Login.php b/framework/Auth/lib/Horde/Auth/Login.php new file mode 100644 index 000000000..9f229be71 --- /dev/null +++ b/framework/Auth/lib/Horde/Auth/Login.php @@ -0,0 +1,76 @@ + + * 'location' - (string) Location of the su binary. + * DEFAULT: /bin/su + * + * + * Copyright 2004-2009 The Horde Project (http://www.horde.org/) + * + * See the enclosed file COPYING for license information (LGPL). If you did + * not receive this file, see http://opensource.org/licenses/lgpl-2.1.php + * + * @author Jan Schneider + * @package Horde_Auth + */ +class Horde_Auth_Login extends Horde_Auth_Driver +{ + /** + * List of users that should be excluded from being listed/handled + * in any way by this driver. + * + * @var array + */ + protected $_exclude = array( + 'root', 'daemon', 'bin', 'sys', 'sync', 'games', 'man', 'lp', 'mail', + 'news', 'uucp', 'proxy', 'postgres', 'www-data', 'backup', 'operator', + 'list', 'irc', 'gnats', 'nobody', 'identd', 'sshd', 'gdm', 'postfix', + 'mysql', 'cyrus', 'ftp' + ); + + /** + * Constructs a new Login authentication object. + * + * @param array $params A hash containing connection parameters. + */ + public function __construct($params = array()) + { + if (empty($params['location'])) { + $params['location'] = '/bin/su'; + } + + parent::__construct($params); + } + + /** + * Find out if a set of login credentials are valid. + * + * @param string $userId The userId to check. + * @param array $credentials An array of login credentials. + * + * @return boolean Whether or not the credentials are valid. + */ + protected function _authenticate($userId, $credentials) + { + if (empty($credentials['password'])) { + throw new Horde_Exception('', Horde_Auth::REASON_BADLOGIN); + } + + $proc = @popen($this->_location . ' -c /bin/true ' . $userId, 'w'); + if (!is_resource($proc)) { + throw new Horde_Exception('', Horde_Auth::REASON_FAILED); + } + + fwrite($proc, $credentials['password']); + if (@pclose($proc) !== 0) { + throw new Horde_Exception('', Horde_Auth::REASON_FAILED); + } + } + +} diff --git a/framework/Auth/lib/Horde/Auth/Msad.php b/framework/Auth/lib/Horde/Auth/Msad.php new file mode 100644 index 000000000..5dc168dd3 --- /dev/null +++ b/framework/Auth/lib/Horde/Auth/Msad.php @@ -0,0 +1,304 @@ + + * @package Horde_Auth + */ +class Horde_Auth_Msad extends Horde_Auth_Ldap +{ + /** + * Constructor. + * + * @param array $params A hash containing connection parameters. + * + * @throws Horde_Exception + */ + public function __construct($params = array()) + { + if (!Horde_Util::extensionExists('ldap')) { + throw new Horde_Exception(_("Horde_Auth_Ldap: Required LDAP extension not found.")); + } + + $params = array_merge(array( + 'adduser' => true, + 'authId' => 'initials', + 'encryption' => 'msad', + 'newuser_objectclass' => 'user', + 'password_expiration' => 'no', + 'port' => 389, + 'ssl' => false, + 'uid' => array('samaccountname') + ), $params); + + if (!is_array($params['uid'])) { + $params['uid'] = array($params['uid']); + } + + /* Ensure we've been provided with all of the necessary parameters. */ + Horde::assertDriverConfig($params, 'auth', + array('hostspec', 'basedn'), 'authentication MSAD'); + + $this->_params = $params; + /* Adjust capabilities: depending on if SSL encryption is + * enabled or not */ + $this->_capabilities = array( + 'add' => ($params['ssl'] || $params['adduser']), + 'list' => true, + 'remove' => true, + 'resetpassword' => $params['ssl'], + 'update' => $params['ssl'] + ); + + parent::__construct($params); + } + + /** + * Add a set of authentication credentials. + * + * @param string $accountName The user sAMAccountName to find. + * @param array $credentials The credentials to be set. + * + * @throws Horde_Exception + */ + public function addUser($accountName, $credentials) + { + /* Connect to the MSAD server. */ + $this->_connect(); + + $entry = Horde::callHook('_horde_hook_authmsad', array($accountName, $credentials), 'horde', null); + if (!is_null($entry)) { + $dn = $entry['dn']; + unset($entry['dn']); + } else { + $basedn = (isset($credentials['basedn'])) ? + $credentials['basedn'] : $this->_params['basedn']; + + /* Set a default CN */ + $dn = 'cn=' . $accountName . ',' . $basedn; + + $entry['cn'] = $accountName; + $entry['samaccountname'] = $accountName; + + $entry['objectclass'][0] = "top"; + $entry['objectclass'][1] = "person"; + $entry['objectclass'][2] = "organizationalPerson"; + $entry['objectclass'][3] = "user"; + + $entry['description'] = (isset($credentials['description'])) ? + $credentials['description'] : 'New horde user'; + + if ($this->_params['ssl']) { + $entry["AccountDisabled"] = false; + } + $entry['userPassword'] = Horde_Auth::getCryptedPassword($credentials['password'],'', + $this->_params['encryption'], + false); + + if (isset($this->_params['binddn'])) { + $entry['manager'] = $this->_params['binddn']; + } + + } + + $success = @ldap_add($this->_ds, $dn, $entry); + + if (!$success) { + throw new Horde_Exception(sprintf(_("Auth_msad: Unable to add user \"%s\". This is what the server said: "), $accountName) . ldap_error($this->_ds)); + } + + @ldap_close($this->_ds); + } + + /** + * Remove a set of authentication credentials. + * + * @param string $accountName The user sAMAccountName to remove. + * + * @throws Horde_Exception + */ + public function removeUser($accountName) + { + /* Connect to the MSAD server. */ + $this->_connect(); + + $entry = Horde::callHook('_horde_hook_authmsad', array($accountName), 'horde', null); + if (!is_null($entry)) { + $dn = $entry['dn']; + } else { + /* Search for the user's full DN. */ + $dn = $this->_findDN($accountName); + } + + if (!@ldap_delete($this->_ds, $dn)) { + throw new Horde_Exception(sprintf(_("Horde_Auth_Msad: Unable to remove user \"%s\""), $accountName)); + } + @ldap_close($this->_ds); + + /* Remove user data */ + Horde_Auth::removeUserData($accountName); + } + + /** + * Update a set of authentication credentials. + * + * @param string $oldID The old userId. + * @param string $newID The new userId. + * @param array $credentials The new credentials + * + * @throws Horde_Exception + */ + public function updateUser($oldId, $newId, $credentials) + { + /* Connect to the MSAD server. */ + $this->_connect(); + + $entry = Horde::callHook('_horde_hook_authmsad', array($oldId, $credentials), 'horde', null); + if (!is_null($entry)) { + $olddn = $entry['dn']; + unset($entry['dn']); + } else { + /* Search for the user's full DN. */ + $dn = $this->_findDN($oldId); + + /* Encrypt the new password */ + if (isset($credentials['password'])) { + $entry['userpassword'] = Horde_Auth::getCryptedPassword($credentials['password'],'', + $this->_params['encryption'], + true); + } + } + + if ($oldID != $newID) { + $newdn = str_replace($oldId, $newID, $dn); + ldap_rename($this->_ds, $olddn, $newdn, $this->_params['basedn'], true); + $success = @ldap_modify($this->_ds, $newdn, $entry); + } else { + $success = @ldap_modify($this->_ds, $olddn, $entry); + } + + if (!$success) { + throw new Horde_Exception(sprintf(_("Horde_Auth_Msad: Unable to update user \"%s\""), $newID), __FILE__, __LINE__); + } + + @ldap_close($this->_ds); + } + + /** + * Reset a user's password. Used for example when the user does not + * remember the existing password. + * + * @param string $user_id The user id for which to reset the password. + * + * @return string The new password on success. + * @throws Horde_Exception + */ + public function resetPassword($user_id) + { + /* Get a new random password. */ + $password = Horde_Auth::genRandomPassword() . '/'; + $this->updateUser($user_id, $user_id, array('userPassword' => $password)); + return $password; + } + + /** + * Does an ldap connect and binds as the guest user. + * + * @throws Horde_Exception + */ + protected function _connect() + { + /* Connect to the MSAD server. */ + $ssl = ($this->_params['ssl']) ? 'ldaps://' : ''; + $this->_ds = ldap_connect($ssl . $this->_params['hostspec'], $this->_params['port']); + if (!$this->_ds) { + return PEAR::raiseError(_("Failed to connect to MSAD server.")); + } + + if (!ldap_set_option($this->_ds, LDAP_OPT_PROTOCOL_VERSION, 3)) { + Horde::logMessage( + sprintf('Set MSAD protocol version to %d failed: [%d] %s', + 3, + ldap_errno($conn), + ldap_error($conn), + __FILE__, __LINE__)); + } + if (!ldap_set_option($this->_ds, LDAP_OPT_REFERRALS, 0)) { + Horde::logMessage( + sprintf('Set MSAD referrals option to %d failed: [%d] %s', + 0, + ldap_errno($conn), + ldap_error($conn), + __FILE__, __LINE__)); + } + + if (isset($this->_params['binddn'])) { + $bind = ldap_bind($this->_ds, + $this->_params['binddn'], + $this->_params['password']); + } else { + $bind = ldap_bind($this->_ds); + } + + if (!$bind) { + return PEAR::raiseError(_("Could not bind to MSAD server.")); + } + + return true; + } + + /** + * Find the user dn + * + * @access private + * + * @param string $userId The user UID to find. + * + * @return string The user's full DN + */ + function _findDN($userId) + { + /* Search for the user's full DN. */ + foreach ($this->_params['uid'] as $uid) { + $entries = array($uid); + if ($uid != $this->_params['authId']) { + array_push($entries, $this->_params['authId']); + } + $search = @ldap_search($this->_ds, $this->_params['basedn'], + $uid . '=' . $userId, + $entries + ); + /* Searching the tree is not successful */ + if (!$search) { + return PEAR::raiseError(_("Could not search the MSAD server.")); + } + + /* Fetch the search result */ + $result = @ldap_get_entries($this->_ds, $search); + /* The result isn't empty: the DN was found */ + if (is_array($result) && (count($result) > 1)) { + break; + } + } + + if (is_array($result) && (count($result) > 1)) { + $dn = $result[0]['dn']; + } else { + return PEAR::raiseError(_("Empty result.")); + } + /* Be sure the horde userId is the configured one */ + $this->_authCredentials['userId'] = $result[0][$this->_params['authId']][0]; + return $dn; + } + +} diff --git a/framework/Auth/lib/Horde/Auth/Pam.php b/framework/Auth/lib/Horde/Auth/Pam.php new file mode 100644 index 000000000..282a0755d --- /dev/null +++ b/framework/Auth/lib/Horde/Auth/Pam.php @@ -0,0 +1,71 @@ + + * 'service' - (string) The name of the PAM service to use when + * authenticating. + * DEFAULT: php + * + * + * Copyright 2004-2009 The Horde Project (http://www.horde.org/) + * + * See the enclosed file COPYING for license information (LGPL). If you did + * not receive this file, see http://opensource.org/licenses/lgpl-2.1.php + * + * @author Jon Parise + * @package Horde_Auth + */ +class Horde_Auth_Pam extends Horde_Auth_Driver +{ + /** + * Constructor. + * + * @param array $params A hash containing connection parameters. + * + * @throws Horde_Exception + */ + public function __construct($params = array()) + { + if (!Horde_Util::extensionExists('pam')) { + throw new Horde_Exception(_("PAM authentication is not available.")); + } + + if (!empty($params['service'])) { + ini_set('pam.servicename', $params['service']); + } + + parent::__construct($params); + } + + /** + * Find out if a set of login credentials are valid. + * + * @param string $userId The userId to check. + * @param array $credentials An array of login credentials. + * + * @throws Horde_Exception + */ + protected function _authenticate($userId, $credentials) + { + if (empty($credentials['password'])) { + throw new Horde_Exception('', Horde_Auth::REASON_BADLOGIN); + } + + $error = null; + if (!pam_auth($userId, $credentials['password'], $error)) { + throw new Horde_Exception($error); + } + } + +} diff --git a/framework/Auth/lib/Horde/Auth/Passwd.php b/framework/Auth/lib/Horde/Auth/Passwd.php new file mode 100644 index 000000000..e1a6040ec --- /dev/null +++ b/framework/Auth/lib/Horde/Auth/Passwd.php @@ -0,0 +1,421 @@ + + * 'filename' - (string) The passwd file to use. + * + * + * Optional parameters: + *
+ * 'encryption'       The encryption to use to store the password in
+ *                    the table (e.g. plain, crypt, md5-hex,
+ *                    md5-base64, smd5, sha, ssha, aprmd5).
+ *                    DEFAULT: 'crypt-des'
+ * 'lock'             Should we lock the passwd file? (boolean) The password
+ *                    file cannot be changed (add, edit, or delete users)
+ *                    unless this is true.
+ *                    DEFAULT: false<
+ * 'show_encryption'  Whether or not to prepend the encryption in the
+ *                    password field.
+ *                    DEFAULT: 'false'
+ * 
+ * + * Copyright 1997-2007 Rasmus Lerdorf + * Copyright 2002-2009 The Horde Project (http://www.horde.org/) + * + * See the enclosed file COPYING for license information (LGPL). If you did + * not receive this file, see http://opensource.org/licenses/lgpl-2.1.php + * + * @author Rasmus Lerdorf + * @author Chuck Hagenbuch + * @package Horde_Auth + */ +class Horde_Auth_Passwd extends Horde_Auth_Driver +{ + /** + * An array of capabilities, so that the driver can report which + * operations it supports and which it doesn't. + * + * @var array + */ + protected $_capabilities = array( + 'list' => true + ); + + /** + * Hash list of users. + * + * @var array + */ + protected $_users = null; + + /** + * Array of groups and members. + * + * @var array + */ + protected $_groups = array(); + + /** + * Filehandle for lockfile. + * + * @var integer + */ + protected $_fplock; + + /** + * Locking state. + * + * @var boolean + */ + protected $_locked; + + /** + * List of users that should be excluded from being listed/handled + * in any way by this driver. + * + * @var array + */ + protected $_exclude = array( + 'root', 'daemon', 'bin', 'sys', 'sync', 'games', 'man', 'lp', 'mail', + 'news', 'uucp', 'proxy', 'postgres', 'www-data', 'backup', 'operator', + 'list', 'irc', 'gnats', 'nobody', 'identd', 'sshd', 'gdm', 'postfix', + 'mysql', 'cyrus', 'ftp', + ); + + /** + * Constructor. + * + * @param array $params A hash containing connection parameters. + */ + public function __construct($params = array()) + { + $params = array_merge(array( + 'encryption' => 'crypt-des', + 'lock' => false, + 'show_encryption' => false + ), $params); + + parent::__construct($params); + } + + /** + * Writes changes to passwd file and unlocks it. Takes no arguments and + * has no return value. Called on script shutdown. + */ + public function __destruct() + { + if ($this->_locked) { + foreach ($this->_users as $user => $pass) { + if ($this->_users[$user]) { + fputs($this->_fplock, "$user:$pass:" . $this->_users[$user] . "\n"); + } else { + fputs($this->_fplock, "$user:$pass\n"); + } + } + rename($this->_lockfile, $this->_params['filename']); + flock($this->_fplock, LOCK_UN); + $this->_locked = false; + fclose($this->_fplock); + } + } + + /** + * Queries the current Auth object to find out if it supports the given + * capability. + * + * @param string $capability The capability to test for. + * + * @return boolean Whether or not the capability is supported. + */ + public function hasCapability($capability) + { + if ($this->_params['lock']) { + switch ($capability) { + case 'add': + case 'update': + case 'resetpassword': + case 'remove': + return true; + } + } + + return parent::hasCapability($capability); + } + + /** + * Read and, if requested, lock the password file. + * + * @throws Horde_Exception + */ + protected function _read() + { + if (is_array($this->_users)) { + return; + } + + if (empty($this->_params['filename'])) { + throw new Horde_Exception('No password file set.'); + } + + if ($this->_params['lock']) { + $this->_fplock = fopen(Horde::getTempDir() . '/passwd.lock', 'w'); + flock($this->_fplock, LOCK_EX); + $this->_locked = true; + } + + $fp = fopen($this->_params['filename'], 'r'); + if (!$fp) { + throw new Horde_Exception("Couldn't open '" . $this->_params['filename'] . "'."); + } + + $this->_users = array(); + while (!feof($fp)) { + $line = trim(fgets($fp, 128)); + if (empty($line)) { + continue; + } + + $parts = explode(':', $line); + if (!count($parts)) { + continue; + } + + $user = $parts[0]; + $userinfo = array(); + if (strlen($user) && !in_array($user, $this->_exclude)) { + if (isset($parts[1])) { + $userinfo['password'] = $parts[1]; + } + if (isset($parts[2])) { + $userinfo['uid'] = $parts[2]; + } + if (isset($parts[3])) { + $userinfo['gid'] = $parts[3]; + } + if (isset($parts[4])) { + $userinfo['info'] = $parts[4]; + } + if (isset($parts[5])) { + $userinfo['home'] = $parts[5]; + } + if (isset($parts[6])) { + $userinfo['shell'] = $parts[6]; + } + + $this->_users[$user] = $userinfo; + } + } + + fclose($fp); + + if (!empty($this->_params['group_filename'])) { + $fp = fopen($this->_params['group_filename'], 'r'); + if (!$fp) { + throw new Horde_Exception("Couldn't open '" . $this->_params['group_filename'] . "'."); + } + + $this->_groups = array(); + while (!feof($fp)) { + $line = trim(fgets($fp)); + if (empty($line)) { + continue; + } + + $parts = explode(':', $line); + $group = array_shift($parts); + $users = array_pop($parts); + $this->_groups[$group] = array_flip(preg_split('/\s*[,\s]\s*/', trim($users), -1, PREG_SPLIT_NO_EMPTY)); + } + + fclose($fp); + } + } + + /** + * Find out if a set of login credentials are valid. + * + * @param string $userId The userId to check. + * @param array $credentials An array of login credentials. For MCAL, + * this must contain a password entry. + * + * @throws Horde_Exception + */ + protected function _authenticate($userId, $credentials) + { + if (empty($credentials['password'])) { + throw new Horde_Exception('', Horde_Auth::REASON_BADLOGIN); + } + + try { + $this->_read(); + } catch (Horde_Exception $e) { + Horde::logMessage($e, __FILE__, __LINE__, PEAR_LOG_ERR); + throw new Horde_Exception('', Horde_Auth::REASON_FAILED); + } + + if (!isset($this->_users[$userId]) || + !$this->_comparePasswords($this->_users[$userId]['password'], $credentials['password'])) { + throw new Horde_Exception('', Horde_Auth::REASON_BADLOGIN); + } + + if (!empty($this->_params['required_groups'])) { + $allowed = false; + foreach ($this->_params['required_groups'] as $group) { + if (isset($this->_groups[$group][$userId])) { + $allowed = true; + break; + } + } + + if (!$allowed) { + throw new Horde_Exception('', Horde_Auth::REASON_BADLOGIN); + } + } + } + + /** + * List all users in the system. + * + * @return array The array of userIds. + * @throws Horde_Exception + */ + public function listUsers() + { + $this->_read(); + + $users = array_keys($this->_users); + if (empty($this->_params['required_groups'])) { + return $users; + } + + $groupUsers = array(); + foreach ($this->_params['required_groups'] as $group) { + $groupUsers = array_merge($groupUsers, array_intersect($users, array_keys($this->_groups[$group]))); + } + + return $groupUsers; + } + + /** + * Add a set of authentication credentials. + * + * @param string $userId The userId to add. + * @param array $credentials The credentials to add. + * + * @throws Horde_Exception + */ + public function addUser($userId, $credentials) + { + $this->_read(); + + if (!$this->_locked) { + throw new Horde_Exception('Password file not locked'); + } + + if (isset($this->_users[$userId])) { + throw new Horde_Exception("Couldn't add user '$user', because the user already exists."); + } + + $this->_users[$userId] = array( + 'password' => Horde_Auth::getCryptedPassword($credentials['password'], + '', + $this->_params['encryption'], + $this->_params['show_encryption']), + + ); + } + + /** + * Update a set of authentication credentials. + * + * @param string $oldID The old userId. + * @param string $newID The new userId. + * @param array $credentials The new credentials + * + * @throws Horde_Exception + */ + public function updateUser($oldID, $newID, $credentials) + { + $this->_read(); + + if (!$this->_locked) { + throw new Horde_Exception('Password file not locked'); + } + + if (!isset($this->_users[$userId])) { + throw new Horde_Exception("Couldn't modify user '$oldID', because the user doesn't exist."); + } + + $this->_users[$newID] = array( + 'password' => Horde_Auth::getCryptedPassword($credentials['password'], + '', + $this->_params['encryption'], + $this->_params['show_encryption']), + ); + return true; + } + + /** + * Reset a user's password. Used for example when the user does not + * remember the existing password. + * + * @param string $userId The user id for which to reset the password. + * + * @return string The new password. + * @throws Horde_Exception + */ + public function resetPassword($userId) + { + /* Get a new random password. */ + $password = Horde_Auth::genRandomPassword(); + $this->updateUser($userId, $userId, array('password' => $password)); + return $password; + } + + /** + * Delete a set of authentication credentials. + * + * @param string $userId The userId to delete. + * + * @throws Horde_Exception + */ + public function removeUser($userId) + { + $this->_read(); + + if (!$this->_locked) { + throw new Horde_Exception('Password file not locked'); + } + + if (!isset($this->_users[$userId])) { + throw new Horde_Exception("Couldn't delete user '$oldID', because the user doesn't exist."); + } + + unset($this->_users[$userId]); + + Horde_Auth::removeUserData($userId); + } + + + /** + * Compare an encrypted password to a plaintext string to see if + * they match. + * + * @param string $encrypted The crypted password to compare against. + * @param string $plaintext The plaintext password to verify. + * + * @return boolean True if matched, false otherwise. + */ + protected function _comparePasswords($encrypted, $plaintext) + { + return $encrypted == Horde_Auth::getCryptedPassword($plaintext, + $encrypted, + $this->_params['encryption'], + $this->_params['show_encryption']); + } + +} diff --git a/framework/Auth/lib/Horde/Auth/Peclsasl.php b/framework/Auth/lib/Horde/Auth/Peclsasl.php new file mode 100644 index 000000000..600756e7e --- /dev/null +++ b/framework/Auth/lib/Horde/Auth/Peclsasl.php @@ -0,0 +1,79 @@ + + * 'app' The name of the authenticating application. + * DEFAULT: horde + * 'service' The name of the SASL service to use when authenticating. + * DEFAULT: php + * + * + * Copyright 2004-2009 The Horde Project (http://www.horde.org/) + * + * See the enclosed file COPYING for license information (LGPL). If you did + * not receive this file, see http://opensource.org/licenses/lgpl-2.1.php + * + * @author Jon Parise + * @package Horde_Auth + */ +class Horde_Auth_Peclsasl extends Horde_Auth_Driver +{ + /** + * Constructor. + * + * @param array $params A hash containing connection parameters. + * + * @throws Horde_Exception + */ + public function __construct($params = array()) + { + if (!Horde_Util::extensionExists('sasl')) { + throw new Horde_Exception('Horde_Auth_Peclsasl:: requires the sasl PECL extension to be loaded.'); + } + + $params = array_merge(array( + 'app' => 'horde', + 'service' => 'php' + ), $params); + + parent::__construct($params); + + sasl_server_init($this->_params['app']); + } + + /** + * Find out if a set of login credentials are valid. + * + * @param string $userId The userId to check. + * @param array $credentials An array of login credentials. + * + * @throws Horde_Exception + */ + protected function _authenticate($userId, $credentials) + { + if (empty($credentials['password'])) { + throw new Horde_Exception('', Horde_Auth::REASON_BADLOGIN); + } + + $conn = sasl_server_new($this->_params['service']); + if (!is_resource($conn)) { + throw new Horde_Exception(_("Failed to create new SASL connection.")); + } + + if (!sasl_checkpass($conn, $userId, $credentials['password'])) { + throw new Horde_Exception(sasl_errdetail($conn)); + } + } + +} diff --git a/framework/Auth/lib/Horde/Auth/Radius.php b/framework/Auth/lib/Horde/Auth/Radius.php new file mode 100644 index 000000000..804c391b5 --- /dev/null +++ b/framework/Auth/lib/Horde/Auth/Radius.php @@ -0,0 +1,163 @@ + + * pecl install radius + * + * + * Then, edit your php.ini file and make sure the following line is present: + *
+ *   For Windows machines:  extension=php_radius.dll
+ *   For all others:        extension=radius.so
+ * 
+ * + * Required parameters: + *
+ * 'host' - (string) The RADIUS host to use (IP address or fully qualified
+ *          hostname).
+ * 'method' - (string) The RADIUS method to use for validating the request.
+ *            Either: 'PAP', 'CHAP_MD5', 'MSCHAPv1', or 'MSCHAPv2'.
+ *            ** CURRENTLY, only 'PAP' is supported. **
+ * 'secret' - (string) The RADIUS shared secret string for the host. The
+ *            RADIUS protocol ignores all but the leading 128 bytes
+ *            of the shared secret.
+ * 
+ * + * Optional parameters: + *
+ * 'nas' - (string) The RADIUS NAS identifier to use.
+ *         DEFAULT: The value of $_SERVER['HTTP_HOST'] or, if not
+ *                  defined, then 'localhost'.
+ * 'port' - (integer) The port to use on the RADIUS server.
+ *          DEFAULT: Whatever the local system identifies as the
+ *                   'radius' UDP port
+ * 'retries' - (integer) The maximum number of repeated requests to make
+ *             before giving up.
+ *             DEFAULT: 3
+ * 'suffix' - (string) The domain name to add to unqualified user names.
+ *             DEFAULT: NONE
+ * 'timeout' - (integer) The timeout for receiving replies from the server (in
+ *             seconds).
+ *             DEFAULT: 3
+ * 
+ * + * Copyright 2002-2009 The Horde Project (http://www.horde.org/) + * + * See the enclosed file COPYING for license information (LGPL). If you did + * not receive this file, see http://opensource.org/licenses/lgpl-2.1.php + * + * @author Michael Slusarz + * @package Horde_Auth + */ +class Horde_Auth_Radius extends Horde_Auth_Driver +{ + /** + * Constructor. + * + * @param array $params A hash containing connection parameters. + * + * @throws Horde_Exception + */ + public function __construct($params = array()) + { + parent::__construct($params); + + if (!Horde_Util::extensionExists('radius')) { + throw new Horde_Exception('Horde_Auth_Radius:: requires the radius PECL extension to be loaded.'); + } + + /* A RADIUS host is required. */ + if (empty($this->_params['host'])) { + throw new Horde_Exception('Horde_Auth_Radius:: requires a RADIUS host to connect to.'); + } + + /* A RADIUS secret string is required. */ + if (empty($this->_params['secret'])) { + throw new Horde_Exception('Horde_Auth_Radius:: requires a RADIUS secret string.'); + } + + /* A RADIUS authentication method is required. */ + if (empty($this->_params['method'])) { + throw new Horde_Exception('Horde_Auth_Radius:: requires a RADIUS authentication method.'); + } + + /* RADIUS NAS Identifier. */ + if (empty($this->_params['nas'])) { + $this->_params['nas'] = isset($_SERVER['HTTP_HOST']) + ? $_SERVER['HTTP_HOST'] + : 'localhost'; + } + + /* Suffix to add to unqualified user names. */ + if (empty($this->_params['suffix'])) { + $this->_params['suffix'] = ''; + } + + /* The RADIUS port to use. */ + if (empty($this->_params['port'])) { + $this->_params['port'] = 0; + } + + /* Maximum number of retries. */ + if (empty($this->_params['retries'])) { + $this->_params['retries'] = 3; + } + + /* RADIUS timeout. */ + if (empty($this->_params['timeout'])) { + $this->_params['timeout'] = 3; + } + } + + /** + * Find out if a set of login credentials are valid. + * + * @param string $username The userId to check. + * @param array $credentials An array of login credentials. + * For radius, this must contain a password + * entry. + * + * @throws Horde_Exception + */ + protected function _authenticate($username, $credentials) + { + /* Password is required. */ + if (!isset($credentials['password'])) { + throw new Horde_Exception(_("Password required for RADIUS authentication.")); + } + + $res = radius_auth_open(); + radius_add_server($res, $this->_params['host'], $this->_params['port'], $this->_params['secret'], $this->_params['timeout'], $this->_params['retries']); + radius_create_request($res, RADIUS_ACCESS_REQUEST); + radius_put_attr($res, RADIUS_NAS_IDENTIFIER, $this->_params['nas']); + radius_put_attr($res, RADIUS_NAS_PORT_TYPE, RADIUS_VIRTUAL); + radius_put_attr($res, RADIUS_SERVICE_TYPE, RADIUS_FRAMED); + radius_put_attr($res, RADIUS_FRAMED_PROTOCOL, RADIUS_PPP); + radius_put_attr($res, RADIUS_CALLING_STATION_ID, isset($_SERVER['REMOTE_HOST']) ? $_SERVER['REMOTE_HOST'] : '127.0.0.1'); + + /* Insert username/password into request. */ + radius_put_attr($res, RADIUS_USER_NAME, $username); + radius_put_attr($res, RADIUS_USER_PASSWORD, $credentials['password']); + + /* Send request. */ + $success = radius_send_request($res); + + switch ($success) { + case RADIUS_ACCESS_ACCEPT: + break; + + case RADIUS_ACCESS_REJECT: + throw new Horde_Exception(_("Authentication rejected by RADIUS server.")); + + default: + throw new Horde_Exception(radius_strerror($res)); + } + } + +} diff --git a/framework/Auth/lib/Horde/Auth/Shibboleth.php b/framework/Auth/lib/Horde/Auth/Shibboleth.php new file mode 100644 index 000000000..810147e5a --- /dev/null +++ b/framework/Auth/lib/Horde/Auth/Shibboleth.php @@ -0,0 +1,96 @@ + + * 'username_header' - (string) Name of the header holding the username of the + * logged in user. + * + * + * Optional Parameters: + *
+ * 'password_header' - (string) Name of the header holding the password of the
+ *                     logged in user.
+ * 'password_holder' - (string) Where the hordeauth password is stored.
+ * 'password_preference' - (string) Name of the Horde preference holding the
+ *                         password of the logged in user.
+ * 
+ * + * Copyright 9Star Research, Inc. 2006 http://www.protectnetwork.org/ + * + * See the enclosed file COPYING for license information (LGPL). If you did + * not receive this file, see http://opensource.org/licenses/lgpl-2.1.php + * + * @author Cassio Nishiguchi + * @package Horde_Auth + */ +class Horde_Auth_Shibboleth extends Horde_Auth_Driver +{ + /** + * An array of capabilities, so that the driver can report which + * operations it supports and which it doesn't. + * + * @var array + */ + protected $_capabilities = array( + 'transparent' => true + ); + + /** + * Constructor. + * + * @param array $params A hash containing parameters. + */ + public function __construct($params = array()) + { + Horde::assertDriverConfig($params, 'auth', array('username_header'), 'Authentication Shib'); + + $params = array_merge(array( + 'password_header' => '', + 'password_holder' => '', + 'password_preference' => '' + ), $params); + + parent::__construct($params); + } + + /** + * Automatic authentication: Check if the username is set in the + * configured header. + * + * @return boolean Whether or not the client is allowed. + * @throws Horde_Exception + */ + protected function _transparent() + { + if (empty($_SERVER[$this->_params['username_header']])) { + throw new Horde_Exception(_("Shibboleth authentication not available.")); + } + + $username = $_SERVER[$this->_params['username_header']]; + + // Remove scope from username, if present. + $pos = strrpos($username, '@'); + if ($pos !== false) { + $username = substr($username, 0, $pos); + } + + if (!Horde_Auth::setAuth($username, array('transparent' => 1))) { + return false; + } + + // Set password for hordeauth login. + if ($this->_params['password_holder'] == 'header') { + Horde_Auth::setCredential('password', $_SERVER[$this->_params['password_header']]); + } elseif ($this->_params['password_holder'] == 'preferences') { + Horde_Auth::setCredential('password', $GLOBALS['prefs']->getValue($this->_params['password_preference'])); + } + + return true; + } + +} diff --git a/framework/Auth/lib/Horde/Auth/Signup.php b/framework/Auth/lib/Horde/Auth/Signup.php new file mode 100644 index 000000000..2abdf6202 --- /dev/null +++ b/framework/Auth/lib/Horde/Auth/Signup.php @@ -0,0 +1,305 @@ + + * @package Horde_Auth + */ +class Horde_Auth_Signup +{ + /** + * Attempts to return a concrete Auth_Signup instance based on $driver. + * + * @param string $driver The type of the concrete Auth_Signup subclass + * to return. The class name is based on the + * storage driver ($driver). The code is + * dynamically included. + * @param array $params A hash containing any additional configuration or + * connection parameters a subclass might need. + * + * @return Auth_Signup The newly created concrete Auth_Signup instance, + * or false on an error. + */ + static public function factory($driver = null, $params = null) + { + if ($driver === null) { + if (!empty($GLOBALS['conf']['signup']['driver'])) { + $driver = $GLOBALS['conf']['signup']['driver']; + } else { + $driver = 'datatree'; + } + } else { + $driver = basename($driver); + } + + if ($params === null) { + $params = Horde::getDriverConfig('signup', $driver); + } + + $class = 'Auth_Signup_' . $driver; + if (!class_exists($class)) { + include dirname(__FILE__) . '/Signup/' . $driver . '.php'; + } + if (class_exists($class)) { + return new $class($params); + } else { + return PEAR::raiseError(_("You must configure a backend to use Signups.")); + } + } + + /** + * Adds a new user to the system and handles any extra fields that may have + * been compiled, relying on the hooks.php file. + * + * @params mixed $info Reference to array of parameteres to be passed + * to hook + * + * @return mixed PEAR_Error if any errors, otherwise true. + */ + public function addSignup(&$info) + { + global $auth, $conf; + + // Perform any preprocessing if requested. + if ($conf['signup']['preprocess']) { + $info = Horde::callHook('_horde_hook_signup_preprocess', array($info)); + if (is_a($info, 'PEAR_Error')) { + return $info; + } + } + + // Check to see if the username already exists. + if ($auth->exists($info['user_name']) || + $this->exists($info['user_name'])) { + return PEAR::raiseError(sprintf(_("Username \"%s\" already exists."), $info['user_name'])); + } + + // Attempt to add the user to the system. + $result = $auth->addUser($info['user_name'], array('password' => $info['password'])); + if (is_a($result, 'PEAR_Error')) { + return $result; + } + + $result = true; + // Attempt to add/update any extra data handed in. + if (!empty($info['extra'])) { + $result = false; + $result = Horde::callHook('_horde_hook_signup_addextra', + array($info['user_name'], $info['extra'])); + if (is_a($result, 'PEAR_Error')) { + Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_EMERG); + return $result; + } + } + + return $result; + } + + /** + * Queues the user's submitted registration info for later admin approval. + * + * @params mixed $info Reference to array of parameteres to be passed + * to hook + * + * @return mixed PEAR_Error if any errors, otherwise true. + * + * @throws Horde_Mime_Exception + */ + public function queueSignup(&$info) + { + global $auth, $conf; + + // Perform any preprocessing if requested. + if ($conf['signup']['preprocess']) { + $info = Horde::callHook('_horde_hook_signup_preprocess', + array($info)); + if (is_a($info, 'PEAR_Error')) { + return $info; + } + } + + // Check to see if the username already exists. + if ($auth->exists($info['user_name']) || + $this->exists($info['user_name'])) { + return PEAR::raiseError(sprintf(_("Username \"%s\" already exists."), $info['user_name'])); + } + + // If it's a unique username, go ahead and queue the request. + $signup = $this->newSignup($info['user_name']); + if (!empty($info['extra'])) { + $signup->data = array_merge($info['extra'], + array('password' => $info['password'], + 'dateReceived' => time())); + } else { + $signup->data = array('password' => $info['password'], + 'dateReceived' => time()); + } + + $result = $this->_queueSignup($signup); + if (is_a($result, 'PEAR_Error')) { + return $result; + } + + if ($conf['signup']['queue']) { + $result = Horde::callHook('_horde_hook_signup_queued', + array($info['user_name'], $info)); + } + + if (!empty($conf['signup']['email'])) { + $link = Horde_Util::addParameter(Horde::url($GLOBALS['registry']->get('webroot', 'horde') . '/admin/signup_confirm.php', true, -1), + array('u' => $signup->name, + 'h' => hash_hmac('sha1', $signup->name, $conf['secret_key'])), + null, false); + $message = sprintf(_("A new account for the user \"%s\" has been requested through the signup form."), $signup->name) + . "\n\n" + . _("Approve the account:") + . "\n" . Horde_Util::addParameter($link, 'a', 'approve') . "\n" + . _("Deny the account:") + . "\n" . Horde_Util::addParameter($link, 'a', 'deny'); + $mail = new Horde_Mime_Mail( + sprintf(_("Account signup request for \"%s\""), $signup->name), + $message, + $conf['signup']['email'], + $conf['signup']['email'], + NLS::getCharset()); + $mail->send($conf['mailer']['type'], $conf['mailer']['params']); + } + } + + /** + * Queues the user's submitted registration info for later admin approval. + * + * @params mixed $info Reference to array of parameteres to be passed + * to hook + * + * @return mixed PEAR_Error if any errors, otherwise true. + */ + protected function &_queueSignup(&$info) + { + return PEAR::raiseError('Not implemented'); + } + + /** + * Get a user's queued signup information. + * + * @param string $username The username to retrieve the queued info for. + * + * @return object The bject for the requested signup. + */ + public function getQueuedSignup($username) + { + return PEAR::raiseError('Not implemented'); + } + + /** + * Get the queued information for all pending signups. + * + * @return array An array of objects, one for each signup in the queue. + */ + public function getQueuedSignups() + { + return PEAR::raiseError('Not implemented'); + } + + /** + * Remove a queued signup. + * + * @param string $username The user to remove from the signup queue. + */ + public function removeQueuedSignup($username) + { + return PEAR::raiseError('Not implemented'); + } + + /** + * Return a new signup object. + * + * @param string $name The signups's name. + * + * @return object A new signup object. + */ + public function newSignup($name) + { + return PEAR::raiseError('Not implemented'); + } + +} + +/** + * Horde Signup Form. + * + * Copyright 2003-2009 The Horde Project (http://www.horde.org/) + * + * See the enclosed file COPYING for license information (LGPL). If you did + * not receive this file, see http://opensource.org/licenses/lgpl-2.1.php + * + * @author Marko Djukic + * @package Horde_Auth + */ +class HordeSignupForm extends Horde_Form { + + var $_useFormToken = true; + + function HordeSignupForm(&$vars) + { + global $registry; + + parent::Horde_Form($vars, sprintf(_("%s Sign Up"), $registry->get('name'))); + + $this->setButtons(_("Sign up"), true); + + $this->addHidden('', 'url', 'text', false); + + /* Use hooks get any extra fields required in signing up. */ + $extra = Horde::callHook('_horde_hook_signup_getextra'); + if (!is_a($extra, 'PEAR_Error') && !empty($extra)) { + if (!isset($extra['user_name'])) { + $this->addVariable(_("Choose a username"), 'user_name', 'text', true); + } + if (!isset($extra['password'])) { + $this->addVariable(_("Choose a password"), 'password', 'passwordconfirm', true, false, _("type the password twice to confirm")); + } + foreach ($extra as $field_name => $field) { + $readonly = isset($field['readonly']) ? $field['readonly'] : null; + $desc = isset($field['desc']) ? $field['desc'] : null; + $required = isset($field['required']) ? $field['required'] : false; + $field_params = isset($field['params']) ? $field['params'] : array(); + + $this->addVariable($field['label'], 'extra[' . $field_name . ']', + $field['type'], $required, $readonly, + $desc, $field_params); + } + } else { + $this->addVariable(_("Choose a username"), 'user_name', 'text', true); + $this->addVariable(_("Choose a password"), 'password', 'passwordconfirm', true, false, _("type the password twice to confirm")); + } + } + + /** + * Fetch the field values of the submitted form. + * + * @param Variables $vars A Variables instance, optional since Horde 3.2. + * @param array $info Array to be filled with the submitted field + * values. + */ + function getInfo($vars, &$info) + { + parent::getInfo($vars, $info); + + if (!isset($info['user_name']) && isset($info['extra']['user_name'])) { + $info['user_name'] = $info['extra']['user_name']; + } + + if (!isset($info['password']) && isset($info['extra']['password'])) { + $info['password'] = $info['extra']['password']; + } + } + +} diff --git a/framework/Auth/lib/Horde/Auth/Signup/Datatree.php b/framework/Auth/lib/Horde/Auth/Signup/Datatree.php new file mode 100644 index 000000000..a3e19f412 --- /dev/null +++ b/framework/Auth/lib/Horde/Auth/Signup/Datatree.php @@ -0,0 +1,141 @@ + + * @package Horde_Auth + */ +class Horde_Auth_Signup_Datatree extends Horde_Auth_Signup { + + /** + * Pointer to a DataTree instance to manage/store signups + * + * @var DataTree + */ + var $_datatree; + + public function _-construct() + { + global $conf; + + if (empty($conf['datatree']['driver'])) { + Horde::fatal(_("You must configure a DataTree backend to use Signups."), __FILE__, __LINE__); + } + $driver = $conf['datatree']['driver']; + $this->_datatree = DataTree::singleton($driver, + array_merge(Horde::getDriverConfig('datatree', $driver), + array('group' => 'horde.signup'))); + } + + /** + * Stores the signup data in the backend. + * + * @params DataTreeObject_Signup $signup Signup data. + */ + protected function _queueSignup($signup) + { + return $this->_datatree->add($signup); + } + + /** + * Get a user's queued signup information. + * + * @param string $username The username to retrieve the queued info for. + * + * @return DataTreeObject_Signup The DataTreeObject for the requested + * signup. + */ + public function getQueuedSignup($username) + { + return $this->_datatree->getObject($username, 'DataTreeObject_Signup'); + } + + /** + * Get the queued information for all pending signups. + * + * @return array An array of DataTreeObject_Signup objects, one for + * each signup in the queue. + */ + public function getQueuedSignups() + { + $signups = array(); + foreach ($this->_datatree->get(DATATREE_FORMAT_FLAT, DATATREE_ROOT, true) as $username) { + if ($username != DATATREE_ROOT) { + $signups[] = $this->_datatree->getObject($username); + } + } + return $signups; + } + + /** + * Remove a queued signup. + * + * @param string $username The user to remove from the signup queue. + */ + public function removeQueuedSignup($username) + { + $this->_datatree->remove($username); + } + + /** + * Return a new signup object. + * + * @param string $name The signups's name. + * + * @return DataTreeObject_Signup A new signup object. + */ + public function newSignup($name) + { + if (empty($name)) { + return PEAR::raiseError('Signup names must be non-empty'); + } + return new DataTreeObject_Signup($name); + } + +} + +/** + * Extension of the DataTreeObject class for storing Signup + * information in the DataTree driver. If you want to store + * specialized Signup information, you should extend this class + * instead of extending DataTreeObject directly. + * + * @author Marko Djukic + * @package Horde_Auth + */ +class DataTreeObject_Signup extends DataTreeObject { + + /** + * We want to see queued signups in descending order of receipt. + * Insert new signups at position 0 and push the rest down. + * + * @var integer + */ + var $order = 0; + + /** + * The DataTreeObject_Signup constructor. Just makes sure to call + * the parent constructor so that the signup's is is set + * properly. + * + * @param string $id The id of the signup. + */ + function DataTreeObject_Signup($id) + { + parent::DataTreeObject($id); + if (is_null($this->data)) { + $this->data = array(); + } + } + +} diff --git a/framework/Auth/lib/Horde/Auth/Signup/Sql.php b/framework/Auth/lib/Horde/Auth/Signup/Sql.php new file mode 100644 index 000000000..fc745de96 --- /dev/null +++ b/framework/Auth/lib/Horde/Auth/Signup/Sql.php @@ -0,0 +1,358 @@ + + * @package Horde_Auth + */ +class Horde_Auth_Signup_Sql extends Horde_Auth_Signup +{ + /** + * Handle for the current database connection. + * + * @var DB + */ + protected $_db; + + /** + * Handle for the current database connection, used for writing. Defaults + * to the same handle as $db if a separate write database is not required. + * + * @var DB + */ + protected $_write_db; + + /** + * SQL connection parameters + */ + protected $_params = array(); + + /** + * Connect to DB. + */ + public function __construct($params) + { + $this->_params = $params; + $this->_connect(); + } + + /** + * Stores the signup data in the backend. + * + * @params SQLObject_Signup $signup Signup data. + */ + protected function _queueSignup($signup) + { + $query = 'INSERT INTO ' . $this->_params['table'] + . ' (user_name, signup_date, signup_host, signup_data) VALUES (?, ?, ?, ?) '; + $values = array($signup->name, + time(), + $_SERVER['REMOTE_ADDR'], + serialize($signup->data)); + Horde::logMessage('SQL query by Auth_Signup_sql::_queueSignup(): ' . $query, + __FILE__, __LINE__, PEAR_LOG_DEBUG); + $stmt = $this->_write_db->prepare($query, null, MDB2_PREPARE_MANIP); + if (is_a($stmt, 'PEAR_Error')) { + Horde::logMessage($stmt, __FILE__, __LINE__, PEAR_LOG_ERR); + return $stmt; + } + $result = $stmt->execute($values); + if (is_a($result, 'PEAR_Error')) { + Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR); + return $result; + } + $stmt->free(); + } + + /** + * Checks if a user exists in the system. + * + * @param string $user The user to check. + * + * @return boolean True if the user exists. + */ + public function exists($user) + { + $stmt = $this->_db->prepare('SELECT 1 FROM ' . $this->_params['table'] + . ' WHERE user_name = ?'); + + if (is_a($stmt, 'PEAR_Error')) { + Horde::logMessage($stmt, __FILE__, __LINE__, PEAR_LOG_ERR); + return $stmt; + } + $result = $stmt->execute(array($user)); + if (is_a($result, 'PEAR_Error')) { + Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR); + return $result; + } + + $exists = (bool)$result->fetchOne(); + $stmt->free(); + $result->free(); + + return $exists; + } + + /** + * Get a user's queued signup information. + * + * @param string $username The username to retrieve the queued info for. + * + * @return SQLObject_Signup The SQLObject for the requested + * signup. + */ + public function getQueuedSignup($username) + { + $stmt = $this->_db->prepare('SELECT * FROM ' . $this->_params['table'] . ' WHERE user_name = ?'); + if (is_a($stmt, 'PEAR_Error')) { + Horde::logMessage($stmt, __FILE__, __LINE__, PEAR_LOG_ERR); + return $stmt; + } + $results = $stmt->execute(array($username)); + if (is_a($results, 'PEAR_Error')) { + Horde::logMessage($results, __FILE__, __LINE__, PEAR_LOG_ERR); + return $results; + } + $data = $results->fetchRow(MDB2_FETCHMODE_ASSOC); + if (is_a($data, 'PEAR_Error')) { + Horde::logMessage($data, __FILE__, __LINE__, PEAR_LOG_ERR); + return $data; + } elseif (empty($data)) { + return PEAR::RaiseError(sprintf(_("User \"%s\" does not exist."), $name)); + } + $stmt->free(); + $results->free(); + + $object = new SQLObject_Signup($data['user_name']); + $object->setData($data); + + return $object; + } + + /** + * Get the queued information for all pending signups. + * + * @return array An array of SQLObject_Signup objects, one for + * each signup in the queue. + */ + public function getQueuedSignups() + { + $query = 'SELECT * FROM ' . $this->_params['table'] . ' ORDER BY signup_date'; + $result = $this->_db->query($query); + if (is_a($result, 'PEAR_Error')) { + Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR); + return $result; + } elseif (empty($result)) { + return array(); + } + + $signups = array(); + while ($signup = $result->fetchRow(MDB2_FETCHMODE_ASSOC)) { + $object = new SQLObject_Signup($signup['user_name']); + $object->setData($signup); + $signups[] = $object; + } + + $result->free(); + + return $signups; + } + + /** + * Remove a queued signup. + * + * @param string $username The user to remove from the signup queue. + */ + public function removeQueuedSignup($username) + { + $stmt = $this->_write_db->prepare('DELETE FROM ' . $this->_params['table'] . ' WHERE user_name = ?', null, MDB2_PREPARE_MANIP); + if (is_a($stmt, 'PEAR_Error')) { + Horde::logMessage($stmt, __FILE__, __LINE__, PEAR_LOG_ERR); + return $stmt; + } + $result = $stmt->execute(array($username)); + if (is_a($result, 'PEAR_Error')) { + Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR); + return $result; + } + $stmt->free(); + + return true; + } + + /** + * Return a new signup object. + * + * @param string $name The signups's name. + * + * @return SQLObject_Signup A new signup object. + */ + public function newSignup($name) + { + if (empty($name)) { + return PEAR::raiseError('Signup names must be non-empty'); + } + return new SQLObject_Signup($name); + } + + /** + * Attempts to open a connection to the sql server. + * + * @return boolean True on success; exits (Horde::fatal()) on error. + */ + protected function _connect() + { + if (!isset($this->_params['database'])) { + $this->_params['database'] = ''; + } + if (!isset($this->_params['username'])) { + $this->_params['username'] = ''; + } + if (!isset($this->_params['hostspec'])) { + $this->_params['hostspec'] = ''; + } + if (!isset($this->_params['table'])) { + $this->_params['table'] = 'horde_signups'; + } + + /* Connect to the sql server using the supplied parameters. */ + $params = $this->_params; + unset($params['charset']); + $this->_write_db = MDB2::factory($params); + if (is_a($this->_write_db, 'PEAR_Error')) { + Horde::fatal($this->_write_db, __FILE__, __LINE__); + } + + /* Set DB portability options. */ + switch ($this->_write_db->phptype) { + case 'mssql': + $this->_write_db->setOption('field_case', CASE_LOWER); + $this->_write_db->setOption('portability', MDB2_PORTABILITY_FIX_CASE | MDB2_PORTABILITY_ERRORS | MDB2_PORTABILITY_RTRIM | MDB2_PORTABILITY_FIX_ASSOC_FIELD_NAMES); + break; + default: + $this->_write_db->setOption('field_case', CASE_LOWER); + $this->_write_db->setOption('portability', MDB2_PORTABILITY_FIX_CASE | MDB2_PORTABILITY_ERRORS | MDB2_PORTABILITY_FIX_ASSOC_FIELD_NAMES); + } + + /* Check if we need to set up the read DB connection seperately. */ + if (!empty($this->_params['splitread'])) { + $params = array_merge($params, $this->_params['read']); + $this->_db = MDB2::factory($params); + if (is_a($this->_db, 'PEAR_Error')) { + Horde::fatal($this->_db, __FILE__, __LINE__); + } + + /* Set DB portability options. */ + switch ($this->_db->phptype) { + case 'mssql': + $this->_db->setOption('field_case', CASE_LOWER); + $this->_db->setOption('portability', MDB2_PORTABILITY_FIX_CASE | MDB2_PORTABILITY_ERRORS | MDB2_PORTABILITY_RTRIM | MDB2_PORTABILITY_FIX_ASSOC_FIELD_NAMES); + break; + default: + $this->_db->setOption('portability', MDB2_PORTABILITY_FIX_CASE | MDB2_PORTABILITY_ERRORS | MDB2_PORTABILITY_FIX_ASSOC_FIELD_NAMES); + } + } else { + /* Default to the same DB handle as the writer for reading too */ + $this->_db = $this->_write_db; + } + + return true; + } + +} + +/** + * Extension of the SQLObject class for storing Signup + * information in the SQL driver. If you want to store + * specialized Signup information, you should extend this class + * instead of extending SQLObject directly. + * + * @author Duck + * @package Horde_Auth + */ +class SQLObject_Signup { + + /** + * Key-value hash that will be serialized. + * + * @see getData() + * @var array + */ + var $data = array(); + + /** + * The unique name of this object. + * These names have the same requirements as other object names - they must + * be unique, etc. + * + * @var string + */ + var $name; + + /** + * The SQLObject_Signup constructor. Just makes sure to call + * the parent constructor so that the signup's is is set + * properly. + * + * @param string $id The id of the signup. + */ + function SQLObject_Signup($id) + { + if (is_null($this->data)) { + $this->data = array(); + } + + $this->name = $id; + } + + /** + * Gets the data array. + * + * @return array The internal data array. + */ + function getData() + { + return $this->data; + } + + /** + * Sets the data array. + * + * @param array The data array to store internally. + */ + function setData($data) + { + $part = unserialize($data['signup_data']); + if (!empty($part) && is_array($part)) { + if (!empty($part['extra'])) { + $extra = $part['extra']; + unset($part['extra']); + $part = array_merge($part, $extra); + } + $this->data = array_merge($data, $part); + } else { + $this->data = $data; + } + + unset($this->data['signup_data']); + $this->data['dateReceived'] = $data['signup_date']; + } + + /** + * Gets the name of this object. + * + * @return string The object name. + */ + function getName() + { + return $this->name; + } + +} diff --git a/framework/Auth/lib/Horde/Auth/Smb.php b/framework/Auth/lib/Horde/Auth/Smb.php new file mode 100644 index 000000000..0a02c9a05 --- /dev/null +++ b/framework/Auth/lib/Horde/Auth/Smb.php @@ -0,0 +1,87 @@ + + * 'hostspec' - IP, DNS Name, or NetBios Name of the SMB server to + * authenticate with. + * 'domain' - The domain name to authenticate with. + * + * + * Optional parameters: + *
+ * 'group' - Group name that the user must be a member of. Will be
+ *           ignored if the value passed is a zero length string.
+ * 
+ * + * Copyright 1999-2009 The Horde Project (http://www.horde.org/) + * + * See the enclosed file COPYING for license information (LGPL). If you did + * not receive this file, see http://opensource.org/licenses/lgpl-2.1.php + * + * @author Jon Parise + * @author Marcus I. Ryan + * @package Horde_Auth + */ +class Horde_Auth_Smb extends Horde_Auth_Driver +{ + /** + * Constructor. + * + * @param array $params A hash containing connection parameters. + * + * @throws Horde_Exception + */ + public function __construct($params = array()) + { + if (!Util::extensionExists('smbauth')) { + throw new Horde_Exception(_("Horde_Auth_Smb: Required smbauth extension not found.")); + } + + /* Ensure we've been provided with all of the necessary parameters. */ + Horde::assertDriverConfig($params, 'auth', + array('hostspec', 'domain'), + 'authentication Samba'); + + parent::__construct($params); + } + + /** + * Find out if the given set of login credentials are valid. + * + * @param string $userId The userId to check. + * @param array $credentials An array of login credentials. + * + * @throws Horde_Exception + */ + public function _authenticate($userId, $credentials) + { + if (empty($credentials['password'])) { + throw new Horde_Exception('', Horde_Auth::REASON_BADLOGIN); + } + + /* Authenticate. */ + $rval = validate($this->_params['hostspec'], + $this->_params['domain'], + empty($this->_params['group']) ? '' : $this->_params['group'], + $userId, + $credentials['password']); + + if ($rval === 1) { + throw new Horde_Exception(_("Failed to connect to SMB server.")); + } elseif ($rval !== 0) { + throw new Horde_Exception(err2str()); + } + } + +} diff --git a/framework/Auth/lib/Horde/Auth/Smbclient.php b/framework/Auth/lib/Horde/Auth/Smbclient.php new file mode 100644 index 000000000..121745e26 --- /dev/null +++ b/framework/Auth/lib/Horde/Auth/Smbclient.php @@ -0,0 +1,82 @@ + + * 'domain' The domain name to authenticate with. + * 'hostspec' IP, DNS Name, or NetBios Name of the SMB server to + * authenticate with. + * 'smbclient_path' The location of the smbclient(1) utility. + * + * + * Optional parameters: + *
+ * 'group' - Group name that the user must be a member of. Will be
+ *           ignored if the value passed is a zero length string.
+ * 
+ * + * Copyright 1999-2009 The Horde Project (http://www.horde.org/) + * + * See the enclosed file COPYING for license information (LGPL). If you did + * not receive this file, see http://opensource.org/licenses/lgpl-2.1.php + * + * @author Jon Parise + * @author Marcus I. Ryan + * @package Horde_Auth + */ +class Horde_Auth_Smbclient extends Horde_Auth_Driver +{ + /** + * Constructor. + * + * @param array $params A hash containing connection parameters. + */ + public function __construct($params = array()) + { + /* Ensure we've been provided with all of the necessary parameters. */ + Horde::assertDriverConfig($params, 'auth', + array('hostspec', 'domain', 'smbclient_path'), + 'authentication smbclient'); + + parent::__construct($params); + } + + /** + * Find out if the given set of login credentials are valid. + * + * @param string $userId The userId to check. + * @param array $credentials An array of login credentials. + * + * @throws Horde_Exception + */ + protected function _authenticate($userId, $credentials) + { + if (empty($credentials['password'])) { + throw new Horde_Exception('', Horde_Auth::REASON_BADLOGIN); + } + + /* Authenticate. */ + $cmdline = implode(' ', array($this->_params['smbclient_path'], + '-L', + $this->_params['hostspec'], + '-W', + $this->_params['domain'], + '-U', + $userId)); + + $sc = popen($cmdline, 'w'); + if ($sc === false) { + throw new Horde_Exception(_("Unable to execute smbclient.")); + } + + fwrite($sc, $credentials['password']); + $rc = pclose($sc); + + if ((int)($rc & 0xff) != 0) { + throw new Horde_Exception('', Horde_Auth::REASON_BADLOGIN); + } + } + +} diff --git a/framework/Auth/lib/Horde/Auth/Sql.php b/framework/Auth/lib/Horde/Auth/Sql.php new file mode 100644 index 000000000..8200d42d4 --- /dev/null +++ b/framework/Auth/lib/Horde/Auth/Sql.php @@ -0,0 +1,484 @@ + + * 'phptype' - (string) The database type (ie. 'pgsql', 'mysql', etc.). + * + * + * Optional parameters: + *
+ * 'encryption' - (string) The encryption to use to store the password in
+ *                the table (e.g. plain, crypt, md5-hex, md5-base64, smd5,
+ *                sha, ssha, aprmd5).
+ *                DEFAULT: 'md5-hex'
+ * 'hard_expiration_field' - (string) The name of the field containing a date
+ *                           after which the account is no longer valid and
+ *                           the user will not be able to log in at all.
+ *                           DEFAULT: none
+ * 'password_field' - (string) The name of the password field in the auth
+ *                    table.
+ *                    DEFAULT: 'user_pass'
+ * 'show_encryption' - (boolean) Whether or not to prepend the encryption in
+ *                     the password field.
+ *                     DEFAULT: false
+ * 'soft_expiration_field' - (string) The name of the field containing a date
+ *                           after which the system will request the user
+ *                           change his or her password.
+ *                           DEFAULT: none
+ * 'table' - (string) The name of the SQL table to use in 'database'.
+ *           DEFAULT: 'horde_users'
+ * 'username_field' - (string) The name of the username field in the auth
+ *                    table.
+ *                    DEFAULT: 'user_uid'
+ * 
+ * + * Required by some database implementations: + *
+ * 'hostspec'     The hostname of the database server.
+ * 'protocol'     The communication protocol ('tcp', 'unix', etc.).
+ * 'database'     The name of the database.
+ * 'username'     The username with which to connect to the database.
+ * 'password'     The password associated with 'username'.
+ * 'options'      Additional options to pass to the database.
+ * 'port'         The port on which to connect to the database.
+ * 'tty'          The TTY on which to connect to the database.
+ * 
+ * + * Optional values when using separate read and write servers, for example + * in replication settings: + *
+ * 'splitread'   Boolean, whether to implement the separation or not.
+ * 'read'        Array containing the parameters which are different for
+ *               the read database connection, currently supported
+ *               only 'hostspec' and 'port' parameters.
+ * 
+ * + * The table structure for the Auth system is in + * horde/scripts/sql/horde_users.sql. + * + * Copyright 1999-2009 The Horde Project (http://www.horde.org/) + * + * See the enclosed file COPYING for license information (LGPL). If you did + * not receive this file, see http://opensource.org/licenses/lgpl-2.1.php + * + * @author Chuck Hagenbuch + * @package Horde_Auth + */ +class Horde_Auth_Sql extends Horde_Auth_Driver +{ + /** + * An array of capabilities, so that the driver can report which + * operations it supports and which it doesn't. + * + * @var array + */ + protected $_capabilities = array( + 'add' => true, + 'list' => true, + 'remove' => true, + 'resetpassword' => true, + 'update' => true + ); + + /** + * Handle for the current database connection. + * + * @var DB + */ + protected $_db; + + /** + * Handle for the current database connection, used for writing. Defaults + * to the same handle as $_db if a separate write database is not required. + * + * @var DB + */ + protected $_write_db; + + /** + * Boolean indicating whether or not we're connected to the SQL server. + * + * @var boolean + */ + protected $_connected = false; + + /** + * Find out if a set of login credentials are valid. + * + * @param string $userId The userId to check. + * @param array $credentials The credentials to use. + * + * @throws Horde_Exception + */ + protected function _authenticate($userId, $credentials) + { + try { + $this->_connect(); + } catch (Horde_Exception $e) { + Horde::logMessage($e, __FILE__, __LINE__, PEAR_LOG_ERR); + throw new Horde_Exception('', Horde_Auth::REASON_FAILED); + } + + /* Build the SQL query. */ + $query = sprintf('SELECT * FROM %s WHERE %s = ?', + $this->_params['table'], + $this->_params['username_field']); + $values = array($userId); + + Horde::logMessage('SQL Query by Auth_sql::_authenticate(): ' . $query, __FILE__, __LINE__, PEAR_LOG_DEBUG); + + $result = $this->_db->query($query, $values); + if ($result instanceof PEAR_Error) { + Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR); + throw new Horde_Exception('', Horde_Auth::REASON_FAILED); + } + + $row = $result->fetchRow(DB_GETMODE_ASSOC); + if (is_array($row)) { + $result->free(); + } else { + throw new Horde_Exception('', Horde_Auth::REASON_BADLOGIN); + } + + if (!$this->_comparePasswords($row[$this->_params['password_field']], + $credentials['password'])) { + throw new Horde_Exception('', Horde_Auth::REASON_BADLOGIN); + } + + $now = time(); + if (!empty($this->_params['hard_expiration_field']) && + !empty($row[$this->_params['hard_expiration_field']]) && + ($now > $row[$this->_params['hard_expiration_field']])) { + throw new Horde_Exception('', Horde_Auth::REASON_EXPIRED); + } + + if (!empty($this->_params['soft_expiration_field']) && + !empty($row[$this->_params['soft_expiration_field']]) && + ($now > $row[$this->_params['soft_expiration_field']])) { + $this->_authCredentials['changeRequested'] = true; + } + } + + /** + * Add a set of authentication credentials. + * + * @param string $userId The userId to add. + * @param array $credentials The credentials to add. + * + * @throws Horde_Exception + */ + public function addUser($userId, $credentials) + { + $this->_connect(); + + /* Build the SQL query. */ + $query = sprintf('INSERT INTO %s (%s, %s) VALUES (?, ?)', + $this->_params['table'], + $this->_params['username_field'], + $this->_params['password_field']); + $values = array($userId, + Horde_Auth::getCryptedPassword($credentials['password'], + '', + $this->_params['encryption'], + $this->_params['show_encryption'])); + + Horde::logMessage('SQL Query by Auth_sql::addUser(): ' . $query, __FILE__, __LINE__, PEAR_LOG_DEBUG); + + $result = $this->_write_db->query($query, $values); + if ($result instanceof PEAR_Error) { + throw new Horde_Exception($result); + } + } + + /** + * Update a set of authentication credentials. + * + * @param string $oldID The old userId. + * @param string $newID The new userId. + * @param array $credentials The new credentials + * + * @throws Horde_Exception + */ + public function updateUser($oldID, $newID, $credentials) + { + $this->_connect(); + + /* Build the SQL query. */ + $tuple = array(); + $tuple[$this->_params['username_field']] = $newID; + $tuple[$this->_params['password_field']] = + Horde_Auth::getCryptedPassword($credentials['password'], + '', + $this->_params['encryption'], + $this->_params['show_encryption']); + + if (empty($this->_params['soft_expiration_window'])) { + if (!empty($this->_params['soft_expiration_field'])) { + $tuple[$this->_params['soft_expiration_field']] = null; + } + } else { + $date = time(); + $datea = localtime($date, true); + $date = mktime($datea['tm_hour'], $datea['tm_min'], + $datea['tm_sec'], $datea['tm_mon'] + 1, + $datea['tm_mday'] + $this->_params['soft_expiration_window'], + $datea['tm_year']); + + $tuple[$this->_params['soft_expiration_field']] = $date; + + global $notification; + if (!empty($notification)) { + $notification->push(strftime(_("New password will expire on %s."), $date), 'horde.message'); + } + + if (empty($this->_params['hard_expiration_window'])) { + $tuple[$this->_params['hard_expiration_field']] = null; + } else { + $datea = localtime($date, true); + $date = mktime($datea['tm_hour'], $datea['tm_min'], + $datea['tm_sec'], $datea['tm_mon'] + 1, + $datea['tm_mday'] + $this->_params['soft_expiration_window'], + $datea['tm_year']); + + $tuple[$this->_params['hard_expiration_field']] = $date; + } + } + + $query = sprintf('UPDATE %s SET %s WHERE %s = ?', + $this->_params['table'], + Horde_SQL::updateValues($this->_write_db, $tuple), + $this->_params['username_field']); + $values = array($oldID); + + Horde::logMessage('SQL Query by Auth_sql:updateUser(): ' . $query, __FILE__, __LINE__, PEAR_LOG_DEBUG); + + $result = $this->_write_db->query($query, $values); + if ($result instanceof PEAR_Error) { + Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR); + throw new Horde_Exception($result); + } + } + + /** + * Reset a user's password. Used for example when the user does not + * remember the existing password. + * + * @param string $userId The user id for which to reset the password. + * + * @return string The new password on success. + * @throws Horde_Exception + */ + public function resetPassword($userId) + { + $this->_connect(); + + /* Get a new random password. */ + $password = Horde_Auth::genRandomPassword(); + + /* Build the SQL query. */ + $query = sprintf('UPDATE %s SET %s = ? WHERE %s = ?', + $this->_params['table'], + $this->_params['password_field'], + $this->_params['username_field']); + $values = array(Horde_Auth::getCryptedPassword($password, + '', + $this->_params['encryption'], + $this->_params['show_encryption']), + $userId); + + Horde::logMessage('SQL Query by Auth_sql::resetPassword(): ' . $query, __FILE__, __LINE__, PEAR_LOG_DEBUG); + + $result = $this->_write_db->query($query, $values); + if ($result instanceof PEAR_Error) { + throw new Horde_Exception($result); + } + + return $password; + } + + /** + * Delete a set of authentication credentials. + * + * @param string $userId The userId to delete. + * + * @throws Horde_Exception + */ + public function removeUser($userId) + { + $this->_connect(); + + /* Build the SQL query. */ + $query = sprintf('DELETE FROM %s WHERE %s = ?', + $this->_params['table'], + $this->_params['username_field']); + $values = array($userId); + + Horde::logMessage('SQL Query by Auth_sql::removeUser(): ' . $query, __FILE__, __LINE__, PEAR_LOG_DEBUG); + + $result = $this->_write_db->query($query, $values); + if ($result instanceof PEAR_Error) { + throw new Horde_Exception($result); + } + + Horde_Auth::removeUserData($userId); + } + + /** + * List all users in the system. + * + * @return array The array of userIds. + * @throws Horde_Exception + */ + public function listUsers() + { + $this->_connect(); + + /* Build the SQL query. */ + $query = sprintf('SELECT %s FROM %s', + $this->_params['username_field'], + $this->_params['table']); + + Horde::logMessage('SQL Query by Auth_sql::listUsers(): ' . $query, __FILE__, __LINE__, PEAR_LOG_DEBUG); + + $res = $this->_db->getCol($query); + if ($res instanceof PEAR_Error) { + throw new Horde_Exception($res); + } + + return $res; + } + + /** + * Checks if a userId exists in the system. + * + * @return boolean Whether or not the userId already exists. + */ + public function exists($userId) + { + $this->_connect(); + + /* Build the SQL query. */ + $query = sprintf('SELECT 1 FROM %s WHERE %s = ?', + $this->_params['table'], + $this->_params['username_field']); + $values = array($userId); + + Horde::logMessage('SQL Query by Auth_sql::exists(): ' . $query, __FILE__, __LINE__, PEAR_LOG_DEBUG); + + $res = $this->_db->getOne($query, $values); + return ($res instanceof PEAR_Error) + ? false + : $res; + } + + /** + * Compare an encrypted password to a plaintext string to see if + * they match. + * + * @param string $encrypted The crypted password to compare against. + * @param string $plaintext The plaintext password to verify. + * + * @return boolean True if matched, false otherwise. + */ + protected function _comparePasswords($encrypted, $plaintext) + { + return $encrypted == Horde_Auth::getCryptedPassword($plaintext, + $encrypted, + $this->_params['encryption'], + $this->_params['show_encryption']); + } + + /** + * Attempts to open a connection to the SQL server. + * + * @throws Horde_Exception + */ + function _connect() + { + if ($this->_connected) { + return; + } + + Horde::assertDriverConfig($this->_params, 'auth', array('phptype'), + 'authentication SQL'); + + if (!isset($this->_params['database'])) { + $this->_params['database'] = ''; + } + if (!isset($this->_params['username'])) { + $this->_params['username'] = ''; + } + if (!isset($this->_params['password'])) { + $this->_params['password'] = ''; + } + if (!isset($this->_params['hostspec'])) { + $this->_params['hostspec'] = ''; + } + if (empty($this->_params['encryption'])) { + $this->_params['encryption'] = 'md5-hex'; + } + if (!isset($this->_params['show_encryption'])) { + $this->_params['show_encryption'] = false; + } + if (empty($this->_params['table'])) { + $this->_params['table'] = 'horde_users'; + } + if (empty($this->_params['username_field'])) { + $this->_params['username_field'] = 'user_uid'; + } else { + $this->_params['username_field'] = Horde_String::lower($this->_params['username_field']); + } + if (empty($this->_params['password_field'])) { + $this->_params['password_field'] = 'user_pass'; + } else { + $this->_params['password_field'] = Horde_String::lower($this->_params['password_field']); + } + + /* Connect to the SQL server using the supplied parameters. */ + $this->_write_db = &DB::connect($this->_params, + array('persistent' => !empty($this->_params['persistent']), + 'ssl' => !empty($this->_params['ssl']))); + if ($this->_write_db instanceof PEAR_Error) { + throw new Horde_Exception($this->_write_db); + } + + // Set DB portability options. + switch ($this->_write_db->phptype) { + case 'mssql': + $this->_write_db->setOption('portability', DB_PORTABILITY_LOWERCASE | DB_PORTABILITY_ERRORS | DB_PORTABILITY_RTRIM); + break; + default: + $this->_write_db->setOption('portability', DB_PORTABILITY_LOWERCASE | DB_PORTABILITY_ERRORS); + } + + /* Check if we need to set up the read DB connection + * seperately. */ + if (!empty($this->_params['splitread'])) { + $params = array_merge($this->_params, $this->_params['read']); + $this->_db = &DB::connect($params, + array('persistent' => !empty($params['persistent']), + 'ssl' => !empty($params['ssl']))); + if ($this->_db instanceof PEAR_Error) { + throw new Horde_Exception($this->_db); + } + + switch ($this->_db->phptype) { + case 'mssql': + $this->_db->setOption('portability', DB_PORTABILITY_LOWERCASE | DB_PORTABILITY_ERRORS | DB_PORTABILITY_RTRIM); + break; + default: + $this->_db->setOption('portability', DB_PORTABILITY_LOWERCASE | DB_PORTABILITY_ERRORS); + } + + } else { + /* Default to the same DB handle for reads. */ + $this->_db =& $this->_write_db; + } + + $this->_connected = true; + } + +} diff --git a/framework/Auth/package.xml b/framework/Auth/package.xml new file mode 100644 index 000000000..71c1255f6 --- /dev/null +++ b/framework/Auth/package.xml @@ -0,0 +1,238 @@ + + + Auth + pear.horde.org + Horde Authentication API + The Horde_Auth:: package provides a common abstracted interface into the various backends for the Horde authentication system. + + + Chuck Hagenbuch + chuck + chuck@horde.org + yes + + + Jan Schneider + jan + jan@horde.org + yes + + 2009-07-06 + + 0.2.0 + 0.2.0 + + + beta + beta + + LGPL + * Split Horde_Auth:: into Horde_Auth:: and Horde_Auth_Driver:: components. + * Initial Horde 4 package. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 5.2.0 + + + 1.5.4 + + + Horde_Framework + pear.horde.org + + + Secret + pear.horde.org + + + Util + pear.horde.org + + + + + DataTree + pear.horde.org + + + Form + pear.horde.org + + + History + pear.horde.org + + + Imap_Client + pear.horde.org + + + Kolab_Server + pear.horde.org + 0.2.0 + + + gettext + + + pam_auth + + + sasl + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2008-10-29 + + 0.1.1 + 0.1.0 + + + beta + beta + + LGPL + * Imap driver now uses Horde_Imap_Client library. + * Add signup drivers to package.xml (Bug #7345). + * Fix the "overwriting realm info from application auth drivers" (Bug #6749) + * Switched Kolab auth handling from IMAP to LDAP by using Kolab_Server. + * Added "add user" capability to the Kolab driver. + * Adapted the Kolab auth driver to the changes in Kolab_Server. + + + + + 0.1.0 + 0.1.0 + + + beta + beta + + LGPL + * Fixed error handling when removing user data. +* Retrieve password where necessary for salt calculations in the custom SQL driver (Bug #3739). +* Added a query for checking existance of a user to the custom SQL driver (Request #5151). +* Completed Cyrus virtual domain support in the cyrsql driver. +* Fixed handling of UID logins in the Kolab driver (Bugs #1317, #4662). +* Fixed case handling in the LDAP driver (Bug #2435). +* Rewrote PAM driver to use PAM extension from PECL. +* Fixed the PAM driver authentication so that it works with both pam and pam_auth extensions (Bug #6860). +* Ensured that the LDAP driver always uses the configured filter. +* Renamed Auth_sasl to Auth_peclsasl (Bug #4547). +* Allow signup hooks to override the user_name and password fields (Request #2904). +* Created an SQL driver for signups and allowing backends other than DataTree (Request #7161). +* Added smbclient driver as an alternative to the smb driver which requires a PHP extension. +* Fix handling of SSHA and SMD5 salts (ulrich-horde@topfen.net, Bug #2863). +* Added readSessionData() function to get Horde authentication data from session strings. +* Allow drivers to include their own isAdmin() implementations (Request #5521). +* Add Active Directory extension of LDAP driver (Request #3769). +* Hide the cyrus account in cyrsql driver (vilius@lnk.lt, Request #5626). +* Bring the passwd driver into compliance with the Auth API. +* Remove dependency on the mhash extension for some encryption types. +* Add authentication against a remote HTTP Authentication endpoint (duck@obala.net). +* CSRF token protections for logout links. +* Call the postauthenticate hook in Horde_Auth::setAuth() instead of authenticate(). +* Modified the Kolab driver to use the new Kolab_Server package. +* Improved handling of multiple IMAP server setups in the Kolab driver. + + + + diff --git a/framework/Auth/test/Horde/Auth/AllTests.php b/framework/Auth/test/Horde/Auth/AllTests.php new file mode 100644 index 000000000..6c5e09128 --- /dev/null +++ b/framework/Auth/test/Horde/Auth/AllTests.php @@ -0,0 +1,88 @@ + + * @license http://www.fsf.org/copyleft/lgpl.html LGPL + * @link http://pear.horde.org/index.php?package=Auth + */ + +/** + * Define the main method + */ +if (!defined('PHPUnit_MAIN_METHOD')) { + define('PHPUnit_MAIN_METHOD', 'Horde_Auth_AllTests::main'); +} + +require_once 'PHPUnit/Framework/TestSuite.php'; +require_once 'PHPUnit/TextUI/TestRunner.php'; + +/** + * Combine the tests for this package. + * + * $Horde: framework/Auth/tests/Horde/Auth/AllTests.php,v 1.2 2009/01/06 17:49:08 jan Exp $ + * + * Copyright 2007-2009 The Horde Project (http://www.horde.org/) + * + * See the enclosed file COPYING for license information (LGPL). If you + * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html. + * + * @category Horde + * @package Auth + * @subpackage UnitTests + * @author Gunnar Wrobel + * @license http://www.fsf.org/copyleft/lgpl.html LGPL + * @link http://pear.horde.org/index.php?package=Auth + */ +class Horde_Auth_AllTests +{ + + /** + * Main entry point for running the suite. + * + * @return NULL + */ + public static function main() + { + PHPUnit_TextUI_TestRunner::run(self::suite()); + } + + /** + * Collect the unit tests of this directory into a new suite. + * + * @return PHPUnit_Framework_TestSuite The test suite. + */ + public static function suite() + { + $suite = new PHPUnit_Framework_TestSuite('Horde Framework - Auth'); + + $basedir = dirname(__FILE__); + $baseregexp = preg_quote($basedir . DIRECTORY_SEPARATOR, '/'); + + foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($basedir)) as $file) { + if ($file->isFile() && preg_match('/Test.php$/', $file->getFilename())) { + $pathname = $file->getPathname(); + require $pathname; + + $class = str_replace(DIRECTORY_SEPARATOR, '_', + preg_replace("/^$baseregexp(.*)\.php/", + '\\1', $pathname)); + $suite->addTestSuite('Horde_Auth_' . $class); + } + } + + return $suite; + } + +} + +if (PHPUnit_MAIN_METHOD == 'Horde_Auth_AllTests::main') { + Horde_Auth_AllTests::main(); +} diff --git a/framework/Auth/test/Horde/Auth/KolabScenarioTest.php b/framework/Auth/test/Horde/Auth/KolabScenarioTest.php new file mode 100644 index 000000000..7edae2c62 --- /dev/null +++ b/framework/Auth/test/Horde/Auth/KolabScenarioTest.php @@ -0,0 +1,57 @@ + + * @license http://www.fsf.org/copyleft/lgpl.html LGPL + * @link http://pear.horde.org/index.php?package=Auth + */ + +/** + * We need the base class + */ +require_once 'Horde/Kolab/Test/Storage.php'; + +/** + * Kolab authentication scenarios. + * + * $Horde: framework/Auth/tests/Horde/Auth/KolabScenarioTest.php,v 1.3 2009/03/20 23:38:13 wrobel Exp $ + * + * Copyright 2008-2009 The Horde Project (http://www.horde.org/) + * + * See the enclosed file COPYING for license information (LGPL). If you + * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html. + * + * @category Kolab + * @package Kolab_Server + * @author Gunnar Wrobel + * @license http://www.fsf.org/copyleft/lgpl.html LGPL + * @link http://pear.horde.org/index.php?package=Kolab_Server + */ +class Horde_Auth_KolabScenarioTest extends Horde_Kolab_Test_Storage +{ + /** + * Test loggin in after a user has been added. + * + * @scenario + * + * @return NULL + */ + public function login() + { + $test_user = $this->provideBasicUserOne(); + + $this->given('a populated Kolab setup') + ->and('the Kolab auth driver has been selected') + ->when('logging in as a user with a password', $test_user['mail'], + $test_user['userPassword']) + ->then('the login was successful'); + } +} \ No newline at end of file diff --git a/framework/Auth/test/Horde/Auth/KolabTest.php b/framework/Auth/test/Horde/Auth/KolabTest.php new file mode 100644 index 000000000..596ec34ac --- /dev/null +++ b/framework/Auth/test/Horde/Auth/KolabTest.php @@ -0,0 +1,92 @@ + + * @license http://www.fsf.org/copyleft/lgpl.html LGPL + * @link http://pear.horde.org/index.php?package=Auth + */ + +/** + * We need the base class + */ +require_once 'Horde/Kolab/Test/Storage.php'; + +/** + * Kolab authentication tests. + * + * $Horde: framework/Auth/tests/Horde/Auth/KolabTest.php,v 1.4 2009/04/01 07:59:47 wrobel Exp $ + * + * Copyright 2008-2009 The Horde Project (http://www.horde.org/) + * + * See the enclosed file COPYING for license information (LGPL). If you + * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html. + * + * @category Kolab + * @package Kolab_Server + * @author Gunnar Wrobel + * @license http://www.fsf.org/copyleft/lgpl.html LGPL + * @link http://pear.horde.org/index.php?package=Kolab_Server + */ +class Horde_Auth_KolabTest extends Horde_Kolab_Test_Storage +{ + /** + * Test loggin in after a user has been added. + * + * @return NULL + */ + public function testLogin() + { + /** Create the test base */ + $world = &$this->prepareBasicSetup(); + $server = $world['server']; + $auth = $world['auth']; + + /** Ensure we always use the test server */ + $GLOBALS['conf']['kolab']['server']['driver'] = 'test'; + + $uid = $server->uidForIdOrMail('wrobel@example.org'); + $this->assertEquals('cn=Gunnar Wrobel,dc=example,dc=org', $uid); + + $result = $auth->authenticate('wrobel@example.org', + array('password' => 'none')); + $this->assertNoError($result); + $this->assertTrue($result); + + $session = Horde_Kolab_Session::singleton(); + $this->assertNoError($session->user_mail); + $this->assertEquals('wrobel@example.org', $session->user_mail); + + $result = $auth->authenticate('wrobel@example.org', + array('password' => 'invalid')); + $this->assertFalse($result); + + /** Ensure we don't use a connection from older tests */ + $server->unbind(); + + $result = $auth->authenticate('wrobel', + array('password' => 'invalid')); + $this->assertNoError($result); + $this->assertFalse($result); + + /** Ensure we don't use a connection from older tests */ + $server->unbind(); + $result = $auth->authenticate('wrobel', + array('password' => 'none')); + $this->assertNoError($result); + $this->assertTrue($result); + + $session = Horde_Kolab_Session::singleton(); + $this->assertNoError($session->user_mail); + $this->assertEquals('wrobel@example.org', $session->user_mail); + + $this->assertEquals('wrobel@example.org', Horde_Auth::getAuth()); + } +} \ No newline at end of file diff --git a/framework/Auth/test/Horde/Auth/credentials.php b/framework/Auth/test/Horde/Auth/credentials.php new file mode 100644 index 000000000..a04419020 Binary files /dev/null and b/framework/Auth/test/Horde/Auth/credentials.php differ diff --git a/framework/Auth/test/Horde/Auth/getCryptedPassword.phpt b/framework/Auth/test/Horde/Auth/getCryptedPassword.phpt new file mode 100644 index 000000000..2aa5924ed Binary files /dev/null and b/framework/Auth/test/Horde/Auth/getCryptedPassword.phpt differ diff --git a/framework/Auth/test/Horde/Auth/getSalt.phpt b/framework/Auth/test/Horde/Auth/getSalt.phpt new file mode 100644 index 000000000..4bc6e27f4 Binary files /dev/null and b/framework/Auth/test/Horde/Auth/getSalt.phpt differ diff --git a/framework/Auth/test/Horde/Auth/passwd.phpt b/framework/Auth/test/Horde/Auth/passwd.phpt new file mode 100644 index 000000000..92c882bbd --- /dev/null +++ b/framework/Auth/test/Horde/Auth/passwd.phpt @@ -0,0 +1,24 @@ +--TEST-- +Horde_Auth_Passwd:: test +--FILE-- + dirname(__FILE__) . '/test.passwd')); + +// List users +var_dump($auth->listUsers()); + +// Authenticate +var_dump($auth->authenticate('user', array('password' => 'password'), false)); + +?> +--EXPECT-- +array(1) { + [0]=> + string(4) "user" +} +bool(true) diff --git a/framework/Auth/test/Horde/Auth/test.passwd b/framework/Auth/test/Horde/Auth/test.passwd new file mode 100644 index 000000000..0ff58ae1e --- /dev/null +++ b/framework/Auth/test/Horde/Auth/test.passwd @@ -0,0 +1 @@ +user:3U6GxZSGmKPGA diff --git a/framework/History/lib/Horde/History.php b/framework/History/lib/Horde/History.php index 90ed3efe7..87c858bd7 100644 --- a/framework/History/lib/Horde/History.php +++ b/framework/History/lib/Horde/History.php @@ -127,7 +127,7 @@ class Horde_History $history = &$this->getHistory($guid); if (!isset($attributes['who'])) { - $attributes['who'] = Auth::getAuth(); + $attributes['who'] = Horde_Auth::getAuth(); } if (!isset($attributes['ts'])) { $attributes['ts'] = time(); diff --git a/framework/Kolab_Server/lib/Horde/Kolab/Test/Server.php b/framework/Kolab_Server/lib/Horde/Kolab/Test/Server.php index 2bd17245d..358c2c31f 100644 --- a/framework/Kolab_Server/lib/Horde/Kolab/Test/Server.php +++ b/framework/Kolab_Server/lib/Horde/Kolab/Test/Server.php @@ -395,7 +395,7 @@ class Horde_Kolab_Test_Server extends PHPUnit_Extensions_Story_TestCase { include_once 'Horde/Auth.php'; - $auth = Auth::singleton('kolab'); + $auth = Horde_Auth::singleton('kolab'); return $auth; } diff --git a/framework/Kolab_Session/lib/Horde/Kolab/Session.php b/framework/Kolab_Session/lib/Horde/Kolab/Session.php index 719b58336..181ccbc34 100644 --- a/framework/Kolab_Session/lib/Horde/Kolab/Session.php +++ b/framework/Kolab_Session/lib/Horde/Kolab/Session.php @@ -110,7 +110,7 @@ class Horde_Kolab_Session global $conf; if (empty($user)) { - $user = Auth::getAuth(); + $user = Horde_Auth::getAuth(); if (empty($user)) { $user = 'anonymous'; } else if (!strpos($user, '@')) { @@ -240,13 +240,13 @@ class Horde_Kolab_Session $params = array(); if ($this->user_uid) { $params['uid'] = $this->user_uid; - $params['pass'] = Auth::getCredential('password'); + $params['pass'] = Horde_Auth::getCredential('password'); } else if (isset($user)) { $params['user'] = $user; if (isset($credentials['password'])) { $params['pass'] = $credentials['password']; } else { - $params['pass'] = Auth::getCredential('password'); + $params['pass'] = Horde_Auth::getCredential('password'); } } return Horde_Kolab_Server::singleton($params); @@ -285,8 +285,8 @@ class Horde_Kolab_Session return $imap; } - $result = $imap->connect(Auth::getAuth(), - Auth::getCredential('password')); + $result = $imap->connect(Horde_Auth::getAuth(), + Horde_Auth::getCredential('password')); if (is_a($result, 'PEAR_Error')) { return $result; } @@ -331,7 +331,7 @@ class Horde_Kolab_Session } if (empty($user)) { - $user = Auth::getAuth(); + $user = Horde_Auth::getAuth(); } if ($destruct || empty($session) diff --git a/framework/LoginTasks/lib/Horde/LoginTasks.php b/framework/LoginTasks/lib/Horde/LoginTasks.php index 46fb2374f..4d084b8f1 100644 --- a/framework/LoginTasks/lib/Horde/LoginTasks.php +++ b/framework/LoginTasks/lib/Horde/LoginTasks.php @@ -130,7 +130,7 @@ class Horde_LoginTasks /* If this application handles Horde auth, need to add Horde tasks * here. */ $app_list = array($this->_app); - if (strnatcasecmp($this->_app, Auth::getProvider()) === 0) { + if (strnatcasecmp($this->_app, Horde_Auth::getProvider()) === 0) { array_unshift($app_list, 'horde'); } @@ -236,7 +236,7 @@ class Horde_LoginTasks if (empty($need_display)) { $lasttasks = unserialize($GLOBALS['prefs']->getValue('last_logintasks')); $lasttasks[$this->_app] = time(); - if (strnatcasecmp($this->_app, Auth::getProvider()) === 0) { + if (strnatcasecmp($this->_app, Horde_Auth::getProvider()) === 0) { $lasttasks['horde'] = time(); } $GLOBALS['prefs']->setValue('last_logintasks', serialize($lasttasks)); diff --git a/framework/Notification/lib/Horde/Notification.php b/framework/Notification/lib/Horde/Notification.php index d97ccd6f7..a45e50c70 100644 --- a/framework/Notification/lib/Horde/Notification.php +++ b/framework/Notification/lib/Horde/Notification.php @@ -205,7 +205,7 @@ class Horde_Notification $options['listeners'] = array_map('strtolower', $options['listeners']); if ($this->_alarm && in_array('status', $options['listeners'])) { - $this->_alarm->notify(Auth::getAuth()); + $this->_alarm->notify(Horde_Auth::getAuth()); } foreach ($options['listeners'] as $listener) { diff --git a/framework/Rpc/lib/Horde/Rpc.php b/framework/Rpc/lib/Horde/Rpc.php index fe2dddac7..587f51747 100644 --- a/framework/Rpc/lib/Horde/Rpc.php +++ b/framework/Rpc/lib/Horde/Rpc.php @@ -13,8 +13,8 @@ * 'contacts.search', * array(array('jan'), array('localsql'), * array('name', 'email')), - * array('user' => Auth::getAuth(), - * 'pass' => Auth::getCredential('password'))); + * array('user' => Horde_Auth::getAuth(), + * 'pass' => Horde_Auth::getCredential('password'))); * * * Copyright 2002-2009 The Horde Project (http://www.horde.org/) @@ -86,7 +86,7 @@ class Horde_Rpc return true; } - $auth = &Auth::singleton($GLOBALS['conf']['auth']['driver']); + $auth = Horde_Auth::singleton($GLOBALS['conf']['auth']['driver']); if (isset($_SERVER['PHP_AUTH_USER'])) { $user = $_SERVER['PHP_AUTH_USER']; diff --git a/framework/Rpc/lib/Horde/Rpc/Phpgw.php b/framework/Rpc/lib/Horde/Rpc/Phpgw.php index 666e1fcf5..56a6c4c2a 100644 --- a/framework/Rpc/lib/Horde/Rpc/Phpgw.php +++ b/framework/Rpc/lib/Horde/Rpc/Phpgw.php @@ -100,8 +100,7 @@ class Horde_Rpc_Phpgw extends Horde_Rpc } // Be authenticated or call system.login. - $auth = &Auth::singleton($GLOBALS['conf']['auth']['driver']); - $authenticated = $auth->isAuthenticated() || $method== "phpgw/system/login"; + $authenticated = Horde_Auth::isAuthenticated() || $method== "phpgw/system/login"; if ($authenticated) { Horde::logMessage("rpc call $method allowed", __FILE__, __LINE__, PEAR_LOG_NOTICE); diff --git a/framework/Rpc/lib/Horde/Rpc/Soap.php b/framework/Rpc/lib/Horde/Rpc/Soap.php index e9278d672..1d8733af1 100644 --- a/framework/Rpc/lib/Horde/Rpc/Soap.php +++ b/framework/Rpc/lib/Horde/Rpc/Soap.php @@ -92,7 +92,7 @@ class Horde_Rpc_Soap extends Horde_Rpc $GLOBALS['__horde_rpc_PhpSoap']['lastMethodCalled'], implode(', ', array_map(create_function('$a', 'return is_array($a) ? "Array" : $a;'), $GLOBALS['__horde_rpc_PhpSoap']['lastMethodParams'])), - Auth::getAuth(), + Horde_Auth::getAuth(), time() - $beginTime, ob_get_length()), __FILE__, __LINE__, PEAR_LOG_INFO diff --git a/framework/Rpc/lib/Horde/Rpc/Webdav.php b/framework/Rpc/lib/Horde/Rpc/Webdav.php index 36f786201..7b5dff509 100644 --- a/framework/Rpc/lib/Horde/Rpc/Webdav.php +++ b/framework/Rpc/lib/Horde/Rpc/Webdav.php @@ -738,7 +738,7 @@ Horde::logMessage(print_r($list, true), __FILE__, __LINE__, PEAR_LOG_ERR); $locktype = HORDE_LOCK_TYPE_EXCLUSIVE; } - $lockid = $locks->setLock(Auth::getAuth(), 'webdav', $params['path'], + $lockid = $locks->setLock(Horde_Auth::getAuth(), 'webdav', $params['path'], $timeout, $locktype); if (is_a($lockid, 'PEAR_Error')) { @@ -750,7 +750,7 @@ Horde::logMessage(print_r($list, true), __FILE__, __LINE__, PEAR_LOG_ERR); } $params['locktoken'] = $lockid; - $params['owner'] = Auth::getAuth(); + $params['owner'] = Horde_Auth::getAuth(); $params['timeout'] = $timeout; return "200"; @@ -849,7 +849,7 @@ Horde::logMessage(print_r($list, true), __FILE__, __LINE__, PEAR_LOG_ERR); */ function check_auth($type, $username, $password) { - $auth = &Auth::singleton($GLOBALS['conf']['auth']['driver']); + $auth = Horde_Auth::singleton($GLOBALS['conf']['auth']['driver']); return $auth->authenticate($username, array('password' => $password)); } diff --git a/framework/SessionHandler/lib/Horde/SessionHandler.php b/framework/SessionHandler/lib/Horde/SessionHandler.php index 48567485c..4a4c6ee6c 100644 --- a/framework/SessionHandler/lib/Horde/SessionHandler.php +++ b/framework/SessionHandler/lib/Horde/SessionHandler.php @@ -332,7 +332,7 @@ class Horde_SessionHandler * Returns a list of authenticated users and data about their session. * * @return array For authenticated users, the sessionid as a key and the - * information returned from Auth::readSessionData() as + * information returned from Horde_Auth::readSessionData() as * values. * @throws Horde_Exception */ @@ -348,7 +348,7 @@ class Horde_SessionHandler } catch (Horde_Exception $e) { continue; } - $data = Auth::readSessionData($data, true); + $data = Horde_Auth::readSessionData($data, true); if ($data !== false) { $info[$id] = $data; }