From 3ee757429439ba2de89f2070c4f142b7f4d67fba Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Thu, 10 Sep 2020 17:12:53 +0200 Subject: [PATCH] * Authentication: allow using multiple backends, even same backend multiple times with different configuration --- api/src/Auth/Ads.php | 24 ++- api/src/Auth/Multiple.php | 272 +++++++++++++++++++++++++ setup/inc/hook_config_validate.inc.php | 19 ++ setup/templates/default/config.tpl | 11 +- 4 files changed, 319 insertions(+), 7 deletions(-) create mode 100644 api/src/Auth/Multiple.php diff --git a/api/src/Auth/Ads.php b/api/src/Auth/Ads.php index 18413bd584..023700fbc4 100644 --- a/api/src/Auth/Ads.php +++ b/api/src/Auth/Ads.php @@ -30,6 +30,18 @@ class Ads implements Backend { var $previous_login = -1; + protected $config; + + /** + * Ads auth constructor + * + * @param array|null $config + */ + function __construct(array $config=null) + { + $this->config = $config; + } + /** * password authentication * @@ -48,7 +60,7 @@ class Ads implements 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 = Api\Accounts\Ads::get_adldap(); + $adldap = Api\Accounts\Ads::get_adldap($this->config); // 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)) { @@ -131,10 +143,10 @@ class Ads implements Backend * @return mixed false on error, 0 if user must change on next login, * or NULL if user never changed his password or timestamp of last change */ - static function getLastPwdChange($username) + function getLastPwdChange($username) { $ret = false; - if (($adldap = Api\Accounts\Ads::get_adldap()) && + if (($adldap = Api\Accounts\Ads::get_adldap($this->config)) && ($data = $adldap->user()->info($username, array('pwdlastset')))) { $ret = !$data[0]['pwdlastset'][0] ? $data[0]['pwdlastset'][0] : @@ -155,10 +167,10 @@ class Ads implements Backend * @param boolean $return_mod =false true return ldap modification instead of executing it * @return boolean|array true if account_lastpwd_change successful changed, false otherwise or array if $return_mod */ - static function setLastPwdChange($account_id=0, $passwd=NULL, $lastpwdchange=NULL, $return_mod=false) + function setLastPwdChange($account_id=0, $passwd=NULL, $lastpwdchange=NULL, $return_mod=false) { unset($passwd); // not used but required by function signature - if (!($adldap = Api\Accounts\Ads::get_adldap())) return false; + if (!($adldap = Api\Accounts\Ads::get_adldap($this->config))) return false; if ($lastpwdchange) { @@ -202,7 +214,7 @@ class Ads implements Backend */ function change_password($old_passwd, $new_passwd, $account_id=0) { - if (!($adldap = Api\Accounts\Ads::get_adldap())) + if (!($adldap = Api\Accounts\Ads::get_adldap($this->config))) { error_log(__METHOD__."(\$old_passwd, \$new_passwd, $account_id) Api\Accounts\Ads::get_adldap() returned false"); return false; diff --git a/api/src/Auth/Multiple.php b/api/src/Auth/Multiple.php new file mode 100644 index 0000000000..0825a9e24c --- /dev/null +++ b/api/src/Auth/Multiple.php @@ -0,0 +1,272 @@ + + * @license http://opensource.org/licenses/lgpl-license.php LGPL - GNU Lesser General Public License + * @package api + * @subpackage auth + */ + +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 Multiple implements Backend +{ + /** + * @var ?array[] with name as key + */ + private $config; + /** + * @var Backend[] with name as key + */ + private $backends = []; + + /** + * Constructor + * + * @param string $config auth_multiple config variable + * @throws \Exception on invalid configuration + */ + function __construct($config=null) + { + if (!isset($config)) $config = $GLOBALS['egw_info']['server']['auth_multiple']; + + $this->config = self::parseConfig($config); + } + + /** + * Parse configuration + * + * @param string $config + * @param boolean $checks true: run some extra checks, used in setup to check config is sane + * @return array + * @throws \Exception on invalid configuration + */ + static public function parseConfig($config, $checks=false) + { + try + { + $config = $config[0] === '{' ? json_decode($config, true, 512, JSON_THROW_ON_ERROR) : + array_combine($csv = preg_split('/,\s*/', $config), array_fill(0, count($csv), null)); + } + catch(\JsonException $e) { + throw new \Exception('Invalid JSON: '.$e->getMessage()); + } + if ($checks) + { + foreach($config as $name => $data) + { + if (!class_exists($class = __NAMESPACE__.'\\'.ucfirst(preg_replace('/\d+$/', '', $name)))) + { + throw new \Exception("Invalid Backend name: '$name', no class $class found!"); + } + if ($data !== null && !is_array($data)) + { + throw new \Exception("Invalid Backend config: must by either null or an object!"); + } + } + } + return $config; + } + + /** + * Iterate over all backends + * + * @return \Generator $name => Backend + */ + protected function backends() + { + foreach($this->config as $name => $config) + { + yield $name => $this->backend($name); + } + } + + /** + * Get a given backend + * + * @param $name + * @return Backend + */ + protected function backend($name) + { + if (!isset($this->backends[$name])) + { + $class = __NAMESPACE__.'\\'.ucfirst(preg_replace('/\d+$/', '', $name)); + $this->backends[$name] = new $class($this->config[$name]); + } + return $this->backends[$name]; + } + + /** + * Authenticate + * + * @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') + { + $ret = false; + if (($name = Api\Cache::getInstance(__CLASS__,'backend_used-'.$username))) + { + $ret = $this->backend($name)->authenticate($username, $passwd, $passwd_type); + } + else + { + foreach ($this->backends() as $name => $backend) + { + if (($ret = $backend->authenticate($username, $passwd, $passwd_type))) + { + Api\Cache::setInstance(__CLASS__, 'backend_used-' . $username, $name); + + break; + } + } + } + //error_log(__METHOD__."('$username', \$passwd, '$passwd_type') backend=$name" returning ".array2string($ret)); + return $ret; + } + + /** + * Changes password in authentication backend + * + * 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) + { + $account_id = $GLOBALS['egw_info']['user']['account_id']; + $username = $GLOBALS['egw_info']['user']['account_lid']; + } + else + { + $username = $GLOBALS['egw']->accounts->id2name($account_id); + } + $ret = false; + if (($name = Api\Cache::getInstance(__CLASS__,'backend_used-'.$username))) + { + $ret = $this->backend($name)->change_password($old_passwd, $new_passwd, $account_id); + } + else + { + foreach($this->backends as $name => $backend) + { + if (($ret = $backend->change_password($old_passwd, $new_passwd, $account_id))) + { + break; + } + } + } + //error_log(__METHOD__."('$old_passwd', '$new_passwd', $account_id) username='$username', backend=$name" returning ".array2string($ret)); + return $ret; + } + + /** + * 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) + { + $ret = false; + if (($name = Api\Cache::getInstance(__CLASS__,'backend_used-'.$username))) + { + $backend = $this->backend($name); + if (method_exists($backend, 'getLastPwdChange')) + { + $ret = $backend->getLastPwdChange($username); + } + } + else + { + foreach($this->backends as $name => $backend) + { + if (method_exists($backend, 'getLastPwdChange') && + ($ret = $backend->getLastPwdChange($username))) + { + break; + } + } + } + //error_log(__METHOD__."('$username'), backend=$name" returning ".array2string($ret)); + return $ret; + } + + /** + * Changes account_lastpwd_change in 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 + * @return boolean true if account_lastpwd_change successful changed, false otherwise + */ + function setLastPwdChange($account_id=0, $passwd=NULL, $lastpwdchange=NULL, $return_mod=false) + { + if(!$account_id || $GLOBALS['egw_info']['flags']['currentapp'] == 'login') + { + $account_id = $GLOBALS['egw_info']['user']['account_id']; + $username = $GLOBALS['egw_info']['user']['account_lid']; + } + else + { + $username = $GLOBALS['egw']->accounts->id2name($account_id); + } + $ret = false; + if (($name = Api\Cache::getInstance(__CLASS__,'backend_used-'.$username))) + { + $backend = $this->backend($name); + if (method_exists($backend, 'setLastPwdChange')) + { + $ret = $backend->setLastPwdChange($account_id, $passwd, $lastpwdchange, $return_mod); + } + } + else + { + foreach($this->backends as $name => $backend) + { + if (method_exists($backend, 'setLastPwdChange') && + ($ret = $backend->setLastPwdChange($account_id, $passwd, $lastpwdchange, $return_mod))) + { + break; + } + } + } + //error_log(__METHOD__."('$username'), backend=$name" returning ".array2string($ret)); + return $ret; + } +} diff --git a/setup/inc/hook_config_validate.inc.php b/setup/inc/hook_config_validate.inc.php index 3d10f35bb3..b0d7f2d500 100644 --- a/setup/inc/hook_config_validate.inc.php +++ b/setup/inc/hook_config_validate.inc.php @@ -23,6 +23,7 @@ $GLOBALS['egw_info']['server']['found_validation_hook'] = array( 'mcrypt_algo', 'ldap_search_filter', 'auth_type', + 'auth_multiple', ); /** @@ -50,6 +51,24 @@ function auth_type($settings) } } +/** + * Validate auth_multiple config + * + * @param array $settings + */ +function auth_multiple(array $settings) +{ + try { + if ($settings['auth_multiple'] !== '') + { + Api\Auth\Multiple::parseConfig($settings['auth_multiple'], true); + } + } + catch (Exception $ex) { + $GLOBALS['config_error'] = $ex->getMessage(); + } +} + /** * Set vfs_fstab depending from what the user selected for vfs_storage_mode * diff --git a/setup/templates/default/config.tpl b/setup/templates/default/config.tpl index e31e0d55c4..070f48c8cf 100644 --- a/setup/templates/default/config.tpl +++ b/setup/templates/default/config.tpl @@ -623,10 +623,19 @@ - +   + + {lang_If_using_Multiple_authentication_providers:} + + + + {lang_Comma-separated_provider_names_or_JSON}: Auth/Multiple.php + + +