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 14/** 15 * Implement Active Directory Specifics 16 */ 17class ADClient extends Client 18{ 19 // see https://docs.microsoft.com/en-us/windows/win32/adsi/search-filter-syntax 20 const LDAP_MATCHING_RULE_IN_CHAIN = '1.2.840.113556.1.4.1941'; 21 22 /** @inheritDoc */ 23 public function getUser($username, $fetchgroups = true) 24 { 25 if (!$this->autoAuth()) return null; 26 $username = $this->simpleUser($username); 27 28 $filter = Filters::and( 29 Filters::equal('objectClass', 'user'), 30 Filters::equal('sAMAccountName', $this->simpleUser($username)) 31 ); 32 $this->debug('Searching ' . $filter->toString(), __FILE__, __LINE__); 33 34 try { 35 /** @var Entries $entries */ 36 $attributes = $this->userAttributes(); 37 $entries = $this->ldap->search(Operations::search($filter, ...$attributes)); 38 } catch (OperationException $e) { 39 $this->fatal($e); 40 return null; 41 } 42 if ($entries->count() !== 1) return null; 43 $entry = $entries->first(); 44 return $this->entry2User($entry); 45 } 46 47 /** @inheritDoc */ 48 public function getGroups($match = null, $filtermethod = self::FILTER_EQUAL) 49 { 50 if (!$this->autoAuth()) return []; 51 52 $filter = Filters::and( 53 Filters::equal('objectClass', 'group') 54 ); 55 if ($match !== null) { 56 // FIXME this is a workaround that removes regex anchors and quoting as passed by the groupuser plugin 57 // a proper fix requires splitbrain/dokuwiki#3028 to be implemented 58 $match = ltrim($match, '^'); 59 $match = rtrim($match, '$'); 60 $match = stripslashes($match); 61 62 $filter->add(Filters::$filtermethod('cn', $match)); 63 } 64 65 $this->debug('Searching ' . $filter->toString(), __FILE__, __LINE__); 66 $search = Operations::search($filter, 'cn'); 67 $paging = $this->ldap->paging($search); 68 69 $groups = []; 70 while ($paging->hasEntries()) { 71 try { 72 $entries = $paging->getEntries(); 73 } catch (ProtocolException $e) { 74 $this->fatal($e); 75 return $groups; // we return what we got so far 76 } 77 78 foreach ($entries as $entry) { 79 /** @var Entry $entry */ 80 $groups[$entry->getDn()->toString()] = $this->cleanGroup($this->attr2str($entry->get('cn'))); 81 } 82 } 83 84 asort($groups); 85 return $groups; 86 } 87 88 /** 89 * Fetch users matching the given filters 90 * 91 * @param array $match 92 * @param string $filtermethod The method to use for filtering 93 * @return array 94 */ 95 public function getFilteredUsers($match, $filtermethod = self::FILTER_EQUAL) 96 { 97 if (!$this->autoAuth()) return []; 98 99 $filter = Filters::and(Filters::equal('objectClass', 'user')); 100 if (isset($match['user'])) { 101 $filter->add(Filters::$filtermethod('sAMAccountName', $this->simpleUser($match['user']))); 102 } 103 if (isset($match['name'])) { 104 $filter->add(Filters::$filtermethod('displayName', $match['name'])); 105 } 106 if (isset($match['mail'])) { 107 $filter->add(Filters::$filtermethod('mail', $match['mail'])); 108 } 109 if (isset($match['grps'])) { 110 // memberOf can not be checked with a substring match, so we need to get the right groups first 111 $groups = $this->getGroups($match['grps'], $filtermethod); 112 $or = Filters::or(); 113 foreach ($groups as $dn => $group) { 114 // domain users membership is in primary group 115 if ($group === $this->config['primarygroup']) { 116 $or->add(Filters::equal('primaryGroupID', 513)); 117 continue; 118 } 119 120 $or->add(Filters::equal('memberOf', $dn)); // FIXME handle recursive groups 121 } 122 $filter->add($or); 123 } 124 $this->debug('Searching ' . $filter->toString(), __FILE__, __LINE__); 125 $attributes = $this->userAttributes(); 126 $search = Operations::search($filter, ...$attributes); 127 $paging = $this->ldap->paging($search); 128 129 $users = []; 130 while ($paging->hasEntries()) { 131 try { 132 $entries = $paging->getEntries(); 133 } catch (ProtocolException $e) { 134 $this->fatal($e); 135 break; // we abort and return what we have so far 136 } 137 138 foreach ($entries as $entry) { 139 $userinfo = $this->entry2User($entry); 140 $users[$userinfo['user']] = $this->entry2User($entry); 141 } 142 } 143 144 ksort($users); 145 return $users; 146 } 147 148 /** @inheritDoc */ 149 public function cleanUser($user) 150 { 151 return $this->simpleUser($user); 152 } 153 154 /** @inheritDoc */ 155 public function cleanGroup($group) 156 { 157 return PhpString::strtolower($group); 158 } 159 160 /** @inheritDoc */ 161 public function prepareBindUser($user) 162 { 163 $user = $this->qualifiedUser($user); // add account suffix 164 return $user; 165 } 166 167 /** 168 * @inheritDoc 169 * userPrincipalName in the form <user>@<suffix> 170 */ 171 protected function qualifiedUser($user) 172 { 173 $user = $this->simpleUser($user); // strip any existing qualifiers 174 if (!$this->config['suffix']) { 175 $this->error('No account suffix set. Logins may fail.', __FILE__, __LINE__); 176 } 177 178 return $user . '@' . $this->config['suffix']; 179 } 180 181 /** 182 * @inheritDoc 183 * Removes the account suffix from the given user. Should match the SAMAccountName 184 */ 185 protected function simpleUser($user) 186 { 187 $user = PhpString::strtolower($user); 188 $user = preg_replace('/@.*$/', '', $user); 189 $user = preg_replace('/^.*\\\\/', '', $user); 190 return $user; 191 } 192 193 /** 194 * Transform an LDAP entry to a user info array 195 * 196 * @param Entry $entry 197 * @return array 198 */ 199 protected function entry2User(Entry $entry) 200 { 201 $user = [ 202 'user' => $this->simpleUser($this->attr2str($entry->get('sAMAccountName'))), 203 'name' => $this->attr2str($entry->get('DisplayName')) ?: $this->attr2str($entry->get('Name')), 204 'mail' => $this->attr2str($entry->get('mail')), 205 'dn' => $entry->getDn()->toString(), 206 'grps' => $this->getUserGroups($entry), // we always return groups because its currently inexpensive 207 ]; 208 209 // get additional attributes 210 foreach ($this->config['attributes'] as $attr) { 211 $user[$attr] = $this->attr2str($entry->get($attr)); 212 } 213 214 return $user; 215 } 216 217 /** 218 * Get the list of groups the given user is member of 219 * 220 * This method currently does no LDAP queries and thus is inexpensive. 221 * 222 * @param Entry $userentry 223 * @return array 224 */ 225 protected function getUserGroups(Entry $userentry) 226 { 227 $groups = [$this->config['defaultgroup']]; // always add default 228 229 // resolving the primary group in AD is complicated but basically never needed 230 // http://support.microsoft.com/?kbid=321360 231 $gid = $userentry->get('primaryGroupID')->firstValue(); 232 if ($gid == 513) { 233 $groups[] = $this->cleanGroup('domain users'); 234 } 235 236 if ($this->config['recursivegroups']) { 237 // we do an additional query for the user's groups asking the AD server to resolve nested 238 // groups for us 239 if (!$this->autoAuth()) return $groups; 240 $filter = Filters::extensible('member', (string)$userentry->getDn(), self::LDAP_MATCHING_RULE_IN_CHAIN, 241 true); 242 $search = Operations::search($filter, 'name'); 243 $paging = $this->ldap->paging($search); 244 while ($paging->hasEntries()) { 245 try { 246 $entries = $paging->getEntries(); 247 } catch (ProtocolException $e) { 248 return $groups; // return what we have 249 } 250 /** @var Entry $entry */ 251 foreach ($entries as $entry) { 252 $groups[] = $this->cleanGroup(($entry->get('name')->getValues())[0]); 253 } 254 } 255 256 } elseif ($userentry->has('memberOf')) { 257 // we simply take the first CN= part of the group DN and return it as the group name 258 // this should be correct for ActiveDirectory and saves us additional LDAP queries 259 foreach ($userentry->get('memberOf')->getValues() as $dn) { 260 list($cn) = explode(',', $dn, 2); 261 $groups[] = $this->cleanGroup(substr($cn, 3)); 262 } 263 } 264 265 sort($groups); 266 return $groups; 267 } 268 269 /** @inheritDoc */ 270 protected function userAttributes() 271 { 272 $attr = parent::userAttributes(); 273 $attr[] = new Attribute('sAMAccountName'); 274 $attr[] = new Attribute('Name'); 275 $attr[] = new Attribute('primaryGroupID'); 276 $attr[] = new Attribute('memberOf'); 277 278 return $attr; 279 } 280} 281