11078ec26SAndreas Gohr<?php 21078ec26SAndreas Gohr 31078ec26SAndreas Gohrnamespace dokuwiki\plugin\pureldap\classes; 41078ec26SAndreas Gohr 580ac552fSAndreas Gohruse dokuwiki\Utf8\PhpString; 69c590892SAndreas Gohruse FreeDSx\Ldap\Entry\Attribute; 71078ec26SAndreas Gohruse FreeDSx\Ldap\Entry\Entries; 81078ec26SAndreas Gohruse FreeDSx\Ldap\Entry\Entry; 91078ec26SAndreas Gohruse FreeDSx\Ldap\Exception\OperationException; 105a3b9122SAndreas Gohruse FreeDSx\Ldap\Exception\ProtocolException; 111078ec26SAndreas Gohruse FreeDSx\Ldap\Operations; 121078ec26SAndreas Gohruse FreeDSx\Ldap\Search\Filters; 131078ec26SAndreas Gohr 14f17bb68bSAndreas Gohr/** 15f17bb68bSAndreas Gohr * Implement Active Directory Specifics 16f17bb68bSAndreas Gohr */ 171078ec26SAndreas Gohrclass ADClient extends Client 181078ec26SAndreas Gohr{ 19e7339d5aSAndreas Gohr /** 20e7339d5aSAndreas Gohr * @var GroupHierarchyCache 21e7339d5aSAndreas Gohr * @see getGroupHierarchyCache 22e7339d5aSAndreas Gohr */ 23e7339d5aSAndreas Gohr protected $gch = null; 241078ec26SAndreas Gohr 251078ec26SAndreas Gohr /** @inheritDoc */ 261078ec26SAndreas Gohr public function getUser($username, $fetchgroups = true) 271078ec26SAndreas Gohr { 281078ec26SAndreas Gohr if (!$this->autoAuth()) return null; 29a1128cc0SAndreas Gohr $username = $this->simpleUser($username); 301078ec26SAndreas Gohr 311078ec26SAndreas Gohr $filter = Filters::and( 321078ec26SAndreas Gohr Filters::equal('objectClass', 'user'), 33a1128cc0SAndreas Gohr Filters::equal('sAMAccountName', $this->simpleUser($username)) 341078ec26SAndreas Gohr ); 35b21740b4SAndreas Gohr $this->debug('Searching ' . $filter->toString(), __FILE__, __LINE__); 361078ec26SAndreas Gohr 371078ec26SAndreas Gohr try { 381078ec26SAndreas Gohr /** @var Entries $entries */ 399c590892SAndreas Gohr $attributes = $this->userAttributes(); 409c590892SAndreas Gohr $entries = $this->ldap->search(Operations::search($filter, ...$attributes)); 411078ec26SAndreas Gohr } catch (OperationException $e) { 42b21740b4SAndreas Gohr $this->fatal($e); 431078ec26SAndreas Gohr return null; 441078ec26SAndreas Gohr } 451078ec26SAndreas Gohr if ($entries->count() !== 1) return null; 461078ec26SAndreas Gohr $entry = $entries->first(); 47b21740b4SAndreas Gohr return $this->entry2User($entry); 48b21740b4SAndreas Gohr } 491078ec26SAndreas Gohr 50b21740b4SAndreas Gohr /** @inheritDoc */ 51204fba68SAndreas Gohr public function getGroups($match = null, $filtermethod = self::FILTER_EQUAL) 52b21740b4SAndreas Gohr { 53b21740b4SAndreas Gohr if (!$this->autoAuth()) return []; 54b21740b4SAndreas Gohr 55b21740b4SAndreas Gohr $filter = Filters::and( 56b21740b4SAndreas Gohr Filters::equal('objectClass', 'group') 57b21740b4SAndreas Gohr ); 58b21740b4SAndreas Gohr if ($match !== null) { 59e7c3e817SAndreas Gohr // FIXME this is a workaround that removes regex anchors and quoting as passed by the groupuser plugin 60e7c3e817SAndreas Gohr // a proper fix requires splitbrain/dokuwiki#3028 to be implemented 61fce018daSAndreas Gohr $match = ltrim($match, '^'); 62fce018daSAndreas Gohr $match = rtrim($match, '$'); 63e7c3e817SAndreas Gohr $match = stripslashes($match); 64fce018daSAndreas Gohr 65b21740b4SAndreas Gohr $filter->add(Filters::$filtermethod('cn', $match)); 66b21740b4SAndreas Gohr } 67b21740b4SAndreas Gohr 68b21740b4SAndreas Gohr $this->debug('Searching ' . $filter->toString(), __FILE__, __LINE__); 69b21740b4SAndreas Gohr $search = Operations::search($filter, 'cn'); 70b21740b4SAndreas Gohr $paging = $this->ldap->paging($search); 71b21740b4SAndreas Gohr 72b21740b4SAndreas Gohr $groups = []; 73b21740b4SAndreas Gohr while ($paging->hasEntries()) { 74b21740b4SAndreas Gohr try { 75b21740b4SAndreas Gohr $entries = $paging->getEntries(); 76b21740b4SAndreas Gohr } catch (ProtocolException $e) { 77b21740b4SAndreas Gohr $this->fatal($e); 78b21740b4SAndreas Gohr return $groups; // we return what we got so far 79b21740b4SAndreas Gohr } 80b21740b4SAndreas Gohr 81b21740b4SAndreas Gohr foreach ($entries as $entry) { 82b21740b4SAndreas Gohr /** @var Entry $entry */ 83204fba68SAndreas Gohr $groups[$entry->getDn()->toString()] = $this->cleanGroup($this->attr2str($entry->get('cn'))); 84b21740b4SAndreas Gohr } 85b21740b4SAndreas Gohr } 86b21740b4SAndreas Gohr 871b0eb9b3SAndreas Gohr asort($groups); 88b21740b4SAndreas Gohr return $groups; 89b21740b4SAndreas Gohr } 90b21740b4SAndreas Gohr 91b21740b4SAndreas Gohr /** 92b21740b4SAndreas Gohr * Fetch users matching the given filters 93b21740b4SAndreas Gohr * 94b21740b4SAndreas Gohr * @param array $match 95b21740b4SAndreas Gohr * @param string $filtermethod The method to use for filtering 96b21740b4SAndreas Gohr * @return array 97b21740b4SAndreas Gohr */ 98204fba68SAndreas Gohr public function getFilteredUsers($match, $filtermethod = self::FILTER_EQUAL) 99b21740b4SAndreas Gohr { 100b21740b4SAndreas Gohr if (!$this->autoAuth()) return []; 101b21740b4SAndreas Gohr 102b21740b4SAndreas Gohr $filter = Filters::and(Filters::equal('objectClass', 'user')); 103b21740b4SAndreas Gohr if (isset($match['user'])) { 104a1128cc0SAndreas Gohr $filter->add(Filters::$filtermethod('sAMAccountName', $this->simpleUser($match['user']))); 105b21740b4SAndreas Gohr } 106b21740b4SAndreas Gohr if (isset($match['name'])) { 107b21740b4SAndreas Gohr $filter->add(Filters::$filtermethod('displayName', $match['name'])); 108b21740b4SAndreas Gohr } 109b21740b4SAndreas Gohr if (isset($match['mail'])) { 110b21740b4SAndreas Gohr $filter->add(Filters::$filtermethod('mail', $match['mail'])); 111b21740b4SAndreas Gohr } 112b21740b4SAndreas Gohr if (isset($match['grps'])) { 113b21740b4SAndreas Gohr // memberOf can not be checked with a substring match, so we need to get the right groups first 114b21740b4SAndreas Gohr $groups = $this->getGroups($match['grps'], $filtermethod); 115e7339d5aSAndreas Gohr $groupDNs = array_keys($groups); 116e7339d5aSAndreas Gohr 117e7339d5aSAndreas Gohr if ($this->config['recursivegroups']) { 118e7339d5aSAndreas Gohr $gch = $this->getGroupHierarchyCache(); 119e7339d5aSAndreas Gohr foreach ($groupDNs as $dn) { 120e7339d5aSAndreas Gohr $groupDNs = array_merge($groupDNs, $gch->getChildren($dn)); 121e7339d5aSAndreas Gohr } 122e7339d5aSAndreas Gohr $groupDNs = array_unique($groupDNs); 123e7339d5aSAndreas Gohr } 124e7339d5aSAndreas Gohr 125b21740b4SAndreas Gohr $or = Filters::or(); 126e7339d5aSAndreas Gohr foreach ($groupDNs as $dn) { 127204fba68SAndreas Gohr // domain users membership is in primary group 128e7339d5aSAndreas Gohr if ($this->dn2group($dn) === $this->config['primarygroup']) { 129204fba68SAndreas Gohr $or->add(Filters::equal('primaryGroupID', 513)); 130204fba68SAndreas Gohr continue; 131204fba68SAndreas Gohr } 1327a36c1b4SAndreas Gohr // find members of this exact group 1337a36c1b4SAndreas Gohr $or->add(Filters::equal('memberOf', $dn)); 134b21740b4SAndreas Gohr } 135b21740b4SAndreas Gohr $filter->add($or); 136b21740b4SAndreas Gohr } 137e7339d5aSAndreas Gohr 138b21740b4SAndreas Gohr $this->debug('Searching ' . $filter->toString(), __FILE__, __LINE__); 1399c590892SAndreas Gohr $attributes = $this->userAttributes(); 1409c590892SAndreas Gohr $search = Operations::search($filter, ...$attributes); 141b21740b4SAndreas Gohr $paging = $this->ldap->paging($search); 142b21740b4SAndreas Gohr 143b21740b4SAndreas Gohr $users = []; 144b21740b4SAndreas Gohr while ($paging->hasEntries()) { 145b21740b4SAndreas Gohr try { 146b21740b4SAndreas Gohr $entries = $paging->getEntries(); 147b21740b4SAndreas Gohr } catch (ProtocolException $e) { 148b21740b4SAndreas Gohr $this->fatal($e); 14980ac552fSAndreas Gohr break; // we abort and return what we have so far 150b21740b4SAndreas Gohr } 151b21740b4SAndreas Gohr 152b21740b4SAndreas Gohr foreach ($entries as $entry) { 153746af42cSAndreas Gohr $userinfo = $this->entry2User($entry, false); 154746af42cSAndreas Gohr $users[$userinfo['user']] = $userinfo; 155b21740b4SAndreas Gohr } 156b21740b4SAndreas Gohr } 157b21740b4SAndreas Gohr 1581b0eb9b3SAndreas Gohr ksort($users); 159b21740b4SAndreas Gohr return $users; 160b21740b4SAndreas Gohr } 161b21740b4SAndreas Gohr 162a1128cc0SAndreas Gohr /** @inheritDoc */ 163a1128cc0SAndreas Gohr public function cleanUser($user) 16480ac552fSAndreas Gohr { 165a1128cc0SAndreas Gohr return $this->simpleUser($user); 16680ac552fSAndreas Gohr } 16780ac552fSAndreas Gohr 168a1128cc0SAndreas Gohr /** @inheritDoc */ 169a1128cc0SAndreas Gohr public function cleanGroup($group) 170a1128cc0SAndreas Gohr { 171a1128cc0SAndreas Gohr return PhpString::strtolower($group); 172a1128cc0SAndreas Gohr } 173a1128cc0SAndreas Gohr 174a1128cc0SAndreas Gohr /** @inheritDoc */ 175a1128cc0SAndreas Gohr public function prepareBindUser($user) 176a1128cc0SAndreas Gohr { 177a1128cc0SAndreas Gohr $user = $this->qualifiedUser($user); // add account suffix 178a1128cc0SAndreas Gohr return $user; 17980ac552fSAndreas Gohr } 18080ac552fSAndreas Gohr 18180ac552fSAndreas Gohr /** 182e7339d5aSAndreas Gohr * Initializes the Group Cache for nested groups 183e7339d5aSAndreas Gohr * 184e7339d5aSAndreas Gohr * @return GroupHierarchyCache 185e7339d5aSAndreas Gohr */ 186e7339d5aSAndreas Gohr public function getGroupHierarchyCache() 187e7339d5aSAndreas Gohr { 188e7339d5aSAndreas Gohr if ($this->gch === null) { 189e7339d5aSAndreas Gohr if (!$this->autoAuth()) return null; 190*5dcabedaSAndreas Gohr $this->gch = new GroupHierarchyCache($this->ldap, $this->config['usefscache']); 191e7339d5aSAndreas Gohr } 192e7339d5aSAndreas Gohr return $this->gch; 193e7339d5aSAndreas Gohr } 194e7339d5aSAndreas Gohr 195e7339d5aSAndreas Gohr /** 19680ac552fSAndreas Gohr * @inheritDoc 197a1128cc0SAndreas Gohr * userPrincipalName in the form <user>@<suffix> 19880ac552fSAndreas Gohr */ 199a1128cc0SAndreas Gohr protected function qualifiedUser($user) 200a1128cc0SAndreas Gohr { 201a1128cc0SAndreas Gohr $user = $this->simpleUser($user); // strip any existing qualifiers 202a1128cc0SAndreas Gohr if (!$this->config['suffix']) { 203a1128cc0SAndreas Gohr $this->error('No account suffix set. Logins may fail.', __FILE__, __LINE__); 204a1128cc0SAndreas Gohr } 205a1128cc0SAndreas Gohr 206a1128cc0SAndreas Gohr return $user . '@' . $this->config['suffix']; 207a1128cc0SAndreas Gohr } 208a1128cc0SAndreas Gohr 209a1128cc0SAndreas Gohr /** 210a1128cc0SAndreas Gohr * @inheritDoc 211a1128cc0SAndreas Gohr * Removes the account suffix from the given user. Should match the SAMAccountName 212a1128cc0SAndreas Gohr */ 213a1128cc0SAndreas Gohr protected function simpleUser($user) 21480ac552fSAndreas Gohr { 21580ac552fSAndreas Gohr $user = PhpString::strtolower($user); 216a1128cc0SAndreas Gohr $user = preg_replace('/@.*$/', '', $user); 217a1128cc0SAndreas Gohr $user = preg_replace('/^.*\\\\/', '', $user); 21880ac552fSAndreas Gohr return $user; 21980ac552fSAndreas Gohr } 22080ac552fSAndreas Gohr 22180ac552fSAndreas Gohr /** 222b21740b4SAndreas Gohr * Transform an LDAP entry to a user info array 223b21740b4SAndreas Gohr * 224b21740b4SAndreas Gohr * @param Entry $entry 225b21740b4SAndreas Gohr * @return array 226b21740b4SAndreas Gohr */ 227b21740b4SAndreas Gohr protected function entry2User(Entry $entry) 228b21740b4SAndreas Gohr { 229b914569fSAndreas Gohr $user = [ 230a1128cc0SAndreas Gohr 'user' => $this->simpleUser($this->attr2str($entry->get('sAMAccountName'))), 2311078ec26SAndreas Gohr 'name' => $this->attr2str($entry->get('DisplayName')) ?: $this->attr2str($entry->get('Name')), 2321078ec26SAndreas Gohr 'mail' => $this->attr2str($entry->get('mail')), 2331078ec26SAndreas Gohr 'dn' => $entry->getDn()->toString(), 2341078ec26SAndreas Gohr 'grps' => $this->getUserGroups($entry), // we always return groups because its currently inexpensive 2351078ec26SAndreas Gohr ]; 236b914569fSAndreas Gohr 237b914569fSAndreas Gohr // get additional attributes 238b914569fSAndreas Gohr foreach ($this->config['attributes'] as $attr) { 239b914569fSAndreas Gohr $user[$attr] = $this->attr2str($entry->get($attr)); 240b914569fSAndreas Gohr } 241b914569fSAndreas Gohr 242b914569fSAndreas Gohr return $user; 2431078ec26SAndreas Gohr } 2441078ec26SAndreas Gohr 2451078ec26SAndreas Gohr /** 2461078ec26SAndreas Gohr * Get the list of groups the given user is member of 2471078ec26SAndreas Gohr * 2481078ec26SAndreas Gohr * This method currently does no LDAP queries and thus is inexpensive. 2491078ec26SAndreas Gohr * 2501078ec26SAndreas Gohr * @param Entry $userentry 2511078ec26SAndreas Gohr * @return array 2521078ec26SAndreas Gohr */ 2531078ec26SAndreas Gohr protected function getUserGroups(Entry $userentry) 2541078ec26SAndreas Gohr { 255e7339d5aSAndreas Gohr $groups = []; 256e7339d5aSAndreas Gohr 257e7339d5aSAndreas Gohr if ($userentry->has('memberOf')) { 258e7339d5aSAndreas Gohr $groupDNs = $userentry->get('memberOf')->getValues(); 259e7339d5aSAndreas Gohr 260e7339d5aSAndreas Gohr if ($this->config['recursivegroups']) { 261e7339d5aSAndreas Gohr $gch = $this->getGroupHierarchyCache(); 262e7339d5aSAndreas Gohr foreach ($groupDNs as $dn) { 263e7339d5aSAndreas Gohr $groupDNs = array_merge($groupDNs, $gch->getParents($dn)); 264e7339d5aSAndreas Gohr } 265e7339d5aSAndreas Gohr 266e7339d5aSAndreas Gohr $groupDNs = array_unique($groupDNs); 267e7339d5aSAndreas Gohr } 268e7339d5aSAndreas Gohr $groups = array_map([$this, 'dn2group'], $groupDNs); 269e7339d5aSAndreas Gohr } 270e7339d5aSAndreas Gohr 271e7339d5aSAndreas Gohr $groups[] = $this->config['defaultgroup']; // always add default 2721078ec26SAndreas Gohr 2731078ec26SAndreas Gohr // resolving the primary group in AD is complicated but basically never needed 2741078ec26SAndreas Gohr // http://support.microsoft.com/?kbid=321360 2751078ec26SAndreas Gohr $gid = $userentry->get('primaryGroupID')->firstValue(); 2761078ec26SAndreas Gohr if ($gid == 513) { 277e7339d5aSAndreas Gohr $groups[] = $this->cleanGroup($this->config['primarygroup']); 27851e92298SAndreas Gohr } 27951e92298SAndreas Gohr 280f17bb68bSAndreas Gohr sort($groups); 281f17bb68bSAndreas Gohr return $groups; 28251e92298SAndreas Gohr } 28351e92298SAndreas Gohr 2849c590892SAndreas Gohr /** @inheritDoc */ 2859c590892SAndreas Gohr protected function userAttributes() 2869c590892SAndreas Gohr { 2879c590892SAndreas Gohr $attr = parent::userAttributes(); 288a1128cc0SAndreas Gohr $attr[] = new Attribute('sAMAccountName'); 2899c590892SAndreas Gohr $attr[] = new Attribute('Name'); 2909c590892SAndreas Gohr $attr[] = new Attribute('primaryGroupID'); 2919c590892SAndreas Gohr $attr[] = new Attribute('memberOf'); 2929c590892SAndreas Gohr 2939c590892SAndreas Gohr return $attr; 2949c590892SAndreas Gohr } 295e7339d5aSAndreas Gohr 296e7339d5aSAndreas Gohr /** 297e7339d5aSAndreas Gohr * Extract the group name from the DN 298e7339d5aSAndreas Gohr * 299e7339d5aSAndreas Gohr * @param string $dn 300e7339d5aSAndreas Gohr * @return string 301e7339d5aSAndreas Gohr */ 302e7339d5aSAndreas Gohr protected function dn2group($dn) 303e7339d5aSAndreas Gohr { 304e7339d5aSAndreas Gohr list($cn) = explode(',', $dn, 2); 305e7339d5aSAndreas Gohr return $this->cleanGroup(substr($cn, 3)); 306e7339d5aSAndreas Gohr } 3071078ec26SAndreas Gohr} 308