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 $cachename = getCacheName($username, '.pureldap-user'); 199 $cachetime = @filemtime($cachename); 200 if ($cachetime && (time() - $cachetime) < $conf['auth_security_timeout']) { 201 $this->userCache[$username] = json_decode(file_get_contents($cachename), true); 202 if (!$fetchgroups || is_array($this->userCache[$username]['grps'])) { 203 return $this->userCache[$username]; 204 } 205 } 206 207 // fetch fresh data 208 $info = $this->getUser($username, $fetchgroups); 209 210 // store in cache 211 if ($info !== null) { 212 $this->userCache[$username] = $info; 213 file_put_contents($cachename, json_encode($info)); 214 } 215 216 return $info; 217 } 218 219 /** 220 * Fetch a single user 221 * 222 * @param string $username 223 * @param bool $fetchgroups Shall groups be fetched, too? 224 * @return null|array 225 */ 226 abstract public function getUser($username, $fetchgroups = true); 227 228 /** 229 * Return a list of all available groups, use cache if available 230 * 231 * @return string[] 232 */ 233 public function getCachedGroups() 234 { 235 if (empty($this->groupCache)) { 236 $this->groupCache = $this->getGroups(); 237 } 238 239 return $this->groupCache; 240 } 241 242 /** 243 * Return a list of all available groups 244 * 245 * Optionally filter the list 246 * 247 * @param null|string $match Filter for this, null for all groups 248 * @param string $filtermethod How to match the groups 249 * @return string[] 250 */ 251 abstract public function getGroups($match = null, $filtermethod = self::FILTER_EQUAL); 252 253 /** 254 * Clean the user name for use in DokuWiki 255 * 256 * @param string $user 257 * @return string 258 */ 259 abstract public function cleanUser($user); 260 261 /** 262 * Clean the group name for use in DokuWiki 263 * 264 * @param string $group 265 * @return string 266 */ 267 abstract public function cleanGroup($group); 268 269 /** 270 * Inheriting classes may want to manipulate the user before binding 271 * 272 * @param string $user 273 * @return string 274 */ 275 protected function prepareBindUser($user) 276 { 277 return $user; 278 } 279 280 /** 281 * Helper method to get the first value of the given attribute 282 * 283 * The given attribute may be null, an empty string is returned then 284 * 285 * @param Attribute|null $attribute 286 * @return string 287 */ 288 protected function attr2str($attribute) 289 { 290 if ($attribute !== null) { 291 return $attribute->firstValue(); 292 } 293 return ''; 294 } 295 296 /** 297 * Get the attributes that should be fetched for a user 298 * 299 * Can be extended in sub classes 300 * 301 * @return Attribute[] 302 */ 303 protected function userAttributes() 304 { 305 // defaults 306 $attr = [ 307 new Attribute('dn'), 308 new Attribute('displayName'), 309 new Attribute('mail'), 310 ]; 311 // additionals 312 foreach ($this->config['attributes'] as $attribute) { 313 $attr[] = new Attribute($attribute); 314 } 315 return $attr; 316 } 317 318 /** 319 * Handle fatal exceptions 320 * 321 * @param \Exception $e 322 */ 323 protected function fatal(\Exception $e) 324 { 325 if (class_exists('\dokuwiki\ErrorHandler')) { 326 \dokuwiki\ErrorHandler::logException($e); 327 } 328 329 if (defined('DOKU_UNITTEST')) { 330 throw new \RuntimeException('', 0, $e); 331 } 332 333 msg('[pureldap] ' . hsc($e->getMessage()), -1); 334 } 335 336 /** 337 * Handle error output 338 * 339 * @param string $msg 340 * @param string $file 341 * @param int $line 342 */ 343 protected function error($msg, $file, $line) 344 { 345 if (class_exists('\dokuwiki\Logger')) { 346 \dokuwiki\Logger::error('[pureldap] ' . $msg, '', $file, $line); 347 } 348 349 if (defined('DOKU_UNITTEST')) { 350 throw new \RuntimeException($msg . ' at ' . $file . ':' . $line); 351 } 352 353 msg('[pureldap] ' . hsc($msg), -1); 354 } 355 356 /** 357 * Handle debug output 358 * 359 * @param string $msg 360 * @param string $file 361 * @param int $line 362 */ 363 protected function debug($msg, $file, $line) 364 { 365 global $conf; 366 367 if (class_exists('\dokuwiki\Logger')) { 368 \dokuwiki\Logger::debug('[pureldap] ' . $msg, '', $file, $line); 369 } 370 371 if ($conf['allowdebug']) { 372 msg('[pureldap] ' . hsc($msg) . ' at ' . $file . ':' . $line, 0); 373 } 374 } 375} 376