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); 431078ec26SAndreas Gohr $this->ldap = new LdapClient($this->config); 441078ec26SAndreas Gohr } 451078ec26SAndreas Gohr 461078ec26SAndreas Gohr /** 471078ec26SAndreas Gohr * Setup sane config defaults 481078ec26SAndreas Gohr * 491078ec26SAndreas Gohr * @param array $config 501078ec26SAndreas Gohr * @return array 511078ec26SAndreas Gohr */ 521078ec26SAndreas Gohr protected function prepareConfig($config) 531078ec26SAndreas Gohr { 541078ec26SAndreas Gohr $defaults = [ 551078ec26SAndreas Gohr 'defaultgroup' => 'user', // we expect this to be passed from global conf 56a1128cc0SAndreas Gohr 'suffix' => '', 571078ec26SAndreas Gohr 'port' => '', 586d90d5c8SAndreas Gohr 'encryption' => false, 591078ec26SAndreas Gohr 'admin_username' => '', 601078ec26SAndreas Gohr 'admin_password' => '', 61b914569fSAndreas Gohr 'page_size' => 150, 626d90d5c8SAndreas Gohr 'use_ssl' => false, 636d90d5c8SAndreas Gohr 'validate' => 'strict', 64*c2500b44SAndreas Gohr 'primarygroup' => 'domain users', 65b914569fSAndreas Gohr 'attributes' => [], 661078ec26SAndreas Gohr ]; 671078ec26SAndreas Gohr 681078ec26SAndreas Gohr $config = array_merge($defaults, $config); 691078ec26SAndreas Gohr 701078ec26SAndreas Gohr // default port depends on SSL setting 711078ec26SAndreas Gohr if (!$config['port']) { 726d90d5c8SAndreas Gohr $config['port'] = ($config['encryption'] === 'ssl') ? 636 : 389; 736d90d5c8SAndreas Gohr } 746d90d5c8SAndreas Gohr 756d90d5c8SAndreas Gohr // set ssl parameters 766d90d5c8SAndreas Gohr $config['use_ssl'] = ($config['encryption'] === 'ssl'); 776d90d5c8SAndreas Gohr if ($config['validate'] === 'none') { 786d90d5c8SAndreas Gohr $config['ssl_validate_cert'] = false; 796d90d5c8SAndreas Gohr } elseif ($config['validate'] === 'self') { 806d90d5c8SAndreas Gohr $config['ssl_allow_self_signed'] = true; 811078ec26SAndreas Gohr } 821078ec26SAndreas Gohr 83a1128cc0SAndreas Gohr $config['suffix'] = PhpString::strtolower($config['suffix']); 84*c2500b44SAndreas Gohr $config['primarygroup'] = $this->cleanGroup($config['primarygroup']); 8580ac552fSAndreas Gohr 861078ec26SAndreas Gohr return $config; 871078ec26SAndreas Gohr } 881078ec26SAndreas Gohr 891078ec26SAndreas Gohr /** 901078ec26SAndreas Gohr * Authenticate as admin 911078ec26SAndreas Gohr */ 921078ec26SAndreas Gohr public function autoAuth() 931078ec26SAndreas Gohr { 941078ec26SAndreas Gohr if ($this->isAuthenticated) return true; 959446f9efSAndreas Gohr 96a1128cc0SAndreas Gohr $user = $this->prepareBindUser($this->config['admin_username']); 979446f9efSAndreas Gohr $ok = $this->authenticate($user, $this->config['admin_password']); 988b2677edSAndreas Gohr if (!$ok) { 99a1128cc0SAndreas Gohr $this->error('Administrative bind failed. Probably wrong user/password.', __FILE__, __LINE__); 1008b2677edSAndreas Gohr } 1018b2677edSAndreas Gohr return $ok; 1021078ec26SAndreas Gohr } 1031078ec26SAndreas Gohr 1041078ec26SAndreas Gohr /** 1051078ec26SAndreas Gohr * Authenticates a given user. This client will remain authenticated 1061078ec26SAndreas Gohr * 1071078ec26SAndreas Gohr * @param string $user 1081078ec26SAndreas Gohr * @param string $pass 1091078ec26SAndreas Gohr * @return bool was the authentication successful? 1105a3b9122SAndreas Gohr * @noinspection PhpRedundantCatchClauseInspection 1111078ec26SAndreas Gohr */ 1121078ec26SAndreas Gohr public function authenticate($user, $pass) 1131078ec26SAndreas Gohr { 114a1128cc0SAndreas Gohr $user = $this->prepareBindUser($user); 11580ac552fSAndreas Gohr 1166d90d5c8SAndreas Gohr if ($this->config['encryption'] === 'tls') { 1171078ec26SAndreas Gohr try { 1181078ec26SAndreas Gohr $this->ldap->startTls(); 1191078ec26SAndreas Gohr } catch (OperationException $e) { 120da369b60SAndreas Gohr $this->fatal($e); 1211078ec26SAndreas Gohr } 1221078ec26SAndreas Gohr } 1231078ec26SAndreas Gohr 1241078ec26SAndreas Gohr try { 1251078ec26SAndreas Gohr $this->ldap->bind($user, $pass); 1261078ec26SAndreas Gohr } catch (BindException $e) { 1271078ec26SAndreas Gohr return false; 1281078ec26SAndreas Gohr } catch (ConnectionException $e) { 129da369b60SAndreas Gohr $this->fatal($e); 1301078ec26SAndreas Gohr return false; 1311078ec26SAndreas Gohr } catch (OperationException $e) { 132da369b60SAndreas Gohr $this->fatal($e); 1331078ec26SAndreas Gohr return false; 1341078ec26SAndreas Gohr } 1351078ec26SAndreas Gohr 1361078ec26SAndreas Gohr $this->isAuthenticated = true; 1371078ec26SAndreas Gohr return true; 1381078ec26SAndreas Gohr } 1391078ec26SAndreas Gohr 1401078ec26SAndreas Gohr /** 1411078ec26SAndreas Gohr * Get info for a single user, use cache if available 1421078ec26SAndreas Gohr * 1431078ec26SAndreas Gohr * @param string $username 1441078ec26SAndreas Gohr * @param bool $fetchgroups Are groups needed? 1451078ec26SAndreas Gohr * @return array|null 1461078ec26SAndreas Gohr */ 1471078ec26SAndreas Gohr public function getCachedUser($username, $fetchgroups = true) 1481078ec26SAndreas Gohr { 1496d90d5c8SAndreas Gohr global $conf; 1506d90d5c8SAndreas Gohr 1516d90d5c8SAndreas Gohr // memory cache first 1521078ec26SAndreas Gohr if (isset($this->userCache[$username])) { 1531078ec26SAndreas Gohr if (!$fetchgroups || is_array($this->userCache[$username]['grps'])) { 1541078ec26SAndreas Gohr return $this->userCache[$username]; 1551078ec26SAndreas Gohr } 1561078ec26SAndreas Gohr } 1571078ec26SAndreas Gohr 1586d90d5c8SAndreas Gohr // disk cache second 1596d90d5c8SAndreas Gohr $cachename = getCacheName($username, '.pureldap-user'); 1606d90d5c8SAndreas Gohr $cachetime = @filemtime($cachename); 1616d90d5c8SAndreas Gohr if ($cachetime && (time() - $cachetime) < $conf['auth_security_timeout']) { 1626d90d5c8SAndreas Gohr $this->userCache[$username] = json_decode(file_get_contents($cachename), true); 1636d90d5c8SAndreas Gohr if (!$fetchgroups || is_array($this->userCache[$username]['grps'])) { 1646d90d5c8SAndreas Gohr return $this->userCache[$username]; 1656d90d5c8SAndreas Gohr } 1666d90d5c8SAndreas Gohr } 1676d90d5c8SAndreas Gohr 1681078ec26SAndreas Gohr // fetch fresh data 1691078ec26SAndreas Gohr $info = $this->getUser($username, $fetchgroups); 1701078ec26SAndreas Gohr 1711078ec26SAndreas Gohr // store in cache 1721078ec26SAndreas Gohr if ($info !== null) { 1731078ec26SAndreas Gohr $this->userCache[$username] = $info; 1746d90d5c8SAndreas Gohr file_put_contents($cachename, json_encode($info)); 1751078ec26SAndreas Gohr } 1761078ec26SAndreas Gohr 1771078ec26SAndreas Gohr return $info; 1781078ec26SAndreas Gohr } 1791078ec26SAndreas Gohr 1801078ec26SAndreas Gohr /** 1811078ec26SAndreas Gohr * Fetch a single user 1821078ec26SAndreas Gohr * 1831078ec26SAndreas Gohr * @param string $username 1841078ec26SAndreas Gohr * @param bool $fetchgroups Shall groups be fetched, too? 1851078ec26SAndreas Gohr * @return null|array 1861078ec26SAndreas Gohr */ 1871078ec26SAndreas Gohr abstract public function getUser($username, $fetchgroups = true); 1881078ec26SAndreas Gohr 1891078ec26SAndreas Gohr /** 1905a3b9122SAndreas Gohr * Return a list of all available groups, use cache if available 1915a3b9122SAndreas Gohr * 1925a3b9122SAndreas Gohr * @return string[] 1935a3b9122SAndreas Gohr */ 1945a3b9122SAndreas Gohr public function getCachedGroups() 1955a3b9122SAndreas Gohr { 1965a3b9122SAndreas Gohr if (empty($this->groupCache)) { 1975a3b9122SAndreas Gohr $this->groupCache = $this->getGroups(); 1985a3b9122SAndreas Gohr } 1995a3b9122SAndreas Gohr 2005a3b9122SAndreas Gohr return $this->groupCache; 2015a3b9122SAndreas Gohr } 2025a3b9122SAndreas Gohr 2035a3b9122SAndreas Gohr /** 2045a3b9122SAndreas Gohr * Return a list of all available groups 2055a3b9122SAndreas Gohr * 206b21740b4SAndreas Gohr * Optionally filter the list 207b21740b4SAndreas Gohr * 208b21740b4SAndreas Gohr * @param null|string $match Filter for this, null for all groups 209b21740b4SAndreas Gohr * @param string $filtermethod How to match the groups 2105a3b9122SAndreas Gohr * @return string[] 2115a3b9122SAndreas Gohr */ 212204fba68SAndreas Gohr abstract public function getGroups($match = null, $filtermethod = self::FILTER_EQUAL); 2135a3b9122SAndreas Gohr 21480ac552fSAndreas Gohr /** 215a1128cc0SAndreas Gohr * Clean the user name for use in DokuWiki 21680ac552fSAndreas Gohr * 217a1128cc0SAndreas Gohr * @param string $user 21880ac552fSAndreas Gohr * @return string 21980ac552fSAndreas Gohr */ 220a1128cc0SAndreas Gohr abstract public function cleanUser($user); 22180ac552fSAndreas Gohr 22280ac552fSAndreas Gohr /** 223a1128cc0SAndreas Gohr * Clean the group name for use in DokuWiki 22480ac552fSAndreas Gohr * 225a1128cc0SAndreas Gohr * @param string $group 22680ac552fSAndreas Gohr * @return string 22780ac552fSAndreas Gohr */ 228a1128cc0SAndreas Gohr abstract public function cleanGroup($group); 229a1128cc0SAndreas Gohr 230a1128cc0SAndreas Gohr /** 231a1128cc0SAndreas Gohr * Inheriting classes may want to manipulate the user before binding 232a1128cc0SAndreas Gohr * 233a1128cc0SAndreas Gohr * @param string $user 234a1128cc0SAndreas Gohr * @return string 235a1128cc0SAndreas Gohr */ 236a1128cc0SAndreas Gohr protected function prepareBindUser($user) 237a1128cc0SAndreas Gohr { 238a1128cc0SAndreas Gohr return $user; 239a1128cc0SAndreas Gohr } 24080ac552fSAndreas Gohr 2415a3b9122SAndreas Gohr /** 2421078ec26SAndreas Gohr * Helper method to get the first value of the given attribute 2431078ec26SAndreas Gohr * 2441078ec26SAndreas Gohr * The given attribute may be null, an empty string is returned then 2451078ec26SAndreas Gohr * 2461078ec26SAndreas Gohr * @param Attribute|null $attribute 2471078ec26SAndreas Gohr * @return string 2481078ec26SAndreas Gohr */ 2495a3b9122SAndreas Gohr protected function attr2str($attribute) 2505a3b9122SAndreas Gohr { 2511078ec26SAndreas Gohr if ($attribute !== null) { 2521078ec26SAndreas Gohr return $attribute->firstValue(); 2531078ec26SAndreas Gohr } 2541078ec26SAndreas Gohr return ''; 2551078ec26SAndreas Gohr } 2561078ec26SAndreas Gohr 2571078ec26SAndreas Gohr /** 2589c590892SAndreas Gohr * Get the attributes that should be fetched for a user 2599c590892SAndreas Gohr * 2609c590892SAndreas Gohr * Can be extended in sub classes 2619c590892SAndreas Gohr * 2629c590892SAndreas Gohr * @return Attribute[] 2639c590892SAndreas Gohr */ 2649c590892SAndreas Gohr protected function userAttributes() 2659c590892SAndreas Gohr { 2669c590892SAndreas Gohr // defaults 2679c590892SAndreas Gohr $attr = [ 2689c590892SAndreas Gohr new Attribute('dn'), 2699c590892SAndreas Gohr new Attribute('displayName'), 2709c590892SAndreas Gohr new Attribute('mail'), 2719c590892SAndreas Gohr ]; 2729c590892SAndreas Gohr // additionals 2739c590892SAndreas Gohr foreach ($this->config['attributes'] as $attribute) { 2749c590892SAndreas Gohr $attr[] = new Attribute($attribute); 2759c590892SAndreas Gohr } 2769c590892SAndreas Gohr return $attr; 2779c590892SAndreas Gohr } 2789c590892SAndreas Gohr 2799c590892SAndreas Gohr /** 280da369b60SAndreas Gohr * Handle fatal exceptions 2811078ec26SAndreas Gohr * 2821078ec26SAndreas Gohr * @param \Exception $e 2831078ec26SAndreas Gohr */ 284da369b60SAndreas Gohr protected function fatal(\Exception $e) 2851078ec26SAndreas Gohr { 286c872f0e3SAndreas Gohr if (class_exists('\dokuwiki\ErrorHandler')) { 287c872f0e3SAndreas Gohr \dokuwiki\ErrorHandler::logException($e); 288c872f0e3SAndreas Gohr } 289c872f0e3SAndreas Gohr 2901078ec26SAndreas Gohr if (defined('DOKU_UNITTEST')) { 2918595f73eSAndreas Gohr throw new \RuntimeException('', 0, $e); 2921078ec26SAndreas Gohr } 293c872f0e3SAndreas Gohr 294c872f0e3SAndreas Gohr msg('[pureldap] ' . hsc($e->getMessage()), -1); 295da369b60SAndreas Gohr } 2961078ec26SAndreas Gohr 297da369b60SAndreas Gohr /** 298a1128cc0SAndreas Gohr * Handle error output 299a1128cc0SAndreas Gohr * 300a1128cc0SAndreas Gohr * @param string $msg 301a1128cc0SAndreas Gohr * @param string $file 302a1128cc0SAndreas Gohr * @param int $line 303a1128cc0SAndreas Gohr */ 304a1128cc0SAndreas Gohr protected function error($msg, $file, $line) 305a1128cc0SAndreas Gohr { 306c872f0e3SAndreas Gohr if (class_exists('\dokuwiki\Logger')) { 307c872f0e3SAndreas Gohr \dokuwiki\Logger::error('[pureldap] ' . $msg, '', $file, $line); 308c872f0e3SAndreas Gohr } 309c872f0e3SAndreas Gohr 310a1128cc0SAndreas Gohr if (defined('DOKU_UNITTEST')) { 311a1128cc0SAndreas Gohr throw new \RuntimeException($msg . ' at ' . $file . ':' . $line); 312a1128cc0SAndreas Gohr } 313a1128cc0SAndreas Gohr 314c872f0e3SAndreas Gohr msg('[pureldap] ' . hsc($msg), -1); 315a1128cc0SAndreas Gohr } 316a1128cc0SAndreas Gohr 317a1128cc0SAndreas Gohr /** 318da369b60SAndreas Gohr * Handle debug output 319da369b60SAndreas Gohr * 320da369b60SAndreas Gohr * @param string $msg 321da369b60SAndreas Gohr * @param string $file 322da369b60SAndreas Gohr * @param int $line 323da369b60SAndreas Gohr */ 324da369b60SAndreas Gohr protected function debug($msg, $file, $line) 325da369b60SAndreas Gohr { 326c872f0e3SAndreas Gohr global $conf; 327c872f0e3SAndreas Gohr 328c872f0e3SAndreas Gohr if (class_exists('\dokuwiki\Logger')) { 329c872f0e3SAndreas Gohr \dokuwiki\Logger::debug('[pureldap] ' . $msg, '', $file, $line); 330c872f0e3SAndreas Gohr } 331c872f0e3SAndreas Gohr 332c872f0e3SAndreas Gohr if ($conf['allowdebug']) { 33385916a2dSAndreas Gohr msg('[pureldap] ' . hsc($msg) . ' at ' . $file . ':' . $line, 0); 3341078ec26SAndreas Gohr } 3351078ec26SAndreas Gohr } 336c872f0e3SAndreas Gohr} 337