11078ec26SAndreas Gohr<?php 21078ec26SAndreas Gohr 31078ec26SAndreas Gohrnamespace dokuwiki\plugin\pureldap\classes; 41078ec26SAndreas Gohr 580ac552fSAndreas Gohruse dokuwiki\Utf8\PhpString; 6*9c590892SAndreas 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 141078ec26SAndreas Gohrclass ADClient extends Client 151078ec26SAndreas Gohr{ 161078ec26SAndreas Gohr 171078ec26SAndreas Gohr /** @inheritDoc */ 181078ec26SAndreas Gohr public function getUser($username, $fetchgroups = true) 191078ec26SAndreas Gohr { 201078ec26SAndreas Gohr if (!$this->autoAuth()) return null; 211078ec26SAndreas Gohr 221078ec26SAndreas Gohr $filter = Filters::and( 231078ec26SAndreas Gohr Filters::equal('objectClass', 'user'), 2480ac552fSAndreas Gohr Filters::equal('userPrincipalName', $this->qualifiedUser($username)) 251078ec26SAndreas Gohr ); 26b21740b4SAndreas Gohr $this->debug('Searching ' . $filter->toString(), __FILE__, __LINE__); 271078ec26SAndreas Gohr 281078ec26SAndreas Gohr try { 291078ec26SAndreas Gohr /** @var Entries $entries */ 30*9c590892SAndreas Gohr $attributes = $this->userAttributes(); 31*9c590892SAndreas Gohr $entries = $this->ldap->search(Operations::search($filter, ...$attributes)); 321078ec26SAndreas Gohr } catch (OperationException $e) { 33b21740b4SAndreas Gohr $this->fatal($e); 341078ec26SAndreas Gohr return null; 351078ec26SAndreas Gohr } 361078ec26SAndreas Gohr if ($entries->count() !== 1) return null; 371078ec26SAndreas Gohr $entry = $entries->first(); 38b21740b4SAndreas Gohr return $this->entry2User($entry); 39b21740b4SAndreas Gohr } 401078ec26SAndreas Gohr 41b21740b4SAndreas Gohr /** @inheritDoc */ 42b21740b4SAndreas Gohr public function getGroups($match = null, $filtermethod = 'equal') 43b21740b4SAndreas Gohr { 44b21740b4SAndreas Gohr if (!$this->autoAuth()) return []; 45b21740b4SAndreas Gohr 46b21740b4SAndreas Gohr $filter = Filters::and( 47b21740b4SAndreas Gohr Filters::equal('objectClass', 'group') 48b21740b4SAndreas Gohr ); 49b21740b4SAndreas Gohr if ($match !== null) { 50fce018daSAndreas Gohr // FIXME this is a workaround that removes regex anchors as passed by the groupuser plugin 51fce018daSAndreas Gohr // a proper fix requires splitbrain/dokuwiki#3028 to be properly fixed 52fce018daSAndreas Gohr $match = ltrim($match, '^'); 53fce018daSAndreas Gohr $match = rtrim($match, '$'); 54fce018daSAndreas Gohr 55b21740b4SAndreas Gohr $filter->add(Filters::$filtermethod('cn', $match)); 56b21740b4SAndreas Gohr } 57b21740b4SAndreas Gohr 58b21740b4SAndreas Gohr $this->debug('Searching ' . $filter->toString(), __FILE__, __LINE__); 59b21740b4SAndreas Gohr $search = Operations::search($filter, 'cn'); 60b21740b4SAndreas Gohr $paging = $this->ldap->paging($search); 61b21740b4SAndreas Gohr 62b21740b4SAndreas Gohr $groups = []; 63b21740b4SAndreas Gohr while ($paging->hasEntries()) { 64b21740b4SAndreas Gohr try { 65b21740b4SAndreas Gohr $entries = $paging->getEntries(); 66b21740b4SAndreas Gohr } catch (ProtocolException $e) { 67b21740b4SAndreas Gohr $this->fatal($e); 68b21740b4SAndreas Gohr return $groups; // we return what we got so far 69b21740b4SAndreas Gohr } 70b21740b4SAndreas Gohr 71b21740b4SAndreas Gohr foreach ($entries as $entry) { 72b21740b4SAndreas Gohr /** @var Entry $entry */ 73b21740b4SAndreas Gohr $groups[$entry->getDn()->toString()] = $this->attr2str($entry->get('cn')); 74b21740b4SAndreas Gohr } 75b21740b4SAndreas Gohr } 76b21740b4SAndreas Gohr 771b0eb9b3SAndreas Gohr asort($groups); 78b21740b4SAndreas Gohr return $groups; 79b21740b4SAndreas Gohr } 80b21740b4SAndreas Gohr 81b21740b4SAndreas Gohr /** 82b21740b4SAndreas Gohr * Fetch users matching the given filters 83b21740b4SAndreas Gohr * 84b21740b4SAndreas Gohr * @param array $match 85b21740b4SAndreas Gohr * @param string $filtermethod The method to use for filtering 86b21740b4SAndreas Gohr * @return array 87b21740b4SAndreas Gohr */ 88b21740b4SAndreas Gohr public function getFilteredUsers($match, $filtermethod = 'equal') 89b21740b4SAndreas Gohr { 90b21740b4SAndreas Gohr if (!$this->autoAuth()) return []; 91b21740b4SAndreas Gohr 92b21740b4SAndreas Gohr $filter = Filters::and(Filters::equal('objectClass', 'user')); 93b21740b4SAndreas Gohr if (isset($match['user'])) { 94b21740b4SAndreas Gohr $filter->add(Filters::$filtermethod('userPrincipalName', $match['user'])); 95b21740b4SAndreas Gohr } 96b21740b4SAndreas Gohr if (isset($match['name'])) { 97b21740b4SAndreas Gohr $filter->add(Filters::$filtermethod('displayName', $match['name'])); 98b21740b4SAndreas Gohr } 99b21740b4SAndreas Gohr if (isset($match['mail'])) { 100b21740b4SAndreas Gohr $filter->add(Filters::$filtermethod('mail', $match['mail'])); 101b21740b4SAndreas Gohr } 102b21740b4SAndreas Gohr if (isset($match['grps'])) { 103b21740b4SAndreas Gohr // memberOf can not be checked with a substring match, so we need to get the right groups first 104b21740b4SAndreas Gohr $groups = $this->getGroups($match['grps'], $filtermethod); 105b21740b4SAndreas Gohr $or = Filters::or(); 106b21740b4SAndreas Gohr foreach ($groups as $dn => $group) { 107b21740b4SAndreas Gohr $or->add(Filters::equal('memberOf', $dn)); 108b21740b4SAndreas Gohr } 109b21740b4SAndreas Gohr $filter->add($or); 110b21740b4SAndreas Gohr } 111b21740b4SAndreas Gohr $this->debug('Searching ' . $filter->toString(), __FILE__, __LINE__); 112*9c590892SAndreas Gohr $attributes = $this->userAttributes(); 113*9c590892SAndreas Gohr $search = Operations::search($filter, ...$attributes); 114b21740b4SAndreas Gohr $paging = $this->ldap->paging($search); 115b21740b4SAndreas Gohr 116b21740b4SAndreas Gohr $users = []; 117b21740b4SAndreas Gohr while ($paging->hasEntries()) { 118b21740b4SAndreas Gohr try { 119b21740b4SAndreas Gohr $entries = $paging->getEntries(); 120b21740b4SAndreas Gohr } catch (ProtocolException $e) { 121b21740b4SAndreas Gohr $this->fatal($e); 12280ac552fSAndreas Gohr break; // we abort and return what we have so far 123b21740b4SAndreas Gohr } 124b21740b4SAndreas Gohr 125b21740b4SAndreas Gohr foreach ($entries as $entry) { 12680ac552fSAndreas Gohr $userinfo = $this->entry2User($entry); 12780ac552fSAndreas Gohr $users[$userinfo['user']] = $this->entry2User($entry); 128b21740b4SAndreas Gohr } 129b21740b4SAndreas Gohr } 130b21740b4SAndreas Gohr 1311b0eb9b3SAndreas Gohr ksort($users); 132b21740b4SAndreas Gohr return $users; 133b21740b4SAndreas Gohr } 134b21740b4SAndreas Gohr 135b21740b4SAndreas Gohr /** 13680ac552fSAndreas Gohr * @inheritDoc 13780ac552fSAndreas Gohr * userPrincipalName in the form <user>@<domain> 13880ac552fSAndreas Gohr */ 13980ac552fSAndreas Gohr public function qualifiedUser($user) 14080ac552fSAndreas Gohr { 14180ac552fSAndreas Gohr $user = PhpString::strtolower($user); 14280ac552fSAndreas Gohr if (!$this->config['domain']) return $user; 14380ac552fSAndreas Gohr 14480ac552fSAndreas Gohr list($user, $domain) = explode('@', $user, 2); 14580ac552fSAndreas Gohr if (!$domain) { 14680ac552fSAndreas Gohr $domain = $this->config['domain']; 14780ac552fSAndreas Gohr } 14880ac552fSAndreas Gohr 14980ac552fSAndreas Gohr return $user . '@' . $domain; 15080ac552fSAndreas Gohr } 15180ac552fSAndreas Gohr 15280ac552fSAndreas Gohr /** 15380ac552fSAndreas Gohr * @inheritDoc 15480ac552fSAndreas Gohr * Removes the account suffix from the given user 15580ac552fSAndreas Gohr */ 15680ac552fSAndreas Gohr public function simpleUser($user) 15780ac552fSAndreas Gohr { 15880ac552fSAndreas Gohr $user = PhpString::strtolower($user); 15980ac552fSAndreas Gohr if (!$this->config['domain']) return $user; 16080ac552fSAndreas Gohr 16180ac552fSAndreas Gohr // strip account suffix 16280ac552fSAndreas Gohr list($luser, $suffix) = explode('@', $user, 2); 16380ac552fSAndreas Gohr if ($suffix === $this->config['domain']) return $luser; 16480ac552fSAndreas Gohr 16580ac552fSAndreas Gohr return $user; 16680ac552fSAndreas Gohr } 16780ac552fSAndreas Gohr 16880ac552fSAndreas Gohr /** 169b21740b4SAndreas Gohr * Transform an LDAP entry to a user info array 170b21740b4SAndreas Gohr * 171b21740b4SAndreas Gohr * @param Entry $entry 172b21740b4SAndreas Gohr * @return array 173b21740b4SAndreas Gohr */ 174b21740b4SAndreas Gohr protected function entry2User(Entry $entry) 175b21740b4SAndreas Gohr { 176b914569fSAndreas Gohr $user = [ 17780ac552fSAndreas Gohr 'user' => $this->simpleUser($this->attr2str($entry->get('UserPrincipalName'))), 1781078ec26SAndreas Gohr 'name' => $this->attr2str($entry->get('DisplayName')) ?: $this->attr2str($entry->get('Name')), 1791078ec26SAndreas Gohr 'mail' => $this->attr2str($entry->get('mail')), 1801078ec26SAndreas Gohr 'dn' => $entry->getDn()->toString(), 1811078ec26SAndreas Gohr 'grps' => $this->getUserGroups($entry), // we always return groups because its currently inexpensive 1821078ec26SAndreas Gohr ]; 183b914569fSAndreas Gohr 184b914569fSAndreas Gohr // get additional attributes 185b914569fSAndreas Gohr foreach ($this->config['attributes'] as $attr) { 186b914569fSAndreas Gohr $user[$attr] = $this->attr2str($entry->get($attr)); 187b914569fSAndreas Gohr } 188b914569fSAndreas Gohr 189b914569fSAndreas Gohr return $user; 1901078ec26SAndreas Gohr } 1911078ec26SAndreas Gohr 1921078ec26SAndreas Gohr /** 1931078ec26SAndreas Gohr * Get the list of groups the given user is member of 1941078ec26SAndreas Gohr * 1951078ec26SAndreas Gohr * This method currently does no LDAP queries and thus is inexpensive. 1961078ec26SAndreas Gohr * 1971078ec26SAndreas Gohr * @param Entry $userentry 1981078ec26SAndreas Gohr * @return array 1996d90d5c8SAndreas Gohr * @todo implement nested group memberships FIXME already correct? 2001078ec26SAndreas Gohr */ 2011078ec26SAndreas Gohr protected function getUserGroups(Entry $userentry) 2021078ec26SAndreas Gohr { 2031078ec26SAndreas Gohr $groups = [$this->config['defaultgroup']]; // always add default 2041078ec26SAndreas Gohr 2051078ec26SAndreas Gohr // we simply take the first CN= part of the group DN and return it as the group name 2061078ec26SAndreas Gohr // this should be correct for ActiveDirectory and saves us additional LDAP queries 2071078ec26SAndreas Gohr if ($userentry->has('memberOf')) { 208b21740b4SAndreas Gohr foreach ($userentry->get('memberOf')->getValues() as $dn) { 209b21740b4SAndreas Gohr list($cn) = explode(',', $dn, 2); 21080ac552fSAndreas Gohr $groups[] = PhpString::strtolower(substr($cn, 3)); 2111078ec26SAndreas Gohr } 2121078ec26SAndreas Gohr } 2131078ec26SAndreas Gohr 2141078ec26SAndreas Gohr // resolving the primary group in AD is complicated but basically never needed 2151078ec26SAndreas Gohr // http://support.microsoft.com/?kbid=321360 2161078ec26SAndreas Gohr $gid = $userentry->get('primaryGroupID')->firstValue(); 2171078ec26SAndreas Gohr if ($gid == 513) { 21880ac552fSAndreas Gohr $groups[] = 'domain users'; 2191078ec26SAndreas Gohr } 2201078ec26SAndreas Gohr 2211b0eb9b3SAndreas Gohr sort($groups); 2221078ec26SAndreas Gohr return $groups; 2231078ec26SAndreas Gohr } 224*9c590892SAndreas Gohr 225*9c590892SAndreas Gohr /** @inheritDoc */ 226*9c590892SAndreas Gohr protected function userAttributes() 227*9c590892SAndreas Gohr { 228*9c590892SAndreas Gohr $attr = parent::userAttributes(); 229*9c590892SAndreas Gohr $attr[] = new Attribute('UserPrincipalName'); 230*9c590892SAndreas Gohr $attr[] = new Attribute('Name'); 231*9c590892SAndreas Gohr $attr[] = new Attribute('primaryGroupID'); 232*9c590892SAndreas Gohr $attr[] = new Attribute('memberOf'); 233*9c590892SAndreas Gohr 234*9c590892SAndreas Gohr return $attr; 235*9c590892SAndreas Gohr } 236*9c590892SAndreas Gohr 2371078ec26SAndreas Gohr} 238