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 * Access to the config values 116 * 117 * Because the client class does configuration cleanup, the auth class should access 118 * config values through the client 119 * 120 * @param string $key 121 * @return mixed returns null on missing config 122 */ 123 public function getConf($key) { 124 if(!isset($this->config[$key])) return null; 125 return $this->config[$key]; 126 } 127 128 /** 129 * Authenticate as admin 130 */ 131 public function autoAuth() 132 { 133 if ($this->isAuthenticated) return true; 134 135 $user = $this->prepareBindUser($this->config['admin_username']); 136 $ok = $this->authenticate($user, $this->config['admin_password']); 137 if (!$ok) { 138 $this->error('Administrative bind failed. Probably wrong user/password.', __FILE__, __LINE__); 139 } 140 return $ok; 141 } 142 143 /** 144 * Authenticates a given user. This client will remain authenticated 145 * 146 * @param string $user 147 * @param string $pass 148 * @return bool was the authentication successful? 149 * @noinspection PhpRedundantCatchClauseInspection 150 */ 151 public function authenticate($user, $pass) 152 { 153 $user = $this->prepareBindUser($user); 154 155 if ($this->config['encryption'] === 'tls') { 156 try { 157 $this->ldap->startTls(); 158 } catch (OperationException $e) { 159 $this->fatal($e); 160 } 161 } 162 163 try { 164 $this->ldap->bind($user, $pass); 165 } catch (BindException $e) { 166 return false; 167 } catch (ConnectionException $e) { 168 $this->fatal($e); 169 return false; 170 } catch (OperationException $e) { 171 $this->fatal($e); 172 return false; 173 } 174 175 $this->isAuthenticated = true; 176 return true; 177 } 178 179 /** 180 * Get info for a single user, use cache if available 181 * 182 * @param string $username 183 * @param bool $fetchgroups Are groups needed? 184 * @return array|null 185 */ 186 public function getCachedUser($username, $fetchgroups = true) 187 { 188 global $conf; 189 190 // memory cache first 191 if (isset($this->userCache[$username])) { 192 if (!$fetchgroups || is_array($this->userCache[$username]['grps'])) { 193 return $this->userCache[$username]; 194 } 195 } 196 197 // disk cache second 198 if($this->config['usefscache']) { 199 $cachename = getCacheName($username, '.pureldap-user'); 200 $cachetime = @filemtime($cachename); 201 if ($cachetime && (time() - $cachetime) < $conf['auth_security_timeout']) { 202 $this->userCache[$username] = json_decode(file_get_contents($cachename), true); 203 if (!$fetchgroups || is_array($this->userCache[$username]['grps'])) { 204 return $this->userCache[$username]; 205 } 206 } 207 } 208 209 // fetch fresh data 210 $info = $this->getUser($username, $fetchgroups); 211 212 // store in cache 213 if ($this->config['usefscache'] && $info !== null) { 214 $this->userCache[$username] = $info; 215 file_put_contents($cachename, json_encode($info)); 216 } 217 218 return $info; 219 } 220 221 /** 222 * Fetch a single user 223 * 224 * @param string $username 225 * @param bool $fetchgroups Shall groups be fetched, too? 226 * @return null|array 227 */ 228 abstract public function getUser($username, $fetchgroups = true); 229 230 /** 231 * Return a list of all available groups, use cache if available 232 * 233 * @return string[] 234 */ 235 public function getCachedGroups() 236 { 237 if (empty($this->groupCache)) { 238 $this->groupCache = $this->getGroups(); 239 } 240 241 return $this->groupCache; 242 } 243 244 /** 245 * Return a list of all available groups 246 * 247 * Optionally filter the list 248 * 249 * @param null|string $match Filter for this, null for all groups 250 * @param string $filtermethod How to match the groups 251 * @return string[] 252 */ 253 abstract public function getGroups($match = null, $filtermethod = self::FILTER_EQUAL); 254 255 /** 256 * Clean the user name for use in DokuWiki 257 * 258 * @param string $user 259 * @return string 260 */ 261 abstract public function cleanUser($user); 262 263 /** 264 * Clean the group name for use in DokuWiki 265 * 266 * @param string $group 267 * @return string 268 */ 269 abstract public function cleanGroup($group); 270 271 /** 272 * Inheriting classes may want to manipulate the user before binding 273 * 274 * @param string $user 275 * @return string 276 */ 277 protected function prepareBindUser($user) 278 { 279 return $user; 280 } 281 282 /** 283 * Helper method to get the first value of the given attribute 284 * 285 * The given attribute may be null, an empty string is returned then 286 * 287 * @param Attribute|null $attribute 288 * @return string 289 */ 290 protected function attr2str($attribute) 291 { 292 if ($attribute !== null) { 293 return $attribute->firstValue(); 294 } 295 return ''; 296 } 297 298 /** 299 * Get the attributes that should be fetched for a user 300 * 301 * Can be extended in sub classes 302 * 303 * @return Attribute[] 304 */ 305 protected function userAttributes() 306 { 307 // defaults 308 $attr = [ 309 new Attribute('dn'), 310 new Attribute('displayName'), 311 new Attribute('mail'), 312 ]; 313 // additionals 314 foreach ($this->config['attributes'] as $attribute) { 315 $attr[] = new Attribute($attribute); 316 } 317 return $attr; 318 } 319 320 /** 321 * Handle fatal exceptions 322 * 323 * @param \Exception $e 324 */ 325 protected function fatal(\Exception $e) 326 { 327 if (class_exists('\dokuwiki\ErrorHandler')) { 328 \dokuwiki\ErrorHandler::logException($e); 329 } 330 331 if (defined('DOKU_UNITTEST')) { 332 throw new \RuntimeException('', 0, $e); 333 } 334 335 msg('[pureldap] ' . hsc($e->getMessage()), -1); 336 } 337 338 /** 339 * Handle error output 340 * 341 * @param string $msg 342 * @param string $file 343 * @param int $line 344 */ 345 protected function error($msg, $file, $line) 346 { 347 if (class_exists('\dokuwiki\Logger')) { 348 \dokuwiki\Logger::error('[pureldap] ' . $msg, '', $file, $line); 349 } 350 351 if (defined('DOKU_UNITTEST')) { 352 throw new \RuntimeException($msg . ' at ' . $file . ':' . $line); 353 } 354 355 msg('[pureldap] ' . hsc($msg), -1); 356 } 357 358 /** 359 * Handle debug output 360 * 361 * @param string $msg 362 * @param string $file 363 * @param int $line 364 */ 365 protected function debug($msg, $file, $line) 366 { 367 global $conf; 368 369 if (class_exists('\dokuwiki\Logger')) { 370 \dokuwiki\Logger::debug('[pureldap] ' . $msg, '', $file, $line); 371 } 372 373 if ($conf['allowdebug']) { 374 msg('[pureldap] ' . hsc($msg) . ' at ' . $file . ':' . $line, 0); 375 } 376 } 377} 378