From b95727bb6f4a5d200d8ca282a07b9b541311d3b9 Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Sun, 6 Mar 2016 20:47:10 +0000 Subject: [PATCH] move auth classes to Api\Auth, only Sql is currently tested! --- api/src/Accounts/Ldap.php | 5 +- api/src/Auth.php | 781 ++++++++++++++++++ .../src/Auth/Ads.php | 32 +- api/src/Auth/Backend.php | 40 + .../src/Auth/Cas.php | 17 +- .../src/Auth/Fallback.php | 57 +- api/src/Auth/Fallbackmail2sql.php | 29 + .../src/Auth/Http.php | 23 +- .../src/Auth/Ldap.php | 72 +- .../src/Auth/Mail.php | 8 +- .../src/Auth/Nis.php | 12 +- .../src/Auth/Pam.php | 15 +- .../src/Auth/Sql.php | 45 +- .../src/Auth/Sqlssl.php | 34 +- api/src/Contacts/Ldap.php | 9 +- api/src/Csrf.php | 9 +- api/src/Ldap.php | 43 +- api/src/Session.php | 8 +- phpgwapi/inc/class.auth.inc.php | 764 +---------------- .../inc/class.auth_fallbackmail2sql.inc.php | 139 ---- phpgwapi/inc/class.common.inc.php | 25 +- phpgwapi/inc/class.egw.inc.php | 7 +- setup/inc/class.setup_cmd_ldap.inc.php | 15 +- 23 files changed, 1122 insertions(+), 1067 deletions(-) create mode 100644 api/src/Auth.php rename phpgwapi/inc/class.auth_ads.inc.php => api/src/Auth/Ads.php (91%) create mode 100644 api/src/Auth/Backend.php rename phpgwapi/inc/class.auth_cas.inc.php => api/src/Auth/Cas.php (79%) rename phpgwapi/inc/class.auth_fallback.inc.php => api/src/Auth/Fallback.php (78%) create mode 100644 api/src/Auth/Fallbackmail2sql.php rename phpgwapi/inc/class.auth_http.inc.php => api/src/Auth/Http.php (69%) rename phpgwapi/inc/class.auth_ldap.inc.php => api/src/Auth/Ldap.php (83%) rename phpgwapi/inc/class.auth_mail.inc.php => api/src/Auth/Mail.php (95%) rename phpgwapi/inc/class.auth_nis.inc.php => api/src/Auth/Nis.php (81%) rename phpgwapi/inc/class.auth_pam.inc.php => api/src/Auth/Pam.php (80%) rename phpgwapi/inc/class.auth_sql.inc.php => api/src/Auth/Sql.php (83%) rename phpgwapi/inc/class.auth_sqlssl.inc.php => api/src/Auth/Sqlssl.php (56%) delete mode 100644 phpgwapi/inc/class.auth_fallbackmail2sql.inc.php diff --git a/api/src/Accounts/Ldap.php b/api/src/Accounts/Ldap.php index 363820cc31..9a0fede31f 100644 --- a/api/src/Accounts/Ldap.php +++ b/api/src/Accounts/Ldap.php @@ -158,8 +158,7 @@ class Ldap // enable the caching in the session, done by the accounts class extending this class. $this->use_session_cache = true; - $this->ldap = new Api\Ldap(true); - $this->ds = $this->ldap->ldapConnect($this->frontend->config['ldap_host'], + $this->ds = Api\Ldap::factory(true, $this->frontend->config['ldap_host'], $this->frontend->config['ldap_root_dn'],$this->frontend->config['ldap_root_pw']); $this->user_context = $this->frontend->config['ldap_context']; @@ -1207,7 +1206,7 @@ class Ldap */ function __wakeup() { - $this->ds = $this->ldap->ldapConnect($this->frontend->config['ldap_host'], + $this->ds = Api\Ldap::factory(true, $this->frontend->config['ldap_host'], $this->frontend->config['ldap_root_dn'],$this->frontend->config['ldap_root_pw']); } } diff --git a/api/src/Auth.php b/api/src/Auth.php new file mode 100644 index 0000000000..c348b2ba89 --- /dev/null +++ b/api/src/Auth.php @@ -0,0 +1,781 @@ + + * @author Miles Lott + * @copyright 2004 by Miles Lott + * @license http://opensource.org/licenses/lgpl-license.php LGPL - GNU Lesser General Public License + * @package api + * @subpackage auth + * @version $Id$ + */ + +namespace EGroupware\Api; + +// explicit import classes still in phpgwapi +use egw; // invalidate_session_cache + +// allow to set an application depending authentication type (eg. for syncml, groupdav, ...) +if (isset($GLOBALS['egw_info']['server']['auth_type_'.$GLOBALS['egw_info']['flags']['currentapp']]) && + $GLOBALS['egw_info']['server']['auth_type_'.$GLOBALS['egw_info']['flags']['currentapp']]) +{ + $GLOBALS['egw_info']['server']['auth_type'] = $GLOBALS['egw_info']['server']['auth_type_'.$GLOBALS['egw_info']['flags']['currentapp']]; +} +if(empty($GLOBALS['egw_info']['server']['auth_type'])) +{ + $GLOBALS['egw_info']['server']['auth_type'] = 'sql'; +} +//error_log('using auth_type='.$GLOBALS['egw_info']['server']['auth_type'].', currentapp='.$GLOBALS['egw_info']['flags']['currentapp']); + +/** + * Authentication, password hashing and crypt functions + * + * Many functions based on code from Frank Thomas + * which can be seen at http://www.thomas-alfeld.de/frank/ + * + * Other functions from class.common.inc.php originally from phpGroupWare + */ +class Auth +{ + static $error; + + /** + * Holds instance of backend + * + * @var auth_backend + */ + private $backend; + + /** + * Constructor + * + * @throws Exception\AssertionFailed if backend is not an Auth\Backend + */ + function __construct() + { + $this->backend = self::backend(); + } + + /** + * Instanciate a backend + * + * @param Backend $type =null + */ + static function backend($type=null) + { + if (is_null($type)) $type = $GLOBALS['egw_info']['server']['auth_type']; + + $backend_class = __CLASS__.'\\'.ucfirst($type); + + // try old location / name, if not found + if (!class_exists($backend_class)) + { + $backend_class = 'auth_'.$type; + } + $backend = new $backend_class; + + if (!($backend instanceof Auth\Backend)) + { + throw new Exception\AssertionFailed("Auth backend class $backend_class is NO EGroupware\\Api\Auth\\Backend!"); + } + return $backend; + } + + /** + * check if users are supposed to change their password every x sdays, then check if password is of old age + * or the devil-admin reset the users password and forced the user to change his password on next login. + * + * @param string& $message =null on return false: message why password needs to be changed + * @return boolean true: all good, false: password change required, null: password expires in N days + */ + static function check_password_change(&$message=null) + { + // dont check anything for anonymous sessions/ users that are flagged as anonymous + if (is_object($GLOBALS['egw']->session) && $GLOBALS['egw']->session->session_flags == 'A') return true; + + // some statics (and initialisation to make information and timecalculation a) more readable in conditions b) persistent per request + // if user has to be warned about an upcomming passwordchange, remember for the session, that he was informed + static $UserKnowsAboutPwdChange=null; + if (is_null($UserKnowsAboutPwdChange)) $UserKnowsAboutPwdChange =& Cache::getSession('phpgwapi','auth_UserKnowsAboutPwdChange'); + + // retrieve the timestamp regarding the last change of the password from auth system and store it with the session + static $alpwchange_val=null; + static $pwdTsChecked=null; + if (is_null($pwdTsChecked) && is_null($alpwchange_val) || (string)$alpwchange_val === '0') + { + $alpwchange_val =& Cache::getSession('phpgwapi','auth_alpwchange_val'); // set that one with the session stored value + // initalize statics - better readability of conditions + if (is_null($alpwchange_val) || (string)$alpwchange_val === '0') + { + $backend = self::backend(); + // this may change behavior, as it should detect forced PasswordChanges from your Authentication System too. + // on the other side, if your auth system does not require an forcedPasswordChange, you will not be asked. + if (method_exists($backend,'getLastPwdChange')) + { + $alpwchange_val = $backend->getLastPwdChange($GLOBALS['egw']->session->account_lid); + $pwdTsChecked = true; + } + // if your authsystem does not provide that information, its likely, that you cannot change your password there, + // thus checking for expiration, is not needed + if ($alpwchange_val === false) + { + $alpwchange_val = null; + } + //error_log(__METHOD__.__LINE__.'#'.$alpwchange_val.'# is null:'.is_null($alpwchange_val).'# is empty:'.empty($alpwchange_val).'# is set:'.isset($alpwchange_val)); + } + } + static $passwordAgeBorder=null; + static $daysLeftUntilChangeReq=null; + + // if neither timestamp isset return true, nothing to do (exept this means the password is too old) + if (is_null($alpwchange_val) && + empty($GLOBALS['egw_info']['server']['change_pwd_every_x_days'])) + { + return true; + } + if (is_null($passwordAgeBorder) && $GLOBALS['egw_info']['server']['change_pwd_every_x_days']) + { + $passwordAgeBorder = (DateTime::to('now','ts')-($GLOBALS['egw_info']['server']['change_pwd_every_x_days']*86400)); + } + if (is_null($daysLeftUntilChangeReq) && $GLOBALS['egw_info']['server']['warn_about_upcoming_pwd_change']) + { + // maxage - passwordage = days left until change is required + $daysLeftUntilChangeReq = ($GLOBALS['egw_info']['server']['change_pwd_every_x_days'] - ((DateTime::to('now','ts')-($alpwchange_val?$alpwchange_val:0))/86400)); + } + if ($alpwchange_val == 0 || // admin requested password change + $passwordAgeBorder > $alpwchange_val || // change password every N days policy requests change + // user should be warned N days in advance about change and is not yet + $GLOBALS['egw_info']['server']['change_pwd_every_x_days'] && + $GLOBALS['egw_info']['user']['apps']['preferences'] && + $GLOBALS['egw_info']['server']['warn_about_upcoming_pwd_change'] && + $GLOBALS['egw_info']['server']['warn_about_upcoming_pwd_change'] > $daysLeftUntilChangeReq && + $UserKnowsAboutPwdChange !== true) + { + if ($alpwchange_val == 0) + { + $message = lang('An admin required that you must change your password upon login.'); + } + elseif ($passwordAgeBorder > $alpwchange_val && $alpwchange_val > 0) + { + error_log(__METHOD__.' Password of '.$GLOBALS['egw_info']['user']['account_lid'].' ('.$GLOBALS['egw_info']['user']['account_fullname'].') is of old age.'.array2string(array( + 'ts'=> $alpwchange_val, + 'date'=>DateTime::to($alpwchange_val)))); + $message = lang('It has been more then %1 days since you changed your password',$GLOBALS['egw_info']['server']['change_pwd_every_x_days']); + } + else + { + // login page does not inform user about passwords about to expire + if ($GLOBALS['egw_info']['flags']['currentapp'] != 'login' && + ($GLOBALS['egw_info']['flags']['currentapp'] != 'home' || + strpos($_SERVER['SCRIPT_NAME'], '/home/') !== false)) + { + $UserKnowsAboutPwdChange = true; + } + $message = lang('Your password is about to expire in %1 days, you may change your password now',round($daysLeftUntilChangeReq)); + // user has no rights to change password --> do NOT warn, as only forced check ignores rights + if ($GLOBALS['egw']->acl->check('nopasswordchange', 1, 'preferences')) return true; + return null; + } + return false; + } + return true; + } + + /** + * fetch the last pwd change for the user + * + * @param string $username username of account to authenticate + * @return mixed false or shadowlastchange*24*3600 + */ + function getLastPwdChange($username) + { + if (method_exists($this->backend,'getLastPwdChange')) + { + return $this->backend->getLastPwdChange($username); + } + return false; + } + + /** + * changes account_lastpwd_change in ldap datababse + * + * @param int $account_id account id of user whose passwd should be changed + * @param string $passwd must be cleartext, usually not used, but may be used to authenticate as user to do the change -> ldap + * @param int $lastpwdchange must be a unixtimestamp + * @return boolean true if account_lastpwd_change successful changed, false otherwise + */ + function setLastPwdChange($account_id=0, $passwd=NULL, $lastpwdchange=NULL) + { + if (method_exists($this->backend,'setLastPwdChange')) + { + return $this->backend->setLastPwdChange($account_id, $passwd, $lastpwdchange); + } + return false; + } + + /** + * password authentication against password stored in sql datababse + * + * @param string $username username of account to authenticate + * @param string $passwd corresponding password + * @param string $passwd_type ='text' 'text' for cleartext passwords (default) + * @return boolean true if successful authenticated, false otherwise + */ + function authenticate($username, $passwd, $passwd_type='text') + { + return $this->backend->authenticate($username, $passwd, $passwd_type); + } + + /** + * Calls crackcheck to enforce password strength (if configured) and changes password + * + * @param string $old_passwd must be cleartext + * @param string $new_passwd must be cleartext + * @param int $account_id account id of user whose passwd should be changed + * @throws Exception\WrongUserinput if configured password strength is not meat + * @throws Exception from backends having extra requirements + * @return boolean true if password successful changed, false otherwise + */ + function change_password($old_passwd, $new_passwd, $account_id=0) + { + if (($err = self::crackcheck($new_passwd,null,null,null,$account_id))) + { + throw new Exception\WrongUserinput($err); + } + if (($ret = $this->backend->change_password($old_passwd, $new_passwd, $account_id))) + { + if ($account_id == $GLOBALS['egw']->session->account_id) + { + // need to change current users password in session + Cache::setSession('phpgwapi', 'password', base64_encode($new_passwd)); + $GLOBALS['egw_info']['user']['passwd'] = $new_passwd; + $GLOBALS['egw_info']['user']['account_lastpwd_change'] = DateTime::to('now','ts'); + // invalidate EGroupware session, as password is stored in egw_info in session + egw::invalidate_session_cache(); + } + Accounts::cache_invalidate($account_id); + // run changepwasswd hook + $GLOBALS['hook_values'] = array( + 'account_id' => $account_id, + 'account_lid' => accounts::id2name($account_id), + 'old_passwd' => $old_passwd, + 'new_passwd' => $new_passwd, + ); + $GLOBALS['egw']->hooks->process($GLOBALS['hook_values']+array( + 'location' => 'changepassword' + ),False,True); // called for every app now, not only enabled ones) + } + return $ret; + } + + /** + * return a random string of letters [0-9a-zA-Z] of size $size + * + * @param $size int-size of random string to return + */ + static function randomstring($size) + { + static $random_char = array( + '0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f', + 'g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v', + 'w','x','y','z','A','B','C','D','E','F','G','H','I','J','K','L', + 'M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z' + ); + + $s = ''; + for ($i=0; $i<$size; $i++) + { + $s .= $random_char[mt_rand(1,61)]; + } + return $s; + } + + /** + * encrypt password + * + * uses the encryption type set in setup and calls the appropriate encryption functions + * + * @param $password password to encrypt + */ + static function encrypt_password($password,$sql=False) + { + if($sql) + { + return self::encrypt_sql($password); + } + return self::encrypt_ldap($password); + } + + /** + * compares an encrypted password + * + * encryption type set in setup and calls the appropriate encryption functions + * + * @param string $cleartext cleartext password + * @param string $encrypted encrypted password, can have a {hash} prefix, which overrides $type + * @param string $type_in type of encryption + * @param string $username used as optional key of encryption for md5_hmac + * @param string &$type =null on return detected type of hash + * @return boolean + */ + static function compare_password($cleartext, $encrypted, $type_in, $username='', &$type=null) + { + // allow to specify the hash type to prefix the hash, to easy migrate passwords from ldap + $type = $type_in; + $saved_enc = $encrypted; + $matches = null; + if (preg_match('/^\\{([a-z_5]+)\\}(.+)$/i',$encrypted,$matches)) + { + $type = strtolower($matches[1]); + $encrypted = $matches[2]; + + switch($type) // some hashs are specially "packed" in ldap + { + case 'md5': + $encrypted = implode('',unpack('H*',base64_decode($encrypted))); + break; + case 'plain': + case 'crypt': + // nothing to do + break; + default: + $encrypted = $saved_enc; + break; + } + } + elseif($encrypted[0] == '$') + { + $type = 'crypt'; + } + + switch($type) + { + case 'plain': + $ret = $cleartext === $encrypted; + break; + case 'smd5': + $ret = self::smd5_compare($cleartext,$encrypted); + break; + case 'sha': + $ret = self::sha_compare($cleartext,$encrypted); + break; + case 'ssha': + $ret = self::ssha_compare($cleartext,$encrypted); + break; + case 'crypt': + case 'des': + case 'md5_crypt': + case 'blowish_crypt': // was for some time a typo in setup + case 'blowfish_crypt': + case 'ext_crypt': + case 'sha256_crypt': + case 'sha512_crypt': + $ret = self::crypt_compare($cleartext, $encrypted, $type); + break; + case 'md5_hmac': + $ret = self::md5_hmac_compare($cleartext,$encrypted,$username); + break; + default: + $type = 'md5'; + // fall through + case 'md5': + $ret = md5($cleartext) === $encrypted; + break; + } + //error_log(__METHOD__."('$cleartext', '$encrypted', '$type_in', '$username') type='$type' returning ".array2string($ret)); + return $ret; + } + + /** + * Parameters used for crypt: const name, salt prefix, len of random salt, postfix + * + * @var array + */ + static $crypt_params = array( // + 'crypt' => array('CRYPT_STD_DES', '', 2, ''), + 'ext_crypt' => array('CRYPT_EXT_DES', '_J9..', 4, ''), + 'md5_crypt' => array('CRYPT_MD5', '$1$', 8, '$'), + //'old_blowfish_crypt' => array('CRYPT_BLOWFISH', '$2$', 13, ''), // old blowfish hash not in line with php.net docu, but could be in use + 'blowfish_crypt' => array('CRYPT_BLOWFISH', '$2a$12$', 22, ''), // $2a$12$ = 2^12 = 4096 rounds + 'sha256_crypt' => array('CRYPT_SHA256', '$5$', 16, '$'), // no "round=N$" --> default of 5000 rounds + 'sha512_crypt' => array('CRYPT_SHA512', '$6$', 16, '$'), // no "round=N$" --> default of 5000 rounds + ); + + /** + * compare crypted passwords for authentication whether des,ext_des,md5, or blowfish crypt + * + * @param string $form_val user input value for comparison + * @param string $db_val stored value / hash (from database) + * @param string &$type detected crypt type on return + * @return boolean True on successful comparison + */ + static function crypt_compare($form_val, $db_val, &$type) + { + // detect type of hash by salt part of $db_val + list($first, $dollar, $salt, $salt2) = explode('$', $db_val); + foreach(self::$crypt_params as $type => $params) + { + list(,$prefix, $random, $postfix) = $params; + list(,$d) = explode('$', $prefix); + if ($dollar === $d || !$dollar && ($first[0] === $prefix[0] || $first[0] !== '_' && !$prefix)) + { + $len = !$postfix ? strlen($prefix)+$random : strlen($prefix.$salt.$postfix); + // sha(256|512) might contain options, explicit $rounds=N$ prefix in salt + if (($type == 'sha256_crypt' || $type == 'sha512_crypt') && substr($salt, 0, 7) === 'rounds=') + { + $len += strlen($salt2)+1; + } + break; + } + } + + $full_salt = substr($db_val, 0, $len); + $new_hash = crypt($form_val, $full_salt); + //error_log(__METHOD__."('$form_val', '$db_val') type=$type --> len=$len --> salt='$full_salt' --> new_hash='$new_hash' returning ".array2string($db_val === $new_hash)); + + return $db_val === $new_hash; + } + + /** + * encrypt password for ldap + * + * uses the encryption type set in setup and calls the appropriate encryption functions + * + * @param string $password password to encrypt + * @param string $type =null default to $GLOBALS['egw_info']['server']['ldap_encryption_type'] + * @return string + */ + static function encrypt_ldap($password, $type=null) + { + if (is_null($type)) $type = $GLOBALS['egw_info']['server']['ldap_encryption_type']; + + $salt = ''; + switch(strtolower($type)) + { + default: // eg. setup >> config never saved + case 'des': + case 'blowish_crypt': // was for some time a typo in setup + $type = $type == 'blowish_crypt' ? 'blowfish_crypt' : 'crypt'; + // fall through + case 'crypt': + case 'sha256_crypt': + case 'sha512_crypt': + case 'blowfish_crypt': + case 'md5_crypt': + case 'ext_crypt': + list($const, $prefix, $len, $postfix) = self::$crypt_params[$type]; + if(defined($const) && constant($const) == 1) + { + $salt = $prefix.self::randomstring($len).$postfix; + $e_password = '{crypt}'.crypt($password, $salt); + break; + } + self::$error = 'no '.str_replace('_', ' ', $type); + $e_password = false; + break; + case 'md5': + /* New method taken from the openldap-software list as recommended by + * Kervin L. Pierre" + */ + $e_password = '{md5}' . base64_encode(pack("H*",md5($password))); + break; + case 'smd5': + $salt = self::randomstring(16); + $hash = md5($password . $salt, true); + $e_password = '{SMD5}' . base64_encode($hash . $salt); + break; + case 'sha': + $e_password = '{SHA}' . base64_encode(sha1($password,true)); + break; + case 'ssha': + $salt = self::randomstring(16); + $hash = sha1($password . $salt, true); + $e_password = '{SSHA}' . base64_encode($hash . $salt); + break; + case 'plain': + // if plain no type is prepended + $e_password = $password; + break; + } + //error_log(__METHOD__."('$password', ".array2string($type).") returning ".array2string($e_password).(self::$error ? ' error='.self::$error : '')); + return $e_password; + } + + /** + * Create a password for storage in the accounts table + * + * @param string $password + * @param string $type =null default $GLOBALS['egw_info']['server']['sql_encryption_type'] + * @return string hash + */ + static function encrypt_sql($password, $type=null) + { + /* Grab configured type, or default to md5() (old method) */ + if (is_null($type)) + { + $type = @$GLOBALS['egw_info']['server']['sql_encryption_type'] ? + strtolower($GLOBALS['egw_info']['server']['sql_encryption_type']) : 'md5'; + } + switch($type) + { + case 'plain': + // since md5 is the default, type plain must be prepended, for eGroupware to understand + $e_password = '{PLAIN}'.$password; + break; + + case 'md5': + /* This is the old standard for password storage in SQL */ + $e_password = md5($password); + break; + + // all other types are identical to ldap, so no need to doublicate the code here + case 'des': + case 'blowish_crypt': // was for some time a typo in setup + case 'crypt': + case 'sha256_crypt': + case 'sha512_crypt': + case 'blowfish_crypt': + case 'md5_crypt': + case 'ext_crypt': + case 'smd5': + case 'sha': + case 'ssha': + $e_password = self::encrypt_ldap($password, $type); + break; + + default: + self::$error = 'no valid encryption available'; + $e_password = false; + break; + } + //error_log(__METHOD__."('$password') using '$type' returning ".array2string($e_password).(self::$error ? ' error='.self::$error : '')); + return $e_password; + } + + /** + * Get available password hashes sorted by securest first + * + * @param string &$securest =null on return securest available hash + * @return array hash => label + */ + public static function passwdhashes(&$securest=null) + { + $hashes = array(); + + /* Check for available crypt methods based on what is defined by php */ + if(defined('CRYPT_BLOWFISH') && CRYPT_BLOWFISH == 1) + { + $hashes['blowfish_crypt'] = 'blowfish_crypt'; + } + if(defined('CRYPT_SHA512') && CRYPT_SHA512 == 1) + { + $hashes['sha512_crypt'] = 'sha512_crypt'; + } + if(defined('CRYPT_SHA256') && CRYPT_SHA256 == 1) + { + $hashes['sha256_crypt'] = 'sha256_crypt'; + } + if(defined('CRYPT_MD5') && CRYPT_MD5 == 1) + { + $hashes['md5_crypt'] = 'md5_crypt'; + } + if(defined('CRYPT_EXT_DES') && CRYPT_EXT_DES == 1) + { + $hashes['ext_crypt'] = 'ext_crypt'; + } + $hashes += array( + 'ssha' => 'ssha', + 'smd5' => 'smd5', + 'sha' => 'sha', + ); + if(@defined('CRYPT_STD_DES') && CRYPT_STD_DES == 1) + { + $hashes['crypt'] = 'crypt'; + } + + $hashes += array( + 'md5' => 'md5', + 'plain' => 'plain', + ); + + // mark the securest algorithm for the user + list($securest) = each($hashes); reset($hashes); + $hashes[$securest] .= ' ('.lang('securest').')'; + + return $hashes; + } + + /** + * Checks if a given password is "safe" + * + * @link http://technet.microsoft.com/en-us/library/cc786468(v=ws.10).aspx + * In contrary to whats documented in above link, windows seems to treet numbers as delimiters too. + * + * Windows compatible check is $reqstrength=3, $minlength=7, $forbid_name=true + * + * @param string $passwd + * @param int $reqstrength =null defaults to whatever set in config for "force_pwd_strength" + * @param int $minlength =null defaults to whatever set in config for "check_save_passwd" + * @param string $forbid_name =null if "yes" username or full-name split by delimiters AND longer then 3 chars are + * forbidden to be included in password, default to whatever set in config for "passwd_forbid_name" + * @param array|int $account =null array with account_lid and account_fullname or account_id for $forbid_name check + * @return mixed false if password is considered "safe" (or no requirements) or a string $message if "unsafe" + */ + static function crackcheck($passwd, $reqstrength=null, $minlength=null, $forbid_name=null, $account=null) + { + if (!isset($reqstrength)) $reqstrength = $GLOBALS['egw_info']['server']['force_pwd_strength']; + if (!isset($minlength)) $minlength = $GLOBALS['egw_info']['server']['force_pwd_length']; + if (!isset($forbid_name)) $forbid_name = $GLOBALS['egw_info']['server']['passwd_forbid_name']; + + // load preferences translations, as changepassword get's called from admin too + Translation::add_app('preferences'); + + // check for and if necessary convert old values True and 5 to new separate values for length and char-classes + if ($GLOBALS['egw_info']['server']['check_save_passwd'] || $reqstrength == 5) + { + if (!isset($reqstrength) || $reqstrength == 5) + { + Config::save_value('force_pwd_strength', $reqstrength=4, 'phpgwapi'); + } + if (!isset($minlength)) + { + Config::save_value('force_pwd_length', $minlength=7, 'phpgwapi'); + } + Config::save_value('check_save_passwd', null, 'phpgwapi'); + } + + $errors = array(); + + if ($minlength && strlen($passwd) < $minlength) + { + $errors[] = lang('password must have at least %1 characters', $minlength); + } + + if ($forbid_name === 'yes') + { + if (!$account || !is_array($account) && !($account = $GLOBALS['egw']->accounts->read($account))) + { + throw new Exception\WrongParameter('crackcheck(..., forbid_name=true, account) requires account-data!'); + } + $parts = preg_split("/[,._ \t0-9-]+/", $account['account_fullname'].','.$account['account_lid']); + foreach($parts as $part) + { + if (strlen($part) > 2 && stripos($passwd, $part) !== false) + { + $errors[] = lang('password contains with "%1" a parts of your user- or full-name (3 or more characters long)', $part); + break; + } + } + } + + if ($reqstrength) + { + $missing = array(); + if (!preg_match('/(.*\d.*){'. ($non=1). ',}/',$passwd)) + { + $missing[] = lang('numbers'); + } + if (!preg_match('/(.*[[:upper:]].*){'. ($nou=1). ',}/',$passwd)) + { + $missing[] = lang('uppercase letters'); + } + if (!preg_match('/(.*[[:lower:]].*){'. ($nol=1). ',}/',$passwd)) + { + $missing[] = lang('lowercase letters'); + } + if (!preg_match('/['.preg_quote('~!@#$%^&*_-+=`|\(){}[]:;"\'<>,.?/', '/').']/', $passwd)) + { + $missing[] = lang('special characters'); + } + if (4 - count($missing) < $reqstrength) + { + $errors[] = lang('password contains only %1 of required %2 character classes: no %3', + 4-count($missing), $reqstrength, implode(', ', $missing)); + } + } + if ($errors) + { + return lang('Your password does not have required strength:'). + "
\n- ".implode("
\n- ", $errors); + } + return false; + } + + /** + * compare SMD5-encrypted passwords for authentication + * + * @param string $form_val user input value for comparison + * @param string $db_val stored value (from database) + * @return boolean True on successful comparison + */ + static function smd5_compare($form_val,$db_val) + { + /* Start with the first char after {SMD5} */ + $hash = base64_decode(substr($db_val,6)); + + /* SMD5 hashes are 16 bytes long */ + $orig_hash = cut_bytes($hash, 0, 16); // binary string need to use cut_bytes, not mb_substr(,,'utf-8')! + $salt = cut_bytes($hash, 16); + + $new_hash = md5($form_val . $salt,true); + //echo '
DB: ' . base64_encode($orig_hash) . '
FORM: ' . base64_encode($new_hash); + + return strcmp($orig_hash,$new_hash) == 0; + } + + /** + * compare SHA-encrypted passwords for authentication + * + * @param string $form_val user input value for comparison + * @param string $db_val stored value (from database) + * @return boolean True on successful comparison + */ + static function sha_compare($form_val,$db_val) + { + /* Start with the first char after {SHA} */ + $hash = base64_decode(substr($db_val,5)); + $new_hash = sha1($form_val,true); + //echo '
DB: ' . base64_encode($orig_hash) . '
FORM: ' . base64_encode($new_hash); + + return strcmp($hash,$new_hash) == 0; + } + + /** + * compare SSHA-encrypted passwords for authentication + * + * @param string $form_val user input value for comparison + * @param string $db_val stored value (from database) + * @return boolean True on successful comparison + */ + static function ssha_compare($form_val,$db_val) + { + /* Start with the first char after {SSHA} */ + $hash = base64_decode(substr($db_val, 6)); + + // SHA-1 hashes are 160 bits long + $orig_hash = cut_bytes($hash, 0, 20); // binary string need to use cut_bytes, not mb_substr(,,'utf-8')! + $salt = cut_bytes($hash, 20); + $new_hash = sha1($form_val . $salt,true); + + //error_log(__METHOD__."('$form_val', '$db_val') hash='$hash', orig_hash='$orig_hash', salt='$salt', new_hash='$new_hash' returning ".array2string(strcmp($orig_hash,$new_hash) == 0)); + return strcmp($orig_hash,$new_hash) == 0; + } + + /** + * compare md5_hmac-encrypted passwords for authentication (see RFC2104) + * + * @param string $form_val user input value for comparison + * @param string $db_val stored value (from database) + * @param string $_key key for md5_hmac-encryption (username for imported smf users) + * @return boolean True on successful comparison + */ + static function md5_hmac_compare($form_val,$db_val,$_key) + { + $key = str_pad(strlen($_key) <= 64 ? $_key : pack('H*', md5($_key)), 64, chr(0x00)); + $md5_hmac = md5(($key ^ str_repeat(chr(0x5c), 64)) . pack('H*', md5(($key ^ str_repeat(chr(0x36), 64)). $form_val))); + + return strcmp($md5_hmac,$db_val) == 0; + } +} diff --git a/phpgwapi/inc/class.auth_ads.inc.php b/api/src/Auth/Ads.php similarity index 91% rename from phpgwapi/inc/class.auth_ads.inc.php rename to api/src/Auth/Ads.php index 82a7bf35dd..155b34fe35 100644 --- a/phpgwapi/inc/class.auth_ads.inc.php +++ b/api/src/Auth/Ads.php @@ -19,10 +19,14 @@ * @version $Id$ */ +namespace EGroupware\Api\Auth; + +use EGroupware\Api; + /** * Authentication agains a ADS Server */ -class auth_ads implements auth_backend +class Ads implements Backend { var $previous_login = -1; @@ -44,7 +48,7 @@ class auth_ads implements auth_backend // harden ldap auth, by removing \000 bytes, causing passwords to be not empty by php, but empty to c libaries $passwd = str_replace("\000", '', $_passwd); - $adldap = accounts_ads::get_adldap(); + $adldap = Api\Accounts\Ads::get_adldap(); // bind with username@ads_domain, only if a non-empty password given, in case anonymous search is enabled if(empty($passwd) || !$adldap->authenticate($username, $passwd)) { @@ -97,7 +101,7 @@ class auth_ads implements auth_backend } if ($GLOBALS['egw_info']['server']['auto_create_acct']) { - $GLOBALS['auto_create_acct']['account_id'] = accounts_ads::sid2account_id($allValues[0]['objectsid'][0]); + $GLOBALS['auto_create_acct']['account_id'] = Api\Accounts\Ads::sid2account_id($allValues[0]['objectsid'][0]); // create a global array with all availible info about that account foreach(array( @@ -107,7 +111,7 @@ class auth_ads implements auth_backend ) as $ldap_name => $acct_name) { $GLOBALS['auto_create_acct'][$acct_name] = - translation::convert($allValues[0][$ldap_name][0],'utf-8'); + Api\Translation::convert($allValues[0][$ldap_name][0],'utf-8'); } //error_log(__METHOD__."() \$GLOBALS[auto_create_acct]=".array2string($GLOBALS['auto_create_acct'])); return True; @@ -130,7 +134,7 @@ class auth_ads implements auth_backend static function getLastPwdChange($username) { $ret = false; - if (($adldap = accounts_ads::get_adldap()) && + if (($adldap = Api\Accounts\Ads::get_adldap()) && ($data = $adldap->user()->info($username, array('pwdlastset')))) { $ret = !$data[0]['pwdlastset'][0] ? $data[0]['pwdlastset'][0] : @@ -154,7 +158,7 @@ class auth_ads implements auth_backend static function setLastPwdChange($account_id=0, $passwd=NULL, $lastpwdchange=NULL, $return_mod=false) { unset($passwd); // not used but required by function signature - if (!($adldap = accounts_ads::get_adldap())) return false; + if (!($adldap = Api\Accounts\Ads::get_adldap())) return false; if ($lastpwdchange) { @@ -172,13 +176,13 @@ class auth_ads implements auth_backend } if ($lastpwdchange && $lastpwdchange != -1) { - $lastpwdchange = accounts_ads::convertUnixTimeToWindowsTime($lastpwdchange); + $lastpwdchange = Api\Accounts\Ads::convertUnixTimeToWindowsTime($lastpwdchange); } $mod = array('pwdlastset' => $lastpwdchange); if ($return_mod) return $mod; $ret = false; - if ($account_id && ($username = accounts::id2name($account_id, 'account_lid')) && + if ($account_id && ($username = Api\Accounts::id2name($account_id, 'account_lid')) && ($data = $adldap->user()->info($username, array('pwdlastset')))) { $ret = ldap_modify($adldap->getLdapConnection(), $data[0]['dn'], $mod); @@ -194,19 +198,19 @@ class auth_ads implements auth_backend * @param string $new_passwd must be cleartext * @param int $account_id account id of user whose passwd should be changed * @return boolean true if password successful changed, false otherwise - * @throws egw_exception_wrong_userinput + * @throws Api\Exception_wrong_userinput */ function change_password($old_passwd, $new_passwd, $account_id=0) { - if (!($adldap = accounts_ads::get_adldap())) + if (!($adldap = Api\Accounts\Ads::get_adldap())) { - error_log(__METHOD__."(\$old_passwd, \$new_passwd, $account_id) accounts_ads::get_adldap() returned false"); + error_log(__METHOD__."(\$old_passwd, \$new_passwd, $account_id) Api\Accounts\Ads::get_adldap() returned false"); return false; } if (!($adldap->getUseSSL() || $adldap->getUseTLS())) { - throw new egw_exception(lang('Failed to change password.').' '.lang('Active directory requires SSL or TLS to change passwords!')); + throw new Api\Exception(lang('Failed to change password.').' '.lang('Active directory requires SSL or TLS to change passwords!')); } if(!$account_id || $GLOBALS['egw_info']['flags']['currentapp'] == 'login') @@ -234,7 +238,7 @@ class auth_ads implements auth_backend } catch (Exception $e) { // as we cant detect what the problem is, we do a password strength check and throw it's message, if it fails - $error = auth::crackcheck($new_passwd, + $error = Api\Auth::crackcheck($new_passwd, // if admin has nothing configured use windows default of 3 char classes, 7 chars min and name-part-check $GLOBALS['egw_info']['server']['force_pwd_strength'] ? $GLOBALS['egw_info']['server']['force_pwd_strength'] : 3, $GLOBALS['egw_info']['server']['force_pwd_length'] ? $GLOBALS['egw_info']['server']['force_pwd_length'] : 7, @@ -245,7 +249,7 @@ class auth_ads implements auth_backend 'Server is unwilling to perform.' => lang('Server is unwilling to perform.'), 'Your password might not match the password policy.' => lang('Your password might not match the password policy.'), )); - throw new egw_exception('

'.lang('Failed to change password.')."

\n".$msg.($error ? "\n

".$error."

\n" : '')); + throw new Api\Exception('

'.lang('Failed to change password.')."

\n".$msg.($error ? "\n

".$error."

\n" : '')); } return false; } diff --git a/api/src/Auth/Backend.php b/api/src/Auth/Backend.php new file mode 100644 index 0000000000..ba56002566 --- /dev/null +++ b/api/src/Auth/Backend.php @@ -0,0 +1,40 @@ + + * @license http://opensource.org/licenses/lgpl-license.php LGPL - GNU Lesser General Public License + * @package api + * @subpackage authentication + * @version $Id$ + */ + +namespace EGroupware\Api\Auth; + +/** + * Interface for authentication backend + */ +interface Backend +{ + /** + * password authentication against password stored in sql datababse + * + * @param string $username username of account to authenticate + * @param string $passwd corresponding password + * @param string $passwd_type ='text' 'text' for cleartext passwords (default) + * @return boolean true if successful authenticated, false otherwise + */ + function authenticate($username, $passwd, $passwd_type='text'); + + /** + * changes password in sql datababse + * + * @param string $old_passwd must be cleartext + * @param string $new_passwd must be cleartext + * @param int $account_id account id of user whose passwd should be changed + * @throws Exception to give a verbose error, why changing password failed + * @return boolean true if password successful changed, false otherwise + */ + function change_password($old_passwd, $new_passwd, $account_id=0); +} diff --git a/phpgwapi/inc/class.auth_cas.inc.php b/api/src/Auth/Cas.php similarity index 79% rename from phpgwapi/inc/class.auth_cas.inc.php rename to api/src/Auth/Cas.php index f0f7496a49..2037fdac4d 100644 --- a/phpgwapi/inc/class.auth_cas.inc.php +++ b/api/src/Auth/Cas.php @@ -1,6 +1,6 @@ 'primary_group', ) as $ldap_name => $acct_name) { - $GLOBALS['auto_create_acct'][$acct_name] = $GLOBALS['egw']->translation->convert($allValues[0][$ldap_name][0],'utf-8'); + $GLOBALS['auto_create_acct'][$acct_name] = Api\Translation::convert($allValues[0][$ldap_name][0],'utf-8'); } return True; } @@ -57,7 +62,7 @@ class auth_cas implements auth_backend * * @param string $old_passwd must be cleartext or empty to not to be checked * @param string $new_passwd must be cleartext - * @param int $account_id=0 account id of user whose passwd should be changed + * @param int $account_id =0 account id of user whose passwd should be changed * @return boolean true if password successful changed, false otherwise */ function change_password($old_passwd, $new_passwd, $account_id=0) diff --git a/phpgwapi/inc/class.auth_fallback.inc.php b/api/src/Auth/Fallback.php similarity index 78% rename from phpgwapi/inc/class.auth_fallback.inc.php rename to api/src/Auth/Fallback.php index 455cd0c093..57816acb40 100644 --- a/phpgwapi/inc/class.auth_fallback.inc.php +++ b/api/src/Auth/Fallback.php @@ -6,39 +6,46 @@ * @author Ralf Becker * @license http://opensource.org/licenses/lgpl-license.php LGPL - GNU Lesser General Public License * @package api - * @subpackage authentication + * @subpackage auth * @version $Id$ */ +namespace EGroupware\Api\Auth; + +use EGroupware\Api; + /** * Authentication agains a LDAP Server with fallback to SQL * * For other fallback types, simply change auth backends in constructor call */ -class auth_fallback implements auth_backend +class Fallback implements Backend { /** * Primary auth backend * - * @var auth_backend + * @var Backend */ private $primary_backend; /** * Fallback auth backend * - * @var auth_backend + * @var Backend */ private $fallback_backend; /** * Constructor + * + * @param string $primary ='ldap' + * @param string $fallback ='sql' */ - function __construct($primary='auth_ldap',$fallback='auth_sql') + function __construct($primary='ldap',$fallback='sql') { - $this->primary_backend = new $primary; + $this->primary_backend = Api\Auth::backend(str_replace('auth_', '', $primary)); - $this->fallback_backend = new $fallback; + $this->fallback_backend = Api\Auth::backend(str_replace('auth_', '', $fallback)); } /** @@ -52,14 +59,14 @@ class auth_fallback implements auth_backend { if ($this->primary_backend->authenticate($username, $passwd, $passwd_type)) { - egw_cache::setInstance(__CLASS__,'backend_used-'.$username,'primary'); + Api\Cache::setInstance(__CLASS__,'backend_used-'.$username,'primary'); // check if fallback has correct password, if not update it if (($account_id = $GLOBALS['egw']->accounts->name2id($username)) && !$this->fallback_backend->authenticate($username,$passwd, $passwd_type)) { $backup_currentapp = $GLOBALS['egw_info']['flags']['currentapp']; $GLOBALS['egw_info']['flags']['currentapp'] = 'admin'; // otherwise - $ret = $this->fallback_backend->change_password('', $passwd, $account_id); + $this->fallback_backend->change_password('', $passwd, $account_id); $GLOBALS['egw_info']['flags']['currentapp'] = $backup_currentapp; //error_log(__METHOD__."('$username', \$passwd) updated password for #$account_id on fallback ".($ret ? 'successfull' : 'failed!')); } @@ -67,7 +74,7 @@ class auth_fallback implements auth_backend } if ($this->fallback_backend->authenticate($username,$passwd, $passwd_type)) { - egw_cache::setInstance(__CLASS__,'backend_used-'.$username,'fallback'); + Api\Cache::setInstance(__CLASS__,'backend_used-'.$username,'fallback'); return true; } return false; @@ -88,7 +95,6 @@ class auth_fallback implements auth_backend { if(!$account_id || $GLOBALS['egw_info']['flags']['currentapp'] == 'login') { - $admin = False; $account_id = $GLOBALS['egw_info']['user']['account_id']; $username = $GLOBALS['egw_info']['user']['account_lid']; } @@ -96,7 +102,7 @@ class auth_fallback implements auth_backend { $username = $GLOBALS['egw']->accounts->id2name($account_id); } - if (egw_cache::getInstance(__CLASS__,'backend_used-'.$username) == 'primary') + if (Api\Cache::getInstance(__CLASS__,'backend_used-'.$username) == 'primary') { if (($ret = $this->primary_backend->change_password($old_passwd, $new_passwd, $account_id))) { @@ -108,7 +114,7 @@ class auth_fallback implements auth_backend { $ret = $this->fallback_backend->change_password($old_passwd, $new_passwd, $account_id); } - //error_log(__METHOD__."('$old_passwd', '$new_passwd', $account_id) username='$username', backend=".egw_cache::getInstance(__CLASS__,'backend_used-'.$username)." returning ".array2string($ret)); + //error_log(__METHOD__."('$old_passwd', '$new_passwd', $account_id) username='$username', backend=".Api\Cache::getInstance(__CLASS__,'backend_used-'.$username)." returning ".array2string($ret)); return $ret; } @@ -120,11 +126,17 @@ class auth_fallback implements auth_backend */ function getLastPwdChange($username) { - if (egw_cache::getInstance(__CLASS__,'backend_used-'.$username) == 'primary') + if (Api\Cache::getInstance(__CLASS__,'backend_used-'.$username) == 'primary') { - if (method_exists($this->primary_backend,'getLastPwdChange')) return $this->primary_backend->getLastPwdChange($username); + if (method_exists($this->primary_backend,'getLastPwdChange')) + { + return $this->primary_backend->getLastPwdChange($username); + } + } + if (method_exists($this->fallback_backend,'getLastPwdChange')) + { + return $this->fallback_backend->getLastPwdChange($username); } - if (method_exists($this->fallback_backend,'getLastPwdChange')) return $this->fallback_backend->getLastPwdChange($username); return false; } @@ -140,7 +152,6 @@ class auth_fallback implements auth_backend { if(!$account_id || $GLOBALS['egw_info']['flags']['currentapp'] == 'login') { - $admin = False; $account_id = $GLOBALS['egw_info']['user']['account_id']; $username = $GLOBALS['egw_info']['user']['account_lid']; } @@ -148,11 +159,17 @@ class auth_fallback implements auth_backend { $username = $GLOBALS['egw']->accounts->id2name($account_id); } - if (egw_cache::getInstance(__CLASS__,'backend_used-'.$username) == 'primary') + if (Api\Cache::getInstance(__CLASS__,'backend_used-'.$username) == 'primary') { - if (method_exists($this->primary_backend,'setLastPwdChange')) return $this->primary_backend->setLastPwdChange($username); + if (method_exists($this->primary_backend,'setLastPwdChange')) + { + return $this->primary_backend->setLastPwdChange($username); + } + } + if (method_exists($this->fallback_backend,'setLastPwdChange')) + { + return $this->fallback_backend->setLastPwdChange($account_id, $passwd, $lastpwdchange); } - if (method_exists($this->fallback_backend,'setLastPwdChange')) return $this->fallback_backend->setLastPwdChange($account_id, $passwd, $lastpwdchange); return false; } } diff --git a/api/src/Auth/Fallbackmail2sql.php b/api/src/Auth/Fallbackmail2sql.php new file mode 100644 index 0000000000..ee54db709e --- /dev/null +++ b/api/src/Auth/Fallbackmail2sql.php @@ -0,0 +1,29 @@ + + * @license http://opensource.org/licenses/lgpl-license.php LGPL - GNU Lesser General Public License + * @package api + * @subpackage auth + * @version $Id$ + */ + +namespace EGroupware\Api\Auth; + +/** + * Authentication agains a mail Server with fallback to SQL + * + * For other fallback types, simply change auth backends in constructor call + */ +class Fallbackmail2sql extends Fallback +{ + /** + * Constructor + */ + function __construct($primary='mail', $fallback='sql') + { + parent::__construct($primary, $fallback); + } +} diff --git a/phpgwapi/inc/class.auth_http.inc.php b/api/src/Auth/Http.php similarity index 69% rename from phpgwapi/inc/class.auth_http.inc.php rename to api/src/Auth/Http.php index 5e79fff1e3..de1edc5f84 100644 --- a/phpgwapi/inc/class.auth_http.inc.php +++ b/api/src/Auth/Http.php @@ -1,6 +1,6 @@ @@ -8,14 +8,16 @@ * Copyright (C) 2000, 2001 Dan Kuykendall * @license http://opensource.org/licenses/lgpl-license.php LGPL - GNU Lesser General Public License * @package api - * @subpackage authentication + * @subpackage auth * @version $Id$ */ +namespace EGroupware\Api\Auth; + /** * Authentication based on HTTP auth */ -class auth_http implements auth_backend +class Http implements Backend { var $previous_login = -1; @@ -24,19 +26,14 @@ class auth_http implements auth_backend * * @param string $username username of account to authenticate * @param string $passwd corresponding password - * @param string $passwd_type='text' 'text' for cleartext passwords (default) + * @param string $passwd_type ='text' 'text' for cleartext passwords (default) * @return boolean true if successful authenticated, false otherwise */ function authenticate($username, $passwd, $passwd_type='text') { - if (isset($_SERVER['PHP_AUTH_USER'])) - { - return True; - } - else - { - return False; - } + unset($username, $passwd, $passwd_type); // not used, but required by interface + + return isset($_SERVER['PHP_AUTH_USER']) && $_SERVER['PHP_AUTH_USER'] === $username; } /** @@ -49,6 +46,8 @@ class auth_http implements auth_backend */ function change_password($old_passwd, $new_passwd, $account_id=0) { + unset($old_passwd, $new_passwd, $account_id); // not used, but required by interface + return False; } } diff --git a/phpgwapi/inc/class.auth_ldap.inc.php b/api/src/Auth/Ldap.php similarity index 83% rename from phpgwapi/inc/class.auth_ldap.inc.php rename to api/src/Auth/Ldap.php index 3a236f07e5..564ccd168f 100644 --- a/phpgwapi/inc/class.auth_ldap.inc.php +++ b/api/src/Auth/Ldap.php @@ -19,10 +19,14 @@ * @version $Id$ */ +namespace EGroupware\Api\Auth; + +use EGroupware\Api; + /** * Authentication agains a LDAP Server */ -class auth_ldap implements auth_backend +class Ldap implements Backend { var $previous_login = -1; /** @@ -44,25 +48,24 @@ class auth_ldap implements auth_backend unset($passwd_type); // not used by required by function signature // allow non-ascii in username & password - $username = translation::convert($_username,translation::charset(),'utf-8'); + $username = Api\Translation::convert($_username,Api\Translation::charset(),'utf-8'); // harden ldap auth, by removing \000 bytes, causing passwords to be not empty by php, but empty to c libaries - $passwd = str_replace("\000", '', translation::convert($_passwd,translation::charset(),'utf-8')); + $passwd = str_replace("\000", '', Api\Translation::convert($_passwd,Api\Translation::charset(),'utf-8')); - if(!$ldap = common::ldapConnect()) - { - return False; + // Login with the LDAP Admin. User to find the User DN. + try { + $ldap = Api\Ldap::factory(); } - - /* Login with the LDAP Admin. User to find the User DN. */ - if(!@ldap_bind($ldap, $GLOBALS['egw_info']['server']['ldap_root_dn'], $GLOBALS['egw_info']['server']['ldap_root_pw'])) + catch(Api\Exception\NoPermission $e) { + unset($e); if ($this->debug) error_log(__METHOD__."('$username',\$password) can NOT bind with ldap_root_dn to search!"); return False; } /* find the dn for this uid, the uid is not always in the dn */ $attributes = array('uid','dn','givenName','sn','mail','uidNumber','shadowExpire','homeDirectory'); - $filter = str_replace(array('%user','%domain'),array(ldap::quote($username),$GLOBALS['egw_info']['user']['domain']), + $filter = str_replace(array('%user','%domain'),array(Api\Ldap::quote($username),$GLOBALS['egw_info']['user']['domain']), $GLOBALS['egw_info']['server']['ldap_search_filter'] ? $GLOBALS['egw_info']['server']['ldap_search_filter'] : '(uid=%user)'); if ($GLOBALS['egw_info']['server']['account_repository'] == 'ldap') @@ -114,7 +117,7 @@ class auth_ldap implements auth_backend ) as $ldap_name => $acct_name) { $GLOBALS['auto_create_acct'][$acct_name] = - translation::convert($allValues[0][$ldap_name][0],'utf-8'); + Api\Translation::convert($allValues[0][$ldap_name][0],'utf-8'); } $ret = true; } @@ -141,8 +144,8 @@ class auth_ldap implements auth_backend if (($sri = ldap_search($ldap, $userDN,"(objectclass=*)", array('userPassword'))) && ($values = ldap_get_entries($ldap, $sri)) && isset($values[0]['userpassword'][0]) && ($type = preg_match('/^{(.+)}/',$values[0]['userpassword'][0],$matches) ? strtolower($matches[1]) : 'plain') && - // for crypt use auth::crypt_compare to detect correct sub-type, strlen("{crypt}")=7 - ($type != 'crypt' || auth::crypt_compare($passwd, substr($values[0]['userpassword'][0], 7), $type)) && + // for crypt use Api\Auth::crypt_compare to detect correct sub-type, strlen("{crypt}")=7 + ($type != 'crypt' || Api\Auth::crypt_compare($passwd, substr($values[0]['userpassword'][0], 7), $type)) && in_array($type, explode(',',strtolower($GLOBALS['egw_info']['server']['pwd_migration_types'])))) { $this->change_password($passwd, $passwd, $allValues[0]['uidnumber'][0], false); @@ -165,23 +168,21 @@ class auth_ldap implements auth_backend function getLastPwdChange($_username) { // allow non-ascii in username & password - $username = translation::convert($_username,translation::charset(),'utf-8'); + $username = Api\Translation::convert($_username,Api\Translation::charset(),'utf-8'); - if(!$ldap = common::ldapConnect()) - { - return False; + // Login with the LDAP Admin. User to find the User DN. + try { + $ldap = Api\Ldap::factory(); } - - /* Login with the LDAP Admin. User to find the User DN. */ - if(!@ldap_bind($ldap, $GLOBALS['egw_info']['server']['ldap_root_dn'], $GLOBALS['egw_info']['server']['ldap_root_pw'])) - { + catch (Api\Exception\NoPermission $ex) { + unset($ex); if ($this->debug) error_log(__METHOD__."('$username') can NOT bind with ldap_root_dn to search!"); return false; } /* find the dn for this uid, the uid is not always in the dn */ $attributes = array('uid','dn','shadowexpire','shadowlastchange'); - $filter = str_replace(array('%user','%domain'),array(ldap::quote($username),$GLOBALS['egw_info']['user']['domain']), + $filter = str_replace(array('%user','%domain'),array(Api\Ldap::quote($username),$GLOBALS['egw_info']['user']['domain']), $GLOBALS['egw_info']['server']['ldap_search_filter'] ? $GLOBALS['egw_info']['server']['ldap_search_filter'] : '(uid=%user)'); if ($GLOBALS['egw_info']['server']['account_repository'] == 'ldap') @@ -236,15 +237,15 @@ class auth_ldap implements auth_backend } else { - $username = translation::convert($GLOBALS['egw']->accounts->id2name($account_id), - translation::charset(),'utf-8'); + $username = Api\Translation::convert($GLOBALS['egw']->accounts->id2name($account_id), + Api\Translation::charset(),'utf-8'); } - //echo "

auth_ldap::change_password('$old_passwd','$new_passwd',$account_id) username='$username'

\n"; + //echo "

auth_Api\Ldap::change_password('$old_passwd','$new_passwd',$account_id) username='$username'

\n"; $filter = str_replace(array('%user','%domain'),array($username,$GLOBALS['egw_info']['user']['domain']), $GLOBALS['egw_info']['server']['ldap_search_filter'] ? $GLOBALS['egw_info']['server']['ldap_search_filter'] : '(uid=%user)'); - $ds = common::ldapConnect(); + $ds = Api\Ldap::factory(); $sri = ldap_search($ds, $GLOBALS['egw_info']['server']['ldap_context'], $filter); $allValues = ldap_get_entries($ds, $sri); @@ -254,14 +255,14 @@ class auth_ldap implements auth_backend if(!$admin && $passwd) // if old password given (not called by admin) --> bind as that user to change the pw { - $ds = common::ldapConnect('',$dn,$passwd); + $ds = Api\Ldap::factory('', $dn, $passwd); } if (!@ldap_modify($ds, $dn, $entry)) { return false; } // using time() is sufficient to represent the current time, we do not need the timestamp written to the storage - if (!$admin) egw_cache::setSession('phpgwapi','auth_alpwchange_val',(is_null($lastpwdchange) || $lastpwdchange<0 ? time():$lastpwdchange)); + if (!$admin) Api\Cache::setSession('phpgwapi','auth_alpwchange_val',(is_null($lastpwdchange) || $lastpwdchange<0 ? time():$lastpwdchange)); return true; } @@ -285,19 +286,19 @@ class auth_ldap implements auth_backend } else { - $username = translation::convert($GLOBALS['egw']->accounts->id2name($account_id), - translation::charset(),'utf-8'); + $username = Api\Translation::convert($GLOBALS['egw']->accounts->id2name($account_id), + Api\Translation::charset(),'utf-8'); } if ($this->debug) error_log(__METHOD__."('$old_passwd','$new_passwd',$account_id, $update_lastchange) username='$username'"); $filter = str_replace(array('%user','%domain'),array($username,$GLOBALS['egw_info']['user']['domain']), $GLOBALS['egw_info']['server']['ldap_search_filter'] ? $GLOBALS['egw_info']['server']['ldap_search_filter'] : '(uid=%user)'); - $ds = $ds_admin = common::ldapConnect(); + $ds = $ds_admin = Api\Ldap::factory(); $sri = ldap_search($ds, $GLOBALS['egw_info']['server']['ldap_context'], $filter); $allValues = ldap_get_entries($ds, $sri); - $entry['userpassword'] = auth::encrypt_password($new_passwd); + $entry['userpassword'] = Api\Auth::encrypt_password($new_passwd); if ($update_lastchange) { $entry['shadowlastchange'] = round((time()-date('Z')) / (24*3600)); @@ -307,11 +308,10 @@ class auth_ldap implements auth_backend if($old_passwd) // if old password given (not called by admin) --> bind as that user to change the pw { - $user_ds = new ldap(true); // true throw exceptions in case of error try { - $ds = $user_ds->ldapConnect('',$dn,$old_passwd); + $ds = Api\Ldap\factory('',$dn,$old_passwd); } - catch (egw_exception_no_permission $e) { + catch (Api\Exception\NoPermission $e) { unset($e); return false; // wrong old user password } @@ -325,7 +325,7 @@ class auth_ldap implements auth_backend if($old_passwd) // if old password given (not called by admin) update the password in the session { // using time() is sufficient to represent the current time, we do not need the timestamp written to the storage - egw_cache::setSession('phpgwapi','auth_alpwchange_val',time()); + Api\Cache::setSession('phpgwapi','auth_alpwchange_val',time()); } return $entry['userpassword']; } diff --git a/phpgwapi/inc/class.auth_mail.inc.php b/api/src/Auth/Mail.php similarity index 95% rename from phpgwapi/inc/class.auth_mail.inc.php rename to api/src/Auth/Mail.php index c818cb9696..a100f11323 100644 --- a/phpgwapi/inc/class.auth_mail.inc.php +++ b/api/src/Auth/Mail.php @@ -1,6 +1,6 @@ @@ -11,10 +11,14 @@ * @version $Id$ */ +namespace EGroupware\Api\Auth; + +use Horde_Imap_Client_Socket, Horde_Imap_Client_Exception; + /** * Authentication agains mail server */ -class auth_mail implements auth_backend +class Mail implements Backend { var $previous_login = -1; diff --git a/phpgwapi/inc/class.auth_nis.inc.php b/api/src/Auth/Nis.php similarity index 81% rename from phpgwapi/inc/class.auth_nis.inc.php rename to api/src/Auth/Nis.php index 529e015444..ff742f03ce 100644 --- a/phpgwapi/inc/class.auth_nis.inc.php +++ b/api/src/Auth/Nis.php @@ -11,21 +11,25 @@ * @version $Id$ */ +namespace EGroupware\Api\Auth; + /** * Auth from NIS */ -class auth_nis implements auth_backend +class Nis implements Backend { /** * password authentication * * @param string $username username of account to authenticate * @param string $passwd corresponding password - * @param string $passwd_type='text' 'text' for cleartext passwords (default) + * @param string $passwd_type ='text' 'text' for cleartext passwords (default) * @return boolean true if successful authenticated, false otherwise */ function authenticate($username, $passwd, $passwd_type='text') { + unset($passwd_type); // not used but required by interface + $domain = yp_get_default_domain(); if(!empty($GLOBALS['egw_info']['server']['nis_domain'])) { @@ -56,11 +60,13 @@ class auth_nis implements auth_backend * * @param string $old_passwd must be cleartext or empty to not to be checked * @param string $new_passwd must be cleartext - * @param int $account_id=0 account id of user whose passwd should be changed + * @param int $account_id =0 account id of user whose passwd should be changed * @return boolean true if password successful changed, false otherwise */ function change_password($old_passwd, $new_passwd, $account_id=0) { + unset($old_passwd, $new_passwd, $account_id); // not used but required by interface + // can't change passwords unless server runs as root (bad idea) return( False ); } diff --git a/phpgwapi/inc/class.auth_pam.inc.php b/api/src/Auth/Pam.php similarity index 80% rename from phpgwapi/inc/class.auth_pam.inc.php rename to api/src/Auth/Pam.php index d27c5f8b43..241188bb50 100644 --- a/phpgwapi/inc/class.auth_pam.inc.php +++ b/api/src/Auth/Pam.php @@ -9,6 +9,11 @@ * @version $Id$ */ +namespace EGroupware\Api\Auth; + +// explicitly import classes still in phpgwapi +use common; // email_address + /** * Auth from PAM * @@ -16,18 +21,20 @@ * * To read full name from password file PHP's posix extension is needed (sometimes in package php_process) */ -class auth_pam implements auth_backend +class Pam implements Backend { /** * password authentication * * @param string $username username of account to authenticate * @param string $passwd corresponding password - * @param string $passwd_type='text' 'text' for cleartext passwords (default) + * @param string $passwd_type ='text' 'text' for cleartext passwords (default) * @return boolean true if successful authenticated, false otherwise */ function authenticate($username, $passwd, $passwd_type='text') { + unset($passwd_type); // not used but required by interface + if (pam_auth($username, get_magic_quotes_gpc() ? stripslashes($passwd) : $passwd)) { // for new accounts read full name from password file and pass it to EGroupware @@ -60,11 +67,13 @@ class auth_pam implements auth_backend * * @param string $old_passwd must be cleartext or empty to not to be checked * @param string $new_passwd must be cleartext - * @param int $account_id=0 account id of user whose passwd should be changed + * @param int $account_id =0 account id of user whose passwd should be changed * @return boolean true if password successful changed, false otherwise */ function change_password($old_passwd, $new_passwd, $account_id=0) { + unset($old_passwd, $new_passwd, $account_id); // not used but required by interface + // deny password changes. return False; } diff --git a/phpgwapi/inc/class.auth_sql.inc.php b/api/src/Auth/Sql.php similarity index 83% rename from phpgwapi/inc/class.auth_sql.inc.php rename to api/src/Auth/Sql.php index 695cd08313..fcd22016f2 100644 --- a/phpgwapi/inc/class.auth_sql.inc.php +++ b/api/src/Auth/Sql.php @@ -1,6 +1,6 @@ @@ -13,6 +13,10 @@ * @version $Id$ */ +namespace EGroupware\Api\Auth; + +use EGroupware\Api; + /** * eGroupWare API - Authentication based on SQL table of accounts * @@ -21,12 +25,12 @@ * * Massive code cleanup and added password migration by Cornelius Weiss db->capabilities[egw_db::CAPABILITY_CASE_INSENSITIV_LIKE].' '.$this->db->quote($username); + $where[] = 'account_lid '.$this->db->capabilities[Api\Db::CAPABILITY_CASE_INSENSITIV_LIKE].' '.$this->db->quote($username); unset($where['account_lid']); } if($passwd_type == 'text') @@ -69,7 +73,8 @@ class auth_sql implements auth_backend { return false; } - if(!($match = auth::compare_password($passwd, $row['account_pwd'], $this->type, strtolower($username), $type)) || + $type = null; + if(!($match = Api\Auth::compare_password($passwd, $row['account_pwd'], $this->type, strtolower($username), $type)) || $type != $this->type && in_array($type, explode(',',strtolower($GLOBALS['egw_info']['server']['pwd_migration_types'])))) { // do we have to migrate an old password ? @@ -79,7 +84,7 @@ class auth_sql implements auth_backend { foreach(explode(',', $GLOBALS['egw_info']['server']['pwd_migration_types']) as $type) { - if(($match = auth::compare_password($passwd,$row['account_pwd'],$type,strtolower($username)))) + if(($match = Api\Auth::compare_password($passwd,$row['account_pwd'],$type,strtolower($username)))) { break; } @@ -87,7 +92,7 @@ class auth_sql implements auth_backend } if ($match) { - $encrypted_passwd = auth::encrypt_sql($passwd); + $encrypted_passwd = Api\Auth::encrypt_sql($passwd); $this->_update_passwd($encrypted_passwd,$passwd,$row['account_id'],false,true); } } @@ -126,7 +131,7 @@ class auth_sql implements auth_backend ); if (!$GLOBALS['egw_info']['server']['case_sensitive_username']) // = is case sensitiv eg. on postgres, but not on mysql! { - $where[] = 'account_lid '.$this->db->capabilities[egw_db::CAPABILITY_CASE_INSENSITIV_LIKE].' '.$this->db->quote($username); + $where[] = 'account_lid '.$this->db->capabilities[Api\Db::CAPABILITY_CASE_INSENSITIV_LIKE].' '.$this->db->quote($username); unset($where['account_lid']); } if (!($row = $this->db->select($this->table,'account_lid,account_lastpwd_change',$where,__LINE__,__FILE__)->fetch()) || @@ -145,10 +150,10 @@ class auth_sql implements auth_backend * * @param int $account_id account id of user whose passwd should be changed * @param string $passwd must be cleartext, usually not used, but may be used to authenticate as user to do the change -> ldap - * @param int $lastpwdchange must be a unixtimestamp + * @param int $_lastpwdchange =null must be a unixtimestamp * @return boolean true if account_lastpwd_change successful changed, false otherwise */ - function setLastPwdChange($account_id=0, $passwd=NULL, $lastpwdchange=NULL) + function setLastPwdChange($account_id=0, $passwd=NULL, $_lastpwdchange=NULL) { $admin = True; // Don't allow password changes for other accounts when using XML-RPC @@ -172,11 +177,11 @@ class auth_sql implements auth_backend return false; // account not found } // Check the passwd to make sure this is legal - if(!$admin && !auth::compare_password($passwd,$pw,$this->type,strtolower($username))) + if(!$admin && !Api\Auth::compare_password($passwd,$pw,$this->type,strtolower($username))) { return false; } - $lastpwdchange = (is_null($lastpwdchange) || $lastpwdchange<0 ? time():$lastpwdchange); + $lastpwdchange = (is_null($_lastpwdchange) || $_lastpwdchange < 0 ? time() : $_lastpwdchange); $this->db->update($this->table,array( 'account_lastpwd_change' => $lastpwdchange, ),array( @@ -184,7 +189,7 @@ class auth_sql implements auth_backend ),__LINE__,__FILE__); if(!$this->db->affected_rows()) return false; - if (!$admin) egw_cache::setSession('phpgwapi','auth_alpwchange_val',$lastpwdchange); + if (!$admin) Api\Cache::setSession('phpgwapi', 'auth_alpwchange_val', $lastpwdchange); return true; } @@ -219,13 +224,13 @@ class auth_sql implements auth_backend return false; // account not found } // Check the old_passwd to make sure this is legal - if(!$admin && !auth::compare_password($old_passwd,$pw,$this->type,strtolower($username))) + if(!$admin && !Api\Auth::compare_password($old_passwd,$pw,$this->type,strtolower($username))) { return false; } // old password ok, or admin called the function from the admin application (no old passwd available). - return $this->_update_passwd(auth::encrypt_sql($new_passwd),$new_passwd,$account_id,$admin); + return $this->_update_passwd(Api\Auth::encrypt_sql($new_passwd),$new_passwd,$account_id,$admin); } /** @@ -234,12 +239,14 @@ class auth_sql implements auth_backend * @param string $encrypted_passwd * @param string $new_passwd cleartext * @param int $account_id account id of user whose passwd should be changed - * @param boolean $admin=false called by admin, if not update password in the session - * @param boolean $update_lastpw_change=true + * @param boolean $admin =false called by admin, if not update password in the session + * @param boolean $update_lastpw_change =true * @return boolean true if password successful changed, false otherwise */ private function _update_passwd($encrypted_passwd,$new_passwd,$account_id,$admin=false,$update_lastpw_change=true) { + unset($new_passwd); // not used, but required by function signature + $update = array('account_pwd' => $encrypted_passwd); if ($update_lastpw_change) $update['account_lastpwd_change'] = time(); @@ -252,7 +259,7 @@ class auth_sql implements auth_backend if(!$admin) { - egw_cache::setSession('phpgwapi','auth_alpwchange_val',$update['account_lastpwd_change']); + Api\Cache::setSession('phpgwapi','auth_alpwchange_val',$update['account_lastpwd_change']); } return true; } diff --git a/phpgwapi/inc/class.auth_sqlssl.inc.php b/api/src/Auth/Sqlssl.php similarity index 56% rename from phpgwapi/inc/class.auth_sqlssl.inc.php rename to api/src/Auth/Sqlssl.php index 686687850e..56adcefa37 100644 --- a/phpgwapi/inc/class.auth_sqlssl.inc.php +++ b/api/src/Auth/Sqlssl.php @@ -10,9 +10,13 @@ * @version $Id$ */ +namespace EGroupware\Api\Auth; + +use EGroupware\Api; + /** * Authentication based on SQL table and X.509 certificates - * + * * @todo rewrite using auth_sql backend class */ class auth_sqlssl extends auth_sql @@ -22,24 +26,24 @@ class auth_sqlssl extends auth_sql * * @param string $username username of account to authenticate * @param string $passwd corresponding password - * @param string $passwd_type='text' 'text' for cleartext passwords (default) + * @param string $passwd_type ='text' 'text' for cleartext passwords (default) * @return boolean true if successful authenticated, false otherwise */ function authenticate($username, $passwd, $passwd_type='text') { + unset($passwd_type); // not used but required by interface + $local_debug = False; if($local_debug) { echo "Debug SQL: uid - $username passwd - $passwd"; } - $this->db->select($this->table,'account_lid,account_pwd',array( + if (!($row = $this->db->select($this->table,'account_lid,account_pwd',array( 'account_lid' => $username, 'account_status' => 'A', 'account_type' => 'u', - ),__LINE__,__FILE__); - - if (!$this->db->next_record() || $GLOBALS['egw_info']['server']['case_sensitive_username'] && $this->db->f('account_lid') != $username) + ),__LINE__,__FILE__)->fetch()) || $GLOBALS['egw_info']['server']['case_sensitive_username'] && $row['account_lid'] != $username) { return false; } @@ -50,8 +54,24 @@ class auth_sqlssl extends auth_sql if(!isset($_SERVER['SSL_CLIENT_S_DN'])) { # if we're not doing SSL authentication, behave like auth_sql - return auth::compare_password($passwd,$this->db->f('account_pwd'),$this->type,strtolower($username)); + return Api\Auth::compare_password($passwd, $row['account_pwd'], 'md5', strtolower($username)); } return True; } + + /** + * changes password + * + * @param string $old_passwd must be cleartext or empty to not to be checked + * @param string $new_passwd must be cleartext + * @param int $account_id =0 account id of user whose passwd should be changed + * @return boolean true if password successful changed, false otherwise + */ + function change_password($old_passwd, $new_passwd, $account_id=0) + { + unset($old_passwd, $new_passwd, $account_id); // not used but required by interface + + // deny password changes. + return False; + } } diff --git a/api/src/Contacts/Ldap.php b/api/src/Contacts/Ldap.php index 3c126e2a1d..7cadce7f24 100644 --- a/api/src/Contacts/Ldap.php +++ b/api/src/Contacts/Ldap.php @@ -325,23 +325,20 @@ class Ldap */ function connect($admin = false) { - // if egw object does not yet exists (eg. in sharing), we have to use our own ldap instance! - $ldap = isset($GLOBALS['egw']) && is_a($GLOBALS['egw']->ldap, 'ldap') ? $GLOBALS['egw']->ldap : new ldap(); - if ($admin) { - $this->ds = $ldap->ldapConnect(); + $this->ds = Api\Ldap::factory(); } // if ldap is NOT the contact repository, we only do accounts and need to use the account-data elseif (substr($GLOBALS['egw_info']['server']['contact_repository'],-4) != 'ldap') // not (ldap or sql-ldap) { $this->ldap_config['ldap_contact_host'] = $this->ldap_config['ldap_host']; $this->allContactsDN = $this->ldap_config['ldap_context']; - $this->ds = $ldap->ldapConnect(); + $this->ds = Api\Ldap::factory(); } else { - $this->ds = $ldap->ldapConnect( + $this->ds = Api\Ldap::factory(true, $this->ldap_config['ldap_contact_host'], $GLOBALS['egw_info']['user']['account_dn'], $GLOBALS['egw_info']['user']['passwd'] diff --git a/api/src/Csrf.php b/api/src/Csrf.php index f2a4dc6ef8..4cd88e6937 100644 --- a/api/src/Csrf.php +++ b/api/src/Csrf.php @@ -12,15 +12,12 @@ namespace EGroupware\Api; -// explicitly reference classes still in phpgwapi -use auth; - /** * Class supplying methods to prevent successful CSRF by requesting a random token, * stored on server and validated when request get posted. * * CSRF token generation used openssl_random_pseudo_bytes, if available, otherwise - * mt_rand based auth::randomstring is used. + * mt_rand based Auth::randomstring is used. * * CSRF tokens are stored (incl. optional purpose) in user session. * @@ -41,10 +38,10 @@ class Csrf { throw new egw_exception_wrong_parameter(__METHOD__.'(NULL) $_purspose must NOT be NULL!'); } - // generate random token (using oppenssl if available otherwise mt_rand based auth::randomstring) + // generate random token (using oppenssl if available otherwise mt_rand based Auth::randomstring) $token = function_exists('openssl_random_pseudo_bytes') ? base64_encode(openssl_random_pseudo_bytes(64)) : - auth::randomstring(64); + Auth::randomstring(64); // store it in session for later validation Cache::setSession(__CLASS__, $token, $_purpose); diff --git a/api/src/Ldap.php b/api/src/Ldap.php index a9c91cafcb..4df44bd569 100644 --- a/api/src/Ldap.php +++ b/api/src/Ldap.php @@ -25,6 +25,8 @@ namespace EGroupware\Api; * If multiple (space-separated) ldap hosts or urls are given, try them in order and * move first successful one to first place in session, to try not working ones * only once per session. + * + * Use Api\Ldap::factory($resource=true, $host='', $dn='', $passwd='') to open only a single connection. */ class Ldap { @@ -60,6 +62,40 @@ class Ldap $this->restoreSessionData(); } + /** + * Connections created with factory method + * + * @var array + */ + protected static $connections = array(); + + /** + * Connect to ldap server and return a handle or Api\Ldap object + * + * Use this factory method to open only a single connection to LDAP server! + * + * @param boolean $ressource =true true: return LDAP ressource for ldap_*-methods, + * false: return connected instances of this Api\Ldap class + * @param string $host ='' ldap host, default $GLOBALS['egw_info']['server']['ldap_host'] + * @param string $dn ='' ldap dn, default $GLOBALS['egw_info']['server']['ldap_root_dn'] + * @param string $passwd ='' ldap pw, default $GLOBALS['egw_info']['server']['ldap_root_pw'] + * @return resource|Ldap resource from ldap_connect() or false on error + * @throws Exception\AssertingFailed 'LDAP support unavailable!' (no ldap extension) + * @throws Exception\NoPermission if bind fails + */ + public static function factory($ressource=true, $host='', $dn='', $passwd='') + { + $key = md5($host.':'.$dn.':'.$passwd); + + if (!isset(self::$connections[$key])) + { + self::$connections[$key] = new Ldap(true); + + self::$connections[$key]->ldapConnect($host, $dn, $passwd); + } + return $ressource ? self::$connections[$key]->ds : self::$connections[$key]; + } + /** * Returns information about connected ldap server * @@ -121,11 +157,12 @@ class Ldap * move first successful one to first place in session, to try not working ones * only once per session. * - * @param $host ='' ldap host, default $GLOBALS['egw_info']['server']['ldap_host'] - * @param $dn ='' ldap dn, default $GLOBALS['egw_info']['server']['ldap_root_dn'] - * @param $passwd ='' ldap pw, default $GLOBALS['egw_info']['server']['ldap_root_pw'] + * @param string $host ='' ldap host, default $GLOBALS['egw_info']['server']['ldap_host'] + * @param string $dn ='' ldap dn, default $GLOBALS['egw_info']['server']['ldap_root_dn'] + * @param string $passwd ='' ldap pw, default $GLOBALS['egw_info']['server']['ldap_root_pw'] * @return resource|boolean resource from ldap_connect() or false on error * @throws Exception\AssertingFailed 'LDAP support unavailable!' (no ldap extension) + * @throws Exception\NoPermission if bind fails */ function ldapConnect($host='', $dn='', $passwd='') { diff --git a/api/src/Session.php b/api/src/Session.php index b5d218cd0a..682968ecab 100644 --- a/api/src/Session.php +++ b/api/src/Session.php @@ -24,10 +24,8 @@ namespace EGroupware\Api; // explicitly reference classes still in phpgwapi use egw_mailer; -use common; // randomstring use egw_digest_auth; // egw_digest_auth::parse_digest use html; // html::$ua_mobile -use auth; // check_password_change /** * Create, verifies or destroys an EGroupware session @@ -230,7 +228,7 @@ class Session } if (!isset($GLOBALS['egw_info']['server']['install_id'])) { - $GLOBALS['egw_info']['server']['install_id'] = md5(common::randomstring(15)); + $GLOBALS['egw_info']['server']['install_id'] = md5(Auth::randomstring(15)); } if (!isset($GLOBALS['egw_info']['server']['max_history'])) { @@ -511,7 +509,7 @@ class Session if (self::ERROR_LOG_DEBUG) error_log(__METHOD__."($this->login,$this->passwd,$this->passwd_type,$no_session,$auth_check) UNSUCCESSFULL ($this->reason)"); return false; } - if ($fail_on_forced_password_change && auth::check_password_change($this->reason) === false) + if ($fail_on_forced_password_change && Auth::check_password_change($this->reason) === false) { $this->cd_reason = self::CD_FORCE_PASSWORD_CHANGE; return false; @@ -550,7 +548,7 @@ class Session } $this->sessionid = session_id(); } - $this->kp3 = common::randomstring(24); + $this->kp3 = Auth::randomstring(24); $GLOBALS['egw_info']['user'] = $this->read_repositories(); if ($GLOBALS['egw']->accounts->is_expired($GLOBALS['egw_info']['user'])) diff --git a/phpgwapi/inc/class.auth.inc.php b/phpgwapi/inc/class.auth.inc.php index e10ea2251c..c594446179 100644 --- a/phpgwapi/inc/class.auth.inc.php +++ b/phpgwapi/inc/class.auth.inc.php @@ -1,6 +1,6 @@ @@ -12,150 +12,15 @@ * @version $Id$ */ -// allow to set an application depending authentication type (eg. for syncml, groupdav, ...) -if (isset($GLOBALS['egw_info']['server']['auth_type_'.$GLOBALS['egw_info']['flags']['currentapp']]) && - $GLOBALS['egw_info']['server']['auth_type_'.$GLOBALS['egw_info']['flags']['currentapp']]) -{ - $GLOBALS['egw_info']['server']['auth_type'] = $GLOBALS['egw_info']['server']['auth_type_'.$GLOBALS['egw_info']['flags']['currentapp']]; -} -if(empty($GLOBALS['egw_info']['server']['auth_type'])) -{ - $GLOBALS['egw_info']['server']['auth_type'] = 'sql'; -} -//error_log('using auth_type='.$GLOBALS['egw_info']['server']['auth_type'].', currentapp='.$GLOBALS['egw_info']['flags']['currentapp']); +use EGroupware\Api; /** - * eGroupWare API - Authentication baseclass, password auth and crypt functions + * Authentication * - * Many functions based on code from Frank Thomas - * which can be seen at http://www.thomas-alfeld.de/frank/ - * - * Other functions from class.common.inc.php originally from phpGroupWare + * @deprecated use Api\Auth */ -class auth +class auth extends Api\Auth { - static $error; - - /** - * Holds instance of backend - * - * @var auth_backend - */ - private $backend; - - function __construct() - { - $backend_class = 'auth_'.$GLOBALS['egw_info']['server']['auth_type']; - - $this->backend = new $backend_class; - - if (!($this->backend instanceof auth_backend)) - { - throw new egw_exception_assertion_failed("Auth backend class $backend_class is NO auth_backend!"); - } - } - - /** - * check if users are supposed to change their password every x sdays, then check if password is of old age - * or the devil-admin reset the users password and forced the user to change his password on next login. - * - * @param string& $message =null on return false: message why password needs to be changed - * @return boolean true: all good, false: password change required, null: password expires in N days - */ - static function check_password_change(&$message=null) - { - // dont check anything for anonymous sessions/ users that are flagged as anonymous - if (is_object($GLOBALS['egw']->session) && $GLOBALS['egw']->session->session_flags == 'A') return true; - - // some statics (and initialisation to make information and timecalculation a) more readable in conditions b) persistent per request - // if user has to be warned about an upcomming passwordchange, remember for the session, that he was informed - static $UserKnowsAboutPwdChange=null; - if (is_null($UserKnowsAboutPwdChange)) $UserKnowsAboutPwdChange =& egw_cache::getSession('phpgwapi','auth_UserKnowsAboutPwdChange'); - - // retrieve the timestamp regarding the last change of the password from auth system and store it with the session - static $alpwchange_val=null; - static $pwdTsChecked=null; - if (is_null($pwdTsChecked) && is_null($alpwchange_val) || (string)$alpwchange_val === '0') - { - $alpwchange_val =& egw_cache::getSession('phpgwapi','auth_alpwchange_val'); // set that one with the session stored value - // initalize statics - better readability of conditions - if (is_null($alpwchange_val) || (string)$alpwchange_val === '0') - { - $backend_class = 'auth_'.$GLOBALS['egw_info']['server']['auth_type']; - $backend = new $backend_class; - // this may change behavior, as it should detect forced PasswordChanges from your Authentication System too. - // on the other side, if your auth system does not require an forcedPasswordChange, you will not be asked. - if (method_exists($backend,'getLastPwdChange')) - { - $alpwchange_val = $backend->getLastPwdChange($GLOBALS['egw']->session->account_lid); - $pwdTsChecked = true; - } - // if your authsystem does not provide that information, its likely, that you cannot change your password there, - // thus checking for expiration, is not needed - if ($alpwchange_val === false) - { - $alpwchange_val = null; - } - //error_log(__METHOD__.__LINE__.'#'.$alpwchange_val.'# is null:'.is_null($alpwchange_val).'# is empty:'.empty($alpwchange_val).'# is set:'.isset($alpwchange_val)); - } - } - static $passwordAgeBorder=null; - static $daysLeftUntilChangeReq=null; - - // if neither timestamp isset return true, nothing to do (exept this means the password is too old) - if (is_null($alpwchange_val) && - empty($GLOBALS['egw_info']['server']['change_pwd_every_x_days'])) - { - return true; - } - if (is_null($passwordAgeBorder) && $GLOBALS['egw_info']['server']['change_pwd_every_x_days']) - { - $passwordAgeBorder = (egw_time::to('now','ts')-($GLOBALS['egw_info']['server']['change_pwd_every_x_days']*86400)); - } - if (is_null($daysLeftUntilChangeReq) && $GLOBALS['egw_info']['server']['warn_about_upcoming_pwd_change']) - { - // maxage - passwordage = days left until change is required - $daysLeftUntilChangeReq = ($GLOBALS['egw_info']['server']['change_pwd_every_x_days'] - ((egw_time::to('now','ts')-($alpwchange_val?$alpwchange_val:0))/86400)); - } - if ($alpwchange_val == 0 || // admin requested password change - $passwordAgeBorder > $alpwchange_val || // change password every N days policy requests change - // user should be warned N days in advance about change and is not yet - $GLOBALS['egw_info']['server']['change_pwd_every_x_days'] && - $GLOBALS['egw_info']['user']['apps']['preferences'] && - $GLOBALS['egw_info']['server']['warn_about_upcoming_pwd_change'] && - $GLOBALS['egw_info']['server']['warn_about_upcoming_pwd_change'] > $daysLeftUntilChangeReq && - $UserKnowsAboutPwdChange !== true) - { - if ($alpwchange_val == 0) - { - $message = lang('An admin required that you must change your password upon login.'); - } - elseif ($passwordAgeBorder > $alpwchange_val && $alpwchange_val > 0) - { - error_log(__METHOD__.' Password of '.$GLOBALS['egw_info']['user']['account_lid'].' ('.$GLOBALS['egw_info']['user']['account_fullname'].') is of old age.'.array2string(array( - 'ts'=> $alpwchange_val, - 'date'=>egw_time::to($alpwchange_val)))); - $message = lang('It has been more then %1 days since you changed your password',$GLOBALS['egw_info']['server']['change_pwd_every_x_days']); - } - else - { - // login page does not inform user about passwords about to expire - if ($GLOBALS['egw_info']['flags']['currentapp'] != 'login' && - ($GLOBALS['egw_info']['flags']['currentapp'] != 'home' || - strpos($_SERVER['SCRIPT_NAME'], '/home/') !== false)) - { - $UserKnowsAboutPwdChange = true; - } - $message = lang('Your password is about to expire in %1 days, you may change your password now',round($daysLeftUntilChangeReq)); - // user has no rights to change password --> do NOT warn, as only forced check ignores rights - if ($GLOBALS['egw']->acl->check('nopasswordchange', 1, 'preferences')) return true; - return null; - } - return false; - } - return true; - } - /** * Retired password check method called all over the place * @@ -165,621 +30,14 @@ class auth { return true; // no change } - - /** - * fetch the last pwd change for the user - * - * @param string $username username of account to authenticate - * @return mixed false or shadowlastchange*24*3600 - */ - function getLastPwdChange($username) - { - if (method_exists($this->backend,'getLastPwdChange')) return $this->backend->getLastPwdChange($username); - return false; - } - - /** - * changes account_lastpwd_change in ldap datababse - * - * @param int $account_id account id of user whose passwd should be changed - * @param string $passwd must be cleartext, usually not used, but may be used to authenticate as user to do the change -> ldap - * @param int $lastpwdchange must be a unixtimestamp - * @return boolean true if account_lastpwd_change successful changed, false otherwise - */ - function setLastPwdChange($account_id=0, $passwd=NULL, $lastpwdchange=NULL) - { - if (method_exists($this->backend,'setLastPwdChange')) return $this->backend->setLastPwdChange($account_id, $passwd, $lastpwdchange); - return false; - } - - /** - * password authentication against password stored in sql datababse - * - * @param string $username username of account to authenticate - * @param string $passwd corresponding password - * @param string $passwd_type ='text' 'text' for cleartext passwords (default) - * @return boolean true if successful authenticated, false otherwise - */ - function authenticate($username, $passwd, $passwd_type='text') - { - return $this->backend->authenticate($username, $passwd, $passwd_type); - } - - /** - * Calls crackcheck to enforce password strength (if configured) and changes password - * - * @param string $old_passwd must be cleartext - * @param string $new_passwd must be cleartext - * @param int $account_id account id of user whose passwd should be changed - * @throws egw_exception_wrong_userinput if configured password strength is not meat - * @throws Exception from backends having extra requirements - * @return boolean true if password successful changed, false otherwise - */ - function change_password($old_passwd, $new_passwd, $account_id=0) - { - if (($err = self::crackcheck($new_passwd,null,null,null,$account_id))) - { - throw new egw_exception_wrong_userinput($err); - } - if (($ret = $this->backend->change_password($old_passwd, $new_passwd, $account_id))) - { - if ($account_id == $GLOBALS['egw']->session->account_id) - { - // need to change current users password in session - egw_cache::setSession('phpgwapi', 'password', base64_encode($new_passwd)); - $GLOBALS['egw_info']['user']['passwd'] = $new_passwd; - $GLOBALS['egw_info']['user']['account_lastpwd_change'] = egw_time::to('now','ts'); - // invalidate EGroupware session, as password is stored in egw_info in session - egw::invalidate_session_cache(); - } - accounts::cache_invalidate($account_id); - // run changepwasswd hook - $GLOBALS['hook_values'] = array( - 'account_id' => $account_id, - 'account_lid' => accounts::id2name($account_id), - 'old_passwd' => $old_passwd, - 'new_passwd' => $new_passwd, - ); - $GLOBALS['egw']->hooks->process($GLOBALS['hook_values']+array( - 'location' => 'changepassword' - ),False,True); // called for every app now, not only enabled ones) - } - return $ret; - } - - /** - * return a random string of letters [0-9a-zA-Z] of size $size - * - * @param $size int-size of random string to return - */ - static function randomstring($size) - { - static $random_char = array( - '0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f', - 'g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v', - 'w','x','y','z','A','B','C','D','E','F','G','H','I','J','K','L', - 'M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z' - ); - - $s = ''; - for ($i=0; $i<$size; $i++) - { - $s .= $random_char[mt_rand(1,61)]; - } - return $s; - } - - /** - * encrypt password - * - * uses the encryption type set in setup and calls the appropriate encryption functions - * - * @param $password password to encrypt - */ - static function encrypt_password($password,$sql=False) - { - if($sql) - { - return self::encrypt_sql($password); - } - return self::encrypt_ldap($password); - } - - /** - * compares an encrypted password - * - * encryption type set in setup and calls the appropriate encryption functions - * - * @param string $cleartext cleartext password - * @param string $encrypted encrypted password, can have a {hash} prefix, which overrides $type - * @param string $type_in type of encryption - * @param string $username used as optional key of encryption for md5_hmac - * @param string &$type =null on return detected type of hash - * @return boolean - */ - static function compare_password($cleartext, $encrypted, $type_in, $username='', &$type=null) - { - // allow to specify the hash type to prefix the hash, to easy migrate passwords from ldap - $type = $type_in; - $saved_enc = $encrypted; - $matches = null; - if (preg_match('/^\\{([a-z_5]+)\\}(.+)$/i',$encrypted,$matches)) - { - $type = strtolower($matches[1]); - $encrypted = $matches[2]; - - switch($type) // some hashs are specially "packed" in ldap - { - case 'md5': - $encrypted = implode('',unpack('H*',base64_decode($encrypted))); - break; - case 'plain': - case 'crypt': - // nothing to do - break; - default: - $encrypted = $saved_enc; - break; - } - } - elseif($encrypted[0] == '$') - { - $type = 'crypt'; - } - - switch($type) - { - case 'plain': - $ret = $cleartext === $encrypted; - break; - case 'smd5': - $ret = self::smd5_compare($cleartext,$encrypted); - break; - case 'sha': - $ret = self::sha_compare($cleartext,$encrypted); - break; - case 'ssha': - $ret = self::ssha_compare($cleartext,$encrypted); - break; - case 'crypt': - case 'des': - case 'md5_crypt': - case 'blowish_crypt': // was for some time a typo in setup - case 'blowfish_crypt': - case 'ext_crypt': - case 'sha256_crypt': - case 'sha512_crypt': - $ret = self::crypt_compare($cleartext, $encrypted, $type); - break; - case 'md5_hmac': - $ret = self::md5_hmac_compare($cleartext,$encrypted,$username); - break; - default: - $type = 'md5'; - // fall through - case 'md5': - $ret = md5($cleartext) === $encrypted; - break; - } - //error_log(__METHOD__."('$cleartext', '$encrypted', '$type_in', '$username') type='$type' returning ".array2string($ret)); - return $ret; - } - - /** - * Parameters used for crypt: const name, salt prefix, len of random salt, postfix - * - * @var array - */ - static $crypt_params = array( // - 'crypt' => array('CRYPT_STD_DES', '', 2, ''), - 'ext_crypt' => array('CRYPT_EXT_DES', '_J9..', 4, ''), - 'md5_crypt' => array('CRYPT_MD5', '$1$', 8, '$'), - //'old_blowfish_crypt' => array('CRYPT_BLOWFISH', '$2$', 13, ''), // old blowfish hash not in line with php.net docu, but could be in use - 'blowfish_crypt' => array('CRYPT_BLOWFISH', '$2a$12$', 22, ''), // $2a$12$ = 2^12 = 4096 rounds - 'sha256_crypt' => array('CRYPT_SHA256', '$5$', 16, '$'), // no "round=N$" --> default of 5000 rounds - 'sha512_crypt' => array('CRYPT_SHA512', '$6$', 16, '$'), // no "round=N$" --> default of 5000 rounds - ); - - /** - * compare crypted passwords for authentication whether des,ext_des,md5, or blowfish crypt - * - * @param string $form_val user input value for comparison - * @param string $db_val stored value / hash (from database) - * @param string &$type detected crypt type on return - * @return boolean True on successful comparison - */ - static function crypt_compare($form_val, $db_val, &$type) - { - // detect type of hash by salt part of $db_val - list($first, $dollar, $salt, $salt2) = explode('$', $db_val); - foreach(self::$crypt_params as $type => $params) - { - list(,$prefix, $random, $postfix) = $params; - list(,$d) = explode('$', $prefix); - if ($dollar === $d || !$dollar && ($first[0] === $prefix[0] || $first[0] !== '_' && !$prefix)) - { - $len = !$postfix ? strlen($prefix)+$random : strlen($prefix.$salt.$postfix); - // sha(256|512) might contain options, explicit $rounds=N$ prefix in salt - if (($type == 'sha256_crypt' || $type == 'sha512_crypt') && substr($salt, 0, 7) === 'rounds=') - { - $len += strlen($salt2)+1; - } - break; - } - } - - $full_salt = substr($db_val, 0, $len); - $new_hash = crypt($form_val, $full_salt); - //error_log(__METHOD__."('$form_val', '$db_val') type=$type --> len=$len --> salt='$full_salt' --> new_hash='$new_hash' returning ".array2string($db_val === $new_hash)); - - return $db_val === $new_hash; - } - - /** - * encrypt password for ldap - * - * uses the encryption type set in setup and calls the appropriate encryption functions - * - * @param string $password password to encrypt - * @param string $type =null default to $GLOBALS['egw_info']['server']['ldap_encryption_type'] - * @return string - */ - static function encrypt_ldap($password, $type=null) - { - if (is_null($type)) $type = $GLOBALS['egw_info']['server']['ldap_encryption_type']; - - $salt = ''; - switch(strtolower($type)) - { - default: // eg. setup >> config never saved - case 'des': - case 'blowish_crypt': // was for some time a typo in setup - $type = $type == 'blowish_crypt' ? 'blowfish_crypt' : 'crypt'; - // fall through - case 'crypt': - case 'sha256_crypt': - case 'sha512_crypt': - case 'blowfish_crypt': - case 'md5_crypt': - case 'ext_crypt': - list($const, $prefix, $len, $postfix) = self::$crypt_params[$type]; - if(defined($const) && constant($const) == 1) - { - $salt = $prefix.self::randomstring($len).$postfix; - $e_password = '{crypt}'.crypt($password, $salt); - break; - } - self::$error = 'no '.str_replace('_', ' ', $type); - $e_password = false; - break; - case 'md5': - /* New method taken from the openldap-software list as recommended by - * Kervin L. Pierre" - */ - $e_password = '{md5}' . base64_encode(pack("H*",md5($password))); - break; - case 'smd5': - $salt = self::randomstring(16); - $hash = md5($password . $salt, true); - $e_password = '{SMD5}' . base64_encode($hash . $salt); - break; - case 'sha': - $e_password = '{SHA}' . base64_encode(sha1($password,true)); - break; - case 'ssha': - $salt = self::randomstring(16); - $hash = sha1($password . $salt, true); - $e_password = '{SSHA}' . base64_encode($hash . $salt); - break; - case 'plain': - // if plain no type is prepended - $e_password = $password; - break; - } - //error_log(__METHOD__."('$password', ".array2string($type).") returning ".array2string($e_password).(self::$error ? ' error='.self::$error : '')); - return $e_password; - } - - /** - * Create a password for storage in the accounts table - * - * @param string $password - * @param string $type =null default $GLOBALS['egw_info']['server']['sql_encryption_type'] - * @return string hash - */ - static function encrypt_sql($password, $type=null) - { - /* Grab configured type, or default to md5() (old method) */ - if (is_null($type)) - { - $type = @$GLOBALS['egw_info']['server']['sql_encryption_type'] ? - strtolower($GLOBALS['egw_info']['server']['sql_encryption_type']) : 'md5'; - } - switch($type) - { - case 'plain': - // since md5 is the default, type plain must be prepended, for eGroupware to understand - $e_password = '{PLAIN}'.$password; - break; - - case 'md5': - /* This is the old standard for password storage in SQL */ - $e_password = md5($password); - break; - - // all other types are identical to ldap, so no need to doublicate the code here - case 'des': - case 'blowish_crypt': // was for some time a typo in setup - case 'crypt': - case 'sha256_crypt': - case 'sha512_crypt': - case 'blowfish_crypt': - case 'md5_crypt': - case 'ext_crypt': - case 'smd5': - case 'sha': - case 'ssha': - $e_password = self::encrypt_ldap($password, $type); - break; - - default: - self::$error = 'no valid encryption available'; - $e_password = false; - break; - } - //error_log(__METHOD__."('$password') using '$type' returning ".array2string($e_password).(self::$error ? ' error='.self::$error : '')); - return $e_password; - } - - /** - * Get available password hashes sorted by securest first - * - * @param string &$securest =null on return securest available hash - * @return array hash => label - */ - public static function passwdhashes(&$securest=null) - { - $hashes = array(); - - /* Check for available crypt methods based on what is defined by php */ - if(defined('CRYPT_BLOWFISH') && CRYPT_BLOWFISH == 1) - { - $hashes['blowfish_crypt'] = 'blowfish_crypt'; - } - if(defined('CRYPT_SHA512') && CRYPT_SHA512 == 1) - { - $hashes['sha512_crypt'] = 'sha512_crypt'; - } - if(defined('CRYPT_SHA256') && CRYPT_SHA256 == 1) - { - $hashes['sha256_crypt'] = 'sha256_crypt'; - } - if(defined('CRYPT_MD5') && CRYPT_MD5 == 1) - { - $hashes['md5_crypt'] = 'md5_crypt'; - } - if(defined('CRYPT_EXT_DES') && CRYPT_EXT_DES == 1) - { - $hashes['ext_crypt'] = 'ext_crypt'; - } - $hashes += array( - 'ssha' => 'ssha', - 'smd5' => 'smd5', - 'sha' => 'sha', - ); - if(@defined('CRYPT_STD_DES') && CRYPT_STD_DES == 1) - { - $hashes['crypt'] = 'crypt'; - } - - $hashes += array( - 'md5' => 'md5', - 'plain' => 'plain', - ); - - // mark the securest algorithm for the user - list($securest) = each($hashes); reset($hashes); - $hashes[$securest] .= ' ('.lang('securest').')'; - - return $hashes; - } - - /** - * Checks if a given password is "safe" - * - * @link http://technet.microsoft.com/en-us/library/cc786468(v=ws.10).aspx - * In contrary to whats documented in above link, windows seems to treet numbers as delimiters too. - * - * Windows compatible check is $reqstrength=3, $minlength=7, $forbid_name=true - * - * @param string $passwd - * @param int $reqstrength =null defaults to whatever set in config for "force_pwd_strength" - * @param int $minlength =null defaults to whatever set in config for "check_save_passwd" - * @param string $forbid_name =null if "yes" username or full-name split by delimiters AND longer then 3 chars are - * forbidden to be included in password, default to whatever set in config for "passwd_forbid_name" - * @param array|int $account =null array with account_lid and account_fullname or account_id for $forbid_name check - * @return mixed false if password is considered "safe" (or no requirements) or a string $message if "unsafe" - */ - static function crackcheck($passwd, $reqstrength=null, $minlength=null, $forbid_name=null, $account=null) - { - if (!isset($reqstrength)) $reqstrength = $GLOBALS['egw_info']['server']['force_pwd_strength']; - if (!isset($minlength)) $minlength = $GLOBALS['egw_info']['server']['force_pwd_length']; - if (!isset($forbid_name)) $forbid_name = $GLOBALS['egw_info']['server']['passwd_forbid_name']; - - // load preferences translations, as changepassword get's called from admin too - translation::add_app('preferences'); - - // check for and if necessary convert old values True and 5 to new separate values for length and char-classes - if ($GLOBALS['egw_info']['server']['check_save_passwd'] || $reqstrength == 5) - { - if (!isset($reqstrength) || $reqstrength == 5) - { - config::save_value('force_pwd_strength', $reqstrength=4, 'phpgwapi'); - } - if (!isset($minlength)) - { - config::save_value('force_pwd_length', $minlength=7, 'phpgwapi'); - } - config::save_value('check_save_passwd', null, 'phpgwapi'); - } - - $errors = array(); - - if ($minlength && strlen($passwd) < $minlength) - { - $errors[] = lang('password must have at least %1 characters', $minlength); - } - - if ($forbid_name === 'yes') - { - if (!$account || !is_array($account) && !($account = $GLOBALS['egw']->accounts->read($account))) - { - throw new egw_exception_wrong_parameter('crackcheck(..., forbid_name=true, account) requires account-data!'); - } - $parts = preg_split("/[,._ \t0-9-]+/", $account['account_fullname'].','.$account['account_lid']); - foreach($parts as $part) - { - if (strlen($part) > 2 && stripos($passwd, $part) !== false) - { - $errors[] = lang('password contains with "%1" a parts of your user- or full-name (3 or more characters long)', $part); - break; - } - } - } - - if ($reqstrength) - { - $missing = array(); - if (!preg_match('/(.*\d.*){'. ($non=1). ',}/',$passwd)) - { - $missing[] = lang('numbers'); - } - if (!preg_match('/(.*[[:upper:]].*){'. ($nou=1). ',}/',$passwd)) - { - $missing[] = lang('uppercase letters'); - } - if (!preg_match('/(.*[[:lower:]].*){'. ($nol=1). ',}/',$passwd)) - { - $missing[] = lang('lowercase letters'); - } - if (!preg_match('/['.preg_quote('~!@#$%^&*_-+=`|\(){}[]:;"\'<>,.?/', '/').']/', $passwd)) - { - $missing[] = lang('special characters'); - } - if (4 - count($missing) < $reqstrength) - { - $errors[] = lang('password contains only %1 of required %2 character classes: no %3', - 4-count($missing), $reqstrength, implode(', ', $missing)); - } - } - if ($errors) - { - return lang('Your password does not have required strength:'). - "
\n- ".implode("
\n- ", $errors); - } - return false; - } - - /** - * compare SMD5-encrypted passwords for authentication - * - * @param string $form_val user input value for comparison - * @param string $db_val stored value (from database) - * @return boolean True on successful comparison - */ - static function smd5_compare($form_val,$db_val) - { - /* Start with the first char after {SMD5} */ - $hash = base64_decode(substr($db_val,6)); - - /* SMD5 hashes are 16 bytes long */ - $orig_hash = cut_bytes($hash, 0, 16); // binary string need to use cut_bytes, not mb_substr(,,'utf-8')! - $salt = cut_bytes($hash, 16); - - $new_hash = md5($form_val . $salt,true); - //echo '
DB: ' . base64_encode($orig_hash) . '
FORM: ' . base64_encode($new_hash); - - return strcmp($orig_hash,$new_hash) == 0; - } - - /** - * compare SHA-encrypted passwords for authentication - * - * @param string $form_val user input value for comparison - * @param string $db_val stored value (from database) - * @return boolean True on successful comparison - */ - static function sha_compare($form_val,$db_val) - { - /* Start with the first char after {SHA} */ - $hash = base64_decode(substr($db_val,5)); - $new_hash = sha1($form_val,true); - //echo '
DB: ' . base64_encode($orig_hash) . '
FORM: ' . base64_encode($new_hash); - - return strcmp($hash,$new_hash) == 0; - } - - /** - * compare SSHA-encrypted passwords for authentication - * - * @param string $form_val user input value for comparison - * @param string $db_val stored value (from database) - * @return boolean True on successful comparison - */ - static function ssha_compare($form_val,$db_val) - { - /* Start with the first char after {SSHA} */ - $hash = base64_decode(substr($db_val, 6)); - - // SHA-1 hashes are 160 bits long - $orig_hash = cut_bytes($hash, 0, 20); // binary string need to use cut_bytes, not mb_substr(,,'utf-8')! - $salt = cut_bytes($hash, 20); - $new_hash = sha1($form_val . $salt,true); - - //error_log(__METHOD__."('$form_val', '$db_val') hash='$hash', orig_hash='$orig_hash', salt='$salt', new_hash='$new_hash' returning ".array2string(strcmp($orig_hash,$new_hash) == 0)); - return strcmp($orig_hash,$new_hash) == 0; - } - - /** - * compare md5_hmac-encrypted passwords for authentication (see RFC2104) - * - * @param string $form_val user input value for comparison - * @param string $db_val stored value (from database) - * @param string $_key key for md5_hmac-encryption (username for imported smf users) - * @return boolean True on successful comparison - */ - static function md5_hmac_compare($form_val,$db_val,$_key) - { - $key = str_pad(strlen($_key) <= 64 ? $_key : pack('H*', md5($_key)), 64, chr(0x00)); - $md5_hmac = md5(($key ^ str_repeat(chr(0x5c), 64)) . pack('H*', md5(($key ^ str_repeat(chr(0x36), 64)). $form_val))); - - return strcmp($md5_hmac,$db_val) == 0; - } } /** - * Interface for authentication backend + * @deprecated use Api\Auth\Backend */ -interface auth_backend -{ - /** - * password authentication against password stored in sql datababse - * - * @param string $username username of account to authenticate - * @param string $passwd corresponding password - * @param string $passwd_type ='text' 'text' for cleartext passwords (default) - * @return boolean true if successful authenticated, false otherwise - */ - function authenticate($username, $passwd, $passwd_type='text'); +interface auth_backend extends Api\Auth\Backend {} - /** - * changes password in sql datababse - * - * @param string $old_passwd must be cleartext - * @param string $new_passwd must be cleartext - * @param int $account_id account id of user whose passwd should be changed - * @throws Exception to give a verbose error, why changing password failed - * @return boolean true if password successful changed, false otherwise - */ - function change_password($old_passwd, $new_passwd, $account_id=0); -} +/** + * @deprecated use Api\Auth\Fallback + */ +class auth_fallback extends Api\Auth\Fallback {} diff --git a/phpgwapi/inc/class.auth_fallbackmail2sql.inc.php b/phpgwapi/inc/class.auth_fallbackmail2sql.inc.php deleted file mode 100644 index 9cdfcc1f81..0000000000 --- a/phpgwapi/inc/class.auth_fallbackmail2sql.inc.php +++ /dev/null @@ -1,139 +0,0 @@ - - * @license http://opensource.org/licenses/lgpl-license.php LGPL - GNU Lesser General Public License - * @package api - * @subpackage authentication - * @version $Id$ - */ - -/** - * Authentication agains a mail Server with fallback to SQL - * - * For other fallback types, simply change auth backends in constructor call - */ -class auth_fallbackmail2sql implements auth_backend -{ - /** - * Primary auth backend - * - * @var auth_backend - */ - private $primary_backend; - - /** - * Fallback auth backend - * - * @var auth_backend - */ - private $fallback_backend; - - /** - * Constructor - */ - function __construct($primary='auth_mail',$fallback='auth_sql') - { - $this->primary_backend = new $primary; - - $this->fallback_backend = new $fallback; - } - - /** - * authentication against LDAP with fallback to SQL - * - * @param string $username username of account to authenticate - * @param string $passwd corresponding password - * @return boolean true if successful authenticated, false otherwise - */ - function authenticate($username, $passwd, $passwd_type='text') - { - if ($this->primary_backend->authenticate($username, $passwd, $passwd_type)) - { - egw_cache::setInstance(__CLASS__,'backend_used-'.$username,'primary'); - return true; - } - if ($this->fallback_backend->authenticate($username,$passwd, $passwd_type)) - { - egw_cache::setInstance(__CLASS__,'backend_used-'.$username,'fallback'); - return true; - } - return false; - } - - /** - * changes password in LDAP - * - * If $old_passwd is given, the password change is done binded as user and NOT with the - * "root" dn given in the configurations. - * - * @param string $old_passwd must be cleartext or empty to not to be checked - * @param string $new_passwd must be cleartext - * @param int $account_id account id of user whose passwd should be changed - * @return boolean true if password successful changed, false otherwise - */ - function change_password($old_passwd, $new_passwd, $account_id=0) - { - if(!$account_id || $GLOBALS['egw_info']['flags']['currentapp'] == 'login') - { - $admin = False; - $account_id = $GLOBALS['egw_info']['user']['account_id']; - $username = $GLOBALS['egw_info']['user']['account_lid']; - } - else - { - $username = $GLOBALS['egw']->accounts->id2name($account_id); - } - if (egw_cache::getInstance(__CLASS__,'backend_used-'.$username) == 'primary') - { - return false; - } - return $this->fallback_backend->change_password($old_passwd, $new_passwd, $account_id); - } - - /** - * fetch the last pwd change for the user - * - * @param string $username username of account to authenticate - * @return mixed false or account_lastpwd_change - */ - function getLastPwdChange($username) - { - if (egw_cache::getInstance(__CLASS__,'backend_used-'.$username) == 'primary') - { - return false; - } - if (method_exists($this->fallback_backend,'getLastPwdChange')) return $this->fallback_backend->getLastPwdChange($username); - return false; - } - - /** - * changes account_lastpwd_change in sql datababse - * - * @param int $account_id account id of user whose passwd should be changed - * @param string $passwd must be cleartext, usually not used, but may be used to authenticate as user to do the change -> ldap - * @param int $lastpwdchange must be a unixtimestamp - * @return boolean true if account_lastpwd_change successful changed, false otherwise - */ - function setLastPwdChange($account_id=0, $passwd=NULL, $lastpwdchange=NULL) - { - if(!$account_id || $GLOBALS['egw_info']['flags']['currentapp'] == 'login') - { - $admin = False; - $account_id = $GLOBALS['egw_info']['user']['account_id']; - $username = $GLOBALS['egw_info']['user']['account_lid']; - } - else - { - $username = $GLOBALS['egw']->accounts->id2name($account_id); - } - if (egw_cache::getInstance(__CLASS__,'backend_used-'.$username) == 'primary') - { - return false; - } - if (method_exists($this->fallback_backend,'setLastPwdChange')) return $this->fallback_backend->setLastPwdChange($account_id, $passwd, $lastpwdchange); - return false; - } -} diff --git a/phpgwapi/inc/class.common.inc.php b/phpgwapi/inc/class.common.inc.php index 247cac1f91..c4254dc2fc 100644 --- a/phpgwapi/inc/class.common.inc.php +++ b/phpgwapi/inc/class.common.inc.php @@ -250,16 +250,16 @@ class common /** * connect to the ldap server and return a handle * - * @deprecated use Api\Ldap::ldapConnect() - * @param $host ldap host - * @param $dn ldap_root_dn - * @param $passwd ldap_root_pw + * @deprecated use Api\Ldap::factory(true, $host, $dn, $passwd) + * @param string $host ='' ldap host + * @param string $dn ='' ldap_root_dn + * @param string $passwd ='' ldap_root_pw * @return resource */ static function ldapConnect($host='', $dn='', $passwd='') { // use Lars new ldap class - return $GLOBALS['egw']->ldap->ldapConnect($host,$dn,$passwd); + return Api\Ldap::factory(true, $host, $dn, $passwd); } /** @@ -287,22 +287,11 @@ class common * return a random string of size $size * * @param $size int-size of random string to return + * @deprecated use Api\Auth::randomstring($size) */ static function randomstring($size) { - static $random_char = array( - '0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f', - 'g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v', - 'w','x','y','z','A','B','C','D','E','F','G','H','I','J','K','L', - 'M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z' - ); - - $s = ''; - for ($i=0; $i < $size; $i++) - { - $s .= $random_char[mt_rand(0,count($random_char)-1)]; - } - return $s; + return Api\Auth::randomstring($size); } /** diff --git a/phpgwapi/inc/class.egw.inc.php b/phpgwapi/inc/class.egw.inc.php index cc99a081be..e8465d6fdc 100644 --- a/phpgwapi/inc/class.egw.inc.php +++ b/phpgwapi/inc/class.egw.inc.php @@ -668,8 +668,9 @@ class egw_minimal 'framework' => true, // special handling in __get() 'template' => 'Template', // classes moved to new api dir - 'session' => 'EGroupware\Api\Session', - 'ldap' => 'EGroupware\Api\Ldap', + 'session' => 'EGroupware\\Api\\Session', + 'ldap' => true, + 'auth' => 'EGroupware\\Api\\Auth', ); /** @@ -716,6 +717,8 @@ class egw_minimal return null; } return $this->template = new Template($tpl_dir); + case 'ldap': + return $this->ldap = Api\Ldap::factory(false); default: $class = isset(self::$sub_objects[$name]) ? self::$sub_objects[$name] : $name; break; diff --git a/setup/inc/class.setup_cmd_ldap.inc.php b/setup/inc/class.setup_cmd_ldap.inc.php index 28f0a71023..8798ee5d04 100644 --- a/setup/inc/class.setup_cmd_ldap.inc.php +++ b/setup/inc/class.setup_cmd_ldap.inc.php @@ -812,17 +812,12 @@ class setup_cmd_ldap extends setup_cmd { throw new Api\Exception\WrongUserInput(lang('You need to specify a password!')); } - $this->test_ldap = new ldap(); - $error_rep = error_reporting(); - error_reporting($error_rep & ~E_WARNING); // switch warnings of, in case they are on - ob_start(); - $ds = $this->test_ldap->ldapConnect($host,$dn,$pw); - ob_end_clean(); - error_reporting($error_rep); - - if (!$ds) - { + try { + $this->test_ldap = Api\Ldap::factory(false, $host, $dn, $pw); + } + catch (Api\Exception\NoPermission $e) { + _egw_log_exception($e); throw new Api\Exception\WrongUserInput(lang('Can not connect to LDAP server on host %1 using DN %2!', $host,$dn).($this->test_ldap->ds ? ' ('.ldap_error($this->test_ldap->ds).')' : '')); }