1<?php 2 3namespace dokuwiki\plugin\pureldap\classes; 4 5use dokuwiki\Utf8\PhpString; 6use FreeDSx\Ldap\Entry\Attribute; 7use FreeDSx\Ldap\Entry\Entries; 8use FreeDSx\Ldap\Entry\Entry; 9use FreeDSx\Ldap\Exception\OperationException; 10use FreeDSx\Ldap\Exception\ProtocolException; 11use FreeDSx\Ldap\Operations; 12use FreeDSx\Ldap\Search\Filters; 13 14class ADClient extends Client 15{ 16 17 /** @inheritDoc */ 18 public function getUser($username, $fetchgroups = true) 19 { 20 if (!$this->autoAuth()) return null; 21 22 $filter = Filters::and( 23 Filters::equal('objectClass', 'user'), 24 Filters::equal('userPrincipalName', $this->qualifiedUser($username)) 25 ); 26 $this->debug('Searching ' . $filter->toString(), __FILE__, __LINE__); 27 28 try { 29 /** @var Entries $entries */ 30 $attributes = $this->userAttributes(); 31 $entries = $this->ldap->search(Operations::search($filter, ...$attributes)); 32 } catch (OperationException $e) { 33 $this->fatal($e); 34 return null; 35 } 36 if ($entries->count() !== 1) return null; 37 $entry = $entries->first(); 38 return $this->entry2User($entry); 39 } 40 41 /** @inheritDoc */ 42 public function getGroups($match = null, $filtermethod = 'equal') 43 { 44 if (!$this->autoAuth()) return []; 45 46 $filter = Filters::and( 47 Filters::equal('objectClass', 'group') 48 ); 49 if ($match !== null) { 50 // FIXME this is a workaround that removes regex anchors as passed by the groupuser plugin 51 // a proper fix requires splitbrain/dokuwiki#3028 to be properly fixed 52 $match = ltrim($match, '^'); 53 $match = rtrim($match, '$'); 54 55 $filter->add(Filters::$filtermethod('cn', $match)); 56 } 57 58 $this->debug('Searching ' . $filter->toString(), __FILE__, __LINE__); 59 $search = Operations::search($filter, 'cn'); 60 $paging = $this->ldap->paging($search); 61 62 $groups = []; 63 while ($paging->hasEntries()) { 64 try { 65 $entries = $paging->getEntries(); 66 } catch (ProtocolException $e) { 67 $this->fatal($e); 68 return $groups; // we return what we got so far 69 } 70 71 foreach ($entries as $entry) { 72 /** @var Entry $entry */ 73 $groups[$entry->getDn()->toString()] = $this->attr2str($entry->get('cn')); 74 } 75 } 76 77 asort($groups); 78 return $groups; 79 } 80 81 /** 82 * Fetch users matching the given filters 83 * 84 * @param array $match 85 * @param string $filtermethod The method to use for filtering 86 * @return array 87 */ 88 public function getFilteredUsers($match, $filtermethod = 'equal') 89 { 90 if (!$this->autoAuth()) return []; 91 92 $filter = Filters::and(Filters::equal('objectClass', 'user')); 93 if (isset($match['user'])) { 94 $filter->add(Filters::$filtermethod('userPrincipalName', $match['user'])); 95 } 96 if (isset($match['name'])) { 97 $filter->add(Filters::$filtermethod('displayName', $match['name'])); 98 } 99 if (isset($match['mail'])) { 100 $filter->add(Filters::$filtermethod('mail', $match['mail'])); 101 } 102 if (isset($match['grps'])) { 103 // memberOf can not be checked with a substring match, so we need to get the right groups first 104 $groups = $this->getGroups($match['grps'], $filtermethod); 105 $or = Filters::or(); 106 foreach ($groups as $dn => $group) { 107 $or->add(Filters::equal('memberOf', $dn)); 108 } 109 $filter->add($or); 110 } 111 $this->debug('Searching ' . $filter->toString(), __FILE__, __LINE__); 112 $attributes = $this->userAttributes(); 113 $search = Operations::search($filter, ...$attributes); 114 $paging = $this->ldap->paging($search); 115 116 $users = []; 117 while ($paging->hasEntries()) { 118 try { 119 $entries = $paging->getEntries(); 120 } catch (ProtocolException $e) { 121 $this->fatal($e); 122 break; // we abort and return what we have so far 123 } 124 125 foreach ($entries as $entry) { 126 $userinfo = $this->entry2User($entry); 127 $users[$userinfo['user']] = $this->entry2User($entry); 128 } 129 } 130 131 ksort($users); 132 return $users; 133 } 134 135 /** 136 * @inheritDoc 137 * userPrincipalName in the form <user>@<domain> 138 */ 139 public function qualifiedUser($user) 140 { 141 $user = PhpString::strtolower($user); 142 if (!$this->config['domain']) return $user; 143 144 list($user, $domain) = explode('@', $user, 2); 145 if (!$domain) { 146 $domain = $this->config['domain']; 147 } 148 149 return $user . '@' . $domain; 150 } 151 152 /** 153 * @inheritDoc 154 * Removes the account suffix from the given user 155 */ 156 public function simpleUser($user) 157 { 158 $user = PhpString::strtolower($user); 159 if (!$this->config['domain']) return $user; 160 161 // strip account suffix 162 list($luser, $suffix) = explode('@', $user, 2); 163 if ($suffix === $this->config['domain']) return $luser; 164 165 return $user; 166 } 167 168 /** 169 * Transform an LDAP entry to a user info array 170 * 171 * @param Entry $entry 172 * @return array 173 */ 174 protected function entry2User(Entry $entry) 175 { 176 $user = [ 177 'user' => $this->simpleUser($this->attr2str($entry->get('UserPrincipalName'))), 178 'name' => $this->attr2str($entry->get('DisplayName')) ?: $this->attr2str($entry->get('Name')), 179 'mail' => $this->attr2str($entry->get('mail')), 180 'dn' => $entry->getDn()->toString(), 181 'grps' => $this->getUserGroups($entry), // we always return groups because its currently inexpensive 182 ]; 183 184 // get additional attributes 185 foreach ($this->config['attributes'] as $attr) { 186 $user[$attr] = $this->attr2str($entry->get($attr)); 187 } 188 189 return $user; 190 } 191 192 /** 193 * Get the list of groups the given user is member of 194 * 195 * This method currently does no LDAP queries and thus is inexpensive. 196 * 197 * @param Entry $userentry 198 * @return array 199 * @todo implement nested group memberships FIXME already correct? 200 */ 201 protected function getUserGroups(Entry $userentry) 202 { 203 $groups = [$this->config['defaultgroup']]; // always add default 204 205 // we simply take the first CN= part of the group DN and return it as the group name 206 // this should be correct for ActiveDirectory and saves us additional LDAP queries 207 if ($userentry->has('memberOf')) { 208 foreach ($userentry->get('memberOf')->getValues() as $dn) { 209 list($cn) = explode(',', $dn, 2); 210 $groups[] = PhpString::strtolower(substr($cn, 3)); 211 } 212 } 213 214 // resolving the primary group in AD is complicated but basically never needed 215 // http://support.microsoft.com/?kbid=321360 216 $gid = $userentry->get('primaryGroupID')->firstValue(); 217 if ($gid == 513) { 218 $groups[] = 'domain users'; 219 } 220 221 sort($groups); 222 return $groups; 223 } 224 225 /** @inheritDoc */ 226 protected function userAttributes() 227 { 228 $attr = parent::userAttributes(); 229 $attr[] = new Attribute('UserPrincipalName'); 230 $attr[] = new Attribute('Name'); 231 $attr[] = new Attribute('primaryGroupID'); 232 $attr[] = new Attribute('memberOf'); 233 234 return $attr; 235 } 236 237} 238