* Authentication: allow using multiple backends, even same backend multiple times with different configuration

This commit is contained in:
Ralf Becker 2020-09-10 17:12:53 +02:00
parent ca9526988f
commit 3ee7574294
4 changed files with 319 additions and 7 deletions

View File

@ -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;

272
api/src/Auth/Multiple.php Normal file
View File

@ -0,0 +1,272 @@
<?php
/**
* EGroupware API - Authentication against multiple backends
*
* The first backend against which authentication succeeds is used, so you either need *somehow* to make sure usernames are unique,
* or the backends in the *right* order. The name of the succeeding backend is stored in the instance cache.
*
* Specified via auth_multiple config variable with either
* - a comma-separated string, eg. "Ldap,Sql" to first try LDAP then SQL authentication configured directly in setup
* - a JSON encoded object eg.
* {
* "Ads": null, <-- uses default Ads config from Setup
* "Ads2": { <-- 2nd Ads using given config (append a number to use backends multiple times)
* "ads_host":"...",
* "ads_domain":"...",
* "ads_admin_user":"...",
* "ads_admin_passwd":"...",
* optional attributes like: "ads_connection":"tls"|"ssl", "ads_context", "ads_user_filter", "ads_group_filter"
* },
* optional further backend objects
* }
*
* @link https://www.egroupware.org
* @author Ralf Becker <rb@egroupware.org>
* @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;
}
}

View File

@ -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
*

View File

@ -623,10 +623,19 @@
<td><input name="newsettings[cas_cert]" value="{value_cas_cert}" size="40" /></td>
</tr>
<tr class="row_on">
<tr class="row_off">
<td colspan="2">&nbsp;</td>
</tr>
<tr class="th">
<td colspan="2"><b>{lang_If_using_Multiple_authentication_providers:}</b></td>
</tr>
<tr class="row_on">
<td>{lang_Comma-separated_provider_names_or_JSON}: <a href="https://github.com/EGroupware/egroupware/blob/master/api/src/Auth/Multiple.php" target="_blank">Auth/Multiple.php</a></td>
<td><textarea name="newsettings[auth_multiple]" cols="64" rows="1" onfocus="this.style='height: '+this.scrollHeight+'px'" onblur="this.style='height: auto'">{value_auth_multiple}</textarea></td>
</tr>
<tr class="row_off">
<td colspan="2">&nbsp;</td>
</tr>