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