11078ec26SAndreas Gohr<?php 21078ec26SAndreas Gohr 31078ec26SAndreas Gohrnamespace dokuwiki\plugin\pureldap\classes; 41078ec26SAndreas Gohr 508ace392SAndreas Gohruse dokuwiki\ErrorHandler; 608ace392SAndreas Gohruse dokuwiki\Logger; 708ace392SAndreas Gohruse dokuwiki\Utf8\Clean; 880ac552fSAndreas Gohruse dokuwiki\Utf8\PhpString; 91078ec26SAndreas Gohruse FreeDSx\Ldap\Entry\Attribute; 101078ec26SAndreas Gohruse FreeDSx\Ldap\Exception\BindException; 111078ec26SAndreas Gohruse FreeDSx\Ldap\Exception\ConnectionException; 121078ec26SAndreas Gohruse FreeDSx\Ldap\Exception\OperationException; 131078ec26SAndreas Gohruse FreeDSx\Ldap\LdapClient; 141078ec26SAndreas Gohr 151078ec26SAndreas Gohrabstract class Client 161078ec26SAndreas Gohr{ 17208fe81aSAndreas Gohr public const FILTER_EQUAL = 'equal'; 18208fe81aSAndreas Gohr public const FILTER_CONTAINS = 'contains'; 19208fe81aSAndreas Gohr public const FILTER_STARTSWITH = 'startsWith'; 20208fe81aSAndreas Gohr public const FILTER_ENDSWITH = 'endsWith'; 21204fba68SAndreas Gohr 221078ec26SAndreas Gohr /** @var array the configuration */ 231078ec26SAndreas Gohr protected $config; 241078ec26SAndreas Gohr 251078ec26SAndreas Gohr /** @var LdapClient */ 261078ec26SAndreas Gohr protected $ldap; 271078ec26SAndreas Gohr 2808ace392SAndreas Gohr /** @var bool|string is this client authenticated already? Contains username if yes */ 291078ec26SAndreas Gohr protected $isAuthenticated = false; 301078ec26SAndreas Gohr 311078ec26SAndreas Gohr /** @var array cached user info */ 321078ec26SAndreas Gohr protected $userCache = []; 331078ec26SAndreas Gohr 345a3b9122SAndreas Gohr /** @var array cached group list */ 355a3b9122SAndreas Gohr protected $groupCache = []; 365a3b9122SAndreas Gohr 371078ec26SAndreas Gohr /** 381078ec26SAndreas Gohr * Client constructor. 391078ec26SAndreas Gohr * @param array $config 401078ec26SAndreas Gohr */ 411078ec26SAndreas Gohr public function __construct($config) 421078ec26SAndreas Gohr { 43208fe81aSAndreas Gohr require_once __DIR__ . '/../vendor/autoload.php'; 44208fe81aSAndreas Gohr 451078ec26SAndreas Gohr $this->config = $this->prepareConfig($config); 46bf69b89cSAndreas Gohr $this->prepareSSO(); 471078ec26SAndreas Gohr $this->ldap = new LdapClient($this->config); 481078ec26SAndreas Gohr } 491078ec26SAndreas Gohr 501078ec26SAndreas Gohr /** 511078ec26SAndreas Gohr * Setup sane config defaults 521078ec26SAndreas Gohr * 531078ec26SAndreas Gohr * @param array $config 541078ec26SAndreas Gohr * @return array 551078ec26SAndreas Gohr */ 561078ec26SAndreas Gohr protected function prepareConfig($config) 571078ec26SAndreas Gohr { 581a4f0e1fSAndreas Gohr // ensure we have the default keys 591a4f0e1fSAndreas Gohr /** @var array $conf */ 601a4f0e1fSAndreas Gohr include __DIR__ . '/../conf/default.php'; 611a4f0e1fSAndreas Gohr $defaults = $conf; 621a4f0e1fSAndreas Gohr $defaults['defaultgroup'] = 'user'; // we expect this to be passed from global conf 631078ec26SAndreas Gohr 641078ec26SAndreas Gohr $config = array_merge($defaults, $config); 651078ec26SAndreas Gohr 661078ec26SAndreas Gohr // default port depends on SSL setting 671078ec26SAndreas Gohr if (!$config['port']) { 686d90d5c8SAndreas Gohr $config['port'] = ($config['encryption'] === 'ssl') ? 636 : 389; 696d90d5c8SAndreas Gohr } 706d90d5c8SAndreas Gohr 716d90d5c8SAndreas Gohr // set ssl parameters 726d90d5c8SAndreas Gohr $config['use_ssl'] = ($config['encryption'] === 'ssl'); 736d90d5c8SAndreas Gohr if ($config['validate'] === 'none') { 746d90d5c8SAndreas Gohr $config['ssl_validate_cert'] = false; 756d90d5c8SAndreas Gohr } elseif ($config['validate'] === 'self') { 766d90d5c8SAndreas Gohr $config['ssl_allow_self_signed'] = true; 771078ec26SAndreas Gohr } 781078ec26SAndreas Gohr 79fde03b26SAndreas Gohr $config['suffix'] = ltrim(PhpString::strtolower($config['suffix']), '@'); 80c2500b44SAndreas Gohr $config['primarygroup'] = $this->cleanGroup($config['primarygroup']); 8180ac552fSAndreas Gohr 821078ec26SAndreas Gohr return $config; 831078ec26SAndreas Gohr } 841078ec26SAndreas Gohr 851078ec26SAndreas Gohr /** 86bf69b89cSAndreas Gohr * Extract user info from environment for SSO 87bf69b89cSAndreas Gohr */ 88bf69b89cSAndreas Gohr protected function prepareSSO() 89bf69b89cSAndreas Gohr { 90bf69b89cSAndreas Gohr global $INPUT; 91bf69b89cSAndreas Gohr 92bf69b89cSAndreas Gohr if (!$this->config['sso']) return; 93bf69b89cSAndreas Gohr if ($INPUT->server->str('REMOTE_USER') === '') return; 94bf69b89cSAndreas Gohr 95bf69b89cSAndreas Gohr $user = $INPUT->server->str('REMOTE_USER'); 96bf69b89cSAndreas Gohr 97bf69b89cSAndreas Gohr // make sure the right encoding is used 98bf69b89cSAndreas Gohr if ($this->config['sso_charset']) { 9908ace392SAndreas Gohr if (function_exists('iconv')) { 100bf69b89cSAndreas Gohr $user = iconv($this->config['sso_charset'], 'UTF-8', $user); 10108ace392SAndreas Gohr } elseif (function_exists('mb_convert_encoding')) { 10208ace392SAndreas Gohr $user = mb_convert_encoding($user, 'UTF-8', $this->config['sso_charset']); 10308ace392SAndreas Gohr } 10408ace392SAndreas Gohr } elseif (!Clean::isUtf8($user)) { 105bf69b89cSAndreas Gohr $user = utf8_encode($user); 106bf69b89cSAndreas Gohr } 107bf69b89cSAndreas Gohr $user = $this->cleanUser($user); 108bf69b89cSAndreas Gohr 109bf69b89cSAndreas Gohr // Prepare SSO 110bf69b89cSAndreas Gohr 111bf69b89cSAndreas Gohr // trust the incoming user 112bf69b89cSAndreas Gohr $INPUT->server->set('REMOTE_USER', $user); 113bf69b89cSAndreas Gohr 114bf69b89cSAndreas Gohr // we need to simulate a login 115bf69b89cSAndreas Gohr if (empty($_COOKIE[DOKU_COOKIE])) { 116bf69b89cSAndreas Gohr $INPUT->set('u', $user); 117bf69b89cSAndreas Gohr $INPUT->set('p', 'sso_only'); 118bf69b89cSAndreas Gohr } 119bf69b89cSAndreas Gohr } 120bf69b89cSAndreas Gohr 121bf69b89cSAndreas Gohr /** 12222654fdeSAndreas Gohr * Access to the config values 12322654fdeSAndreas Gohr * 12422654fdeSAndreas Gohr * Because the client class does configuration cleanup, the auth class should access 12522654fdeSAndreas Gohr * config values through the client 12622654fdeSAndreas Gohr * 12722654fdeSAndreas Gohr * @param string $key 12822654fdeSAndreas Gohr * @return mixed returns null on missing config 12922654fdeSAndreas Gohr */ 13008ace392SAndreas Gohr public function getConf($key) 13108ace392SAndreas Gohr { 13222654fdeSAndreas Gohr if (!isset($this->config[$key])) return null; 13322654fdeSAndreas Gohr return $this->config[$key]; 13422654fdeSAndreas Gohr } 13522654fdeSAndreas Gohr 13622654fdeSAndreas Gohr /** 13708ace392SAndreas Gohr * Authenticate as admin if not authenticated yet 1381078ec26SAndreas Gohr */ 1391078ec26SAndreas Gohr public function autoAuth() 1401078ec26SAndreas Gohr { 1411078ec26SAndreas Gohr if ($this->isAuthenticated) return true; 1429446f9efSAndreas Gohr 143a1128cc0SAndreas Gohr $user = $this->prepareBindUser($this->config['admin_username']); 144*fb75804eSAndreas Gohr try { 145*fb75804eSAndreas Gohr $this->authenticate($user, $this->config['admin_password']); 146*fb75804eSAndreas Gohr return true; 147*fb75804eSAndreas Gohr } catch (\Exception $e) { 14808ace392SAndreas Gohr $this->error('Automatic bind failed. Probably wrong user/password.', __FILE__, __LINE__); 1498b2677edSAndreas Gohr } 150*fb75804eSAndreas Gohr return false; 1511078ec26SAndreas Gohr } 1521078ec26SAndreas Gohr 1531078ec26SAndreas Gohr /** 1541078ec26SAndreas Gohr * Authenticates a given user. This client will remain authenticated 1551078ec26SAndreas Gohr * 1561078ec26SAndreas Gohr * @param string $user 1571078ec26SAndreas Gohr * @param string $pass 1585a3b9122SAndreas Gohr * @noinspection PhpRedundantCatchClauseInspection 159*fb75804eSAndreas Gohr * @return true 160*fb75804eSAndreas Gohr * @throws ConnectionException 161*fb75804eSAndreas Gohr * @throws OperationException 162*fb75804eSAndreas Gohr * @throws BindException 1631078ec26SAndreas Gohr */ 1641078ec26SAndreas Gohr public function authenticate($user, $pass) 1651078ec26SAndreas Gohr { 166a1128cc0SAndreas Gohr $user = $this->prepareBindUser($user); 16708ace392SAndreas Gohr $this->isAuthenticated = false; 16880ac552fSAndreas Gohr 16908ace392SAndreas Gohr if (!$this->ldap->isConnected() && $this->config['encryption'] === 'tls') { 1701078ec26SAndreas Gohr try { 1711078ec26SAndreas Gohr $this->ldap->startTls(); 17208ace392SAndreas Gohr } catch (ConnectionException|OperationException $e) { 173da369b60SAndreas Gohr $this->fatal($e); 174*fb75804eSAndreas Gohr throw $e; 1751078ec26SAndreas Gohr } 1761078ec26SAndreas Gohr } 1771078ec26SAndreas Gohr 1781078ec26SAndreas Gohr try { 1791078ec26SAndreas Gohr $this->ldap->bind($user, $pass); 1801078ec26SAndreas Gohr } catch (BindException $e) { 181fde03b26SAndreas Gohr $this->debug("Bind for $user failed: " . $e->getMessage(), $e->getFile(), $e->getLine()); 182*fb75804eSAndreas Gohr throw $e; 18308ace392SAndreas Gohr } catch (ConnectionException|OperationException $e) { 184da369b60SAndreas Gohr $this->fatal($e); 185*fb75804eSAndreas Gohr throw $e; 1861078ec26SAndreas Gohr } 1871078ec26SAndreas Gohr 18808ace392SAndreas Gohr $this->isAuthenticated = $user; 1891078ec26SAndreas Gohr return true; 1901078ec26SAndreas Gohr } 1911078ec26SAndreas Gohr 1921078ec26SAndreas Gohr /** 1931078ec26SAndreas Gohr * Get info for a single user, use cache if available 1941078ec26SAndreas Gohr * 1951078ec26SAndreas Gohr * @param string $username 1961078ec26SAndreas Gohr * @param bool $fetchgroups Are groups needed? 1971078ec26SAndreas Gohr * @return array|null 1981078ec26SAndreas Gohr */ 1991078ec26SAndreas Gohr public function getCachedUser($username, $fetchgroups = true) 2001078ec26SAndreas Gohr { 2016d90d5c8SAndreas Gohr global $conf; 2026d90d5c8SAndreas Gohr 2036d90d5c8SAndreas Gohr // memory cache first 2041078ec26SAndreas Gohr if (isset($this->userCache[$username])) { 2051078ec26SAndreas Gohr if (!$fetchgroups || is_array($this->userCache[$username]['grps'])) { 2061078ec26SAndreas Gohr return $this->userCache[$username]; 2071078ec26SAndreas Gohr } 2081078ec26SAndreas Gohr } 2091078ec26SAndreas Gohr 2106d90d5c8SAndreas Gohr // disk cache second 2115dcabedaSAndreas Gohr if ($this->config['usefscache']) { 2126d90d5c8SAndreas Gohr $cachename = getCacheName($username, '.pureldap-user'); 2136d90d5c8SAndreas Gohr $cachetime = @filemtime($cachename); 2146d90d5c8SAndreas Gohr if ($cachetime && (time() - $cachetime) < $conf['auth_security_timeout']) { 215208fe81aSAndreas Gohr $this->userCache[$username] = json_decode( 216208fe81aSAndreas Gohr file_get_contents($cachename), 217208fe81aSAndreas Gohr true, 218208fe81aSAndreas Gohr 512, 219208fe81aSAndreas Gohr JSON_THROW_ON_ERROR 220208fe81aSAndreas Gohr ); 2216d90d5c8SAndreas Gohr if (!$fetchgroups || is_array($this->userCache[$username]['grps'])) { 2226d90d5c8SAndreas Gohr return $this->userCache[$username]; 2236d90d5c8SAndreas Gohr } 2246d90d5c8SAndreas Gohr } 2255dcabedaSAndreas Gohr } 2266d90d5c8SAndreas Gohr 2271078ec26SAndreas Gohr // fetch fresh data 2281078ec26SAndreas Gohr $info = $this->getUser($username, $fetchgroups); 2291078ec26SAndreas Gohr 2301078ec26SAndreas Gohr // store in cache 2315dcabedaSAndreas Gohr if ($this->config['usefscache'] && $info !== null) { 2321078ec26SAndreas Gohr $this->userCache[$username] = $info; 23308ace392SAndreas Gohr /** @noinspection PhpUndefinedVariableInspection We know that cachename is defined */ 234208fe81aSAndreas Gohr file_put_contents($cachename, json_encode($info, JSON_THROW_ON_ERROR)); 2351078ec26SAndreas Gohr } 2361078ec26SAndreas Gohr 2371078ec26SAndreas Gohr return $info; 2381078ec26SAndreas Gohr } 2391078ec26SAndreas Gohr 2401078ec26SAndreas Gohr /** 2411078ec26SAndreas Gohr * Fetch a single user 2421078ec26SAndreas Gohr * 2431078ec26SAndreas Gohr * @param string $username 2441078ec26SAndreas Gohr * @param bool $fetchgroups Shall groups be fetched, too? 2451078ec26SAndreas Gohr * @return null|array 2461078ec26SAndreas Gohr */ 2471078ec26SAndreas Gohr abstract public function getUser($username, $fetchgroups = true); 2481078ec26SAndreas Gohr 2491078ec26SAndreas Gohr /** 25008ace392SAndreas Gohr * Set a new password for a user 25108ace392SAndreas Gohr * 25208ace392SAndreas Gohr * @param string $username 25308ace392SAndreas Gohr * @param string $newpass 25408ace392SAndreas Gohr * @param string $oldpass Needed for self-service password change in AD 25508ace392SAndreas Gohr * @return bool 25608ace392SAndreas Gohr */ 25708ace392SAndreas Gohr abstract public function setPassword($username, $newpass, $oldpass = null); 25808ace392SAndreas Gohr 25908ace392SAndreas Gohr /** 2605a3b9122SAndreas Gohr * Return a list of all available groups, use cache if available 2615a3b9122SAndreas Gohr * 2625a3b9122SAndreas Gohr * @return string[] 2635a3b9122SAndreas Gohr */ 2645a3b9122SAndreas Gohr public function getCachedGroups() 2655a3b9122SAndreas Gohr { 2665a3b9122SAndreas Gohr if (empty($this->groupCache)) { 2675a3b9122SAndreas Gohr $this->groupCache = $this->getGroups(); 2685a3b9122SAndreas Gohr } 2695a3b9122SAndreas Gohr 2705a3b9122SAndreas Gohr return $this->groupCache; 2715a3b9122SAndreas Gohr } 2725a3b9122SAndreas Gohr 2735a3b9122SAndreas Gohr /** 2745a3b9122SAndreas Gohr * Return a list of all available groups 2755a3b9122SAndreas Gohr * 276b21740b4SAndreas Gohr * Optionally filter the list 277b21740b4SAndreas Gohr * 278b21740b4SAndreas Gohr * @param null|string $match Filter for this, null for all groups 279b21740b4SAndreas Gohr * @param string $filtermethod How to match the groups 2805a3b9122SAndreas Gohr * @return string[] 2815a3b9122SAndreas Gohr */ 282204fba68SAndreas Gohr abstract public function getGroups($match = null, $filtermethod = self::FILTER_EQUAL); 2835a3b9122SAndreas Gohr 28480ac552fSAndreas Gohr /** 285a1128cc0SAndreas Gohr * Clean the user name for use in DokuWiki 28680ac552fSAndreas Gohr * 287a1128cc0SAndreas Gohr * @param string $user 28880ac552fSAndreas Gohr * @return string 28980ac552fSAndreas Gohr */ 290a1128cc0SAndreas Gohr abstract public function cleanUser($user); 29180ac552fSAndreas Gohr 29280ac552fSAndreas Gohr /** 293a1128cc0SAndreas Gohr * Clean the group name for use in DokuWiki 29480ac552fSAndreas Gohr * 295a1128cc0SAndreas Gohr * @param string $group 29680ac552fSAndreas Gohr * @return string 29780ac552fSAndreas Gohr */ 298a1128cc0SAndreas Gohr abstract public function cleanGroup($group); 299a1128cc0SAndreas Gohr 300a1128cc0SAndreas Gohr /** 301a1128cc0SAndreas Gohr * Inheriting classes may want to manipulate the user before binding 302a1128cc0SAndreas Gohr * 303a1128cc0SAndreas Gohr * @param string $user 304a1128cc0SAndreas Gohr * @return string 305a1128cc0SAndreas Gohr */ 306a1128cc0SAndreas Gohr protected function prepareBindUser($user) 307a1128cc0SAndreas Gohr { 308a1128cc0SAndreas Gohr return $user; 309a1128cc0SAndreas Gohr } 31080ac552fSAndreas Gohr 3115a3b9122SAndreas Gohr /** 3121078ec26SAndreas Gohr * Helper method to get the first value of the given attribute 3131078ec26SAndreas Gohr * 3141078ec26SAndreas Gohr * The given attribute may be null, an empty string is returned then 3151078ec26SAndreas Gohr * 3161078ec26SAndreas Gohr * @param Attribute|null $attribute 3171078ec26SAndreas Gohr * @return string 3181078ec26SAndreas Gohr */ 3195a3b9122SAndreas Gohr protected function attr2str($attribute) 3205a3b9122SAndreas Gohr { 3211078ec26SAndreas Gohr if ($attribute !== null) { 3228de38791SAndreas Gohr return $attribute->firstValue() ?? ''; 3231078ec26SAndreas Gohr } 3241078ec26SAndreas Gohr return ''; 3251078ec26SAndreas Gohr } 3261078ec26SAndreas Gohr 3271078ec26SAndreas Gohr /** 3289c590892SAndreas Gohr * Get the attributes that should be fetched for a user 3299c590892SAndreas Gohr * 3309c590892SAndreas Gohr * Can be extended in sub classes 3319c590892SAndreas Gohr * 3329c590892SAndreas Gohr * @return Attribute[] 3339c590892SAndreas Gohr */ 3349c590892SAndreas Gohr protected function userAttributes() 3359c590892SAndreas Gohr { 3369c590892SAndreas Gohr // defaults 3379c590892SAndreas Gohr $attr = [ 3389c590892SAndreas Gohr new Attribute('dn'), 3399c590892SAndreas Gohr new Attribute('displayName'), 3409c590892SAndreas Gohr new Attribute('mail'), 3419c590892SAndreas Gohr ]; 3429c590892SAndreas Gohr // additionals 3439c590892SAndreas Gohr foreach ($this->config['attributes'] as $attribute) { 3449c590892SAndreas Gohr $attr[] = new Attribute($attribute); 3459c590892SAndreas Gohr } 3469c590892SAndreas Gohr return $attr; 3479c590892SAndreas Gohr } 3489c590892SAndreas Gohr 3499c590892SAndreas Gohr /** 3500f498d06SAndreas Gohr * Get the maximum age a password may have before it needs to be changed 3510f498d06SAndreas Gohr * 3520f498d06SAndreas Gohr * @return int 0 if no maximum age is set 3530f498d06SAndreas Gohr */ 354208fe81aSAndreas Gohr public function getMaxPasswordAge() 355208fe81aSAndreas Gohr { 3560f498d06SAndreas Gohr return 0; 3570f498d06SAndreas Gohr } 3580f498d06SAndreas Gohr 3590f498d06SAndreas Gohr /** 360da369b60SAndreas Gohr * Handle fatal exceptions 3611078ec26SAndreas Gohr * 3621078ec26SAndreas Gohr * @param \Exception $e 3631078ec26SAndreas Gohr */ 364da369b60SAndreas Gohr protected function fatal(\Exception $e) 3651078ec26SAndreas Gohr { 36608ace392SAndreas Gohr ErrorHandler::logException($e); 367c872f0e3SAndreas Gohr 3681078ec26SAndreas Gohr if (defined('DOKU_UNITTEST')) { 3698595f73eSAndreas Gohr throw new \RuntimeException('', 0, $e); 3701078ec26SAndreas Gohr } 371c872f0e3SAndreas Gohr 372c872f0e3SAndreas Gohr msg('[pureldap] ' . hsc($e->getMessage()), -1); 373da369b60SAndreas Gohr } 3741078ec26SAndreas Gohr 375da369b60SAndreas Gohr /** 376a1128cc0SAndreas Gohr * Handle error output 377a1128cc0SAndreas Gohr * 378a1128cc0SAndreas Gohr * @param string $msg 379a1128cc0SAndreas Gohr * @param string $file 380a1128cc0SAndreas Gohr * @param int $line 381a1128cc0SAndreas Gohr */ 38208ace392SAndreas Gohr public function error($msg, $file, $line) 383a1128cc0SAndreas Gohr { 38408ace392SAndreas Gohr Logger::error('[pureldap] ' . $msg, '', $file, $line); 385c872f0e3SAndreas Gohr 386a1128cc0SAndreas Gohr if (defined('DOKU_UNITTEST')) { 387a1128cc0SAndreas Gohr throw new \RuntimeException($msg . ' at ' . $file . ':' . $line); 388a1128cc0SAndreas Gohr } 389a1128cc0SAndreas Gohr 390c872f0e3SAndreas Gohr msg('[pureldap] ' . hsc($msg), -1); 391a1128cc0SAndreas Gohr } 392a1128cc0SAndreas Gohr 393a1128cc0SAndreas Gohr /** 394da369b60SAndreas Gohr * Handle debug output 395da369b60SAndreas Gohr * 396da369b60SAndreas Gohr * @param string $msg 397da369b60SAndreas Gohr * @param string $file 398da369b60SAndreas Gohr * @param int $line 399da369b60SAndreas Gohr */ 40008ace392SAndreas Gohr public function debug($msg, $file, $line) 401da369b60SAndreas Gohr { 402c872f0e3SAndreas Gohr global $conf; 403c872f0e3SAndreas Gohr 40408ace392SAndreas Gohr Logger::debug('[pureldap] ' . $msg, '', $file, $line); 405c872f0e3SAndreas Gohr 406c872f0e3SAndreas Gohr if ($conf['allowdebug']) { 40708ace392SAndreas Gohr msg('[pureldap] ' . hsc($msg) . ' at ' . $file . ':' . $line); 4081078ec26SAndreas Gohr } 4091078ec26SAndreas Gohr } 410c872f0e3SAndreas Gohr} 411