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