* @author Olivier Salaun * @author Yoann Lecuyer * Version: 1.2 * last modified: 2009-11-27 * * Work based on : * ssl authentication backend: * @author Dominique Launay * Sympa Soap server authentication backend: * @author David Pepin * * The nusoap library is required * **/ define('DOKU_AUTH', dirname(__FILE__)); require_once(DOKU_AUTH.'/basic.class.php'); define('AUTH_USERFILE',DOKU_CONF.'users.auth.php'); class auth_shibboleth extends auth_basic { var $users = null; var $sympaDefaultGroup = null; var $sympaSoapService = null; var $soap_client = null; var $log_file = null; /** * * Constructor * * @author Yoann Lecuyer * **/ // This function is run for every web access to the wiki // Most of the Shibboleth authentication code is run here because we never run the do_login action // With Shibboleth, dokuwiki never collects user password function auth_shibboleth() { global $conf; //msg('auth_shibboleth'); $this->cando['external'] = true; //$this->cando['getGroups'] = true; if (method_exists($this, 'auth_basic')){ parent::auth_basic(); } // check if the server configuration has correctly been done if (!isset($conf['plugin']['shibbolethauth']['shibbolethEmailAttribute']) || empty($conf['plugin']['shibbolethauth']['shibbolethEmailAttribute'])) { $this->success = false; return; } if (isset($conf['plugin']['shibbolethauth']['useSympa']) && $conf['plugin']['shibbolethauth']['useSympa'] == 1 && empty($conf['plugin']['shibbolethauth']['sympaSoapService'])) { $this->success = false; return; } if(!isset($conf['plugin']['shibbolethauth']['shibbolethLoginURL']) || empty($conf['plugin']['shibbolethauth']['shibbolethLoginURL'])) { $this->success = false; return; } if(!isset($conf['plugin']['shibbolethauth']['shibbolethLogoutURL']) || empty($conf['plugin']['shibbolethauth']['shibbolethLogoutURL'])) { $this->success = false; return; } if (isset($conf['defaultgroup'])) { $this->sympaDefaultGroup = $conf['defaultgroup']; } $this->success = true; return; } /** * Do all authentication [ OPTIONAL ] * * Set $this->cando['external'] = true when implemented * * If this function is implemented it will be used to * authenticate a user - all other DokuWiki internals * will not be used for authenticating, thus * implementing the functions below becomes optional. * * The function can be used to authenticate against third * party cookies or Apache auth mechanisms and replaces * the auth_login() function * * The function will be called with or without a set * username. If the Username is given it was called * from the login form and the given credentials might * need to be checked. If no username was given it * the function needs to check if the user is logged in * by other means (cookie, environment). * * The function needs to set some globals needed by * DokuWiki like auth_login() does. * * @see auth_login() * @author Andreas Gohr * @author Olivier Salaün * @author Dominique Launay * * @param string $user Username * @param string $pass Cleartext Password * @param bool $sticky Cookie should not expire * @return bool true on successful auth */ function trustExternal($user,$pass,$sticky=false){ //msg('trust_external'); // some example: global $USERINFO; global $conf; $sticky ? $sticky = true : $sticky = false; //sanity check // do the checking here //see if we got cookie if(isset($_SESSION['DW_SHIB'])){ // case logout if($_REQUEST['do'] == 'logout'){ unset($_SESSION['DW_SHIB']); unset($_SESSION[DOKU_COOKIE]['auth']['user']); unset($_SESSION[DOKU_COOKIE]['auth']['pass']); unset($_SESSION[DOKU_COOKIE]['auth']['info']); error_log('shibbolethauth : authenticated user redirected for logout to '.$conf['plugin']['shibbolethauth']['shibbolethLogoutURL']); header("Location: ".$conf['plugin']['shibbolethauth']['shibbolethLogoutURL']."?return=".urlencode($_SERVER['HTTP_REFERER'])); exit; }else{ // fill vars with cookie vars $_SESSION[DOKU_COOKIE]['auth']['user'] = $user = $_SESSION['DW_SHIB']['user']; $_SESSION[DOKU_COOKIE]['auth']['pass'] = $pass = $_SESSION['DW_SHIB']['pass']; $_SESSION[DOKU_COOKIE]['auth']['info'] = $USERINFO = $_SESSION['DW_SHIB']['info']; $_SERVER['REMOTE_USER'] = $_SESSION['DW_SHIB']['user']; return true; } }else{ // save the Email done by a Shibboleth attribute if this attribute is set // we need to check if the identityProvider attribute is set, otherwise we can't distinguish // normal unauthenticated access from authenticated access with a missing email address if(isset($_SERVER[$conf['plugin']['shibbolethauth']['shibbolethEmailAttribute']]) && $_SERVER[$conf['plugin']['shibbolethauth']['shibbolethEmailAttribute']]!=""){ // if user wants to log out if($_REQUEST['do'] == 'logout'){ unset($_SESSION['DW_SHIB']); error_log('shibbolethauth : authenticated user redirected for logout to '.$conf['plugin']['shibbolethauth']['shibbolethLogoutURL']); header("Location: ".$conf['plugin']['shibbolethauth']['shibbolethLogoutURL']."?return=".urlencode($_SERVER['HTTP_REFERER'])); exit; }else{ //attributes set fill convenient variables $USERINFO['name'] = $user = $_SERVER[$conf['plugin']['shibbolethauth']['shibbolethEmailAttribute']]; $USERINFO['mail'] = $mail = $_SERVER[$conf['plugin']['shibbolethauth']['shibbolethEmailAttribute']]; // load the groups of users contained on the file users.auth.php if($this->users === null) $this->_loadUserData(); // groups the person belongs to $grps = array(); // Test if $user is an email or not. If it is not, it is an user id if(strpbrk($user, '@') != false){ // We are in a Shibboteth context, and we get the datas for the autorization // Test if we use the autorisation with Sympa server $sympaIsOK = true; if(isset($conf['plugin']['shibbolethauth']['useSympa']) && $conf['plugin']['shibbolethauth']['useSympa'] == 1){ // Get the groups list on the Sympa server //create soap client try{ $this->soap_client = new SoapClient($conf['plugin']['shibbolethauth']['sympaSoapService']); }catch(SoapFault $fault){ $sympaIsOK = false; error_log("shibbolethauth : unable to establish a SOAP connection with ".$conf['plugin']['shibbolethauth']['sympaSoapService']); msg("No valid SOAP server",0); } // get the groups list if($sympaIsOK){ try{ $res = $this->soap_client->authenticateRemoteAppAndRun($conf['plugin']['shibbolethauth']['sympaApplicationId'],$conf['plugin']['shibbolethauth']['sympaApplicationPwd'], 'USER_EMAIL='.$user,'complexWhich'); }catch(SoapFault $fault){ $sympaIsOK = false; msg("dokuwiki groups only",0); error_log("shibbolethauth : unable to authenticate on ".$conf['plugin']['shibbolethauth']['sympaSoapService']); } if($sympaIsOK){ if (isset($res) && gettype($res) == 'array') { foreach ($res as $list) { if (empty($grps[0])) { $grps[0] = $list->listAddress; }else{ array_unshift($grps,$list->listAddress); } if($list->isOwner == 'true'){ $listrequest = preg_replace('/@/','-request@',$list->listAddress); array_unshift($grps,$listrequest); } if ($list->isEditor == 'true') { $listeditor = preg_replace('/@/','-editor@',$list->listAddress); array_unshift($grps,$listeditor); } } } } } } // Collect the DokuWiki's data with the Sympa's data (if we are in Sympa context) $users = $this->setUserGroups($mail,$grps); } // set the globals if authed $USERINFO['grps'] = $users['grps']; $_SERVER['REMOTE_USER'] = $user; $_SESSION[DOKU_COOKIE]['auth']['user'] = $user; $_SESSION[DOKU_COOKIE]['auth']['pass'] = $user; $_SESSION[DOKU_COOKIE]['auth']['info'] = $USERINFO; $_SESSION['DW_SHIB']['user']= $user; $_SESSION['DW_SHIB']['pass']= $user; $_SESSION['DW_SHIB']['info'] = $USERINFO; error_log('shibbolethauth : authenticated user'); return true; } // if this attribute is not set, we log off the system }else{ if($_REQUEST['do'] == 'login'){ error_log('shibbolethauth: redirect user for login to '.$conf['plugin']['shibbolethauth']['shibbolethLoginURL']); header("Location: ".$conf['plugin']['shibbolethauth']['shibbolethLoginURL']."?target=".urlencode($_SERVER['HTTP_REFERER'])); exit; }else{ error_log('shibbolethauth: no email address to log'); unset($_SESSION[DOKU_COOKIE]['auth']['user']); unset($_SESSION[DOKU_COOKIE]['auth']['pass']); unset($_SESSION[DOKU_COOKIE]['auth']['info']); return false; } } } } /** * Checks if the Email attibute of Shibboleth is correctly set * * @author Yoann Lecuyer * **/ function checkPass($user,$pass){ } /** * Return user info * * Accessing to sympa is done through the nusoap library * for the autorization part * * Reading the user's group on DokuWiki's files * for the user manager part * * The returned field are : * * name string full name of the user * mail string email addres of the user * groups array list of groups the user is in * * @author Yoann Lecuyer * **/ function getUserData( $user ) { $userData = false; $userData['name'] = $user; $userData['mail'] = $user; $userData['grps'] = $this->getUserGroups($user); return $userData; } /** * Return user groups * **/ function getUserGroups($user) { global $conf; $mail=$user; // load the groups of users contained on the file users.auth.php if($this->users === null) $this->_loadUserData(); // groups the person belongs to $grps = array(); // Test if $user is an email or not. If it is not, it is an user id if(strpbrk($user, '@') != false){ // We are in a Shibboteth context, and we get the datas for the autorization // Test if we use the autorisation with Sympa server $sympaIsOK = true; if(isset($conf['plugin']['shibbolethauth']['useSympa']) && $conf['plugin']['shibbolethauth']['useSympa'] == 1){ // Get the groups list on the Sympa server //create soap client try{ $this->soap_client = new SoapClient($conf['plugin']['shibbolethauth']['sympaSoapService']); }catch(SoapFault $fault){ $sympaIsOK = false; error_log("shibbolethauth : unable to establish a SOAP connection with ".$conf['plugin']['shibbolethauth']['sympaSoapService']); } // get the groups list if($sympaIsOK){ try{ $res = $this->soap_client->authenticateRemoteAppAndRun($conf['plugin']['shibbolethauth']['sympaApplicationId'],$conf['plugin']['shibbolethauth']['sympaApplicationPwd'], 'USER_EMAIL='.$user,'complexWhich'); }catch(SoapFault $fault){ $sympaIsOK = false; msg("dokuwiki groups only",0); error_log("shibbolethauth : unable to authenticate on ".$conf['plugin']['shibbolethauth']['sympaSoapService']); } if($sympaIsOK){ if (isset($res) && gettype($res) == 'array') { foreach ($res as $list) { if (empty($grps[0])) { $grps[0] = $list->listAddress; }else{ array_unshift($grps,$list->listAddress); } if($list->isOwner == 'true'){ $listrequest = preg_replace('/@/','-request@',$list->listAddress); array_unshift($grps,$listrequest); } if ($list->isEditor == 'true') { $listeditor = preg_replace('/@/','-editor@',$list->listAddress); array_unshift($grps,$listeditor); } } } } } } // Collect the DokuWiki's data with the Sympa's data (if we are in Sympa context) $users = $this->setUserGroups($mail,$grps); } // set the globals if authed return $users['grps']; } /** * Set the DokuWiki's groups and the Sympa's groups together * * @author Yoann Lecuyer * **/ function setUserGroups($mail,$grps){ foreach ($this->users as $user){ if($user['mail'] == $mail){ // Add or set the groups of the user if(!empty($grps[0])){ foreach($grps as $group){ array_unshift($user['grps'],$group); } } // Verify if the table contains the sympaDefaultGroup value $defaultgroupset = false; foreach ($user['grps'] as $group){ if($group == $this->sympaDefaultGroup) $defaultgroupset = true; } // Put the sympaDefaultGroup value if it is not on the table if(!$defaultgroupset) array_unshift($user['grps'],$this->sympaDefaultGroup); return $user; } } // the mail is not on the table of DokuWiki's users, we create an other user // Verify if the table contains the sympaDefaultGroup value $defaultgrpset = false; foreach ($grps as $grp){ if($grp == $this->sympaDefaultGroup) $defaultgrpset = true; } // Put the sympaDefaultGroup value if it is not on the table if(!$defaultgrpset) array_unshift($grps,$this->sympaDefaultGroup); $this->users[$mail]['name'] = $mail; $this->users[$mail]['mail'] = $mail; $this->users[$mail]['grps'] = $grps; return $this->users[$mail]; } /** * Logoff user : * * everything that was about the session is unset * * @author Yoann Lecuyer * **/ function logOff() { if(isset($_SESSION[DOKU_COOKIE]['auth']['info'])) { unset($_SESSION[DOKU_COOKIE]['auth']['info']); } unset($this->soap_client); unset($_REQUEST['u']); return; } function server() { return($this->sympaSoapService); } /** * Create a new User * * Returns false if the user already exists, null when an error * occured and true if everything went well. * * The new user will be added to the default group by this * function if grps are not specified (default behaviour). * * @author Andreas Gohr * @author Chris Smith */ function createUser($user,$pwd,$name,$mail,$grps=null){ global $conf; // user mustn't already exist if ($this->getUserData($user) !== false) return false; $pass = auth_cryptPassword($pwd); $name = $user; // set default group if no groups specified if (!is_array($grps)) $grps = array($conf['defaultgroup']); // prepare user line $groups = join(',',$grps); $userline = join(':',array($user,$pass,$name,$mail,$groups))."\n"; if (io_saveFile(AUTH_USERFILE,$userline,true)) { $this->users[$user] = compact('pass','name','mail','grps'); return $pass; } msg('The '.AUTH_USERFILE.' file is not writable. Please inform the Wiki-Admin',-1); return null; } /** * Modify user data * * @author Chris Smith * @param $user nick of the user to be changed * @param $changes array of field/value pairs to be changed (password will be clear text) * @return bool */ function modifyUser($user, $changes) { global $conf; global $ACT; global $INFO; // sanity checks, user must already exist and there must be something to change if (($userinfo = $this->getUserData($user)) === false) return false; if (!is_array($changes) || !count($changes)) return true; // update userinfo with new data, remembering to encrypt any password $newuser = $user; foreach ($changes as $field => $value) { if ($field == 'user') { $newuser = $value; continue; } if ($field == 'pass') $value = auth_cryptPassword($value); $userinfo[$field] = $value; } $groups = join(',',$userinfo['grps']); $userline = join(':',array($newuser, $userinfo['pass'], $userinfo['name'], $userinfo['mail'], $groups))."\n"; if (!$this->deleteUsers(array($user))) { msg('Unable to modify user data. Please inform the Wiki-Admin',-1); return false; } if (!io_saveFile(AUTH_USERFILE,$userline,true)) { msg('There was an error modifying your user data. You should register again.',-1); // FIXME, user has been deleted but not recreated, should force a logout and redirect to login page $ACT == 'register'; return false; } $this->users[$newuser] = $userinfo; return true; } /** * Remove one or more users from the list of registered users * * @author Christopher Smith * @param array $users array of users to be deleted * @return int the number of users deleted */ function deleteUsers($users) { if (!is_array($users) || empty($users)) return 0; if ($this->users === null) $this->_loadUserData(); $deleted = array(); foreach ($users as $user) { if (isset($this->users[$user])) $deleted[] = preg_quote($user,'/'); } if (empty($deleted)) return 0; $pattern = '/^('.join('|',$deleted).'):/'; if (io_deleteFromFile(AUTH_USERFILE,$pattern,true)) { foreach ($deleted as $user) unset($this->users[$user]); return count($deleted); } // problem deleting, reload the user list and count the difference $count = count($this->users()); $this->_loadUserData(); $count -= $count($this->users()); return $count; } /** * Return a count of the number of user which meet $filter criteria * * @author Chris Smith */ function getUserCount($filter=array()) { if($this->users === null) $this->_loadUserData(); if (!count($filter)) return count($this->users); $count = 0; $this->_constructPattern($filter); foreach ($this->users as $user => $info) { $count += $this->_filter($user, $info); } return $count; } /** * Bulk retrieval of user data * * @author Chris Smith * @param start index of first user to be returned * @param limit max number of users to be returned * @param filter array of field/pattern pairs * @return array of userinfo (refer getUserData for internal userinfo details) */ function retrieveUsers($start=0,$limit=0,$filter=array()) { if ($this->users === null) $this->_loadUserData(); ksort($this->users); $i = 0; $count = 0; $out = array(); $this->_constructPattern($filter); foreach ($this->users as $user => $info) { if ($this->_filter($user, $info)) { if ($i >= $start) { $out[$user] = $info; $count++; if (($limit > 0) && ($count >= $limit)) break; } $i++; } } return $out; } /** * Load all user data * * loads the user file into a datastructure * * @author Andreas Gohr */ function _loadUserData(){ $this->users = array(); if(!@file_exists(AUTH_USERFILE)) return; $lines = file(AUTH_USERFILE); foreach($lines as $line){ $line = preg_replace('/#.*$/','',$line); //ignore comments $line = trim($line); if(empty($line)) continue; $row = split(":",$line,5); $groups = split(",",$row[4]); $this->users[$row[0]]['pass'] = $row[1]; $this->users[$row[0]]['name'] = urldecode($row[2]); $this->users[$row[0]]['mail'] = $row[3]; $this->users[$row[0]]['grps'] = $groups; } } /** * return 1 if $user + $info match $filter criteria, 0 otherwise * * @author Chris Smith */ function _filter($user, $info) { // FIXME foreach ($this->_pattern as $item => $pattern) { if ($item == 'user') { if (!preg_match($pattern, $user)) return 0; } else if ($item == 'grps') { if (!count(preg_grep($pattern, $info['grps']))) return 0; } else { if (!preg_match($pattern, $info[$item])) return 0; } } return 1; } function _constructPattern($filter) { $this->_pattern = array(); foreach ($filter as $item => $pattern) { // $this->_pattern[$item] = '/'.preg_quote($pattern,"/").'/'; // don't allow regex characters $this->_pattern[$item] = '/'.str_replace('/','\/',$pattern).'/'; // allow regex characters } } }