1<?php 2/** 3 * Plain CAS authentication plugin 4 * 5 * @licence GPL 2 (http://www.gnu.org/licenses/gpl.html) 6 * @author Fabian Bircher 7 * @version 0.0.2 8 */ 9 10// must be run within Dokuwiki 11if(!defined('DOKU_INC')) die(); 12 13// Look for the phpCAS library in different places. 14if (!class_exists('phpCAS')) { 15 $phpcas_paths = [ 16 DOKU_INC . 'vendor/jasig/phpcas/CAS.php', 17 DOKU_INC . 'phpCAS/CAS.php', 18 DOKU_PLUGIN . 'phpCAS/CAS.php', 19 DOKU_PLUGIN . 'authplaincas/phpCAS/CAS.php', 20 ]; 21 foreach ($phpcas_paths as $file) { 22 if (file_exists($file)) { 23 require_once $file; 24 continue; 25 } 26 } 27} 28 29 30class auth_plugin_authplaincas extends DokuWiki_Auth_Plugin { 31 /** @var array user cache */ 32 protected $users = null; 33 34 /** @var array filter pattern */ 35 protected $_pattern = array(); 36 37 var $_options = array(); 38 var $_userInfo = array(); 39 40 var $casuserfile = null; 41 var $localuserfile = NULL; 42 43 /** 44 * Constructor 45 * 46 * Carry out sanity checks to ensure the object is 47 * able to operate. Set capabilities. 48 * 49 * @author Fabian Bircher <fabian@esn.org> 50 */ 51 public function __construct() { 52 parent::__construct(); 53 global $config_cascade; 54 global $conf; 55 56 // Show an error message instead of a php error when third party conditions 57 // are not met. 58 if (!class_exists('phpCAS')) { 59 msg("CAS err: phpCAS class not found.",-1); 60 $this->success = false; 61 return; 62 } 63 if(!function_exists('curl_init')) { 64 msg("CAS err: CURL php extension not found.",-1); 65 $this->success = false; 66 return; 67 } 68 69 // allow the preloading to configure other user files 70 if( isset($config_cascade['plaincasauth.users']) && isset($config_cascade['plaincasauth.users']['default']) ) { 71 $this->casuserfile = $config_cascade['plaincasauth.users']['default']; 72 } 73 else { 74 $this->casuserfile = DOKU_CONF . 'users.auth.plaincas.php'; 75 } 76 $this->localuserfile = $config_cascade['plainauth.users']['default']; 77 78 // check the state of the file with the users and attempt to create it. 79 if (!@is_readable($this->casuserfile)) { 80 if(! fopen($this->casuserfile, 'w') ) { 81 msg("plainCAS: The CAS users file could not be opened.", -1); 82 $this->success = false; 83 } 84 elseif(!@is_readable($this->casuserfile)){ 85 $this->success = false; 86 } 87 else{ 88 $this->success = true; 89 } 90 // die( "bitch!" ); 91 } 92 if ($this->success) { 93 // the users are not managable through the wiki 94 $this->cando['addUser'] = false; 95 $this->cando['delUser'] = true; 96 $this->cando['modLogin'] = false; //keep this false as CAS name is constant 97 $this->cando['modPass'] = false; 98 $this->cando['modName'] = false; 99 $this->cando['modMail'] = false; 100 $this->cando['modGroups'] = false; 101 $this->cando['getUsers'] = true; 102 $this->cando['getUserCount'] = true; 103 104 $this->cando['external'] = true; 105 $this->cando['login'] = true; 106 $this->cando['logout'] = true; 107 $this->cando['logoff'] = true; 108 109 // The default options which need to be set in the settins file. 110 $defaults = array( 111 // 'server' => 'galaxy.esn.org', 112 // 'rootcas' => '/cas', 113 // 'port' => '443', 114 // 'autologin' => false, 115 // 'handlelogoutrequest' => true, 116 // 'handlelogoutrequestTrustedHosts' => "galaxy.esn.org", 117 // 'caslogout' => false, 118 // 'minimalgroups' => NULL, 119 // 'customgroups' => false, 120 'logFile' => NULL, 121 'cert' => NULL, 122 'cacert' => NULL, 123 'debug' => false, 124 'settings_file' => DOKU_CONF . 'plaincas.settings.php', 125 126 'defaultgroup' => $conf['defaultgroup'], 127 'superuser' => $conf['superuser'], 128 129 ); 130 $this->_options = (array) $conf['plugin']['authplaincas'] + $defaults; 131 132 // Options are set in the configuration and have a proper default value there. 133 $this->_options['server'] = $this->getConf('server'); 134 $this->_options['rootcas'] = $this->getConf('rootcas'); 135 $this->_options['port'] = $this->getConf('port'); 136 $this->_options['samlValidate'] = $this->getConf('samlValidate'); 137 $this->_options['handlelogoutrequest'] = $this->getConf('handlelogoutrequest'); 138 $this->_options['handlelogoutrequestTrustedHosts'] = $this->getConf('handlelogoutrequestTrustedHosts'); 139 $this->_options['minimalgroups'] = $this->getConf('minimalgroups'); 140 $this->_options['localusers'] = $this->getConf('localusers'); 141 // $this->_options['defaultgroup'] = $this->getConf('defaultgroup'); 142 // $this->_options['superuser'] = $this->getConf('superuser'); 143 144 // Configure support for autologin (gateway mode) and redirecting on logout for CAS server single-logout 145 if (preg_match("#(bot)|(slurp)|(netvibes)#i", $_SERVER['HTTP_USER_AGENT'])) { 146 // bots (like search engine indexers) should never be given 302 redirects 147 $this->_options['autologin'] = false; 148 } else { 149 // otherwise, fall back to the individual configuration parameters "autologin" and "caslogout" 150 $this->_options['autologin'] = $this->getConf('autologin'); 151 } 152 153 // no local users at the moment 154 $this->_options['localusers'] = false; 155 156 if($this->_options['localusers'] && !@is_readable($this->localuserfile)) { 157 msg("plainCAS: The local users file is not readable.", -1); 158 $this->success = false; 159 } 160 161 if($this->_getOption("logFile")){ phpCAS::setDebug($this->_getOption("logFile"));} 162 //If $conf['auth']['cas']['logFile'] exist we start phpCAS in debug mode 163 164 $server_version = CAS_VERSION_2_0; 165 if($this->_getOption("samlValidate")) { 166 $server_version = SAML_VERSION_1_1; 167 } 168 phpCAS::client($server_version, $this->_getOption('server'), (int) $this->_getOption('port'), $this->_getOption('rootcas'), false); 169 //False avoids phpCAS from taking care of sessions messing with the session ID. As Dokuwiki introduced new requirements to the session ID, logins will otherwise fail. This causes some PHP warnings on logout so should be updated once supported by phpCAS 170 171 // when using autologin (gateway mode), how often will autologin be attempted 172 if ($this->getConf('autologinonce', false)) { 173 // cache a failed autologin attempt "forever" until the current 174 // anonymous session expires or the user clicks the login button 175 phpCAS::setCacheTimesForAuthRecheck(-1); 176 } else { 177 // retry autologin every pageview, but cache a failed gateway attempt 1 178 // time, to avoid a second gateway attempt on the indexer.php page 179 // asset on the same pageview 180 phpCAS::setCacheTimesForAuthRecheck(1); 181 } 182 183 if($this->_getOption('cert')) { 184 phpCAS::setCasServerCert($this->_getOption('cert')); 185 } 186 elseif($this->_getOption('cacert')) { 187 phpCAS::setCasServerCACert($this->_getOption('cacert')); 188 } 189 else { 190 phpCAS::setNoCasServerValidation(); 191 } 192 193 if($this->_getOption('handlelogoutrequest')) { 194 phpCAS::handleLogoutRequests(true, $this->_getOption('handlelogoutrequestTrustedHosts')); 195 } 196 else { 197 phpCAS::handleLogoutRequests(false); 198 } 199 200 if (@is_readable($this->_getOption('settings_file'))) { 201 include_once($this->_getOption('settings_file')); 202 } 203 else { 204 include_once(DOKU_PLUGIN . 'authplaincas/plaincas.settings.php'); 205 } 206 207 } 208 // 209 } 210 211 function _getOption ($optionName) 212 { 213 if (isset($this->_options[$optionName])) { 214 switch( $optionName ){ 215 case 'minimalgroups': 216 case 'superusers': 217 if (!$this->_options[$optionName]) { 218 return null; 219 } 220 case 'handlelogoutrequestTrustedHosts': 221 $arr = explode(',', $this->_options[$optionName]); 222 foreach( $arr as $key => $item ){ 223 $arr[$key] = trim($item); 224 } 225 return $arr; 226 break; 227 default: 228 return $this->_options[$optionName]; 229 } 230 } 231 return NULL; 232 } 233 234 /** 235 * Inherited canDo function, may be useful for localusers 236 * 237 * @param string $cap 238 * @return bool 239 */ 240 public function canDo($cap) { 241 // We might need to do something to redefine the capabilities for local users 242 return parent::canDo($cap); 243 } 244 245 public function logIn() { 246 global $QUERY; 247 $login_url = DOKU_URL . 'doku.php?id=' . $QUERY; 248 phpCAS::setFixedServiceURL($login_url); 249 phpCAS::forceAuthentication(); 250 } 251 252 public function logOff() { 253 global $QUERY; 254 255 if($this->_getOption('handlelogoutrequest')) { // dokuwiki + cas logout 256 @session_start(); 257 session_destroy(); 258 $logout_url = DOKU_URL . 'doku.php?id=' . $QUERY; 259 //hide warnings of not initalized session, cas session is killed anyway 260 @phpCAS::logoutWithRedirectService($logout_url); 261 } 262 else { // dokuwiki logout only 263 @session_start(); 264 session_destroy(); 265 unset($_SESSION['phpCAS']); 266 } 267 } 268 269function trustExternal ($user,$pass,$sticky=false) 270 { 271 global $USERINFO; 272 $sticky ? $sticky = true : $sticky = false; //sanity check 273 274 if (phpCAS::isAuthenticated() || ( $this->_getOption('autologin') && phpCAS::checkAuthentication() )) { 275 276 $remoteUser = phpCAS::getUser(); 277 $this->_userInfo = $this->getUserData($remoteUser); 278 // msg(print_r($this->_userInfo,true) . __LINE__); 279 280 // Create the user if he doesn't exist 281 if ($this->_userInfo === false) { 282 $attributes = plaincas_user_attributes(phpCAS::getAttributes()); 283 $this->_userInfo = array( 284 'uid' => $remoteUser, 285 'name' => $attributes['name'], 286 'mail' => $attributes['mail'] 287 ); 288 289 $this->_assembleGroups($remoteUser); 290 $this->_saveUserGroup(); 291 $this->_saveUserInfo(); 292 293 // msg(print_r($this->_userInfo,true) . __LINE__); 294 295 $USERINFO = $this->_userInfo; 296 $_SESSION[DOKU_COOKIE]['auth']['user'] = $USERINFO['uid']; 297 $_SESSION[DOKU_COOKIE]['auth']['info'] = $USERINFO; 298 $_SERVER['REMOTE_USER'] = $USERINFO['uid']; 299 return true; 300 301 // User exists, check for updates 302 } else { 303 $this->_userInfo['uid'] = $remoteUser; 304 $this->_assembleGroups($remoteUser); 305 306 $attributes = plaincas_user_attributes(phpCAS::getAttributes()); 307 308 if ($this->_userInfo['grps'] != $this->_userInfo['tmp_grps'] || 309 $attributes['name'] !== $this->_userInfo['name'] || 310 $attributes['mail'] !== $this->_userInfo['mail'] 311 ) { 312 //msg("new roles, email, or name"); 313 $this->deleteUsers(array($remoteUser)); 314 $this->_userInfo = array( 315 'uid' => $remoteUser, 316 'name' => $attributes['name'], 317 'mail' => $attributes['mail'] 318 ); 319 $this->_assembleGroups($remoteUser); 320 $this->_saveUserGroup(); 321 $this->_saveUserInfo(); 322 } 323 324 $USERINFO = $this->_userInfo; 325 $_SESSION[DOKU_COOKIE]['auth']['user'] = $USERINFO['uid']; 326 $_SESSION[DOKU_COOKIE]['auth']['info'] = $USERINFO; 327 $_SERVER['REMOTE_USER'] = $USERINFO['uid']; 328 329 return true; 330 } 331 332 } 333 // else{ 334 // } 335 336 return false; 337 } 338 339 340 function _assembleGroups($remoteUser) { 341 342 $this->_userInfo['tmp_grps'] = array(); 343 344 if (NULL !== $this->_getOption('defaultgroup')) { 345 $this->_addUserGroup($this->_getOption('defaultgroup')); 346 } 347 348 if ((NULL !== $this->_getOption('superusers')) && 349 is_array($this->_getOption('superusers')) && 350 in_array($remoteUser, $this->_getOption('superusers'))) { 351 352 $this->_addUserGroup($this->_getOption('admingroup')); 353 } 354 355 $this->_setCASGroups(); 356 $this->_setCustomGroups($remoteUser); 357 } 358 359 360 function _setCASGroups () 361 { 362 if( phpCAS::checkAuthentication() ) { 363 $attributes = plaincas_pattern_attributes(phpCAS::getAttributes()); 364 if (!is_array($attributes)) { 365 $attributes = array($attributes); 366 } 367 $patterns = plaincas_group_patterns(); 368 if (!empty($patterns)) { 369 foreach ($patterns as $role => $pattern) { 370 foreach ($attributes as $attribute) { 371 // An invalid pattern will generate a php warning and will not be considered. 372 if (preg_match($pattern, $attribute)) { 373 $this->_addUserGroup($role); 374 } 375 } 376 } 377 } 378 else { 379 foreach ($attributes as $attribute) { 380 // Add all attributes as groups 381 $this->_addUserGroup($attribute); 382 } 383 } 384 } 385 } 386 387 388 function _setCustomGroups ($userId) 389 { 390 // assert existence of function for backwards compatibility 391 if (!function_exists('plaincas_custom_groups')) { 392 return; 393 } 394 $customGroups = plaincas_custom_groups(); 395 396 if (! is_array($customGroups) || empty($customGroups)) { 397 return; 398 } 399 400 foreach ($customGroups as $groupName => $groupMembers) { 401 if (! is_array($groupMembers) || empty($groupMembers)) { 402 continue; 403 } 404 if (in_array($userId, $groupMembers)) { 405 $this->_addUserGroup($groupName); 406 } 407 } 408 409 } 410 411 412 function _addUserGroup ($groupName) 413 { 414 if (! isset($this->_userInfo['tmp_grps'])) { 415 $this->_userInfo['tmp_grps'] = array(); 416 } 417 if( !in_array(trim($groupName), $this->_userInfo['tmp_grps'])) { 418 $this->_userInfo['tmp_grps'][] = trim($groupName); 419 } 420 421 } 422 423 function _saveUserGroup() 424 { 425 $this->_userInfo['grps'] = $this->_userInfo['tmp_grps']; 426 } 427 428 function _minimalGroupCheck() { 429 $groups = $this->_getOption('minimalgroups'); 430 if( ! $groups || empty($groups) ) { 431 return true; 432 } 433 elseif (count( array_intersect( $this->_userInfo['grps'], $groups ) )) { 434 return true; 435 } 436 else { 437 return false; 438 } 439 440 } 441 442 function _saveUserInfo () 443 { 444 $save = true; 445 if(!$this->_minimalGroupCheck()) { 446 $save = false; 447 $this->_userInfo['grps'] = array(); 448 $this->_userInfo['tmp_grps'] = array(); 449 } 450 global $USERINFO; 451 452 $USERINFO = $this->_userInfo; 453 $_SESSION[DOKU_COOKIE]['auth']['user'] = $USERINFO['uid']; 454 $_SESSION[DOKU_COOKIE]['auth']['info'] = $USERINFO; 455 456 // Despite setting the user into the session, DokuWiki still uses hard-coded REMOTE_USER variable 457 $_SERVER['REMOTE_USER'] = $USERINFO['uid']; 458 459 // user mustn't already exist 460 if ($this->getUserData($USERINFO['uid']) === false && $save) { 461 // prepare user line 462 $groups = join(',',$USERINFO['grps']); 463 $userline = join(':',array($USERINFO['uid'], $USERINFO['name'], $USERINFO['mail'], $groups))."\n"; 464 465 if (io_saveFile($this->casuserfile,$userline,true)) { 466 $this->users[$USERINFO['uid']] = compact('name','mail','grps'); 467 }else{ 468 msg('The '.$this->casuserfile.' file is not writable. Please inform the Wiki-Admin',-1); 469 } 470 } 471 $this->_log($this->_userInfo); 472 } 473 474 475 function _log ($value) 476 { 477 if ($this->_getOption('debug')) { 478 error_log(print_r($value, true)); 479 var_dump($value); 480 } 481 } 482 483 /** 484 * Modify user data 485 * 486 * @author Chris Smith <chris@jalakai.co.uk> 487 * @param $user nick of the user to be changed 488 * @param $changes array of field/value pairs to be changed (password will be clear text) 489 * @return bool 490 */ 491 function modifyUser($user, $changes) { 492 global $conf; 493 494 // sanity checks, user must already exist and there must be something to change 495 if (($userinfo = $this->getUserData($user)) === false) return false; 496// if (!(count($changes) == 1 and isset($changes['grps']))) return false; 497 if (!is_array($changes) || !count($changes)) return true; 498 499 foreach ($changes as $field => $value) { 500 $userinfo[$field] = $value; 501 } 502 503 $groups = join(',',$userinfo['grps']); 504 $userline = join(':',array($user, $userinfo['name'], $userinfo['mail'], $groups))."\n"; 505 506 if (!$this->deleteUsers(array($user))) { 507 msg('Unable to modify user data. Please inform the Wiki-Admin',-1); 508 return false; 509 } 510 511 if (!io_saveFile($this->casuserfile,$userline,true)) { 512 msg('There was an error modifying the user data. Please inform the Wiki-Admin.',-1); 513 return false; 514 } 515 516 $this->users[$user] = $userinfo; 517 return true; 518 } 519 520 /** 521 * Remove one or more users from the list of registered users 522 * 523 * @author Christopher Smith <chris@jalakai.co.uk> 524 * @param array $users array of users to be deleted 525 * @return int the number of users deleted 526 */ 527 function deleteUsers($users) { 528 if (!is_array($users) || empty($users)) return 0; 529 530 if ($this->users === null) $this->_loadUserData(); 531 532 $deleted = array(); 533 foreach ($users as $user) { 534 if (isset($this->users[$user])) $deleted[] = preg_quote($user,'/'); 535 } 536 537 if (empty($deleted)) return 0; 538 539 $pattern = '/^('.join('|',$deleted).'):/'; 540 541 if (io_deleteFromFile($this->casuserfile,$pattern,true)) { 542 foreach ($deleted as $user) unset($this->users[$user]); 543 return count($deleted); 544 } 545 546 // problem deleting, reload the user list and count the difference 547 $count = count($this->users); 548 $this->_loadUserData(); 549 $count -= count($this->users); 550 return $count; 551 } 552 553 554 /** 555 * Return user info 556 * 557 * Returns info about the given user needs to contain 558 * at least these fields: 559 * 560 * name string full name of the user 561 * mail string email addres of the user 562 * grps array list of groups the user is in 563 * 564 * @author Andreas Gohr <andi@splitbrain.org> 565 */ 566 function getUserData($user, $requireGroups=true) { 567 if($this->users === null) $this->_loadUserData(); 568 return isset($this->users[$user]) ? $this->users[$user] : false; 569 } 570 571 /** 572 * Load all user data 573 * 574 * loads the user file into a datastructure 575 * 576 * @author Andreas Gohr <andi@splitbrain.org> 577 * @author Martin Kos <martin@kos.li> 578 */ 579 function _loadUserData(){ 580 $this->users = array(); 581 582 if(!@file_exists($this->casuserfile)) return; 583 584 $lines = file($this->casuserfile); 585 foreach($lines as $line){ 586 $line = preg_replace('/#.*$/','',$line); //ignore comments 587 $line = trim($line); 588 if(empty($line)) continue; 589 590 $row = explode(":",$line,5); 591 $groups = explode(",",$row[3]); 592 // msg(print_r($row,true). __LINE__); 593 594 $this->users[$row[0]]['name'] = $row[1]; 595 $this->users[$row[0]]['mail'] = $row[2]; 596 $this->users[$row[0]]['grps'] = $groups; 597 } 598 } 599 600 601 /** 602 * Return a count of the number of user which meet $filter criteria 603 * 604 * @author Chris Smith <chris@jalakai.co.uk> 605 */ 606 function getUserCount($filter=array()) { 607 608 if($this->users === null) $this->_loadUserData(); 609 610 if (!count($filter)) return count($this->users); 611 612 $count = 0; 613 $this->_constructPattern($filter); 614 615 foreach ($this->users as $user => $info) { 616 $count += $this->_filter($user, $info); 617 } 618 619 return $count; 620 } 621 622 /** 623 * Bulk retrieval of user data 624 * 625 * @author Chris Smith <chris@jalakai.co.uk> 626 * @param start index of first user to be returned 627 * @param limit max number of users to be returned 628 * @param filter array of field/pattern pairs 629 * @return array of userinfo (refer getUserData for internal userinfo details) 630 */ 631 function retrieveUsers($start=0,$limit=0,$filter=array()) { 632 if ($this->users === null) $this->_loadUserData(); 633 634 ksort($this->users); 635 636 $i = 0; 637 $count = 0; 638 $out = array(); 639 $this->_constructPattern($filter); 640 641 foreach ($this->users as $user => $info) { 642 if ($this->_filter($user, $info)) { 643 if ($i >= $start) { 644 $out[$user] = $info; 645 $count++; 646 if (($limit > 0) && ($count >= $limit)) break; 647 } 648 $i++; 649 } 650 } 651 652 return $out; 653 } 654 655 function cleanUser($user) { 656 $user = str_replace('@', '_', $user); 657 $user = str_replace(':', '_', $user); 658 return $user; 659 } 660 661 function cleanGroup($group) { 662 return $group; 663 } 664 665 /** 666 * return 1 if $user + $info match $filter criteria, 0 otherwise 667 * 668 * @author Chris Smith <chris@jalakai.co.uk> 669 */ 670 function _filter($user, $info) { 671 // FIXME 672 foreach ($this->_pattern as $item => $pattern) { 673 if ($item == 'user') { 674 if (!preg_match($pattern, $user)) return 0; 675 } else if ($item == 'grps') { 676 if (!count(preg_grep($pattern, $info['grps']))) return 0; 677 } else { 678 if (!preg_match($pattern, $info[$item])) return 0; 679 } 680 } 681 return 1; 682 } 683 684 function _constructPattern($filter) { 685 $this->_pattern = array(); 686 foreach ($filter as $item => $pattern) { 687// $this->_pattern[$item] = '/'.preg_quote($pattern,"/").'/i'; // don't allow regex characters 688 $this->_pattern[$item] = '/'.str_replace('/','\/',$pattern).'/i'; // allow regex characters 689 } 690 } 691 692} 693