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); 43*bf69b89cSAndreas 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 /** 83*bf69b89cSAndreas Gohr * Extract user info from environment for SSO 84*bf69b89cSAndreas Gohr */ 85*bf69b89cSAndreas Gohr protected function prepareSSO() 86*bf69b89cSAndreas Gohr { 87*bf69b89cSAndreas Gohr global $INPUT; 88*bf69b89cSAndreas Gohr 89*bf69b89cSAndreas Gohr if (!$this->config['sso']) return; 90*bf69b89cSAndreas Gohr if ($INPUT->server->str('REMOTE_USER') === '') return; 91*bf69b89cSAndreas Gohr 92*bf69b89cSAndreas Gohr $user = $INPUT->server->str('REMOTE_USER'); 93*bf69b89cSAndreas Gohr 94*bf69b89cSAndreas Gohr // make sure the right encoding is used 95*bf69b89cSAndreas Gohr if ($this->config['sso_charset']) { 96*bf69b89cSAndreas Gohr $user = iconv($this->config['sso_charset'], 'UTF-8', $user); 97*bf69b89cSAndreas Gohr } elseif (!\dokuwiki\Utf8\Clean::isUtf8($user)) { 98*bf69b89cSAndreas Gohr $user = utf8_encode($user); 99*bf69b89cSAndreas Gohr } 100*bf69b89cSAndreas Gohr $user = $this->cleanUser($user); 101*bf69b89cSAndreas Gohr 102*bf69b89cSAndreas Gohr // Prepare SSO 103*bf69b89cSAndreas Gohr 104*bf69b89cSAndreas Gohr // trust the incoming user 105*bf69b89cSAndreas Gohr $INPUT->server->set('REMOTE_USER', $user); 106*bf69b89cSAndreas Gohr 107*bf69b89cSAndreas Gohr // we need to simulate a login 108*bf69b89cSAndreas Gohr if (empty($_COOKIE[DOKU_COOKIE])) { 109*bf69b89cSAndreas Gohr $INPUT->set('u', $user); 110*bf69b89cSAndreas Gohr $INPUT->set('p', 'sso_only'); 111*bf69b89cSAndreas Gohr } 112*bf69b89cSAndreas Gohr } 113*bf69b89cSAndreas Gohr 114*bf69b89cSAndreas Gohr /** 1151078ec26SAndreas Gohr * Authenticate as admin 1161078ec26SAndreas Gohr */ 1171078ec26SAndreas Gohr public function autoAuth() 1181078ec26SAndreas Gohr { 1191078ec26SAndreas Gohr if ($this->isAuthenticated) return true; 1209446f9efSAndreas Gohr 121a1128cc0SAndreas Gohr $user = $this->prepareBindUser($this->config['admin_username']); 1229446f9efSAndreas Gohr $ok = $this->authenticate($user, $this->config['admin_password']); 1238b2677edSAndreas Gohr if (!$ok) { 124a1128cc0SAndreas Gohr $this->error('Administrative bind failed. Probably wrong user/password.', __FILE__, __LINE__); 1258b2677edSAndreas Gohr } 1268b2677edSAndreas Gohr return $ok; 1271078ec26SAndreas Gohr } 1281078ec26SAndreas Gohr 1291078ec26SAndreas Gohr /** 1301078ec26SAndreas Gohr * Authenticates a given user. This client will remain authenticated 1311078ec26SAndreas Gohr * 1321078ec26SAndreas Gohr * @param string $user 1331078ec26SAndreas Gohr * @param string $pass 1341078ec26SAndreas Gohr * @return bool was the authentication successful? 1355a3b9122SAndreas Gohr * @noinspection PhpRedundantCatchClauseInspection 1361078ec26SAndreas Gohr */ 1371078ec26SAndreas Gohr public function authenticate($user, $pass) 1381078ec26SAndreas Gohr { 139a1128cc0SAndreas Gohr $user = $this->prepareBindUser($user); 14080ac552fSAndreas Gohr 1416d90d5c8SAndreas Gohr if ($this->config['encryption'] === 'tls') { 1421078ec26SAndreas Gohr try { 1431078ec26SAndreas Gohr $this->ldap->startTls(); 1441078ec26SAndreas Gohr } catch (OperationException $e) { 145da369b60SAndreas Gohr $this->fatal($e); 1461078ec26SAndreas Gohr } 1471078ec26SAndreas Gohr } 1481078ec26SAndreas Gohr 1491078ec26SAndreas Gohr try { 1501078ec26SAndreas Gohr $this->ldap->bind($user, $pass); 1511078ec26SAndreas Gohr } catch (BindException $e) { 1521078ec26SAndreas Gohr return false; 1531078ec26SAndreas Gohr } catch (ConnectionException $e) { 154da369b60SAndreas Gohr $this->fatal($e); 1551078ec26SAndreas Gohr return false; 1561078ec26SAndreas Gohr } catch (OperationException $e) { 157da369b60SAndreas Gohr $this->fatal($e); 1581078ec26SAndreas Gohr return false; 1591078ec26SAndreas Gohr } 1601078ec26SAndreas Gohr 1611078ec26SAndreas Gohr $this->isAuthenticated = true; 1621078ec26SAndreas Gohr return true; 1631078ec26SAndreas Gohr } 1641078ec26SAndreas Gohr 1651078ec26SAndreas Gohr /** 1661078ec26SAndreas Gohr * Get info for a single user, use cache if available 1671078ec26SAndreas Gohr * 1681078ec26SAndreas Gohr * @param string $username 1691078ec26SAndreas Gohr * @param bool $fetchgroups Are groups needed? 1701078ec26SAndreas Gohr * @return array|null 1711078ec26SAndreas Gohr */ 1721078ec26SAndreas Gohr public function getCachedUser($username, $fetchgroups = true) 1731078ec26SAndreas Gohr { 1746d90d5c8SAndreas Gohr global $conf; 1756d90d5c8SAndreas Gohr 1766d90d5c8SAndreas Gohr // memory cache first 1771078ec26SAndreas Gohr if (isset($this->userCache[$username])) { 1781078ec26SAndreas Gohr if (!$fetchgroups || is_array($this->userCache[$username]['grps'])) { 1791078ec26SAndreas Gohr return $this->userCache[$username]; 1801078ec26SAndreas Gohr } 1811078ec26SAndreas Gohr } 1821078ec26SAndreas Gohr 1836d90d5c8SAndreas Gohr // disk cache second 1846d90d5c8SAndreas Gohr $cachename = getCacheName($username, '.pureldap-user'); 1856d90d5c8SAndreas Gohr $cachetime = @filemtime($cachename); 1866d90d5c8SAndreas Gohr if ($cachetime && (time() - $cachetime) < $conf['auth_security_timeout']) { 1876d90d5c8SAndreas Gohr $this->userCache[$username] = json_decode(file_get_contents($cachename), true); 1886d90d5c8SAndreas Gohr if (!$fetchgroups || is_array($this->userCache[$username]['grps'])) { 1896d90d5c8SAndreas Gohr return $this->userCache[$username]; 1906d90d5c8SAndreas Gohr } 1916d90d5c8SAndreas Gohr } 1926d90d5c8SAndreas Gohr 1931078ec26SAndreas Gohr // fetch fresh data 1941078ec26SAndreas Gohr $info = $this->getUser($username, $fetchgroups); 1951078ec26SAndreas Gohr 1961078ec26SAndreas Gohr // store in cache 1971078ec26SAndreas Gohr if ($info !== null) { 1981078ec26SAndreas Gohr $this->userCache[$username] = $info; 1996d90d5c8SAndreas Gohr file_put_contents($cachename, json_encode($info)); 2001078ec26SAndreas Gohr } 2011078ec26SAndreas Gohr 2021078ec26SAndreas Gohr return $info; 2031078ec26SAndreas Gohr } 2041078ec26SAndreas Gohr 2051078ec26SAndreas Gohr /** 2061078ec26SAndreas Gohr * Fetch a single user 2071078ec26SAndreas Gohr * 2081078ec26SAndreas Gohr * @param string $username 2091078ec26SAndreas Gohr * @param bool $fetchgroups Shall groups be fetched, too? 2101078ec26SAndreas Gohr * @return null|array 2111078ec26SAndreas Gohr */ 2121078ec26SAndreas Gohr abstract public function getUser($username, $fetchgroups = true); 2131078ec26SAndreas Gohr 2141078ec26SAndreas Gohr /** 2155a3b9122SAndreas Gohr * Return a list of all available groups, use cache if available 2165a3b9122SAndreas Gohr * 2175a3b9122SAndreas Gohr * @return string[] 2185a3b9122SAndreas Gohr */ 2195a3b9122SAndreas Gohr public function getCachedGroups() 2205a3b9122SAndreas Gohr { 2215a3b9122SAndreas Gohr if (empty($this->groupCache)) { 2225a3b9122SAndreas Gohr $this->groupCache = $this->getGroups(); 2235a3b9122SAndreas Gohr } 2245a3b9122SAndreas Gohr 2255a3b9122SAndreas Gohr return $this->groupCache; 2265a3b9122SAndreas Gohr } 2275a3b9122SAndreas Gohr 2285a3b9122SAndreas Gohr /** 2295a3b9122SAndreas Gohr * Return a list of all available groups 2305a3b9122SAndreas Gohr * 231b21740b4SAndreas Gohr * Optionally filter the list 232b21740b4SAndreas Gohr * 233b21740b4SAndreas Gohr * @param null|string $match Filter for this, null for all groups 234b21740b4SAndreas Gohr * @param string $filtermethod How to match the groups 2355a3b9122SAndreas Gohr * @return string[] 2365a3b9122SAndreas Gohr */ 237204fba68SAndreas Gohr abstract public function getGroups($match = null, $filtermethod = self::FILTER_EQUAL); 2385a3b9122SAndreas Gohr 23980ac552fSAndreas Gohr /** 240a1128cc0SAndreas Gohr * Clean the user name for use in DokuWiki 24180ac552fSAndreas Gohr * 242a1128cc0SAndreas Gohr * @param string $user 24380ac552fSAndreas Gohr * @return string 24480ac552fSAndreas Gohr */ 245a1128cc0SAndreas Gohr abstract public function cleanUser($user); 24680ac552fSAndreas Gohr 24780ac552fSAndreas Gohr /** 248a1128cc0SAndreas Gohr * Clean the group name for use in DokuWiki 24980ac552fSAndreas Gohr * 250a1128cc0SAndreas Gohr * @param string $group 25180ac552fSAndreas Gohr * @return string 25280ac552fSAndreas Gohr */ 253a1128cc0SAndreas Gohr abstract public function cleanGroup($group); 254a1128cc0SAndreas Gohr 255a1128cc0SAndreas Gohr /** 256a1128cc0SAndreas Gohr * Inheriting classes may want to manipulate the user before binding 257a1128cc0SAndreas Gohr * 258a1128cc0SAndreas Gohr * @param string $user 259a1128cc0SAndreas Gohr * @return string 260a1128cc0SAndreas Gohr */ 261a1128cc0SAndreas Gohr protected function prepareBindUser($user) 262a1128cc0SAndreas Gohr { 263a1128cc0SAndreas Gohr return $user; 264a1128cc0SAndreas Gohr } 26580ac552fSAndreas Gohr 2665a3b9122SAndreas Gohr /** 2671078ec26SAndreas Gohr * Helper method to get the first value of the given attribute 2681078ec26SAndreas Gohr * 2691078ec26SAndreas Gohr * The given attribute may be null, an empty string is returned then 2701078ec26SAndreas Gohr * 2711078ec26SAndreas Gohr * @param Attribute|null $attribute 2721078ec26SAndreas Gohr * @return string 2731078ec26SAndreas Gohr */ 2745a3b9122SAndreas Gohr protected function attr2str($attribute) 2755a3b9122SAndreas Gohr { 2761078ec26SAndreas Gohr if ($attribute !== null) { 2771078ec26SAndreas Gohr return $attribute->firstValue(); 2781078ec26SAndreas Gohr } 2791078ec26SAndreas Gohr return ''; 2801078ec26SAndreas Gohr } 2811078ec26SAndreas Gohr 2821078ec26SAndreas Gohr /** 2839c590892SAndreas Gohr * Get the attributes that should be fetched for a user 2849c590892SAndreas Gohr * 2859c590892SAndreas Gohr * Can be extended in sub classes 2869c590892SAndreas Gohr * 2879c590892SAndreas Gohr * @return Attribute[] 2889c590892SAndreas Gohr */ 2899c590892SAndreas Gohr protected function userAttributes() 2909c590892SAndreas Gohr { 2919c590892SAndreas Gohr // defaults 2929c590892SAndreas Gohr $attr = [ 2939c590892SAndreas Gohr new Attribute('dn'), 2949c590892SAndreas Gohr new Attribute('displayName'), 2959c590892SAndreas Gohr new Attribute('mail'), 2969c590892SAndreas Gohr ]; 2979c590892SAndreas Gohr // additionals 2989c590892SAndreas Gohr foreach ($this->config['attributes'] as $attribute) { 2999c590892SAndreas Gohr $attr[] = new Attribute($attribute); 3009c590892SAndreas Gohr } 3019c590892SAndreas Gohr return $attr; 3029c590892SAndreas Gohr } 3039c590892SAndreas Gohr 3049c590892SAndreas Gohr /** 305da369b60SAndreas Gohr * Handle fatal exceptions 3061078ec26SAndreas Gohr * 3071078ec26SAndreas Gohr * @param \Exception $e 3081078ec26SAndreas Gohr */ 309da369b60SAndreas Gohr protected function fatal(\Exception $e) 3101078ec26SAndreas Gohr { 311c872f0e3SAndreas Gohr if (class_exists('\dokuwiki\ErrorHandler')) { 312c872f0e3SAndreas Gohr \dokuwiki\ErrorHandler::logException($e); 313c872f0e3SAndreas Gohr } 314c872f0e3SAndreas Gohr 3151078ec26SAndreas Gohr if (defined('DOKU_UNITTEST')) { 3168595f73eSAndreas Gohr throw new \RuntimeException('', 0, $e); 3171078ec26SAndreas Gohr } 318c872f0e3SAndreas Gohr 319c872f0e3SAndreas Gohr msg('[pureldap] ' . hsc($e->getMessage()), -1); 320da369b60SAndreas Gohr } 3211078ec26SAndreas Gohr 322da369b60SAndreas Gohr /** 323a1128cc0SAndreas Gohr * Handle error output 324a1128cc0SAndreas Gohr * 325a1128cc0SAndreas Gohr * @param string $msg 326a1128cc0SAndreas Gohr * @param string $file 327a1128cc0SAndreas Gohr * @param int $line 328a1128cc0SAndreas Gohr */ 329a1128cc0SAndreas Gohr protected function error($msg, $file, $line) 330a1128cc0SAndreas Gohr { 331c872f0e3SAndreas Gohr if (class_exists('\dokuwiki\Logger')) { 332c872f0e3SAndreas Gohr \dokuwiki\Logger::error('[pureldap] ' . $msg, '', $file, $line); 333c872f0e3SAndreas Gohr } 334c872f0e3SAndreas Gohr 335a1128cc0SAndreas Gohr if (defined('DOKU_UNITTEST')) { 336a1128cc0SAndreas Gohr throw new \RuntimeException($msg . ' at ' . $file . ':' . $line); 337a1128cc0SAndreas Gohr } 338a1128cc0SAndreas Gohr 339c872f0e3SAndreas Gohr msg('[pureldap] ' . hsc($msg), -1); 340a1128cc0SAndreas Gohr } 341a1128cc0SAndreas Gohr 342a1128cc0SAndreas Gohr /** 343da369b60SAndreas Gohr * Handle debug output 344da369b60SAndreas Gohr * 345da369b60SAndreas Gohr * @param string $msg 346da369b60SAndreas Gohr * @param string $file 347da369b60SAndreas Gohr * @param int $line 348da369b60SAndreas Gohr */ 349da369b60SAndreas Gohr protected function debug($msg, $file, $line) 350da369b60SAndreas Gohr { 351c872f0e3SAndreas Gohr global $conf; 352c872f0e3SAndreas Gohr 353c872f0e3SAndreas Gohr if (class_exists('\dokuwiki\Logger')) { 354c872f0e3SAndreas Gohr \dokuwiki\Logger::debug('[pureldap] ' . $msg, '', $file, $line); 355c872f0e3SAndreas Gohr } 356c872f0e3SAndreas Gohr 357c872f0e3SAndreas Gohr if ($conf['allowdebug']) { 35885916a2dSAndreas Gohr msg('[pureldap] ' . hsc($msg) . ' at ' . $file . ':' . $line, 0); 3591078ec26SAndreas Gohr } 3601078ec26SAndreas Gohr } 361c872f0e3SAndreas Gohr} 362