1<?php 2 3namespace dokuwiki\plugin\pureldap\classes; 4 5use dokuwiki\Utf8\PhpString; 6use FreeDSx\Ldap\Entry\Attribute; 7use FreeDSx\Ldap\Exception\BindException; 8use FreeDSx\Ldap\Exception\ConnectionException; 9use FreeDSx\Ldap\Exception\OperationException; 10use FreeDSx\Ldap\LdapClient; 11 12require_once __DIR__ . '/../vendor/autoload.php'; 13 14abstract class Client 15{ 16 const FILTER_EQUAL = 'equal'; 17 const FILTER_CONTAINS = 'contains'; 18 const FILTER_STARTSWITH = 'startsWith'; 19 const FILTER_ENDSWITH = 'endsWith'; 20 21 /** @var array the configuration */ 22 protected $config; 23 24 /** @var LdapClient */ 25 protected $ldap; 26 27 /** @var bool is this client authenticated already? */ 28 protected $isAuthenticated = false; 29 30 /** @var array cached user info */ 31 protected $userCache = []; 32 33 /** @var array cached group list */ 34 protected $groupCache = []; 35 36 /** 37 * Client constructor. 38 * @param array $config 39 */ 40 public function __construct($config) 41 { 42 $this->config = $this->prepareConfig($config); 43 $this->prepareSSO(); 44 $this->ldap = new LdapClient($this->config); 45 } 46 47 /** 48 * Setup sane config defaults 49 * 50 * @param array $config 51 * @return array 52 */ 53 protected function prepareConfig($config) 54 { 55 // ensure we have the default keys 56 /** @var array $conf */ 57 include __DIR__ . '/../conf/default.php'; 58 $defaults = $conf; 59 $defaults['defaultgroup'] = 'user'; // we expect this to be passed from global conf 60 61 $config = array_merge($defaults, $config); 62 63 // default port depends on SSL setting 64 if (!$config['port']) { 65 $config['port'] = ($config['encryption'] === 'ssl') ? 636 : 389; 66 } 67 68 // set ssl parameters 69 $config['use_ssl'] = ($config['encryption'] === 'ssl'); 70 if ($config['validate'] === 'none') { 71 $config['ssl_validate_cert'] = false; 72 } elseif ($config['validate'] === 'self') { 73 $config['ssl_allow_self_signed'] = true; 74 } 75 76 $config['suffix'] = PhpString::strtolower($config['suffix']); 77 $config['primarygroup'] = $this->cleanGroup($config['primarygroup']); 78 79 return $config; 80 } 81 82 /** 83 * Extract user info from environment for SSO 84 */ 85 protected function prepareSSO() 86 { 87 global $INPUT; 88 89 if (!$this->config['sso']) return; 90 if ($INPUT->server->str('REMOTE_USER') === '') return; 91 92 $user = $INPUT->server->str('REMOTE_USER'); 93 94 // make sure the right encoding is used 95 if ($this->config['sso_charset']) { 96 $user = iconv($this->config['sso_charset'], 'UTF-8', $user); 97 } elseif (!\dokuwiki\Utf8\Clean::isUtf8($user)) { 98 $user = utf8_encode($user); 99 } 100 $user = $this->cleanUser($user); 101 102 // Prepare SSO 103 104 // trust the incoming user 105 $INPUT->server->set('REMOTE_USER', $user); 106 107 // we need to simulate a login 108 if (empty($_COOKIE[DOKU_COOKIE])) { 109 $INPUT->set('u', $user); 110 $INPUT->set('p', 'sso_only'); 111 } 112 } 113 114 /** 115 * Authenticate as admin 116 */ 117 public function autoAuth() 118 { 119 if ($this->isAuthenticated) return true; 120 121 $user = $this->prepareBindUser($this->config['admin_username']); 122 $ok = $this->authenticate($user, $this->config['admin_password']); 123 if (!$ok) { 124 $this->error('Administrative bind failed. Probably wrong user/password.', __FILE__, __LINE__); 125 } 126 return $ok; 127 } 128 129 /** 130 * Authenticates a given user. This client will remain authenticated 131 * 132 * @param string $user 133 * @param string $pass 134 * @return bool was the authentication successful? 135 * @noinspection PhpRedundantCatchClauseInspection 136 */ 137 public function authenticate($user, $pass) 138 { 139 $user = $this->prepareBindUser($user); 140 141 if ($this->config['encryption'] === 'tls') { 142 try { 143 $this->ldap->startTls(); 144 } catch (OperationException $e) { 145 $this->fatal($e); 146 } 147 } 148 149 try { 150 $this->ldap->bind($user, $pass); 151 } catch (BindException $e) { 152 return false; 153 } catch (ConnectionException $e) { 154 $this->fatal($e); 155 return false; 156 } catch (OperationException $e) { 157 $this->fatal($e); 158 return false; 159 } 160 161 $this->isAuthenticated = true; 162 return true; 163 } 164 165 /** 166 * Get info for a single user, use cache if available 167 * 168 * @param string $username 169 * @param bool $fetchgroups Are groups needed? 170 * @return array|null 171 */ 172 public function getCachedUser($username, $fetchgroups = true) 173 { 174 global $conf; 175 176 // memory cache first 177 if (isset($this->userCache[$username])) { 178 if (!$fetchgroups || is_array($this->userCache[$username]['grps'])) { 179 return $this->userCache[$username]; 180 } 181 } 182 183 // disk cache second 184 $cachename = getCacheName($username, '.pureldap-user'); 185 $cachetime = @filemtime($cachename); 186 if ($cachetime && (time() - $cachetime) < $conf['auth_security_timeout']) { 187 $this->userCache[$username] = json_decode(file_get_contents($cachename), true); 188 if (!$fetchgroups || is_array($this->userCache[$username]['grps'])) { 189 return $this->userCache[$username]; 190 } 191 } 192 193 // fetch fresh data 194 $info = $this->getUser($username, $fetchgroups); 195 196 // store in cache 197 if ($info !== null) { 198 $this->userCache[$username] = $info; 199 file_put_contents($cachename, json_encode($info)); 200 } 201 202 return $info; 203 } 204 205 /** 206 * Fetch a single user 207 * 208 * @param string $username 209 * @param bool $fetchgroups Shall groups be fetched, too? 210 * @return null|array 211 */ 212 abstract public function getUser($username, $fetchgroups = true); 213 214 /** 215 * Return a list of all available groups, use cache if available 216 * 217 * @return string[] 218 */ 219 public function getCachedGroups() 220 { 221 if (empty($this->groupCache)) { 222 $this->groupCache = $this->getGroups(); 223 } 224 225 return $this->groupCache; 226 } 227 228 /** 229 * Return a list of all available groups 230 * 231 * Optionally filter the list 232 * 233 * @param null|string $match Filter for this, null for all groups 234 * @param string $filtermethod How to match the groups 235 * @return string[] 236 */ 237 abstract public function getGroups($match = null, $filtermethod = self::FILTER_EQUAL); 238 239 /** 240 * Clean the user name for use in DokuWiki 241 * 242 * @param string $user 243 * @return string 244 */ 245 abstract public function cleanUser($user); 246 247 /** 248 * Clean the group name for use in DokuWiki 249 * 250 * @param string $group 251 * @return string 252 */ 253 abstract public function cleanGroup($group); 254 255 /** 256 * Inheriting classes may want to manipulate the user before binding 257 * 258 * @param string $user 259 * @return string 260 */ 261 protected function prepareBindUser($user) 262 { 263 return $user; 264 } 265 266 /** 267 * Helper method to get the first value of the given attribute 268 * 269 * The given attribute may be null, an empty string is returned then 270 * 271 * @param Attribute|null $attribute 272 * @return string 273 */ 274 protected function attr2str($attribute) 275 { 276 if ($attribute !== null) { 277 return $attribute->firstValue(); 278 } 279 return ''; 280 } 281 282 /** 283 * Get the attributes that should be fetched for a user 284 * 285 * Can be extended in sub classes 286 * 287 * @return Attribute[] 288 */ 289 protected function userAttributes() 290 { 291 // defaults 292 $attr = [ 293 new Attribute('dn'), 294 new Attribute('displayName'), 295 new Attribute('mail'), 296 ]; 297 // additionals 298 foreach ($this->config['attributes'] as $attribute) { 299 $attr[] = new Attribute($attribute); 300 } 301 return $attr; 302 } 303 304 /** 305 * Handle fatal exceptions 306 * 307 * @param \Exception $e 308 */ 309 protected function fatal(\Exception $e) 310 { 311 if (class_exists('\dokuwiki\ErrorHandler')) { 312 \dokuwiki\ErrorHandler::logException($e); 313 } 314 315 if (defined('DOKU_UNITTEST')) { 316 throw new \RuntimeException('', 0, $e); 317 } 318 319 msg('[pureldap] ' . hsc($e->getMessage()), -1); 320 } 321 322 /** 323 * Handle error output 324 * 325 * @param string $msg 326 * @param string $file 327 * @param int $line 328 */ 329 protected function error($msg, $file, $line) 330 { 331 if (class_exists('\dokuwiki\Logger')) { 332 \dokuwiki\Logger::error('[pureldap] ' . $msg, '', $file, $line); 333 } 334 335 if (defined('DOKU_UNITTEST')) { 336 throw new \RuntimeException($msg . ' at ' . $file . ':' . $line); 337 } 338 339 msg('[pureldap] ' . hsc($msg), -1); 340 } 341 342 /** 343 * Handle debug output 344 * 345 * @param string $msg 346 * @param string $file 347 * @param int $line 348 */ 349 protected function debug($msg, $file, $line) 350 { 351 global $conf; 352 353 if (class_exists('\dokuwiki\Logger')) { 354 \dokuwiki\Logger::debug('[pureldap] ' . $msg, '', $file, $line); 355 } 356 357 if ($conf['allowdebug']) { 358 msg('[pureldap] ' . hsc($msg) . ' at ' . $file . ':' . $line, 0); 359 } 360 } 361} 362