From 1ded94bd2db2badc7a77b74a19d5db5d3e16309a Mon Sep 17 00:00:00 2001 From: Erwan Colin Date: Wed, 13 Jul 2022 15:25:45 +0200 Subject: [PATCH] Add back ldap-login-plugin. Resolve #453. --- plugins/ldap-login-mapping/LICENSE | 22 +++ plugins/ldap-login-mapping/README | 1 + plugins/ldap-login-mapping/VERSION | 1 + plugins/ldap-login-mapping/index.php | 253 +++++++++++++++++++++++++++ 4 files changed, 277 insertions(+) create mode 100644 plugins/ldap-login-mapping/LICENSE create mode 100644 plugins/ldap-login-mapping/README create mode 100644 plugins/ldap-login-mapping/VERSION create mode 100644 plugins/ldap-login-mapping/index.php diff --git a/plugins/ldap-login-mapping/LICENSE b/plugins/ldap-login-mapping/LICENSE new file mode 100644 index 000000000..335b3845a --- /dev/null +++ b/plugins/ldap-login-mapping/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2014 RainLoop Team +Copyright (c) 2018 +Copyright (c) 2022 + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/plugins/ldap-login-mapping/README b/plugins/ldap-login-mapping/README new file mode 100644 index 000000000..b12d3a77e --- /dev/null +++ b/plugins/ldap-login-mapping/README @@ -0,0 +1 @@ +Plugin which allows you to set up LDAP username by email address (when IMAP/SMTP requires it) diff --git a/plugins/ldap-login-mapping/VERSION b/plugins/ldap-login-mapping/VERSION new file mode 100644 index 000000000..cd5ac039d --- /dev/null +++ b/plugins/ldap-login-mapping/VERSION @@ -0,0 +1 @@ +2.0 diff --git a/plugins/ldap-login-mapping/index.php b/plugins/ldap-login-mapping/index.php new file mode 100644 index 000000000..a0d8e5ac6 --- /dev/null +++ b/plugins/ldap-login-mapping/index.php @@ -0,0 +1,253 @@ +, ZephOne', + 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) + { + $this->oLogger = \MailSo\Log\Logger::SingletonInstance(); + + $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)) + { + $sIP = $_SERVER['REMOTE_ADDR']; + $sResult = $this->ldapSearch($sEmail); + if ( is_array($sResult) ) { + $sLogin = $sResult['login']; + $sEmail = $sResult['email']; + } + syslog(LOG_WARNING, "plugins/ldap-login-mapping/index.php:FilterLoginДredentials() auth try: $sIP/$sEmail, resolved as $sLogin/$sEmail"); + } + } + + /** + * @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'; + $this->oLogger->Write('DEBUG regex ' . $sRegex, Type::INFO, 'LDAP'); + 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', + Type::INFO, + 'LDAP'); + return FALSE; + } + $sLogin = \MailSo\Base\Utils::GetAccountNameFromEmail($sEmail); + + $this->oLogger->Write('ldap_connect: trying...', Type::INFO, 'LDAP'); + + $oCon = @\ldap_connect($this->sHostName, $this->iHostPort); + if (!$oCon) return FALSE; + + $this->oLogger->Write('ldap_connect: connected', Type::INFO, 'LDAP'); + + @\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.')))'; + } + $this->oLogger->Write('ldap_search: start: '.$sSearchDn.' / '.$sFilter, Type::INFO, 'LDAP'); + $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.')', + Type::WARNING, 'LDAP'); + } + } + +}