1<?php 2/** 3 * DokuWiki Plugin authsplit (Auth Component) 4 * 5 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 6 * @author Pieter Hollants <pieter@hollants.com> 7 */ 8 9// must be run within Dokuwiki 10if(!defined('DOKU_INC')) die(); 11 12class auth_plugin_authsplit extends DokuWiki_Auth_Plugin { 13 protected $authplugins; 14 protected $autocreate_users; 15 protected $debug; 16 17 /** 18 * Show a debug message 19 * 20 * @param string $message The message to show 21 * @param int $err -1 for error, 0 for info, 1 for success 22 * @param int $line The line in $file that triggered the message. 23 * @param string $file The filename that triggered the message. 24 * @return void 25 */ 26 protected function _debug($message, $err, $line, $file) { 27 if (!$this->debug) 28 return; 29 msg($message, $err, $line, $file); 30 } 31 32 /** 33 * Constructor. 34 */ 35 public function __construct() { 36 parent::__construct(); // for compatibility 37 38 /* Load the config earlier than usual (we need it below) */ 39 $this->loadConfig(); 40 41 /* Load all referenced auth plugins */ 42 foreach (array('primary', 'secondary') as $type) { 43 $settingName = $type.'_authplugin'; 44 $pluginName = $this->getConf($settingName); 45 if (!$pluginName) { 46 msg(sprintf($this->getLang('nocfg'), $settingName), -1); 47 $this->success = false; 48 return; 49 } 50 if ($pluginName != 'None') { 51 $this->authplugins[$type] = plugin_load('auth', $pluginName); 52 if (!$this->authplugins[$type]) { 53 msg(sprintf($this->getLang('pluginload'), $pluginName), -1); 54 $this->success = false; 55 return; 56 } 57 } else { 58 $this->authplugins[$type] = null; 59 } 60 } 61 62 /* Create users automatically? */ 63 $this->autocreate_users = $this->getConf('autocreate_users', null); 64 if ($this->autocreate_users === null) { 65 msg(sprintf($this->getLang('nocfg'), 'autocreate_users'), -1); 66 $this->success = false; 67 return; 68 } 69 70 /* Convert username's case? */ 71 $this->username_caseconversion = $this->getConf('username_caseconversion', null); 72 if ($this->username_caseconversion === null) { 73 msg(sprintf($this->getLang('nocfg'), 'username_caseconversion'), -1); 74 $this->success = false; 75 return; 76 } 77 78 /* Show debug messages? */ 79 $this->debug = $this->getConf('debug', null); 80 if ($this->debug === null) { 81 msg(sprintf($this->getLang('nocfg'), 'debug'), -1); 82 $this->success = false; 83 return; 84 } 85 86 /* Of course, to modify login names actually BOTH auth plugins must 87 support that. However, at this place we just consider the secondary 88 auth plugin as otherwise admins can not add user accounts there in 89 advance. */ 90 $this->cando['modLogin'] = $this->authplugins['secondary']->canDo('modLogin'); 91 92 /* To modify passwords, the primary auth plugin must support it */ 93 $this->cando['modPass'] = $this->authplugins['primary']->canDo('modPass'); 94 95 /* To add and delete user accounts, modify real names, email addresses, 96 group memberships and groups, it is sufficient for the secondary 97 auth plugin to support it. */ 98 foreach (array('addUser', 'delUser', 'modName', 'modMail', 'modGroups', 99 'getUsers', 'getUserCount', 'getGroups') as $cap) { 100 $this->cando[$cap] = $this->authplugins['secondary']->canDo($cap); 101 } 102 103 /* The primary auth plugin determines whether we do external 104 authentication (eg. against a third party cookie). */ 105 $this->cando['external'] = $this->authplugins['primary']->cando['external']; 106 107 /* Whether we can do logout or not depends on the primary auth plugin */ 108 $this->cando['logout'] = $this->authplugins['primary']->canDo('logout'); 109 110 $msg = 'authsplit:__construct(): '. 111 $this->authplugins['primary']->getPluginName().'/'. 112 $this->authplugins['secondary']->getPluginName().' '. 113 'combination can '; 114 $parts = array( 115 'addUser' => 'add users', 116 'delUser' => 'delete users', 117 'modLogin' => 'modify login names', 118 'modPass' => 'modify passwords', 119 'modName' => 'modify real names', 120 'modMail' => 'modify E-Mail addresses', 121 'modGroups' => 'modify groups', 122 'getUsers' => 'get user list', 123 'getUserCount' => 'get user counts', 124 'getGroups' => 'get groups', 125 'logout' => 'logout users', 126 ); 127 foreach ($this->cando as $key => $value) { 128 if ($this->cando[$key]) 129 $msg .= $parts[$key].', '; 130 } 131 $msg = rtrim($msg, ', ').'.'; 132 $this->_debug($msg, 1, __LINE__, __FILE__); 133 } 134 135 /** 136 * Authenticate user (DokuWiki-style user/password authentication) 137 * 138 * This method is called if DokuWiki's internal authentication system is 139 * used (primary auth plugin's canDo['external'] == false) and the user 140 * supplied a username and a password. 141 * 142 * @param string $user the user name 143 * @param string $pass the clear text password 144 * @return bool 145 */ 146 public function checkPass($user, $pass) { 147 /* First validate the username and password with the primary plugin. */ 148 if (!$this->authplugins['primary']->checkPass($user, $pass)) { 149 $this->_debug( 150 'authsplit:checkPass(): primary auth plugin\'s checkPass() '. 151 'failed', -1, __LINE__, __FILE__ 152 ); 153 return false; 154 } 155 $this->_debug( 156 'authsplit:checkPass(): primary auth plugin authenticated the '. 157 'user successfully.', 1, __LINE__, __FILE__ 158 ); 159 160 /* Then make sure that the secondary auth plugin also knows about the 161 user. */ 162 return $this->_checkUserOnSecondaryAuthPlugin($user); 163 } 164 165 /** 166 * Authenticate user (external authentication) 167 * 168 * This method is called on every page load if external authentication is 169 * used (primary auth plugin's canDo['external'] == true). 170 * 171 * @param string $user The user name (may be empty) 172 * @param string $pass The clear text password (may be empty) 173 * @param bool $sticky Whether the cookie should not expire 174 * @return bool 175 */ 176 public function trustExternal($user, $pass, $sticky = false) { 177 global $USERINFO; 178 179 /* First delegate to the primary plugin's trustExternal() to 180 validate the user (by means of a password, a cookie or whatever). */ 181 if (!$this->authplugins['primary']->trustExternal($user, $pass, $sticky)) { 182 $this->_debug( 183 'authsplit:trustExternal(): primary auth plugin\'s ' . 184 'trustExternal() failed', -1, __LINE__, __FILE__ 185 ); 186 return false; 187 } 188 189 /* As $user may be empty (eg. when authentication is done via a cookie) 190 we set it from the server environment */ 191 $user = $_SERVER['REMOTE_USER']; 192 $this->_debug( 193 'authsplit:trustExternal(): derived user name: ' . $user, 194 -1, __LINE__, __FILE__ 195 ); 196 197 /* Then make sure the secondary auth plugin also knows about the 198 user. */ 199 if ($this->_checkUserOnSecondaryAuthPlugin($user)) { 200 /* With external authentication, $USERINFO must be set on every 201 page load. */ 202 $USERINFO = $this->getUserData($user, true); 203 return true; 204 } 205 return false; 206 } 207 208 /** 209 * Ensures that the user is known to the secondary auth plugin as well. 210 * 211 * @param string $user the user name 212 * @return bool 213 */ 214 public function _checkUserOnSecondaryAuthPlugin($user) { 215 /* User already known? */ 216 $userinfo = $this->authplugins['secondary']->getUserData($user, false); 217 if ($userinfo) 218 return true; 219 220 $this->_debug( 221 'authsplit:'.__FUNCTION__.'(): secondary auth plugin\'s ' . 222 'getUserData() failed, seems user is yet unknown there.', -1, 223 __LINE__, __FILE__ 224 ); 225 226 $this->_debug( 227 'authsplit:'.__FUNCTION__.'(): autocreate_users is set to '. 228 $this->autocreate_users.'.', 229 $this->autocreate_users == 1 ? 1 : -1, 230 __LINE__, __FILE__ 231 ); 232 233 /* Make sure automatic user creation is enabled */ 234 if (!$this->autocreate_users) 235 return false; 236 237 /* Make sure the secondary auth plugin can create user accounts */ 238 if (!$this->authplugins['secondary']->cando['addUser']) { 239 msg( 240 sprintf( 241 $this->getLang('erraddusercap'), 242 $this->authplugins['secondary']->getPluginName() 243 ), 244 -1 245 ); 246 return false; 247 } 248 249 /* Since auth plugins by definition must have a getUserData() 250 method, we use the primary auth plugin's data to create a user 251 account in the secondary auth plugin. */ 252 $params = $this->authplugins['primary']->getUserData($user, true); 253 if (!$params) { 254 msg( 255 sprintf( 256 $this->getLang('erradduserinfo'), 257 $this->authplugins['primary']->getPluginName() 258 ), 259 -1 260 ); 261 return false; 262 } 263 $this->_debug( 264 'authsplit:'.__FUNCTION__.'(): primary auth plugin\'s ' . 265 'getUserData(): '.$this->_dumpUserData($params).'.', 266 1, __LINE__, __FILE__ 267 ); 268 269 /* Create the new user account */ 270 $result = $this->triggerUserMod( 271 'create', 272 array( 273 $user, $pass, 274 $params['name'], $params['mail'], $params['grps'] 275 ) 276 ); 277 if ($result === false || $result === null) 278 { 279 $this->_debug( 280 'authsplit:'.__FUNCTION__.'(): primary auth plugin\'s '. 281 'getUserData() could not supply data.', -1, 282 __LINE__, __FILE__ 283 ); 284 return false; 285 } 286 if(actionOK('profile')) 287 { 288 msg($this->getLang('autocreated'), -1); 289 } 290 291 return true; 292 } 293 294 /** 295 * Logoff the user (useful for external authentication) 296 * 297 * @param string $user the user name 298 * @param string $pass the clear text password 299 * @return bool 300 */ 301 public function logOff() { 302 return $this->authplugins['primary']->logOff(); 303 } 304 305 /** 306 * Log the user In (useful for external authentication) 307 * 308 * @return bool 309 */ 310 public function logIn() { 311 return $this->authplugins['primary']->logIn(); 312 } 313 314 /** 315 * Return user info 316 * 317 * Returned info about the given user needs to contain 318 * at least these fields: 319 * 320 * name string full name of the user 321 * mail string email address of the user 322 * grps array list of groups the user is in 323 * 324 * @param string $user the user name 325 * @param bool $requireGroups whether to return user's groups as well 326 * @return array containing user data or false 327 */ 328 public function getUserData($user, $requireGroups = true) { 329 /* A user must be present in BOTH auth plugins. */ 330 $userinfo = $this->authplugins['primary']->getUserData($user, false); 331 if (!$userinfo) { 332 $this->_debug( 333 'authsplit:checkPass(): primary auth plugin\'s getUserData() '. 334 'failed, seems user is yet unknown there.', 1, 335 __LINE__, __FILE__ 336 ); 337 return false; 338 } 339 340 $userinfo = $this->authplugins['secondary']->getUserData($user, $requireGroups); 341 if (!$userinfo) { 342 $this->_debug( 343 'authsplit:checkPass(): secondary auth plugin\'s getUserData() '. 344 'failed, seems user is yet unknown there.', 1, 345 __LINE__, __FILE__ 346 ); 347 return false; 348 } 349 $this->_debug( 350 'authsplit:getUserData(): secondary auth plugin\'s getUserData(): '. 351 $this->_dumpUserData($userinfo).'.', 1, __LINE__, __FILE__ 352 ); 353 354 return $userinfo; 355 } 356 357 /** 358 * Returns a string representation of user data for debugging purposes 359 * 360 * @param array $user An array with user data 361 * @return string 362 */ 363 protected function _dumpUserData($user) { 364 $msg = 'Name: "'.$user['name'].'", '. 365 'Mail: "'.$user['mail'].'", ' . 366 'Groups: '; 367 foreach ($user['grps'] as $grp) { 368 $msg .= '"'.$grp.'", '; 369 } 370 $msg = rtrim($msg, ', '); 371 return $msg; 372 } 373 374 /** 375 * Create a new User 376 * 377 * Returns false if the user already exists, null when an error 378 * occurred and true if everything went well. 379 * 380 * The new user HAS TO be added to the default group by this 381 * function! 382 * 383 * Set addUser capability when implemented 384 * 385 * @param string $user 386 * @param string $pass 387 * @param string $name 388 * @param string $mail 389 * @param null|array $grps 390 * @return bool|null 391 */ 392 public function createUser($user, $pass, $name, $mail, $grps = null) { 393 /* Does the user not exist yet in the primary auth plugin and does it 394 support creating users? */ 395 $userinfo = $this->authplugins['primary']->getUserData($user, false); 396 if (!$userinfo && $this->authplugins['primary']->cando['addUser']) { 397 $result = $this->authplugins['primary']->createUser( 398 $user, $pass, $name, $email 399 ); 400 if ($result === false || $result === null) { 401 $this->_debug( 402 'authsplit:createUser(): primary auth plugin\'s '. 403 'createUser() failed.', -1, __LINE__, __FILE__ 404 ); 405 return $result; 406 } 407 $this->_debug( 408 'authsplit:createUser(): user created in primary auth plugin.', 409 1, __LINE__, __FILE__ 410 ); 411 } 412 413 /* We need to create the user in the secondary auth plugin in any case. */ 414 $result = $this->authplugins['secondary']->createUser( 415 $user, '', $name, $mail, $grps 416 ); 417 if ($result === false || $result === null) { 418 $this->_debug( 419 'authsplit:createUser(): secondary auth plugin\'s '. 420 'createUser() failed.', -1, __LINE__, __FILE__ 421 ); 422 return $result; 423 } 424 425 $this->_debug( 426 'authsplit:createUser(): user created in secondary auth plugin.', 427 1, __LINE__, __FILE__ 428 ); 429 430 return true; 431 } 432 433 /** 434 * Modify user data 435 * 436 * Set the mod* capabilities according to the implemented features 437 * 438 * @param string $user nick of the user to be changed 439 * @param array $changes array of field/value pairs to be changed 440 * (password will be clear text) 441 * @return bool 442 */ 443 public function modifyUser($user, $changes) { 444 if (!is_array($changes) || !count($changes)) 445 return true; // nothing to change 446 447 foreach ($changes as $field => $value) { 448 if ($field == 'pass') { 449 /* Passwords must be changed in the primary auth plugin */ 450 $result = $this->authplugins['primary']->modifyUser( 451 $user, 452 array( 453 'pass' => $value 454 ) 455 ); 456 if (!$result) { 457 $this->_debug( 458 'authsplit:modifyUser(): primary auth plugin\'s '. 459 'modifyUser() failed.', -1, __LINE__, __FILE__ 460 ); 461 return false; 462 } 463 } 464 elseif ($field == 'grps') { 465 /* Groups are handled by the secondary auth plugin. */ 466 $result = $this->authplugins['secondary']->modifyUser( 467 $user, 468 array( 469 'grps' => $value 470 ) 471 ); 472 if (!$result) { 473 $this->_debug( 474 'authsplit:modifyUser(): secondary auth plugin\'s '. 475 'modifyUser() failed.', -1, __LINE__, __FILE__ 476 ); 477 return false; 478 } 479 } 480 elseif ( ($field == 'login') || ($field == 'name') || ($field == 'mail') ) { 481 /* If the primary auth plugin supports the update, 482 we'll try it there first. */ 483 if ($this->authplugins['primary']->canDo['mod' . ucfirst($field)]) { 484 $result = $this->authplugins['primary']->modifyUser( 485 $user, array( 486 $field => $value 487 ) 488 ); 489 if (!$result) { 490 $this->_debug( 491 'authsplit:modifyUser(): primary auth plugin\'s '. 492 'modifyUser() failed.', -1, __LINE__, __FILE__ 493 ); 494 return false; 495 } 496 } 497 498 /* Now in the secondary auth plugin. */ 499 $result = $this->authplugins['secondary']->modifyUser( 500 $user, 501 array( 502 $field => $value 503 ) 504 ); 505 if (!$result) { 506 $this->_debug( 507 'authsplit:modifyUser(): secondary auth plugin\'s '. 508 'modifyUser() failed.', -1, __LINE__, __FILE__ 509 ); 510 return false; 511 } 512 } 513 } 514 515 return true; 516 } 517 518 /** 519 * Delete one or more users 520 * 521 * Set delUser capability when implemented 522 * 523 * @param array $users 524 * @return int number of users deleted 525 */ 526 public function deleteUsers($users) { 527 /* We do NOT attempt to delete the user with the primary auth plugin. 528 Just because we don't want the account in DokuWiki any more, does not 529 mean that there are no other services that depend on the account's 530 existance in the primary auth source. */ 531 $result = $this->authplugins['secondary']->deleteUsers($users); 532 if (!$result) { 533 $this->_debug( 534 'authsplit:deleteUsers(): secondary auth plugin\'s '. 535 'deleteUsers() failed.', -1, __LINE__, __FILE__ 536 ); 537 } 538 return $result; 539 } 540 541 /** 542 * Bulk retrieval of user data 543 * 544 * Set getUsers capability when implemented 545 * 546 * @param int $start index of first user to be returned 547 * @param int $limit max number of users to be returned 548 * @param array $filter array of field/pattern pairs, null for no filter 549 * @return array list of userinfo (refer to getuseraccts for internal 550 * userinfo details) 551 */ 552 public function retrieveUsers($start = 0, $limit = -1, $filter = null) { 553 /* We're always interested in the users defined in the secondary auth 554 plugin. */ 555 $result = $this->authplugins['secondary']->retrieveUsers( 556 $start, $limit, $filter 557 ); 558 if (!$result) { 559 $this->_debug( 560 'authsplit:retrieveUsers(): secondary auth plugin\'s '. 561 'retrieveUsers() failed.', -1, __LINE__, __FILE__ 562 ); 563 } 564 return $result; 565 } 566 567 /** 568 * Return a count of the number of user which meet $filter criteria 569 * 570 * Set getUserCount capability when implemented 571 * 572 * @param array $filter array of field/pattern pairs, empty array for no filter 573 * @return int 574 */ 575 public function getUserCount($filter = array()) { 576 /* We're always interested in the users defined in the secondary auth plugin. */ 577 $result = $this->authplugins['secondary']->getUserCount($filter); 578 if (!$result) { 579 $this->_debug( 580 'authsplit:getUserCount(): secondary auth plugin\'s '. 581 'getUserCount() failed.', -1, __LINE__, __FILE__ 582 ); 583 } 584 return $result; 585 } 586 587 /** 588 * Define a group 589 * 590 * Set addGroup capability when implemented 591 * 592 * @param string $group 593 * @return bool 594 */ 595 public function addGroup($group) { 596 /* Groups are always defined in the secondary auth plugin. */ 597 $result = $this->authplugins['secondary']->addGroup($group); 598 if (!$result) { 599 $this->_debug( 600 'authsplit:addGroup(): secondary auth plugin\'s addGroup() '. 601 'failed.', -1, __LINE__, __FILE__ 602 ); 603 } 604 return $result; 605 } 606 607 /** 608 * Retrieve groups 609 * 610 * Set getGroups capability when implemented 611 * 612 * @param int $start 613 * @param int $limit 614 * @return array 615 */ 616 public function retrieveGroups($start = 0, $limit = 0) { 617 /* Groups are always defined in the secondary auth plugin. */ 618 $result = $this->authplugins['secondary']->retrieveGroups($start, $limit); 619 if (!$result) { 620 $this->_debug( 621 'authsplit:retrieveGroups(): secondary auth plugin\'s '. 622 'retrieveGroups() failed.', -1, __LINE__, __FILE__ 623 ); 624 } 625 return $result; 626 } 627 628 /** 629 * Return case sensitivity of the backend 630 * 631 * When your backend is caseinsensitive (eg. you can login with USER and 632 * user) then you need to overwrite this method and return false 633 * 634 * @return bool 635 */ 636 public function isCaseSensitive() { 637 /* The primary auth plugin dictates case-sensitivity of login names. */ 638 $result = $this->authplugins['primary']->isCaseSensitive(); 639 $status = $result ? "Yes" : "No"; 640 $this->_debug( 641 'authsplit:isCaseSensitive(): primary auth plugin\'s '. 642 'isCaseSensitive(): '.$status.'.', 1, __LINE__, __FILE__ 643 ); 644 return $result; 645 } 646 647 /** 648 * Sanitize a given username 649 * 650 * This function is applied to any user name that is given to 651 * the backend and should also be applied to any user name within 652 * the backend before returning it somewhere. 653 * 654 * This should be used to enforce username restrictions. 655 * 656 * @param string $user username 657 * @return string the cleaned username 658 */ 659 public function cleanUser($user) { 660 /* The primary auth plugin dictates possible login name restrictions. */ 661 $result = $this->authplugins['primary']->cleanUser($user); 662 $this->_debug( 663 'authsplit:cleanUser(): primary auth plugin\'s '. 664 'cleanUser("'.$user.'"): "'.$result.'".', 1, __LINE__, __FILE__ 665 ); 666 667 /* Apply case conversion? */ 668 if ($this->username_caseconversion == 'To uppercase') { 669 $result = strtoupper($result); 670 $this->_debug( 671 'authsplit:cleanUser(): converted username to uppercase: '. 672 $result, 1, __LINE__, __FILE__ 673 ); 674 } 675 elseif ($this->username_caseconversion == 'To lowercase') { 676 $result = strtolower($result); 677 $this->_debug( 678 'authsplit:cleanUser(): converted username to lowercase: '. 679 $result, 1, __LINE__, __FILE__ 680 ); 681 } 682 683 return $result; 684 } 685 686 /** 687 * Sanitize a given groupname 688 * 689 * This function is applied to any groupname that is given to 690 * the backend and should also be applied to any groupname within 691 * the backend before returning it somewhere. 692 * 693 * This should be used to enforce groupname restrictions. 694 * 695 * Groupnames are to be passed without a leading '@' here. 696 * 697 * @param string $group groupname 698 * @return string the cleaned groupname 699 */ 700 public function cleanGroup($group) { 701 /* The secondary auth plugin dictates possible group names 702 restrictions. */ 703 $result = $this->authplugins['secondary']->cleanGroup($group); 704 $this->_debug( 705 'authsplit:cleanGroup(): secondary auth plugin\'s '. 706 'cleanGroup("'.$group.'"): "'.$result.'".', 1, __LINE__, __FILE__ 707 ); 708 return $result; 709 } 710} 711 712// vim:ts=4:sw=4:et: 713