autoAuth()) return null; $filter = Filters::and( Filters::equal('objectClass', 'user'), Filters::equal('userPrincipalName', $this->qualifiedUser($username)) ); $this->debug('Searching ' . $filter->toString(), __FILE__, __LINE__); try { /** @var Entries $entries */ $attributes = $this->userAttributes(); $entries = $this->ldap->search(Operations::search($filter, ...$attributes)); } catch (OperationException $e) { $this->fatal($e); return null; } if ($entries->count() !== 1) return null; $entry = $entries->first(); return $this->entry2User($entry); } /** @inheritDoc */ public function getGroups($match = null, $filtermethod = 'equal') { if (!$this->autoAuth()) return []; $filter = Filters::and( Filters::equal('objectClass', 'group') ); if ($match !== null) { // FIXME this is a workaround that removes regex anchors as passed by the groupuser plugin // a proper fix requires splitbrain/dokuwiki#3028 to be properly fixed $match = ltrim($match, '^'); $match = rtrim($match, '$'); $filter->add(Filters::$filtermethod('cn', $match)); } $this->debug('Searching ' . $filter->toString(), __FILE__, __LINE__); $search = Operations::search($filter, 'cn'); $paging = $this->ldap->paging($search); $groups = []; while ($paging->hasEntries()) { try { $entries = $paging->getEntries(); } catch (ProtocolException $e) { $this->fatal($e); return $groups; // we return what we got so far } foreach ($entries as $entry) { /** @var Entry $entry */ $groups[$entry->getDn()->toString()] = $this->attr2str($entry->get('cn')); } } asort($groups); return $groups; } /** * Fetch users matching the given filters * * @param array $match * @param string $filtermethod The method to use for filtering * @return array */ public function getFilteredUsers($match, $filtermethod = 'equal') { if (!$this->autoAuth()) return []; $filter = Filters::and(Filters::equal('objectClass', 'user')); if (isset($match['user'])) { $filter->add(Filters::$filtermethod('userPrincipalName', $match['user'])); } if (isset($match['name'])) { $filter->add(Filters::$filtermethod('displayName', $match['name'])); } if (isset($match['mail'])) { $filter->add(Filters::$filtermethod('mail', $match['mail'])); } if (isset($match['grps'])) { // memberOf can not be checked with a substring match, so we need to get the right groups first $groups = $this->getGroups($match['grps'], $filtermethod); $or = Filters::or(); foreach ($groups as $dn => $group) { $or->add(Filters::equal('memberOf', $dn)); } $filter->add($or); } $this->debug('Searching ' . $filter->toString(), __FILE__, __LINE__); $attributes = $this->userAttributes(); $search = Operations::search($filter, ...$attributes); $paging = $this->ldap->paging($search); $users = []; while ($paging->hasEntries()) { try { $entries = $paging->getEntries(); } catch (ProtocolException $e) { $this->fatal($e); break; // we abort and return what we have so far } foreach ($entries as $entry) { $userinfo = $this->entry2User($entry); $users[$userinfo['user']] = $this->entry2User($entry); } } ksort($users); return $users; } /** * @inheritDoc * userPrincipalName in the form @ */ public function qualifiedUser($user) { $user = PhpString::strtolower($user); if (!$this->config['domain']) return $user; list($user, $domain) = explode('@', $user, 2); if (!$domain) { $domain = $this->config['domain']; } return $user . '@' . $domain; } /** * @inheritDoc * Removes the account suffix from the given user */ public function simpleUser($user) { $user = PhpString::strtolower($user); if (!$this->config['domain']) return $user; // strip account suffix list($luser, $suffix) = explode('@', $user, 2); if ($suffix === $this->config['domain']) return $luser; return $user; } /** * Transform an LDAP entry to a user info array * * @param Entry $entry * @return array */ protected function entry2User(Entry $entry) { $user = [ 'user' => $this->simpleUser($this->attr2str($entry->get('UserPrincipalName'))), 'name' => $this->attr2str($entry->get('DisplayName')) ?: $this->attr2str($entry->get('Name')), 'mail' => $this->attr2str($entry->get('mail')), 'dn' => $entry->getDn()->toString(), 'grps' => $this->getUserGroups($entry), // we always return groups because its currently inexpensive ]; // get additional attributes foreach ($this->config['attributes'] as $attr) { $user[$attr] = $this->attr2str($entry->get($attr)); } return $user; } /** * Get the list of groups the given user is member of * * This method currently does no LDAP queries and thus is inexpensive. * * @param Entry $userentry * @return array * @todo implement nested group memberships FIXME already correct? */ protected function getUserGroups(Entry $userentry) { $groups = [$this->config['defaultgroup']]; // always add default // we simply take the first CN= part of the group DN and return it as the group name // this should be correct for ActiveDirectory and saves us additional LDAP queries if ($userentry->has('memberOf')) { foreach ($userentry->get('memberOf')->getValues() as $dn) { list($cn) = explode(',', $dn, 2); $groups[] = PhpString::strtolower(substr($cn, 3)); } } // resolving the primary group in AD is complicated but basically never needed // http://support.microsoft.com/?kbid=321360 $gid = $userentry->get('primaryGroupID')->firstValue(); if ($gid == 513) { $groups[] = 'domain users'; } sort($groups); return $groups; } /** @inheritDoc */ protected function userAttributes() { $attr = parent::userAttributes(); $attr[] = new Attribute('UserPrincipalName'); $attr[] = new Attribute('Name'); $attr[] = new Attribute('primaryGroupID'); $attr[] = new Attribute('memberOf'); return $attr; } }