11078ec26SAndreas Gohr<?php 21078ec26SAndreas Gohr 31078ec26SAndreas Gohrnamespace dokuwiki\plugin\pureldap\classes; 41078ec26SAndreas Gohr 580ac552fSAndreas Gohruse dokuwiki\Utf8\PhpString; 61078ec26SAndreas Gohruse FreeDSx\Ldap\Entry\Attribute; 71078ec26SAndreas Gohruse FreeDSx\Ldap\Exception\BindException; 81078ec26SAndreas Gohruse FreeDSx\Ldap\Exception\ConnectionException; 91078ec26SAndreas Gohruse FreeDSx\Ldap\Exception\OperationException; 101078ec26SAndreas Gohruse FreeDSx\Ldap\LdapClient; 111078ec26SAndreas Gohr 121078ec26SAndreas Gohrrequire_once __DIR__ . '/../vendor/autoload.php'; 131078ec26SAndreas Gohr 141078ec26SAndreas Gohrabstract class Client 151078ec26SAndreas Gohr{ 16204fba68SAndreas Gohr const FILTER_EQUAL = 'equal'; 17204fba68SAndreas Gohr const FILTER_CONTAINS = 'contains'; 18204fba68SAndreas Gohr const FILTER_STARTSWITH = 'startsWith'; 19204fba68SAndreas Gohr const FILTER_ENDSWITH = 'endsWith'; 20204fba68SAndreas Gohr 211078ec26SAndreas Gohr /** @var array the configuration */ 221078ec26SAndreas Gohr protected $config; 231078ec26SAndreas Gohr 241078ec26SAndreas Gohr /** @var LdapClient */ 251078ec26SAndreas Gohr protected $ldap; 261078ec26SAndreas Gohr 271078ec26SAndreas Gohr /** @var bool is this client authenticated already? */ 281078ec26SAndreas Gohr protected $isAuthenticated = false; 291078ec26SAndreas Gohr 301078ec26SAndreas Gohr /** @var array cached user info */ 311078ec26SAndreas Gohr protected $userCache = []; 321078ec26SAndreas Gohr 335a3b9122SAndreas Gohr /** @var array cached group list */ 345a3b9122SAndreas Gohr protected $groupCache = []; 355a3b9122SAndreas Gohr 361078ec26SAndreas Gohr /** 371078ec26SAndreas Gohr * Client constructor. 381078ec26SAndreas Gohr * @param array $config 391078ec26SAndreas Gohr */ 401078ec26SAndreas Gohr public function __construct($config) 411078ec26SAndreas Gohr { 421078ec26SAndreas Gohr $this->config = $this->prepareConfig($config); 43bf69b89cSAndreas Gohr $this->prepareSSO(); 441078ec26SAndreas Gohr $this->ldap = new LdapClient($this->config); 451078ec26SAndreas Gohr } 461078ec26SAndreas Gohr 471078ec26SAndreas Gohr /** 481078ec26SAndreas Gohr * Setup sane config defaults 491078ec26SAndreas Gohr * 501078ec26SAndreas Gohr * @param array $config 511078ec26SAndreas Gohr * @return array 521078ec26SAndreas Gohr */ 531078ec26SAndreas Gohr protected function prepareConfig($config) 541078ec26SAndreas Gohr { 551a4f0e1fSAndreas Gohr // ensure we have the default keys 561a4f0e1fSAndreas Gohr /** @var array $conf */ 571a4f0e1fSAndreas Gohr include __DIR__ . '/../conf/default.php'; 581a4f0e1fSAndreas Gohr $defaults = $conf; 591a4f0e1fSAndreas Gohr $defaults['defaultgroup'] = 'user'; // we expect this to be passed from global conf 601078ec26SAndreas Gohr 611078ec26SAndreas Gohr $config = array_merge($defaults, $config); 621078ec26SAndreas Gohr 631078ec26SAndreas Gohr // default port depends on SSL setting 641078ec26SAndreas Gohr if (!$config['port']) { 656d90d5c8SAndreas Gohr $config['port'] = ($config['encryption'] === 'ssl') ? 636 : 389; 666d90d5c8SAndreas Gohr } 676d90d5c8SAndreas Gohr 686d90d5c8SAndreas Gohr // set ssl parameters 696d90d5c8SAndreas Gohr $config['use_ssl'] = ($config['encryption'] === 'ssl'); 706d90d5c8SAndreas Gohr if ($config['validate'] === 'none') { 716d90d5c8SAndreas Gohr $config['ssl_validate_cert'] = false; 726d90d5c8SAndreas Gohr } elseif ($config['validate'] === 'self') { 736d90d5c8SAndreas Gohr $config['ssl_allow_self_signed'] = true; 741078ec26SAndreas Gohr } 751078ec26SAndreas Gohr 76a1128cc0SAndreas Gohr $config['suffix'] = PhpString::strtolower($config['suffix']); 77c2500b44SAndreas Gohr $config['primarygroup'] = $this->cleanGroup($config['primarygroup']); 7880ac552fSAndreas Gohr 791078ec26SAndreas Gohr return $config; 801078ec26SAndreas Gohr } 811078ec26SAndreas Gohr 821078ec26SAndreas Gohr /** 83bf69b89cSAndreas Gohr * Extract user info from environment for SSO 84bf69b89cSAndreas Gohr */ 85bf69b89cSAndreas Gohr protected function prepareSSO() 86bf69b89cSAndreas Gohr { 87bf69b89cSAndreas Gohr global $INPUT; 88bf69b89cSAndreas Gohr 89bf69b89cSAndreas Gohr if (!$this->config['sso']) return; 90bf69b89cSAndreas Gohr if ($INPUT->server->str('REMOTE_USER') === '') return; 91bf69b89cSAndreas Gohr 92bf69b89cSAndreas Gohr $user = $INPUT->server->str('REMOTE_USER'); 93bf69b89cSAndreas Gohr 94bf69b89cSAndreas Gohr // make sure the right encoding is used 95bf69b89cSAndreas Gohr if ($this->config['sso_charset']) { 96bf69b89cSAndreas Gohr $user = iconv($this->config['sso_charset'], 'UTF-8', $user); 97bf69b89cSAndreas Gohr } elseif (!\dokuwiki\Utf8\Clean::isUtf8($user)) { 98bf69b89cSAndreas Gohr $user = utf8_encode($user); 99bf69b89cSAndreas Gohr } 100bf69b89cSAndreas Gohr $user = $this->cleanUser($user); 101bf69b89cSAndreas Gohr 102bf69b89cSAndreas Gohr // Prepare SSO 103bf69b89cSAndreas Gohr 104bf69b89cSAndreas Gohr // trust the incoming user 105bf69b89cSAndreas Gohr $INPUT->server->set('REMOTE_USER', $user); 106bf69b89cSAndreas Gohr 107bf69b89cSAndreas Gohr // we need to simulate a login 108bf69b89cSAndreas Gohr if (empty($_COOKIE[DOKU_COOKIE])) { 109bf69b89cSAndreas Gohr $INPUT->set('u', $user); 110bf69b89cSAndreas Gohr $INPUT->set('p', 'sso_only'); 111bf69b89cSAndreas Gohr } 112bf69b89cSAndreas Gohr } 113bf69b89cSAndreas Gohr 114bf69b89cSAndreas Gohr /** 11522654fdeSAndreas Gohr * Access to the config values 11622654fdeSAndreas Gohr * 11722654fdeSAndreas Gohr * Because the client class does configuration cleanup, the auth class should access 11822654fdeSAndreas Gohr * config values through the client 11922654fdeSAndreas Gohr * 12022654fdeSAndreas Gohr * @param string $key 12122654fdeSAndreas Gohr * @return mixed returns null on missing config 12222654fdeSAndreas Gohr */ 12322654fdeSAndreas Gohr public function getConf($key) { 12422654fdeSAndreas Gohr if(!isset($this->config[$key])) return null; 12522654fdeSAndreas Gohr return $this->config[$key]; 12622654fdeSAndreas Gohr } 12722654fdeSAndreas Gohr 12822654fdeSAndreas Gohr /** 1291078ec26SAndreas Gohr * Authenticate as admin 1301078ec26SAndreas Gohr */ 1311078ec26SAndreas Gohr public function autoAuth() 1321078ec26SAndreas Gohr { 1331078ec26SAndreas Gohr if ($this->isAuthenticated) return true; 1349446f9efSAndreas Gohr 135a1128cc0SAndreas Gohr $user = $this->prepareBindUser($this->config['admin_username']); 1369446f9efSAndreas Gohr $ok = $this->authenticate($user, $this->config['admin_password']); 1378b2677edSAndreas Gohr if (!$ok) { 138a1128cc0SAndreas Gohr $this->error('Administrative bind failed. Probably wrong user/password.', __FILE__, __LINE__); 1398b2677edSAndreas Gohr } 1408b2677edSAndreas Gohr return $ok; 1411078ec26SAndreas Gohr } 1421078ec26SAndreas Gohr 1431078ec26SAndreas Gohr /** 1441078ec26SAndreas Gohr * Authenticates a given user. This client will remain authenticated 1451078ec26SAndreas Gohr * 1461078ec26SAndreas Gohr * @param string $user 1471078ec26SAndreas Gohr * @param string $pass 1481078ec26SAndreas Gohr * @return bool was the authentication successful? 1495a3b9122SAndreas Gohr * @noinspection PhpRedundantCatchClauseInspection 1501078ec26SAndreas Gohr */ 1511078ec26SAndreas Gohr public function authenticate($user, $pass) 1521078ec26SAndreas Gohr { 153a1128cc0SAndreas Gohr $user = $this->prepareBindUser($user); 15480ac552fSAndreas Gohr 1556d90d5c8SAndreas Gohr if ($this->config['encryption'] === 'tls') { 1561078ec26SAndreas Gohr try { 1571078ec26SAndreas Gohr $this->ldap->startTls(); 1581078ec26SAndreas Gohr } catch (OperationException $e) { 159da369b60SAndreas Gohr $this->fatal($e); 1601078ec26SAndreas Gohr } 1611078ec26SAndreas Gohr } 1621078ec26SAndreas Gohr 1631078ec26SAndreas Gohr try { 1641078ec26SAndreas Gohr $this->ldap->bind($user, $pass); 1651078ec26SAndreas Gohr } catch (BindException $e) { 1661078ec26SAndreas Gohr return false; 1671078ec26SAndreas Gohr } catch (ConnectionException $e) { 168da369b60SAndreas Gohr $this->fatal($e); 1691078ec26SAndreas Gohr return false; 1701078ec26SAndreas Gohr } catch (OperationException $e) { 171da369b60SAndreas Gohr $this->fatal($e); 1721078ec26SAndreas Gohr return false; 1731078ec26SAndreas Gohr } 1741078ec26SAndreas Gohr 1751078ec26SAndreas Gohr $this->isAuthenticated = true; 1761078ec26SAndreas Gohr return true; 1771078ec26SAndreas Gohr } 1781078ec26SAndreas Gohr 1791078ec26SAndreas Gohr /** 1801078ec26SAndreas Gohr * Get info for a single user, use cache if available 1811078ec26SAndreas Gohr * 1821078ec26SAndreas Gohr * @param string $username 1831078ec26SAndreas Gohr * @param bool $fetchgroups Are groups needed? 1841078ec26SAndreas Gohr * @return array|null 1851078ec26SAndreas Gohr */ 1861078ec26SAndreas Gohr public function getCachedUser($username, $fetchgroups = true) 1871078ec26SAndreas Gohr { 1886d90d5c8SAndreas Gohr global $conf; 1896d90d5c8SAndreas Gohr 1906d90d5c8SAndreas Gohr // memory cache first 1911078ec26SAndreas Gohr if (isset($this->userCache[$username])) { 1921078ec26SAndreas Gohr if (!$fetchgroups || is_array($this->userCache[$username]['grps'])) { 1931078ec26SAndreas Gohr return $this->userCache[$username]; 1941078ec26SAndreas Gohr } 1951078ec26SAndreas Gohr } 1961078ec26SAndreas Gohr 1976d90d5c8SAndreas Gohr // disk cache second 198*5dcabedaSAndreas Gohr if($this->config['usefscache']) { 1996d90d5c8SAndreas Gohr $cachename = getCacheName($username, '.pureldap-user'); 2006d90d5c8SAndreas Gohr $cachetime = @filemtime($cachename); 2016d90d5c8SAndreas Gohr if ($cachetime && (time() - $cachetime) < $conf['auth_security_timeout']) { 2026d90d5c8SAndreas Gohr $this->userCache[$username] = json_decode(file_get_contents($cachename), true); 2036d90d5c8SAndreas Gohr if (!$fetchgroups || is_array($this->userCache[$username]['grps'])) { 2046d90d5c8SAndreas Gohr return $this->userCache[$username]; 2056d90d5c8SAndreas Gohr } 2066d90d5c8SAndreas Gohr } 207*5dcabedaSAndreas Gohr } 2086d90d5c8SAndreas Gohr 2091078ec26SAndreas Gohr // fetch fresh data 2101078ec26SAndreas Gohr $info = $this->getUser($username, $fetchgroups); 2111078ec26SAndreas Gohr 2121078ec26SAndreas Gohr // store in cache 213*5dcabedaSAndreas Gohr if ($this->config['usefscache'] && $info !== null) { 2141078ec26SAndreas Gohr $this->userCache[$username] = $info; 2156d90d5c8SAndreas Gohr file_put_contents($cachename, json_encode($info)); 2161078ec26SAndreas Gohr } 2171078ec26SAndreas Gohr 2181078ec26SAndreas Gohr return $info; 2191078ec26SAndreas Gohr } 2201078ec26SAndreas Gohr 2211078ec26SAndreas Gohr /** 2221078ec26SAndreas Gohr * Fetch a single user 2231078ec26SAndreas Gohr * 2241078ec26SAndreas Gohr * @param string $username 2251078ec26SAndreas Gohr * @param bool $fetchgroups Shall groups be fetched, too? 2261078ec26SAndreas Gohr * @return null|array 2271078ec26SAndreas Gohr */ 2281078ec26SAndreas Gohr abstract public function getUser($username, $fetchgroups = true); 2291078ec26SAndreas Gohr 2301078ec26SAndreas Gohr /** 2315a3b9122SAndreas Gohr * Return a list of all available groups, use cache if available 2325a3b9122SAndreas Gohr * 2335a3b9122SAndreas Gohr * @return string[] 2345a3b9122SAndreas Gohr */ 2355a3b9122SAndreas Gohr public function getCachedGroups() 2365a3b9122SAndreas Gohr { 2375a3b9122SAndreas Gohr if (empty($this->groupCache)) { 2385a3b9122SAndreas Gohr $this->groupCache = $this->getGroups(); 2395a3b9122SAndreas Gohr } 2405a3b9122SAndreas Gohr 2415a3b9122SAndreas Gohr return $this->groupCache; 2425a3b9122SAndreas Gohr } 2435a3b9122SAndreas Gohr 2445a3b9122SAndreas Gohr /** 2455a3b9122SAndreas Gohr * Return a list of all available groups 2465a3b9122SAndreas Gohr * 247b21740b4SAndreas Gohr * Optionally filter the list 248b21740b4SAndreas Gohr * 249b21740b4SAndreas Gohr * @param null|string $match Filter for this, null for all groups 250b21740b4SAndreas Gohr * @param string $filtermethod How to match the groups 2515a3b9122SAndreas Gohr * @return string[] 2525a3b9122SAndreas Gohr */ 253204fba68SAndreas Gohr abstract public function getGroups($match = null, $filtermethod = self::FILTER_EQUAL); 2545a3b9122SAndreas Gohr 25580ac552fSAndreas Gohr /** 256a1128cc0SAndreas Gohr * Clean the user name for use in DokuWiki 25780ac552fSAndreas Gohr * 258a1128cc0SAndreas Gohr * @param string $user 25980ac552fSAndreas Gohr * @return string 26080ac552fSAndreas Gohr */ 261a1128cc0SAndreas Gohr abstract public function cleanUser($user); 26280ac552fSAndreas Gohr 26380ac552fSAndreas Gohr /** 264a1128cc0SAndreas Gohr * Clean the group name for use in DokuWiki 26580ac552fSAndreas Gohr * 266a1128cc0SAndreas Gohr * @param string $group 26780ac552fSAndreas Gohr * @return string 26880ac552fSAndreas Gohr */ 269a1128cc0SAndreas Gohr abstract public function cleanGroup($group); 270a1128cc0SAndreas Gohr 271a1128cc0SAndreas Gohr /** 272a1128cc0SAndreas Gohr * Inheriting classes may want to manipulate the user before binding 273a1128cc0SAndreas Gohr * 274a1128cc0SAndreas Gohr * @param string $user 275a1128cc0SAndreas Gohr * @return string 276a1128cc0SAndreas Gohr */ 277a1128cc0SAndreas Gohr protected function prepareBindUser($user) 278a1128cc0SAndreas Gohr { 279a1128cc0SAndreas Gohr return $user; 280a1128cc0SAndreas Gohr } 28180ac552fSAndreas Gohr 2825a3b9122SAndreas Gohr /** 2831078ec26SAndreas Gohr * Helper method to get the first value of the given attribute 2841078ec26SAndreas Gohr * 2851078ec26SAndreas Gohr * The given attribute may be null, an empty string is returned then 2861078ec26SAndreas Gohr * 2871078ec26SAndreas Gohr * @param Attribute|null $attribute 2881078ec26SAndreas Gohr * @return string 2891078ec26SAndreas Gohr */ 2905a3b9122SAndreas Gohr protected function attr2str($attribute) 2915a3b9122SAndreas Gohr { 2921078ec26SAndreas Gohr if ($attribute !== null) { 2931078ec26SAndreas Gohr return $attribute->firstValue(); 2941078ec26SAndreas Gohr } 2951078ec26SAndreas Gohr return ''; 2961078ec26SAndreas Gohr } 2971078ec26SAndreas Gohr 2981078ec26SAndreas Gohr /** 2999c590892SAndreas Gohr * Get the attributes that should be fetched for a user 3009c590892SAndreas Gohr * 3019c590892SAndreas Gohr * Can be extended in sub classes 3029c590892SAndreas Gohr * 3039c590892SAndreas Gohr * @return Attribute[] 3049c590892SAndreas Gohr */ 3059c590892SAndreas Gohr protected function userAttributes() 3069c590892SAndreas Gohr { 3079c590892SAndreas Gohr // defaults 3089c590892SAndreas Gohr $attr = [ 3099c590892SAndreas Gohr new Attribute('dn'), 3109c590892SAndreas Gohr new Attribute('displayName'), 3119c590892SAndreas Gohr new Attribute('mail'), 3129c590892SAndreas Gohr ]; 3139c590892SAndreas Gohr // additionals 3149c590892SAndreas Gohr foreach ($this->config['attributes'] as $attribute) { 3159c590892SAndreas Gohr $attr[] = new Attribute($attribute); 3169c590892SAndreas Gohr } 3179c590892SAndreas Gohr return $attr; 3189c590892SAndreas Gohr } 3199c590892SAndreas Gohr 3209c590892SAndreas Gohr /** 321da369b60SAndreas Gohr * Handle fatal exceptions 3221078ec26SAndreas Gohr * 3231078ec26SAndreas Gohr * @param \Exception $e 3241078ec26SAndreas Gohr */ 325da369b60SAndreas Gohr protected function fatal(\Exception $e) 3261078ec26SAndreas Gohr { 327c872f0e3SAndreas Gohr if (class_exists('\dokuwiki\ErrorHandler')) { 328c872f0e3SAndreas Gohr \dokuwiki\ErrorHandler::logException($e); 329c872f0e3SAndreas Gohr } 330c872f0e3SAndreas Gohr 3311078ec26SAndreas Gohr if (defined('DOKU_UNITTEST')) { 3328595f73eSAndreas Gohr throw new \RuntimeException('', 0, $e); 3331078ec26SAndreas Gohr } 334c872f0e3SAndreas Gohr 335c872f0e3SAndreas Gohr msg('[pureldap] ' . hsc($e->getMessage()), -1); 336da369b60SAndreas Gohr } 3371078ec26SAndreas Gohr 338da369b60SAndreas Gohr /** 339a1128cc0SAndreas Gohr * Handle error output 340a1128cc0SAndreas Gohr * 341a1128cc0SAndreas Gohr * @param string $msg 342a1128cc0SAndreas Gohr * @param string $file 343a1128cc0SAndreas Gohr * @param int $line 344a1128cc0SAndreas Gohr */ 345a1128cc0SAndreas Gohr protected function error($msg, $file, $line) 346a1128cc0SAndreas Gohr { 347c872f0e3SAndreas Gohr if (class_exists('\dokuwiki\Logger')) { 348c872f0e3SAndreas Gohr \dokuwiki\Logger::error('[pureldap] ' . $msg, '', $file, $line); 349c872f0e3SAndreas Gohr } 350c872f0e3SAndreas Gohr 351a1128cc0SAndreas Gohr if (defined('DOKU_UNITTEST')) { 352a1128cc0SAndreas Gohr throw new \RuntimeException($msg . ' at ' . $file . ':' . $line); 353a1128cc0SAndreas Gohr } 354a1128cc0SAndreas Gohr 355c872f0e3SAndreas Gohr msg('[pureldap] ' . hsc($msg), -1); 356a1128cc0SAndreas Gohr } 357a1128cc0SAndreas Gohr 358a1128cc0SAndreas Gohr /** 359da369b60SAndreas Gohr * Handle debug output 360da369b60SAndreas Gohr * 361da369b60SAndreas Gohr * @param string $msg 362da369b60SAndreas Gohr * @param string $file 363da369b60SAndreas Gohr * @param int $line 364da369b60SAndreas Gohr */ 365da369b60SAndreas Gohr protected function debug($msg, $file, $line) 366da369b60SAndreas Gohr { 367c872f0e3SAndreas Gohr global $conf; 368c872f0e3SAndreas Gohr 369c872f0e3SAndreas Gohr if (class_exists('\dokuwiki\Logger')) { 370c872f0e3SAndreas Gohr \dokuwiki\Logger::debug('[pureldap] ' . $msg, '', $file, $line); 371c872f0e3SAndreas Gohr } 372c872f0e3SAndreas Gohr 373c872f0e3SAndreas Gohr if ($conf['allowdebug']) { 37485916a2dSAndreas Gohr msg('[pureldap] ' . hsc($msg) . ' at ' . $file . ':' . $line, 0); 3751078ec26SAndreas Gohr } 3761078ec26SAndreas Gohr } 377c872f0e3SAndreas Gohr} 378