1<?php 2 3/** 4 * Authentication backend using Shibboleth 5 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 6 * @author Dominique Launay <dominique.launay AT cru.fr> 7 * @author Olivier Salaun <olivier.salaun AT cru.fr> 8 * @author Yoann Lecuyer <yoann.lecuyer AT cru.fr> 9 * Version: 1.2 10 * last modified: 2009-11-27 11 * 12 * Work based on : 13 * ssl authentication backend: 14 * @author Dominique Launay <dominique.launay AT cru.fr> 15 * Sympa Soap server authentication backend: 16 * @author David Pepin<sympa-authors AT cru.fr> 17 * 18 * The nusoap library is required 19 * 20 **/ 21 22 23define('DOKU_AUTH', dirname(__FILE__)); 24require_once(DOKU_AUTH.'/basic.class.php'); 25define('AUTH_USERFILE',DOKU_CONF.'users.auth.php'); 26 27class auth_shibboleth extends auth_basic { 28 var $users = null; 29 var $sympaDefaultGroup = null; 30 var $sympaSoapService = null; 31 var $soap_client = null; 32 var $log_file = null; 33 34 /** 35 * 36 * Constructor 37 * 38 * @author Yoann Lecuyer <yoann.lecuyer AT cru.fr> 39 * 40 **/ 41 // This function is run for every web access to the wiki 42 // Most of the Shibboleth authentication code is run here because we never run the do_login action 43 // With Shibboleth, dokuwiki never collects user password 44 function auth_shibboleth() { 45 global $conf; 46 //msg('auth_shibboleth'); 47 $this->cando['external'] = true; 48 //$this->cando['getGroups'] = true; 49 50 51 if (method_exists($this, 'auth_basic')){ 52 parent::auth_basic(); 53 } 54 // check if the server configuration has correctly been done 55 if (!isset($conf['plugin']['shibbolethauth']['shibbolethEmailAttribute']) || empty($conf['plugin']['shibbolethauth']['shibbolethEmailAttribute'])) { 56 $this->success = false; 57 return; 58 } 59 if (isset($conf['plugin']['shibbolethauth']['useSympa']) && $conf['plugin']['shibbolethauth']['useSympa'] == 1 && empty($conf['plugin']['shibbolethauth']['sympaSoapService'])) { 60 $this->success = false; 61 return; 62 } 63 if(!isset($conf['plugin']['shibbolethauth']['shibbolethLoginURL']) || empty($conf['plugin']['shibbolethauth']['shibbolethLoginURL'])) { 64 $this->success = false; 65 return; 66 } 67 if(!isset($conf['plugin']['shibbolethauth']['shibbolethLogoutURL']) || empty($conf['plugin']['shibbolethauth']['shibbolethLogoutURL'])) { 68 $this->success = false; 69 return; 70 } 71 72 if (isset($conf['defaultgroup'])) { 73 $this->sympaDefaultGroup = $conf['defaultgroup']; 74 } 75 76 77 $this->success = true; 78 return; 79 } 80 81 /** 82 * Do all authentication [ OPTIONAL ] 83 * 84 * Set $this->cando['external'] = true when implemented 85 * 86 * If this function is implemented it will be used to 87 * authenticate a user - all other DokuWiki internals 88 * will not be used for authenticating, thus 89 * implementing the functions below becomes optional. 90 * 91 * The function can be used to authenticate against third 92 * party cookies or Apache auth mechanisms and replaces 93 * the auth_login() function 94 * 95 * The function will be called with or without a set 96 * username. If the Username is given it was called 97 * from the login form and the given credentials might 98 * need to be checked. If no username was given it 99 * the function needs to check if the user is logged in 100 * by other means (cookie, environment). 101 * 102 * The function needs to set some globals needed by 103 * DokuWiki like auth_login() does. 104 * 105 * @see auth_login() 106 * @author Andreas Gohr <andi@splitbrain.org> 107 * @author Olivier Salaün <olivier.salaun AT cru.fr> 108 * @author Dominique Launay <dominique.launay AT cru.fr> 109 * 110 * @param string $user Username 111 * @param string $pass Cleartext Password 112 * @param bool $sticky Cookie should not expire 113 * @return bool true on successful auth 114 */ 115 function trustExternal($user,$pass,$sticky=false){ 116 //msg('trust_external'); 117 // some example: 118 119 global $USERINFO; 120 global $conf; 121 $sticky ? $sticky = true : $sticky = false; //sanity check 122 // do the checking here 123 //see if we got cookie 124 if(isset($_SESSION['DW_SHIB'])){ 125 // case logout 126 if($_REQUEST['do'] == 'logout'){ 127 unset($_SESSION['DW_SHIB']); 128 unset($_SESSION[DOKU_COOKIE]['auth']['user']); 129 unset($_SESSION[DOKU_COOKIE]['auth']['pass']); 130 unset($_SESSION[DOKU_COOKIE]['auth']['info']); 131 error_log('shibbolethauth : authenticated user redirected for logout to '.$conf['plugin']['shibbolethauth']['shibbolethLogoutURL']); 132 header("Location: ".$conf['plugin']['shibbolethauth']['shibbolethLogoutURL']."?return=".urlencode($_SERVER['HTTP_REFERER'])); 133 exit; 134 }else{ 135 // fill vars with cookie vars 136 $_SESSION[DOKU_COOKIE]['auth']['user'] = $user = $_SESSION['DW_SHIB']['user']; 137 $_SESSION[DOKU_COOKIE]['auth']['pass'] = $pass = $_SESSION['DW_SHIB']['pass']; 138 $_SESSION[DOKU_COOKIE]['auth']['info'] = $USERINFO = $_SESSION['DW_SHIB']['info']; 139 $_SERVER['REMOTE_USER'] = $_SESSION['DW_SHIB']['user']; 140 return true; 141 } 142 }else{ 143 // save the Email done by a Shibboleth attribute if this attribute is set 144 // we need to check if the identityProvider attribute is set, otherwise we can't distinguish 145 // normal unauthenticated access from authenticated access with a missing email address 146 if(isset($_SERVER[$conf['plugin']['shibbolethauth']['shibbolethEmailAttribute']]) && $_SERVER[$conf['plugin']['shibbolethauth']['shibbolethEmailAttribute']]!=""){ 147 // if user wants to log out 148 if($_REQUEST['do'] == 'logout'){ 149 unset($_SESSION['DW_SHIB']); 150 error_log('shibbolethauth : authenticated user redirected for logout to '.$conf['plugin']['shibbolethauth']['shibbolethLogoutURL']); 151 header("Location: ".$conf['plugin']['shibbolethauth']['shibbolethLogoutURL']."?return=".urlencode($_SERVER['HTTP_REFERER'])); 152 exit; 153 }else{ 154 //attributes set fill convenient variables 155 $USERINFO['name'] = $user = $_SERVER[$conf['plugin']['shibbolethauth']['shibbolethEmailAttribute']]; 156 $USERINFO['mail'] = $mail = $_SERVER[$conf['plugin']['shibbolethauth']['shibbolethEmailAttribute']]; 157 158 159 // load the groups of users contained on the file users.auth.php 160 if($this->users === null) $this->_loadUserData(); 161 // groups the person belongs to 162 $grps = array(); 163 // Test if $user is an email or not. If it is not, it is an user id 164 if(strpbrk($user, '@') != false){ 165 // We are in a Shibboteth context, and we get the datas for the autorization 166 // Test if we use the autorisation with Sympa server 167 $sympaIsOK = true; 168 if(isset($conf['plugin']['shibbolethauth']['useSympa']) && $conf['plugin']['shibbolethauth']['useSympa'] == 1){ 169 // Get the groups list on the Sympa server 170 //create soap client 171 try{ 172 $this->soap_client = new SoapClient($conf['plugin']['shibbolethauth']['sympaSoapService']); 173 }catch(SoapFault $fault){ 174 $sympaIsOK = false; 175 error_log("shibbolethauth : unable to establish a SOAP connection with ".$conf['plugin']['shibbolethauth']['sympaSoapService']); 176 msg("No valid SOAP server",0); 177 } 178 // get the groups list 179 if($sympaIsOK){ 180 try{ 181 $res = $this->soap_client->authenticateRemoteAppAndRun($conf['plugin']['shibbolethauth']['sympaApplicationId'],$conf['plugin']['shibbolethauth']['sympaApplicationPwd'], 'USER_EMAIL='.$user,'complexWhich'); 182 }catch(SoapFault $fault){ 183 $sympaIsOK = false; 184 msg("dokuwiki groups only",0); 185 error_log("shibbolethauth : unable to authenticate on ".$conf['plugin']['shibbolethauth']['sympaSoapService']); 186 } 187 if($sympaIsOK){ 188 if (isset($res) && gettype($res) == 'array') { 189 foreach ($res as $list) { 190 if (empty($grps[0])) { 191 $grps[0] = $list->listAddress; 192 }else{ 193 array_unshift($grps,$list->listAddress); 194 } 195 if($list->isOwner == 'true'){ 196 $listrequest = preg_replace('/@/','-request@',$list->listAddress); 197 array_unshift($grps,$listrequest); 198 } 199 if ($list->isEditor == 'true') { 200 $listeditor = preg_replace('/@/','-editor@',$list->listAddress); 201 array_unshift($grps,$listeditor); 202 } 203 } 204 } 205 } 206 } 207 } 208 // Collect the DokuWiki's data with the Sympa's data (if we are in Sympa context) 209 $users = $this->setUserGroups($mail,$grps); 210 } 211 // set the globals if authed 212 $USERINFO['grps'] = $users['grps']; 213 $_SERVER['REMOTE_USER'] = $user; 214 $_SESSION[DOKU_COOKIE]['auth']['user'] = $user; 215 $_SESSION[DOKU_COOKIE]['auth']['pass'] = $user; 216 $_SESSION[DOKU_COOKIE]['auth']['info'] = $USERINFO; 217 $_SESSION['DW_SHIB']['user']= $user; 218 $_SESSION['DW_SHIB']['pass']= $user; 219 $_SESSION['DW_SHIB']['info'] = $USERINFO; 220 error_log('shibbolethauth : authenticated user'); 221 return true; 222 } 223 // if this attribute is not set, we log off the system 224 }else{ 225 if($_REQUEST['do'] == 'login'){ 226 error_log('shibbolethauth: redirect user for login to '.$conf['plugin']['shibbolethauth']['shibbolethLoginURL']); 227 header("Location: ".$conf['plugin']['shibbolethauth']['shibbolethLoginURL']."?target=".urlencode($_SERVER['HTTP_REFERER'])); 228 exit; 229 }else{ 230 error_log('shibbolethauth: no email address to log'); 231 unset($_SESSION[DOKU_COOKIE]['auth']['user']); 232 unset($_SESSION[DOKU_COOKIE]['auth']['pass']); 233 unset($_SESSION[DOKU_COOKIE]['auth']['info']); 234 return false; 235 } 236 } 237 } 238 } 239 240 241 /** 242 * Checks if the Email attibute of Shibboleth is correctly set 243 * 244 * @author Yoann Lecuyer <yoann.lecuyer AT cru.fr> 245 * 246 **/ 247 248 function checkPass($user,$pass){ 249 } 250 251 /** 252 * Return user info 253 * 254 * Accessing to sympa is done through the nusoap library 255 * for the autorization part 256 * 257 * Reading the user's group on DokuWiki's files 258 * for the user manager part 259 * 260 * The returned field are : 261 * 262 * name string full name of the user 263 * mail string email addres of the user 264 * groups array list of groups the user is in 265 * 266 * @author Yoann Lecuyer <yoann.lecuyer AT cru.fr> 267 * 268 **/ 269 270 function getUserData( $user ) { 271 $userData = false; 272 $userData['name'] = $user; 273 $userData['mail'] = $user; 274 $userData['grps'] = $this->getUserGroups($user); 275 return $userData; 276 } 277 278 /** 279 * Return user groups 280 * 281 **/ 282 283 function getUserGroups($user) { 284 global $conf; 285 $mail=$user; 286 // load the groups of users contained on the file users.auth.php 287 if($this->users === null) $this->_loadUserData(); 288 // groups the person belongs to 289 $grps = array(); 290 // Test if $user is an email or not. If it is not, it is an user id 291 if(strpbrk($user, '@') != false){ 292 // We are in a Shibboteth context, and we get the datas for the autorization 293 // Test if we use the autorisation with Sympa server 294 $sympaIsOK = true; 295 if(isset($conf['plugin']['shibbolethauth']['useSympa']) && $conf['plugin']['shibbolethauth']['useSympa'] == 1){ 296 // Get the groups list on the Sympa server 297 //create soap client 298 try{ 299 $this->soap_client = new SoapClient($conf['plugin']['shibbolethauth']['sympaSoapService']); 300 }catch(SoapFault $fault){ 301 $sympaIsOK = false; 302 error_log("shibbolethauth : unable to establish a SOAP connection with ".$conf['plugin']['shibbolethauth']['sympaSoapService']); 303 } 304 // get the groups list 305 if($sympaIsOK){ 306 try{ 307 $res = $this->soap_client->authenticateRemoteAppAndRun($conf['plugin']['shibbolethauth']['sympaApplicationId'],$conf['plugin']['shibbolethauth']['sympaApplicationPwd'], 'USER_EMAIL='.$user,'complexWhich'); 308 }catch(SoapFault $fault){ 309 $sympaIsOK = false; 310 msg("dokuwiki groups only",0); 311 error_log("shibbolethauth : unable to authenticate on ".$conf['plugin']['shibbolethauth']['sympaSoapService']); 312 } 313 if($sympaIsOK){ 314 if (isset($res) && gettype($res) == 'array') { 315 foreach ($res as $list) { 316 if (empty($grps[0])) { 317 $grps[0] = $list->listAddress; 318 }else{ 319 array_unshift($grps,$list->listAddress); 320 } 321 if($list->isOwner == 'true'){ 322 $listrequest = preg_replace('/@/','-request@',$list->listAddress); 323 array_unshift($grps,$listrequest); 324 } 325 if ($list->isEditor == 'true') { 326 $listeditor = preg_replace('/@/','-editor@',$list->listAddress); 327 array_unshift($grps,$listeditor); 328 } 329 } 330 } 331 } 332 } 333 } 334 335 // Collect the DokuWiki's data with the Sympa's data (if we are in Sympa context) 336 $users = $this->setUserGroups($mail,$grps); 337 } 338 339 340 // set the globals if authed 341 return $users['grps']; 342 } 343 344 345 /** 346 * Set the DokuWiki's groups and the Sympa's groups together 347 * 348 * @author Yoann Lecuyer <yoann.lecuyer AT cru.fr> 349 * 350 **/ 351 352 function setUserGroups($mail,$grps){ 353 foreach ($this->users as $user){ 354 if($user['mail'] == $mail){ 355 // Add or set the groups of the user 356 if(!empty($grps[0])){ 357 foreach($grps as $group){ 358 array_unshift($user['grps'],$group); 359 } 360 } 361 // Verify if the table contains the sympaDefaultGroup value 362 $defaultgroupset = false; 363 foreach ($user['grps'] as $group){ 364 if($group == $this->sympaDefaultGroup) $defaultgroupset = true; 365 } 366 // Put the sympaDefaultGroup value if it is not on the table 367 if(!$defaultgroupset) array_unshift($user['grps'],$this->sympaDefaultGroup); 368 return $user; 369 } 370 } 371 // the mail is not on the table of DokuWiki's users, we create an other user 372 373 // Verify if the table contains the sympaDefaultGroup value 374 $defaultgrpset = false; 375 foreach ($grps as $grp){ 376 if($grp == $this->sympaDefaultGroup) $defaultgrpset = true; 377 } 378 // Put the sympaDefaultGroup value if it is not on the table 379 if(!$defaultgrpset) array_unshift($grps,$this->sympaDefaultGroup); 380 381 $this->users[$mail]['name'] = $mail; 382 $this->users[$mail]['mail'] = $mail; 383 $this->users[$mail]['grps'] = $grps; 384 return $this->users[$mail]; 385 } 386 387 /** 388 * Logoff user : 389 * 390 * everything that was about the session is unset 391 * 392 * @author Yoann Lecuyer <yoann.lecuyer AT cru.fr> 393 * 394 **/ 395 396 function logOff() { 397 if(isset($_SESSION[DOKU_COOKIE]['auth']['info'])) { 398 unset($_SESSION[DOKU_COOKIE]['auth']['info']); 399 } 400 unset($this->soap_client); 401 unset($_REQUEST['u']); 402 return; 403 } 404 405 function server() { 406 return($this->sympaSoapService); 407 } 408 409 /** 410 * Create a new User 411 * 412 * Returns false if the user already exists, null when an error 413 * occured and true if everything went well. 414 * 415 * The new user will be added to the default group by this 416 * function if grps are not specified (default behaviour). 417 * 418 * @author Andreas Gohr <andi@splitbrain.org> 419 * @author Chris Smith <chris@jalakai.co.uk> 420 */ 421 function createUser($user,$pwd,$name,$mail,$grps=null){ 422 global $conf; 423 424 // user mustn't already exist 425 if ($this->getUserData($user) !== false) return false; 426 427 $pass = auth_cryptPassword($pwd); 428 $name = $user; 429 // set default group if no groups specified 430 if (!is_array($grps)) $grps = array($conf['defaultgroup']); 431 432 // prepare user line 433 $groups = join(',',$grps); 434 $userline = join(':',array($user,$pass,$name,$mail,$groups))."\n"; 435 436 if (io_saveFile(AUTH_USERFILE,$userline,true)) { 437 $this->users[$user] = compact('pass','name','mail','grps'); 438 return $pass; 439 } 440 441 msg('The '.AUTH_USERFILE.' file is not writable. Please inform the Wiki-Admin',-1); 442 return null; 443 } 444 445 /** 446 * Modify user data 447 * 448 * @author Chris Smith <chris@jalakai.co.uk> 449 * @param $user nick of the user to be changed 450 * @param $changes array of field/value pairs to be changed (password will be clear text) 451 * @return bool 452 */ 453 454 function modifyUser($user, $changes) { 455 global $conf; 456 global $ACT; 457 global $INFO; 458 459 // sanity checks, user must already exist and there must be something to change 460 if (($userinfo = $this->getUserData($user)) === false) return false; 461 if (!is_array($changes) || !count($changes)) return true; 462 463 // update userinfo with new data, remembering to encrypt any password 464 $newuser = $user; 465 foreach ($changes as $field => $value) { 466 if ($field == 'user') { 467 $newuser = $value; 468 continue; 469 } 470 if ($field == 'pass') $value = auth_cryptPassword($value); 471 $userinfo[$field] = $value; 472 } 473 474 $groups = join(',',$userinfo['grps']); 475 $userline = join(':',array($newuser, $userinfo['pass'], $userinfo['name'], $userinfo['mail'], $groups))."\n"; 476 477 if (!$this->deleteUsers(array($user))) { 478 msg('Unable to modify user data. Please inform the Wiki-Admin',-1); 479 return false; 480 } 481 482 if (!io_saveFile(AUTH_USERFILE,$userline,true)) { 483 msg('There was an error modifying your user data. You should register again.',-1); 484 // FIXME, user has been deleted but not recreated, should force a logout and redirect to login page 485 $ACT == 'register'; 486 return false; 487 } 488 489 $this->users[$newuser] = $userinfo; 490 return true; 491 } 492 493 /** 494 * Remove one or more users from the list of registered users 495 * 496 * @author Christopher Smith <chris@jalakai.co.uk> 497 * @param array $users array of users to be deleted 498 * @return int the number of users deleted 499 */ 500 501 function deleteUsers($users) { 502 503 if (!is_array($users) || empty($users)) return 0; 504 505 if ($this->users === null) $this->_loadUserData(); 506 507 $deleted = array(); 508 foreach ($users as $user) { 509 if (isset($this->users[$user])) $deleted[] = preg_quote($user,'/'); 510 } 511 512 if (empty($deleted)) return 0; 513 514 $pattern = '/^('.join('|',$deleted).'):/'; 515 516 if (io_deleteFromFile(AUTH_USERFILE,$pattern,true)) { 517 foreach ($deleted as $user) unset($this->users[$user]); 518 return count($deleted); 519 } 520 521 // problem deleting, reload the user list and count the difference 522 $count = count($this->users()); 523 $this->_loadUserData(); 524 $count -= $count($this->users()); 525 return $count; 526 } 527 528 /** 529 * Return a count of the number of user which meet $filter criteria 530 * 531 * @author Chris Smith <chris@jalakai.co.uk> 532 */ 533 534 function getUserCount($filter=array()) { 535 536 if($this->users === null) $this->_loadUserData(); 537 538 if (!count($filter)) return count($this->users); 539 540 $count = 0; 541 $this->_constructPattern($filter); 542 543 foreach ($this->users as $user => $info) { 544 $count += $this->_filter($user, $info); 545 } 546 547 return $count; 548 } 549 550 /** 551 * Bulk retrieval of user data 552 * 553 * @author Chris Smith <chris@jalakai.co.uk> 554 * @param start index of first user to be returned 555 * @param limit max number of users to be returned 556 * @param filter array of field/pattern pairs 557 * @return array of userinfo (refer getUserData for internal userinfo details) 558 */ 559 function retrieveUsers($start=0,$limit=0,$filter=array()) { 560 561 if ($this->users === null) $this->_loadUserData(); 562 563 ksort($this->users); 564 565 $i = 0; 566 $count = 0; 567 $out = array(); 568 $this->_constructPattern($filter); 569 570 foreach ($this->users as $user => $info) { 571 if ($this->_filter($user, $info)) { 572 if ($i >= $start) { 573 $out[$user] = $info; 574 $count++; 575 if (($limit > 0) && ($count >= $limit)) break; 576 } 577 $i++; 578 } 579 } 580 581 return $out; 582 } 583 584 /** 585 * Load all user data 586 * 587 * loads the user file into a datastructure 588 * 589 * @author Andreas Gohr <andi@splitbrain.org> 590 */ 591 592 function _loadUserData(){ 593 $this->users = array(); 594 595 if(!@file_exists(AUTH_USERFILE)) return; 596 597 $lines = file(AUTH_USERFILE); 598 foreach($lines as $line){ 599 $line = preg_replace('/#.*$/','',$line); //ignore comments 600 $line = trim($line); 601 if(empty($line)) continue; 602 603 $row = split(":",$line,5); 604 $groups = split(",",$row[4]); 605 $this->users[$row[0]]['pass'] = $row[1]; 606 $this->users[$row[0]]['name'] = urldecode($row[2]); 607 $this->users[$row[0]]['mail'] = $row[3]; 608 $this->users[$row[0]]['grps'] = $groups; 609 } 610 } 611 612 /** 613 * return 1 if $user + $info match $filter criteria, 0 otherwise 614 * 615 * @author Chris Smith <chris@jalakai.co.uk> 616 */ 617 618 function _filter($user, $info) { 619 // FIXME 620 foreach ($this->_pattern as $item => $pattern) { 621 if ($item == 'user') { 622 if (!preg_match($pattern, $user)) return 0; 623 } else if ($item == 'grps') { 624 if (!count(preg_grep($pattern, $info['grps']))) return 0; 625 } else { 626 if (!preg_match($pattern, $info[$item])) return 0; 627 } 628 } 629 return 1; 630 } 631 632 function _constructPattern($filter) { 633 $this->_pattern = array(); 634 foreach ($filter as $item => $pattern) { 635// $this->_pattern[$item] = '/'.preg_quote($pattern,"/").'/'; // don't allow regex characters 636 $this->_pattern[$item] = '/'.str_replace('/','\/',$pattern).'/'; // allow regex characters 637 } 638 } 639} 640