2022-07-13 21:25:45 +08:00
|
|
|
|
<?php
|
|
|
|
|
|
2022-10-15 23:39:49 +08:00
|
|
|
|
use RainLoop\Enumerations\PluginPropertyType;
|
|
|
|
|
use RainLoop\Plugins\AbstractPlugin;
|
2022-07-13 21:25:45 +08:00
|
|
|
|
use RainLoop\Plugins\Property;
|
|
|
|
|
|
|
|
|
|
class LDAPLoginMappingPlugin extends AbstractPlugin
|
|
|
|
|
{
|
|
|
|
|
const
|
|
|
|
|
NAME = 'LDAP login mapping',
|
|
|
|
|
VERSION = '2.0',
|
|
|
|
|
AUTHOR = 'RainLoop Team, Ludovic Pouzenc<ludovic@pouzenc.fr>, ZephOne<zephone@protonmail.com>',
|
|
|
|
|
RELEASE = '2022-07-13',
|
|
|
|
|
REQUIRED = '2.16.3',
|
|
|
|
|
CATEGORY = 'Login',
|
|
|
|
|
DESCRIPTION = 'Enable custom mapping using ldap field';
|
|
|
|
|
/**
|
|
|
|
|
* @var array
|
|
|
|
|
*/
|
|
|
|
|
private $aDomains = array();
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @var string
|
|
|
|
|
*/
|
|
|
|
|
private $sSearchDomain = '';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @var string
|
|
|
|
|
*/
|
|
|
|
|
private $sHostName = '127.0.0.1';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @var int
|
|
|
|
|
*/
|
|
|
|
|
private $iHostPort = 389;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @var string
|
|
|
|
|
*/
|
|
|
|
|
private $sUsersDn = '';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @var string
|
|
|
|
|
*/
|
|
|
|
|
private $sObjectClass = 'inetOrgPerson';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @var string
|
|
|
|
|
*/
|
|
|
|
|
private $sLoginField = 'uid';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @var string
|
|
|
|
|
*/
|
|
|
|
|
private $sEmailField = 'mail';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @var \MailSo\Log\Logger
|
|
|
|
|
*/
|
|
|
|
|
private $oLogger = null;
|
|
|
|
|
|
|
|
|
|
public function Init(): void
|
|
|
|
|
{
|
|
|
|
|
$this->addHook('login.credentials', 'FilterLoginСredentials');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @return string
|
|
|
|
|
*/
|
|
|
|
|
public function Supported(): string
|
|
|
|
|
{
|
|
|
|
|
if (!\function_exists('ldap_connect'))
|
|
|
|
|
{
|
|
|
|
|
return 'The LDAP PHP extension must be installed to use this plugin';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return '';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param string $sEmail
|
|
|
|
|
* @param string $sLogin
|
|
|
|
|
* @param string $sPassword
|
|
|
|
|
*
|
|
|
|
|
* @throws \RainLoop\Exceptions\ClientException
|
|
|
|
|
*/
|
|
|
|
|
public function FilterLoginСredentials(&$sEmail, &$sLogin, &$sPassword)
|
|
|
|
|
{
|
2022-10-15 23:39:49 +08:00
|
|
|
|
$this->oLogger = \RainLoop\Api::Logger();
|
2022-07-13 21:25:45 +08:00
|
|
|
|
|
|
|
|
|
$this->aDomains = explode(',', $this->Config()->Get('plugin', 'domains', ''));
|
|
|
|
|
$this->sSearchDomain = trim($this->Config()->Get('plugin', 'search_domain', ''));
|
|
|
|
|
$this->sHostName = trim($this->Config()->Get('plugin', 'hostname', ''));
|
|
|
|
|
$this->iHostPort = (int) $this->Config()->Get('plugin', 'port', 389);
|
|
|
|
|
$this->sUsersDn = trim($this->Config()->Get('plugin', 'users_dn', ''));
|
|
|
|
|
$this->sObjectClass = trim($this->Config()->Get('plugin', 'object_class', ''));
|
|
|
|
|
$this->sLoginField = trim($this->Config()->Get('plugin', 'login_field', ''));
|
|
|
|
|
$this->sEmailField = trim($this->Config()->Get('plugin', 'mail_field', ''));
|
|
|
|
|
|
|
|
|
|
if (0 < \strlen($this->sObjectClass) && 0 < \strlen($this->sEmailField))
|
|
|
|
|
{
|
2022-10-15 23:39:49 +08:00
|
|
|
|
$sIP = $_SERVER['REMOTE_ADDR'];
|
|
|
|
|
$sResult = $this->ldapSearch($sEmail);
|
2022-07-13 21:25:45 +08:00
|
|
|
|
if ( is_array($sResult) ) {
|
|
|
|
|
$sLogin = $sResult['login'];
|
|
|
|
|
$sEmail = $sResult['email'];
|
|
|
|
|
}
|
2022-10-15 23:39:49 +08:00
|
|
|
|
syslog(LOG_WARNING, "plugins/ldap-login-mapping/index.php:FilterLoginСredentials() auth try: $sIP/$sEmail, resolved as $sLogin/$sEmail");
|
2022-07-13 21:25:45 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @return array
|
|
|
|
|
*/
|
|
|
|
|
public function configMapping(): array
|
|
|
|
|
{
|
|
|
|
|
return [
|
|
|
|
|
Property::NewInstance('domains')
|
|
|
|
|
->SetLabel('LDAP enabled domains')
|
|
|
|
|
->SetDefaultValue('example1.com,example2.com'),
|
|
|
|
|
|
|
|
|
|
Property::NewInstance('search_domain')
|
|
|
|
|
->SetLabel('Forced domain')
|
|
|
|
|
->SetDescription('Force this domain email for LDAP search')
|
|
|
|
|
->SetDefaultValue('example.com'),
|
|
|
|
|
|
|
|
|
|
Property::NewInstance('hostname')
|
|
|
|
|
->SetLabel('LDAP hostname')
|
|
|
|
|
->SetDefaultValue('127.0.0.1'),
|
|
|
|
|
|
|
|
|
|
Property::NewInstance('port')
|
|
|
|
|
->SetLabel('LDAP port')
|
|
|
|
|
->SetType(PluginPropertyType::INT)
|
|
|
|
|
->SetDefaultValue(389),
|
|
|
|
|
|
|
|
|
|
Property::NewInstance('users_dn')
|
|
|
|
|
->SetLabel('Search base DN')
|
|
|
|
|
->SetDescription('LDAP users search base DN. No tokens.')
|
|
|
|
|
->SetDefaultValue('ou=People,dc=domain,dc=com'),
|
|
|
|
|
|
|
|
|
|
Property::NewInstance('object_class')
|
|
|
|
|
->SetLabel('objectClass value')
|
|
|
|
|
->SetDefaultValue('inetOrgPerson'),
|
|
|
|
|
|
|
|
|
|
Property::NewInstance('login_field')
|
|
|
|
|
->SetLabel('Login field')
|
|
|
|
|
->SetDefaultValue('uid'),
|
|
|
|
|
|
|
|
|
|
Property::NewInstance('mail_field')
|
|
|
|
|
->SetLabel('Mail field')
|
|
|
|
|
->SetDefaultValue('mail'),
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param string $sEmailOrLogin
|
|
|
|
|
*
|
|
|
|
|
* @return string
|
|
|
|
|
*/
|
|
|
|
|
private function ldapSearch($sEmail)
|
|
|
|
|
{
|
|
|
|
|
$bFound = FALSE;
|
|
|
|
|
foreach ( $this->aDomains as $sDomain ) {
|
|
|
|
|
$sRegex = '/^[a-z0-9._-]+@' . preg_quote(trim($sDomain)) . '$/i';
|
2022-10-15 23:39:49 +08:00
|
|
|
|
$this->oLogger->Write('DEBUG regex ' . $sRegex, \LOG_INFO, 'LDAP');
|
2022-07-13 21:25:45 +08:00
|
|
|
|
if ( preg_match($sRegex, $sEmail) === 1) {
|
|
|
|
|
$bFound = TRUE;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if ( !$bFound ) {
|
|
|
|
|
$this->oLogger->Write(
|
|
|
|
|
'preg_match: no match in "' . $sEmail . '" for /^[a-z0-9._-]+@{configured-domains}$/i',
|
2022-10-15 23:39:49 +08:00
|
|
|
|
\LOG_INFO,
|
2022-07-13 21:25:45 +08:00
|
|
|
|
'LDAP');
|
|
|
|
|
return FALSE;
|
|
|
|
|
}
|
|
|
|
|
$sLogin = \MailSo\Base\Utils::GetAccountNameFromEmail($sEmail);
|
|
|
|
|
|
2022-10-15 23:39:49 +08:00
|
|
|
|
$this->oLogger->Write('ldap_connect: trying...', \LOG_INFO, 'LDAP');
|
2022-07-13 21:25:45 +08:00
|
|
|
|
|
|
|
|
|
$oCon = @\ldap_connect($this->sHostName, $this->iHostPort);
|
|
|
|
|
if (!$oCon) return FALSE;
|
|
|
|
|
|
2022-10-15 23:39:49 +08:00
|
|
|
|
$this->oLogger->Write('ldap_connect: connected', \LOG_INFO, 'LDAP');
|
2022-07-13 21:25:45 +08:00
|
|
|
|
|
|
|
|
|
@\ldap_set_option($oCon, LDAP_OPT_PROTOCOL_VERSION, 3);
|
|
|
|
|
|
|
|
|
|
if (!@\ldap_bind($oCon)) {
|
|
|
|
|
$this->logLdapError($oCon, 'ldap_bind');
|
|
|
|
|
return FALSE;
|
|
|
|
|
}
|
|
|
|
|
$sSearchDn = $this->sUsersDn;
|
|
|
|
|
$aItems = array($this->sLoginField, $this->sEmailField);
|
|
|
|
|
if ( 0 < \strlen($this->sSearchDomain) ) {
|
|
|
|
|
$sFilter = '(&(objectclass='.$this->sObjectClass.')(|('.$this->sEmailField.'='.$sLogin.'@'.$this->sSearchDomain.')('.$this->sLoginField.'='.$sLogin.')))';
|
|
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
$sFilter = '(&(objectclass='.$this->sObjectClass.')(|('.$this->sEmailField.'='.$sEmail.')('.$this->sLoginField.'='.$sLogin.')))';
|
|
|
|
|
}
|
2022-10-15 23:39:49 +08:00
|
|
|
|
$this->oLogger->Write('ldap_search: start: '.$sSearchDn.' / '.$sFilter, \LOG_INFO, 'LDAP');
|
2022-07-13 21:25:45 +08:00
|
|
|
|
$oS = @\ldap_search($oCon, $sSearchDn, $sFilter, $aItems, 0, 30, 30);
|
|
|
|
|
if (!$oS) {
|
|
|
|
|
$this->logLdapError($oCon, 'ldap_search');
|
|
|
|
|
return FALSE;
|
|
|
|
|
}
|
|
|
|
|
$aEntries = @\ldap_get_entries($oCon, $oS);
|
|
|
|
|
if (!is_array($aEntries)) {
|
|
|
|
|
$this->logLdapError($oCon, 'ldap_get_entries');
|
|
|
|
|
return FALSE;
|
|
|
|
|
}
|
|
|
|
|
if (!isset($aEntries[0])) {
|
|
|
|
|
$this->logLdapError($oCon, 'ldap_get_entries (no result)');
|
|
|
|
|
return FALSE;
|
|
|
|
|
}
|
|
|
|
|
if (!isset($aEntries[0][$this->sLoginField][0])) {
|
|
|
|
|
$this->logLdapError($oCon, 'ldap_get_entries (no login)');
|
|
|
|
|
return FALSE;
|
|
|
|
|
}
|
|
|
|
|
if (!isset($aEntries[0][$this->sEmailField][0])) {
|
|
|
|
|
$this->logLdapError($oCon, 'ldap_get_entries (no mail)');
|
|
|
|
|
return FALSE;
|
|
|
|
|
}
|
|
|
|
|
$sLogin = $aEntries[0][$this->sLoginField][0];
|
|
|
|
|
$sEmail = $aEntries[0][$this->sEmailField][0];
|
|
|
|
|
$this->oLogger->Write('ldap_search: found "' . $this->sLoginField . ': '.$sLogin . '" and "' . $this->sEmailField . ': '.$sEmail . '"');
|
|
|
|
|
|
|
|
|
|
return array(
|
|
|
|
|
'login' => $sLogin,
|
|
|
|
|
'email' => $sEmail,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param mixed $oCon
|
|
|
|
|
* @param string $sCmd
|
|
|
|
|
*
|
|
|
|
|
* @return string
|
|
|
|
|
*/
|
|
|
|
|
private function logLdapError($oCon, $sCmd)
|
|
|
|
|
{
|
|
|
|
|
if ($this->oLogger)
|
|
|
|
|
{
|
|
|
|
|
$sError = $oCon ? @\ldap_error($oCon) : '';
|
|
|
|
|
$iErrno = $oCon ? @\ldap_errno($oCon) : 0;
|
|
|
|
|
|
|
|
|
|
$this->oLogger->Write($sCmd.' error: '.$sError.' ('.$iErrno.')',
|
2022-10-15 23:39:49 +08:00
|
|
|
|
\LOG_WARNING, 'LDAP');
|
2022-07-13 21:25:45 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|