# # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. # http://www.gnu.org/copyleft/gpl.html /** * LdapAuthentication plugin. * * Password authentication, and Smartcard Authentication support are currently * available. All forms of authentication, current and future, should support * group, and attribute based restrictions; preference pulling; and group * syncronization. All forms of authentication should have basic support for * adding users, changing passwords, and updating preferences in LDAP. * * Password authentication has a number of configurations, including straight binds, * proxy based authentication, and anonymous-search based authentication. * * @package MediaWiki */ # # LdapAuthentication.php # # Info available at http://meta.wikimedia.org/wiki/LDAP_Authentication # and at http://meta.wikimedia.org/wiki/LDAP_Authentication_Configuration_Examples # and at http://meta.wikimedia.org/wiki/LDAP_Smartcard_Authentication_Configuration_Examples # # Support is available at http://meta.wikimedia.org/wiki/Talk:LDAP_Authentication # # Version 1.1d / 12/04/2006 # require_once( 'AuthPlugin.php' ); class LdapAuthenticationPlugin extends AuthPlugin { var $email, $lang, $realname, $nickname, $SearchType; var $LDAPUsername; var $userLDAPGroups, $foundUserLDAPGroups; var $allLDAPGroups; function LdapAuthenticationPlugin() { } /** * Check whether there exists a user account with the given name. * The name will be normalized to MediaWiki's requirements, so * you might need to munge it (for instance, for lowercase initial * letters). * * @param string $username * @return bool * @access public */ function userExists( $username ) { global $wgLDAPAddLDAPUsers; $this->printDebug("Entering userExists",1); //If we can't add LDAP users, we don't really need to check //if the user exists, the authenticate method will do this for //us. This will decrease hits to the LDAP server. //We do however, need to use this if we are using smartcard authentication. if ( (!isset($wgLDAPAddLDAPUsers[$_SESSION['wsDomain']]) || !$wgLDAPAddLDAPUsers[$_SESSION['wsDomain']]) && !$this->useSmartcardAuth()) { return true; } $ldapconn = $this->connect(); if ($ldapconn) { $this->printDebug("Successfully connected",1); $searchstring = $this->getSearchString($ldapconn,$username); //If we are using smartcard authentication, and we got //anything back, then the user exists. if ($this->useSmartcardAuth() && $searchstring != '') { //getSearchString is going to bind, but will not unbind //Let's clean up @ldap_unbind(); return true; } //Search for the entry. $entry = @ldap_read($ldapconn, $searchstring, "objectclass=*"); //getSearchString is going to bind, but will not unbind //Let's clean up @ldap_unbind(); if (!$entry) { $this->printDebug("Did not find a matching user in LDAP",1); //user wasn't found return false; } else { $this->printDebug("Found a matching user in LDAP",1); return true; } } else { $this->printDebug("Failed to connect",1); return false; } } /** * Connect to LDAP * * @return resource * @access private */ function connect() { global $wgLDAPServerNames; global $wgLDAPEncryptionType; $this->printDebug("Entering Connect",1); //If the user didn't set an encryption type, we default to tls if ( isset($wgLDAPEncryptionType[$_SESSION['wsDomain']]) ) { $encryptionType = $wgLDAPEncryptionType[$_SESSION['wsDomain']]; } else { $encryptionType = "tls"; } //Set the server string depending on whether we use ssl or not switch($encryptionType) { case "ssl": $this->printDebug("Using SSL",2); $serverpre = "ldaps://"; break; default: $this->printDebug("Using TLS or not using encryption.",2); $serverpre = "ldap://"; } //Make a space seperated list of server strings with the ldap:// or ldaps:// //string added. $servers = ""; $tmpservers = $wgLDAPServerNames[$_SESSION['wsDomain']]; $tok = strtok($tmpservers, " "); while ($tok) { $servers = $servers . " " . $serverpre . $tok; $tok = strtok(" "); } $servers = rtrim($servers); $this->printDebug("Using servers: $servers",2); //Connect and set options $ldapconn = @ldap_connect( $servers ); ldap_set_option( $ldapconn, LDAP_OPT_PROTOCOL_VERSION, 3); ldap_set_option( $ldapconn, LDAP_OPT_REFERRALS, 0); //TLS needs to be started after the connection is made if ( $encryptionType == "tls" ) { $this->printDebug("Using TLS",2); if ( !ldap_start_tls( $ldapconn ) ) { $this->printDebug("Failed to start TLS.",2); return; } } return $ldapconn; } /** * Check if a username+password pair is a valid login, or if the username * is allowed access to the wiki. * The name will be normalized to MediaWiki's requirements, so * you might need to munge it (for instance, for lowercase initial * letters). * * @param string $username * @param string $password * @return bool * @access public */ function authenticate( $username, $password='' ) { global $wgLDAPRetrievePrefs; global $wgLDAPGroupDN, $wgLDAPRequiredGroups; global $wgLDAPGroupUseFullDN, $wgLDAPGroupUseRetrievedUsername; global $wgLDAPUseLDAPGroups; global $wgLDAPRequireAuthAttribute, $wgLDAPAuthAttribute; global $wgLDAPSSLUsername; global $wgLDAPLowerCaseUsername; global $wgLDAPSearchStrings; $this->printDebug("Entering authenticate",1); //We don't handle local authentication if ( 'local' == $_SESSION['wsDomain'] ) { $this->printDebug("User is using a local domain",2); return false; } //If the user is using smartcard authentication, we need to ensure //that he/she isn't trying to fool us by sending a username other //than the one the web server got from the smartcard. if ( $this->useSmartcardAuth() && $wgLDAPSSLUsername != $username ) { $this->printDebug("The username provided doesn't match the username on the smartcard. The user is probably trying to log in to the smartcard domain with password authentication. Denying access.",2); return false; } //We need to ensure that if we require a password, that it is //not blank. We don't allow blank passwords, so we are being //tricked if someone is supplying one when using password auth. //Smartcard authentication uses a pin, and does not require //a password to be given; a blank password here is wanted. if ( '' == $password && !$this->useSmartcardAuth() ) { $this->printDebug("User used a blank password",1); return false; } $ldapconn = $this->connect(); if ( $ldapconn ) { $this->printDebug("Connected successfully",1); //Mediawiki munges the username before authenticate is called, //this can mess with authentication, group pulling/restriction, //preference pulling, etc. Let's allow the user to use //a lowercased username. if ( isset($wgLDAPLowerCaseUsername[$_SESSION['wsDomain']]) && $wgLDAPLowerCaseUsername[$_SESSION['wsDomain']] ) { $username = strtolower($username); $this->printDebug("Lowercasing the username: $username",1); } $userdn = $this->getSearchString($ldapconn, $username); //It is possible that getSearchString will return an //empty string; if this happens, the bind will ALWAYS //return true, and will let anyone in! if ('' == $userdn) { $this->printDebug("User DN is blank",1); // Lets clean up. @ldap_unbind(); return false; } //If we are using password authentication, we need to bind as the //user to make sure the password is correct. if ( !$this->useSmartcardAuth() ) { $this->printDebug("Binding as the user",1); //Let's see if the user can authenticate. $bind = $this->bindAs($ldapconn, $userdn, $password); if (!$bind) { // Lets clean up. @ldap_unbind(); return false; } $this->printDebug("Binded successfully",1); if ( isset( $wgLDAPSearchStrings[$_SESSION['wsDomain']] ) ) { $ss = $wgLDAPSearchStrings[$_SESSION['wsDomain']]; if ( strstr( $ss, "@" ) || strstr( $ss, '\\' ) ) { //We are most likely configured using USER-NAME@DOMAIN, or //DOMAIN\\USER-NAME. //Get the user's full DN so we can search for groups and such. $userdn = $this->getUserDN($ldapconn, $username); $this->printDebug("Pulled the user's DN: $userdn",1); } } if ( (isset($wgLDAPRequireAuthAttribute[$_SESSION['wsDomain']]) && $wgLDAPRequireAuthAttribute[$_SESSION['wsDomain']]) ) { $this->printDebug("Checking for auth attributes",1); $filter = "(" . $wgLDAPAuthAttribute[$_SESSION['wsDomain']] . ")"; $attributes = array("dn"); $entry = ldap_read($ldapconn, $userdn, $filter, $attributes); $info = ldap_get_entries($ldapconn, $entry); if ($info["count"] < 1) { $this->printDebug("Failed auth attribute check",1); // Lets clean up. @ldap_unbind(); return false; } } } //Old style groups, non-nestable and fairly limited on group type (full DN //versus username). DEPRECATED if ($wgLDAPGroupDN) { $this->printDebug("Checking for (old style) group membership",1); if (!$this->isMemberOfLdapGroup($ldapconn, $userdn, $wgLDAPGroupDN)) { $this->printDebug("Failed (old style) group membership check",1); //No point in going on if the user isn't in the required group // Lets clean up. @ldap_unbind(); return false; } } //New style group checking if ( isset($wgLDAPRequiredGroups[$_SESSION['wsDomain']]) ) { $this->printDebug("Checking for (new style) group membership",1); if ( isset($wgLDAPGroupUseFullDN[$_SESSION['wsDomain']]) && $wgLDAPGroupUseFullDN[$_SESSION['wsDomain']] ) { $inGroup = $this->isMemberOfRequiredLdapGroup($ldapconn, $userdn); } else { if ( (isset($wgLDAPGroupUseRetrievedUsername[$_SESSION['wsDomain']]) && $wgLDAPGroupUseRetrievedUsername[$_SESSION['wsDomain']]) && $this->LDAPUsername != '' ) { $this->printDebug("Using the username retrieved from the user's entry.",1); $inGroup = $this->isMemberOfRequiredLdapGroup($ldapconn, $this->LDAPUsername); } else { $inGroup = $this->isMemberOfRequiredLdapGroup($ldapconn, $username); } } if (!$inGroup) { // Lets clean up. @ldap_unbind(); return false; } } //Synch LDAP groups with MediaWiki groups if ( isset($wgLDAPUseLDAPGroups[$_SESSION['wsDomain']]) && $wgLDAPUseLDAPGroups[$_SESSION['wsDomain']] ) { $this->printDebug("Retrieving LDAP group membership",1); //Let's get the user's LDAP groups if ( isset($wgLDAPGroupUseFullDN[$_SESSION['wsDomain']]) && $wgLDAPGroupUseFullDN[$_SESSION['wsDomain']] ) { $this->userLDAPGroups = $this->getUserGroups($ldapconn, $userdn, true); } else { if ( (isset($wgLDAPGroupUseRetrievedUsername[$_SESSION['wsDomain']]) && $wgLDAPGroupUseRetrievedUsername[$_SESSION['wsDomain']]) && $this->LDAPUsername != '' ) { $this->userLDAPGroups = $this->getUserGroups($ldapconn, $this->LDAPUsername, true); } else { $this->userLDAPGroups = $this->getUserGroups($ldapconn, $username, true); } } //If the user doesn't have any groups there is no need to waste another search. if ( $this->foundUserLDAPGroups ) { $this->allLDAPGroups = $this->getAllGroups($ldapconn, true); } } //Retrieve preferences if ( isset($wgLDAPRetrievePrefs[$_SESSION['wsDomain']]) && $wgLDAPRetrievePrefs[$_SESSION['wsDomain']] ) { $this->printDebug("Retrieving preferences",1); $entry = @ldap_read($ldapconn, $userdn, "objectclass=*"); $info = @ldap_get_entries($ldapconn, $entry); $this->email = $info[0]["mail"][0]; $this->lang = $info[0]["preferredlanguage"][0]; $this->nickname = $info[0]["displayname"][0]; $this->realname = $info[0]["cn"][0]; $this->printDebug("Retrieved: $this->email, $this->lang, $this->nickname, $this->realname",2); } // Lets clean up. @ldap_unbind(); } else { $this->printDebug("Failed to connect",1); return false; } $this->printDebug("Authentication passed",1); //We made it this far; the user authenticated and didn't fail any checks, so he/she gets in. return true; } /** * Modify options in the login template. * * @param UserLoginTemplate $template * @access public */ function modifyUITemplate( &$template ) { global $wgLDAPDomainNames, $wgLDAPUseLocal; global $wgLDAPAddLDAPUsers; global $wgLDAPUseSmartcardAuth, $wgLDAPSmartcardDomain; $this->printDebug("Entering modifyUITemplate",1); if ( !isset($wgLDAPAddLDAPUsers[$_SESSION['wsDomain']]) || !$wgLDAPAddLDAPUsers[$_SESSION['wsDomain']] ) { $template->set( 'create', false ); } $template->set( 'usedomain', true ); $template->set( 'useemail', false ); $tempDomArr = $wgLDAPDomainNames; if ( $wgLDAPUseLocal ) { $this->printDebug("Allowing the local domain, adding it to the list.",1); array_push( $tempDomArr, 'local' ); } if ( $wgLDAPUseSmartcardAuth ) { $this->printDebug("Allowing smartcard login, removing the domain from the list.",1); //There is no reason for people to log in directly to the wiki if the are using a //smartcard. If they try to, they are probably up to something fishy. unset( $tempDomArr[array_search($wgLDAPSmartcardDomain, $tempDomArr)] ); } $template->set( 'domainnames', $tempDomArr ); } /** * Return true if the wiki should create a new local account automatically * when asked to login a user who doesn't exist locally but does in the * external auth database. * * This is just a question, and shouldn't perform any actions. * * @return bool * @access public */ function autoCreate() { global $wgLDAPDisableAutoCreate; if ( isset($wgLDAPDisableAutoCreate[$_SESSION['wsDomain']]) && $wgLDAPDisableAutoCreate[$_SESSION['wsDomain']] ) { return false; } else { return true; } } /** * Set the given password in LDAP. * Return true if successful. * * @param User $user * @param string $password * @return bool * @access public */ function setPassword( $user, &$password ) { global $wgLDAPUpdateLDAP, $wgLDAPWriterDN, $wgLDAPWriterPassword; $this->printDebug("Entering setPassword",1); if ($_SESSION['wsDomain'] == 'local') { $this->printDebug("User is using a local domain",1); //We don't set local passwords, but we don't want the wiki //to send the user a failure. return true; } else if ( !isset($wgLDAPUpdateLDAP[$_SESSION['wsDomain']]) || !$wgLDAPUpdateLDAP[$_SESSION['wsDomain']] ) { $this->printDebug("Wiki is set to not allow updates",1); //We aren't allowing the user to change his/her own password return false; } if (!isset($wgLDAPWriterDN[$_SESSION['wsDomain']])) { $this->printDebug("Wiki doesn't have wgLDAPWriterDN set",1); //We can't change a user's password without an account that is //allowed to do it. return false; } $pass = $this->getPasswordHash($password); $ldapconn = $this->connect(); if ($ldapconn) { $this->printDebug("Connected successfully",1); $userdn = $this->getSearchString($ldapconn, $user->getName()); $this->printDebug("Binding as the writerDN",1); $bind = $this->bindAs( $ldapconn, $wgLDAPWriterDN[$_SESSION['wsDomain']], $wgLDAPWriterPassword[$_SESSION['wsDomain']] ); if (!$bind) { return false; } $values["userpassword"] = $pass; //Blank out the password in the database. We don't want to save //domain credentials for security reasons. $password = ''; $success = ldap_modify($ldapconn, $userdn, $values); //Let's clean up @ldap_unbind(); if ($success) { $this->printDebug("Successfully modified the user's password",1); return true; } else { $this->printDebug("Failed to modify the user's password",1); return false; } } else { return false; } } /** * Update user information in LDAP * Return true if successful. * * @param User $user * @return bool * @access public */ function updateExternalDB( $user ) { global $wgLDAPUpdateLDAP; global $wgLDAPWriterDN, $wgLDAPWriterPassword; $this->printDebug("Entering updateExternalDB",1); if ( (!isset($wgLDAPUpdateLDAP[$_SESSION['wsDomain']]) || !$wgLDAPUpdateLDAP[$_SESSION['wsDomain']]) || $_SESSION['wsDomain'] == 'local') { $this->printDebug("Either the user is using a local domain, or the wiki isn't allowing updates",1); //We don't handle local preferences, but we don't want the //wiki to return an error. return true; } if (!isset($wgLDAPWriterDN[$_SESSION['wsDomain']])) { $this->printDebug("The wiki doesn't have wgLDAPWriterDN set",1); //We can't modify LDAP preferences if we don't have a user //capable of editing LDAP attributes. return false; } $this->email = $user->getEmail(); $this->realname = $user->getRealName(); $this->nickname = $user->getOption('nickname'); $this->language = $user->getOption('language'); $ldapconn = $this->connect(); if ($ldapconn) { $this->printDebug("Connected successfully",1); $userdn = $this->getSearchString($ldapconn, $user->getName()); $this->printDebug("Binding as the writerDN",1); $bind = $this->bindAs( $ldapconn, $wgLDAPWriterDN[$_SESSION['wsDomain']], $wgLDAPWriterPassword[$_SESSION['wsDomain']] ); if (!$bind) { return false; } if ('' != $this->email) { $values["mail"] = $this->email; } if ('' != $this->nickname) { $values["displayname"] = $this->nickname; } if ('' != $this->realname) { $values["cn"] = $this->realname; } if ('' != $this->language) { $values["preferredlanguage"] = $this->language; } if (0 != sizeof($values) && ldap_modify($ldapconn, $userdn, $values)) { $this->printDebug("Successfully modified the user's attributes",1); @ldap_unbind(); return true; } else { $this->printDebug("Failed to modify the user's attributes",1); @ldap_unbind(); return false; } } else { $this->printDebug("Failed to Connect",1); return false; } } /** * Can the wiki create accounts in LDAP? * Return true if yes. * * @return bool * @access public */ function canCreateAccounts() { global $wgLDAPAddLDAPUsers; if ( isset($wgLDAPAddLDAPUsers[$_SESSION['wsDomain']]) && $wgLDAPAddLDAPUsers[$_SESSION['wsDomain']] ) { return true; } else { return false; } } /** * Can the wiki change passwords in LDAP? * Return true if yes. * * @return bool * @access public */ function allowPasswordChange() { global $wgLDAPUpdateLDAP, $wgLDAPMailPassword; if ( isset($wgLDAPUpdateLDAP[$_SESSION['wsDomain']]) ) { $updateLDAP = $wgLDAPUpdateLDAP[$_SESSION['wsDomain']]; } else { $updateLDAP = false; } if ( isset($wgLDAPMailPassword[$_SESSION['wsDomain']]) ) { $mailPassword = $wgLDAPMailPassword[$_SESSION['wsDomain']]; } else { $mailPassword = false; } if ( $updateLDAP || $mailPassword ) { return true; } else { return false; } } /** * Add a user to LDAP. * Return true if successful. * * @param User $user * @param string $password * @return bool * @access public */ function addUser( $user, $password ) { global $wgLDAPAddLDAPUsers, $wgLDAPWriterDN, $wgLDAPWriterPassword; global $wgLDAPSearchAttributes; global $wgLDAPWriteLocation; global $wgLDAPRequiredGroups, $wgLDAPGroupDN; global $wgLDAPRequireAuthAttribute, $wgLDAPAuthAttribute; $this->printDebug("Entering addUser",1); if ( (!isset($wgLDAPAddLDAPUsers[$_SESSION['wsDomain']]) || !$wgLDAPAddLDAPUsers[$_SESSION['wsDomain']]) || 'local' == $_SESSION['wsDomain'] ) { $this->printDebug("Either the user is using a local domain, or the wiki isn't allowing users to be added to LDAP",1); //Tell the wiki not to return an error. return true; } if ($wgLDAPRequiredGroups || $wgLDAPGroupDN) { $this->printDebug("The wiki is requiring users to be in specific groups, and cannot add users as this would be a security hole.",1); //It is possible that later we can add users into //groups, but since we don't support it, we don't want //to open holes! return false; } if (!isset($wgLDAPWriterDN[$_SESSION['wsDomain']])) { $this->printDebug("The wiki doesn't have wgLDAPWriterDN set",1); //We can't add users without an LDAP account capable of doing so. return false; } $this->email = $user->getEmail(); $this->realname = $user->getRealName(); $username = $user->getName(); $pass = $this->getPasswordHash($password); $ldapconn = $this->connect(); if ($ldapconn) { $this->printDebug("Successfully connected",1); $userdn = $this->getSearchString($ldapconn, $username); if ('' == $userdn) { $this->printDebug("userdn is blank, attempting to use wgLDAPWriteLocation",1); if (isset($wgLDAPWriteLocation[$_SESSION['wsDomain']])) { $this->printDebug("wgLDAPWriteLocation is set, using that",1); $userdn = $wgLDAPSearchAttributes[$_SESSION['wsDomain']] . "=" . $username . $wgLDAPWriteLocation[$_SESSION['wsDomain']]; } else { $this->printDebug("wgLDAPWriteLocation is not set, failing",1); //getSearchString will bind, but will not unbind @ldap_unbind(); return false; } } $this->printDebug("Binding as the writerDN",1); $bind = $this->bindAs( $ldapconn, $wgLDAPWriterDN[$_SESSION['wsDomain']], $wgLDAPWriterPassword[$_SESSION['wsDomain']] ); if (!$bind) { return false; } //Set up LDAP attributes $values["uid"] = $username; $values["sn"] = $username; if ('' != $this->email) { $values["mail"] = $this->email; } if ('' != $this->realname) {$values["cn"] = $this->realname; } else { $values["cn"] = $username; } $values["userpassword"] = $pass; $values["objectclass"] = "inetorgperson"; if ($wgLDAPRequireAuthAttribute) { $values[$wgLDAPAuthAttribute[$_SESSION['wsDomain']]] = "true"; } if (@ldap_add($ldapconn, $userdn, $values)) { $this->printDebug("Successfully added user",1); @ldap_unbind(); return true; } else { $this->printDebug("Failed to add user",1); @ldap_unbind(); return false; } } else { return false; } } /** * Set the domain this plugin is supposed to use when authenticating. * * @param string $domain * @access public */ function setDomain( $domain ) { $this->printDebug("Setting domain as: $domain",1); $_SESSION['wsDomain'] = $domain; } /** * Check to see if the specific domain is a valid domain. * Return true if the domain is valid. * * @param string $domain * @return bool * @access public */ function validDomain( $domain ) { global $wgLDAPDomainNames, $wgLDAPUseLocal; $this->printDebug("Entering validDomain",1); if (in_array($domain, $wgLDAPDomainNames) || ($wgLDAPUseLocal && 'local' == $domain)) { $this->printDebug("User is using a valid domain.",1); return true; } else { $this->printDebug("User is not using a valid domain.",1); return false; } } /** * When a user logs in, update user with information from LDAP. * * @param User $user * @access public */ function updateUser( &$user ) { global $wgLDAPRetrievePrefs; global $wgLDAPUseLDAPGroups; $this->printDebug("Entering updateUser",1); $saveSettings = false; //If we aren't pulling preferences, we don't want to accidentally //overwrite anything. if ( isset($wgLDAPRetrievePrefs[$_SESSION['wsDomain']]) && $wgLDAPRetrievePrefs[$_SESSION['wsDomain']] ) { $this->printDebug("Setting user preferences.",1); if ('' != $this->lang) { $user->setOption('language',$this->lang); } if ('' != $this->nickname) { $user->setOption('nickname',$this->nickname); } if ('' != $this->realname) { $user->setRealName($this->realname); } if ('' != $this->email) { $user->setEmail($this->email); } $saveSettings = true; } if ( isset($wgLDAPUseLDAPGroups[$_SESSION['wsDomain']]) && $wgLDAPUseLDAPGroups[$_SESSION['wsDomain']] ) { $this->setGroups($user); $saveSettings = true; } if ( $saveSettings ) { $this->printDebug("Saving user settings.",1); $user->saveSettings(); } } /** * Return true to prevent logins that don't authenticate here from being * checked against the local database's password fields. * * This is just a question, and shouldn't perform any actions. * * @return bool * @access public */ function strict() { global $wgLDAPUseLocal, $wgLDAPMailPassword; $this->printDebug("Entering strict.",1); if ($wgLDAPUseLocal || $wgLDAPMailPassword) { $this->printDebug("Returning false in strict().",1); return false; } else { $this->printDebug("Returning true in strict().",1); return true; } } /** * When creating a user account, initialize user with information from LDAP. * * @param User $user * @access public */ function initUser( &$user ) { global $wgLDAPUseLDAPGroups; $this->printDebug("Entering initUser",1); if ('local' == $_SESSION['wsDomain']) { $this->printDebug("User is using a local domain",1); return; } //We are creating an LDAP user, it is very important that we do //NOT set a local password because it could compromise the //security of our domain. $user->mPassword = ''; if ( isset($wgLDAPRetrievePrefs[$_SESSION['wsDomain']]) && $wgLDAPRetrievePrefs[$_SESSION['wsDomain']] ) { if ('' != $this->lang) { $user->setOption('language',$this->lang); } if ('' != $this->nickname) { $user->setOption('nickname',$this->nickname); } if ('' != $this->realname) { $user->setRealName($this->realname); } if ('' != $this->email) { $user->setEmail($this->email); } } if ( isset($wgLDAPUseLDAPGroups[$_SESSION['wsDomain']]) && $wgLDAPUseLDAPGroups[$_SESSION['wsDomain']] ) { $this->setGroups($user); } $user->saveSettings(); } /** * Munge the username to always have a form of uppercase for the first letter, * and lowercase for the rest of the letters. * * @param string $username * @return string * @access public */ function getCanonicalName( $username ) { $this->printDebug("Entering getCanonicalName",1); if ( $username != '' ) { $this->printDebug("Username isn't empty.",1); //We want to use the username returned by LDAP //if it exists if ( $this->LDAPUsername != '' ) { $this->printDebug("Using LDAPUsername.",1); $username = $this->LDAPUsername; } //Change username to lowercase so that multiple user accounts //won't be created for the same user. $username = strtolower($username); //The wiki considers an all lowercase name to be invalid; need to //uppercase the first letter $username[0] = strtoupper($username[0]); } $this->printDebug("Munged username: $username",1); return $username; } /** * Returns the username pulled from LDAP when getSearchString() was called. * * @return string * @access public */ function getLDAPUsername() { return $this->LDAPUsername; } /** * Configures the authentication plugin for use with auto-authentication * plugins. * * @access public */ function autoAuthSetup() { global $wgLDAPUseSmartcardAuth; global $wgLDAPSmartcardDomain; $wgLDAPUseSmartcardAuth = true; $this->setDomain($wgLDAPSmartcardDomain); } /** * Gets the searchstring for a user based upon settings for the domain. * Returns a full DN for a user. * * @param resource $ldapconn * @param string $username * @return string * @access private */ function getSearchString($ldapconn, $username) { global $wgLDAPSearchStrings; global $wgLDAPProxyAgent, $wgLDAPProxyAgentPassword; $this->printDebug("Entering getSearchString",1); if (isset($wgLDAPSearchStrings[$_SESSION['wsDomain']])) { //This is a straight bind $this->printDebug("Doing a straight bind",1); $tmpuserdn = $wgLDAPSearchStrings[$_SESSION['wsDomain']]; $userdn = str_replace("USER-NAME",$username,$tmpuserdn); } else { //This is a proxy bind, or an anonymous bind with a search if (isset($wgLDAPProxyAgent[$_SESSION['wsDomain']])) { //This is a proxy bind $this->printDebug("Doing a proxy bind",1); $bind = $this->bindAs( $ldapconn, $wgLDAPProxyAgent[$_SESSION['wsDomain']], $wgLDAPProxyAgentPassword[$_SESSION['wsDomain']] ); } else { //This is an anonymous bind $this->printDebug("Doing an anonymous bind",1); $bind = $this->bindAs( $ldapconn ); } if (!$bind) { $this->printDebug("Failed to bind",1); return ''; } $userdn = $this->getUserDN($ldapconn, $username); } $this->printDebug("userdn is: $userdn",2); return $userdn; } /** * Gets the DN of a user based upon settings for the domain. * This function will set $this->LDAPUsername * You must bind to the server before calling this. * * @param resource $ldapconn * @param string $username * @return string * @access private */ function getUserDN($ldapconn, $username) { global $wgLDAPSearchAttributes; global $wgLDAPRequireAuthAttribute, $wgLDAPAuthAttribute; global $wgLDAPBaseDNs; $this->printDebug("Entering getUserDN",1); //we need to do a subbase search for the entry //Smartcard auth needs to check LDAP for required attributes. if ( (isset($wgLDAPRequireAuthAttribute[$_SESSION['wsDomain']]) && $wgLDAPRequireAuthAttribute[$_SESSION['wsDomain']]) && $this->useSmartcardAuth() ) { $auth_filter = "(" . $wgLDAPAuthAttribute[$_SESSION['wsDomain']] . ")"; $srch_filter = "(" . $wgLDAPSearchAttributes[$_SESSION['wsDomain']] . "=" . $this->getLdapEscapedString($username) . ")"; $filter = "(&" . $srch_filter . $auth_filter . ")"; $this->printDebug("Created an auth attribute filter: $filter",2); } else { $filter = "(" . $wgLDAPSearchAttributes[$_SESSION['wsDomain']] . "=" . $this->getLdapEscapedString($username) . ")"; $this->printDebug("Created a regular filter: $filter",2); } $attributes = array("*"); $base = $wgLDAPBaseDNs[$_SESSION['wsDomain']]; $this->printDebug("Using base: $base",2); $entry = @ldap_search($ldapconn, $base, $filter, $attributes); if (!$entry) { $this->printDebug("Couldn't find an entry",1); return ''; } $info = @ldap_get_entries($ldapconn, $entry); //This is a pretty useful thing to have for both smartcard authentication, //group checking, and pulling preferences. wfRunHooks('SetUsernameAttributeFromLDAP',array(&$this->LDAPUsername, $info)); if (!is_string($this->LDAPUsername)) { $this->printDebug("Fetched username is not a string (check your hook code...).",1); $this->LDAPUsername = ''; } $userdn = $info[0]["dn"]; return $userdn; } //DEPRECATED function isMemberOfLdapGroup( $ldapconn, $userDN, $groupDN ) { $this->printDebug("Entering isMemberOfLdapGroup (DEPRECATED)",1); //we need to do a subbase search for the entry $filter = "(member=" . $this->getLdapEscapedString($userDN) . ")"; $info = ldap_get_entries( $ldapconn, @ldap_search($ldapconn, $groupDN, $filter) ); return ( $info["count"] >= 1 ); } /** * Determines whether a user is a member of a group, or a nested group. * * @param resource $ldapconn * @param string $userDN * @return bool * @access private */ function isMemberOfRequiredLdapGroup( $ldapconn, $userDN ) { global $wgLDAPRequiredGroups; global $wgLDAPGroupSearchNestedGroups; $this->printDebug("Entering isMemberOfRequiredLdapGroup",1); $reqgroups = $wgLDAPRequiredGroups[$_SESSION['wsDomain']]; for ( $i = 0; $i < count($reqgroups); $i++ ) { $reqgroups[$i] = strtolower( $reqgroups[$i] ); } $searchnested = $wgLDAPGroupSearchNestedGroups[$_SESSION['wsDomain']]; $this->printDebug("Required groups:" . implode(",",$reqgroups) . "",1); $groups = $this->getUserGroups($ldapconn, $userDN); if ( !$this->foundUserLDAPGroups ) { //User isn't in any groups, so he/she obviously can't be in //a required one $this->printDebug("Couldn't find the user in any groups (1).",1); return false; } else { //User is in groups, let's see if a required group is one of them foreach ($groups as $group) { if ( in_array( $group, $reqgroups ) ) { $this->printDebug("Found user in a group.",1); return true; } } //We didn't find the user in the group, lets check nested groups if ( $searchnested ) { //No reason to go on if we aren't allowing nested group //searches if ( $this->searchNestedGroups($ldapconn, $groups) ) { return true; } } $this->printDebug("Couldn't find the user in any groups (2).",1); return false; } } /** * Helper function for isMemberOfRequiredLdapGroup. * $checkedgroups is used for tail recursion and shouldn't be provided * when called externally. * * @param resource $ldapconn * @param string $userDN * @param array $checkedgroups * @return bool * @access private */ function searchNestedGroups( $ldapconn, $groups, $checkedgroups = array() ) { global $wgLDAPRequiredGroups; $this->printDebug("Entering searchNestedGroups",1); //base case, no more groups left to check if (!$groups) { $this->printDebug("Couldn't find user in any nested groups.",1); return false; } $this->printDebug("Checking groups:" . implode(",",$groups) . "",2); $reqgroups = $wgLDAPRequiredGroups[$_SESSION['wsDomain']]; for ( $i = 0; $i < count($reqgroups); $i++ ) { $reqgroups[$i] = strtolower( $reqgroups[$i] ); } $groupstocheck = array(); foreach ( $groups as $group ) { $returnedgroups = $this->getUserGroups($ldapconn, $group); foreach ($returnedgroups as $checkme) { $this->printDebug("Checking membership for: $checkme",2); if ( in_array( $checkme, $checkedgroups ) ) { //We already checked this, move on continue; } else if ( in_array( $checkme, $reqgroups ) ) { $this->printDebug("Found user in a nested group.",1); //Woohoo return true; } else { //We'll need to check this group's members now array_push( $groupstocheck, $checkme ); } } } $checkedgroups = array_unique(array_merge($groups, $checkedgroups)); //Mmmmmm. Tail recursion. Tasty. if ( $this->searchNestedGroups($ldapconn, $groupstocheck, $checkedgroups) ) { return true; } else { return false; } } /** * Helper function for isMemberOfRequiredLdapGroup and searchNestedGroups * Sets $this->foundUserLDAPGroups * * @param resource $ldapconn * @param string $dn * @return array * @access private */ function getUserGroups( $ldapconn, $dn, $getShortnames = false ) { $this->printDebug("Entering getUserGroups",1); //Let's return the saved groups if they are available if ( $getShortnames ) { if ( isset($this->userLDAPShortnameGroupCache) ) { return $this->userLDAPShortnameGroupCache; } } else { if ( isset($this->userLDAPGroupCache) ) { return $this->userLDAPGroupCache; } } //We haven't done a search yet, lets do it now list($groups, $shortnamegroups) = $this->getGroups( $ldapconn, $dn ); //Save the groups for next time we are called $this->userLDAPGroupCache = $groups; $this->userLDAPShortnameGroupCache = $shortnamegroups; //We only need to check one of the two arrays, as they should be //identical from a member standpoint. if (count($groups) == 0) { $this->foundUserLDAPGroups = false; } else { $this->foundUserLDAPGroups = true; } if ( $getShortnames ) { return $shortnamegroups; } else { return $groups; } } /** * Helper function for retrieving all LDAP groups * Sets $this->foundAllLDAPGroups * * @param resource $ldapconn * @param string $dn * @return array * @access private */ function getAllGroups( $ldapconn, $getShortnames = false ) { $this->printDebug("Entering getAllGroups",1); //Let's return the saved groups if they are available if ( $getShortnames ) { if ( isset($this->allLDAPShortnameGroupCache) ) { return $this->allLDAPShortnameGroupCache; } } else { if ( isset($this->allLDAPGroupCache) ) { return $this->allLDAPGroupCache; } } //We haven't done a search yet, lets do it now list($groups, $shortnamegroups) = $this->getGroups( $ldapconn, '*' ); //Save the groups for next time we are called $this->allLDAPGroupCache = $groups; $this->allLDAPShortnameGroupCache = $shortnamegroups; //We only need to check one of the two arrays, as they should be //identical from a member standpoint. if (count($groups) == 0) { $this->foundAllLDAPGroups = false; } else { $this->foundAllLDAPGroups = true; } if ( $getShortnames ) { return $shortnamegroups; } else { return $groups; } } /** * Helper function for getUserGroups and getAllGroups. You shouldn't * call this directly. * * @param resource $ldapconn * @param string $dn * @return array * @access private */ function getGroups( $ldapconn, $dn ) { global $wgLDAPBaseDNs; global $wgLDAPGroupObjectclass, $wgLDAPGroupAttribute, $wgLDAPGroupNameAttribute; global $wgLDAPProxyAgent, $wgLDAPProxyAgentPassword; $this->printDebug("Entering getGroups",1); $base = $wgLDAPBaseDNs[$_SESSION['wsDomain']]; $objectclass = $wgLDAPGroupObjectclass[$_SESSION['wsDomain']]; $attribute = $wgLDAPGroupAttribute[$_SESSION['wsDomain']]; $nameattribute = $wgLDAPGroupNameAttribute[$_SESSION['wsDomain']]; //Search for the groups this user is in $filter = "(&($attribute=" . $this->getLdapEscapedString($dn) . ")(objectclass=$objectclass))"; $this->printDebug("Search string: $filter",2); if ( isset($wgLDAPProxyAgent[$_SESSION['wsDomain']]) ) { //We'll try to bind as the proxyagent as the proxyagent should normally have more //rights than the user. If the proxyagent fails to bind, we will still be able //to search as the normal user (which is why we don't return on fail). $this->printDebug("Binding as the proxyagentDN",1); $bind = $this->bindAs($ldapconn, $wgLDAPProxyAgent[$_SESSION['wsDomain']], $wgLDAPProxyAgentPassword[$_SESSION['wsDomain']]); } $info = @ldap_search($ldapconn, $base, $filter); if ( !$info ) { $this->printDebug("No entries returned from search.",2); //Return an array with two empty arrays so that other functions //don't error out. return array( array(), array() ); } $entries = @ldap_get_entries($ldapconn,$info); //We need to shift because the first entry will be a count array_shift($entries); //Let's get a list of both full dn groups and shortname groups $groups = array(); $shortnamegroups = array(); foreach ($entries as $entry) { $mem = strtolower($entry['dn']); $shortnamemem = strtolower($entry[$nameattribute][0]); array_push($groups,$mem); array_push($shortnamegroups,$shortnamemem); } $both_groups = array(); array_push($both_groups, $groups); array_push($both_groups, $shortnamegroups); $this->printDebug("Returned groups:" . implode(",",$groups) . "",2); $this->printDebug("Returned groups:" . implode(",",$shortnamegroups) . "",2); return $both_groups; } /** * Returns true if this group is in the list of the currently authenticated * user's groups, else false. * * @param string $group * @return bool * @access private */ function hasLDAPGroup( $group ) { $this->printDebug("Entering hasLDAPGroup",1); return in_array( strtolower( $group ), $this->userLDAPGroups ); } /** * Returns true if an LDAP group with this name exists, else false. * * @param string $group * @return bool * @access private */ function isLDAPGroup( $group ) { $this->printDebug("Entering isLDAPGroup",1); return in_array( strtolower( $group ), $this->allLDAPGroups ); } /** * Helper function for updateUser() and initUser(). Adds users into MediaWiki security groups * based upon groups retreived from LDAP. * * @param User $user * @access private */ function setGroups( &$user ) { $this->printDebug("Pulling groups from LDAP.",1); # add groups permissions $localAvailGrps = $user->getAllGroups(); $localUserGrps = $user->getEffectiveGroups(); $this->printDebug("Available groups are: " . implode(",",$localAvailGrps) . "",1); $this->printDebug("Effective groups are: " . implode(",",$localUserGrps) . "",1); # note: $localUserGrps does not need to be updated with $cGroup added, # as $localAvailGrps contains $cGroup only once. foreach ($localAvailGrps as $cGroup) { # did we once add the user to the group? if (in_array($cGroup,$localUserGrps)) { $this->printDebug("Checking to see if we need to remove user from: $cGroup",1); if ((!$this->hasLDAPGroup($cGroup)) && ($this->isLDAPGroup($cGroup))) { $this->printDebug("Removing user from: $cGroup",1); # the ldap group overrides the local group # so as the user is currently not a member of the ldap group, he shall be removed from the local group $user->removeGroup($cGroup); } } else { # no, but maybe the user has recently been added to the ldap group? $this->printDebug("Checking to see if user is in: $cGroup",1); if ($this->hasLDAPGroup($cGroup)) { $this->printDebug("Adding user to: $cGroup",1); # so use the addGroup function $user->addGroup($cGroup); # completedfor $cGroup. } } } } /** * Returns a password that is created via the configured hash settings. * * @param string $password * @return string * @access private */ function getPasswordHash( $password ) { global $wgLDAPPasswordHash; $this->printDebug("Entering getPasswordHash",1); if (isset($wgLDAPPasswordHash[$_SESSION['wsDomain']])) { $hashtouse = $wgLDAPPasswordHash[$_SESSION['wsDomain']]; } else { $hashtouse = ''; } //Set the password hashing based upon admin preference switch ($hashtouse) { case 'crypt': $pass = '{CRYPT}' . crypt($password); break; case 'clear': $pass = $password; break; default: $pwd_md5 = base64_encode(pack('H*',sha1($password))); $pass = "{SHA}".$pwd_md5; break; } $this->printDebug("Password is $pass",2); return $pass; } /** * Prints debugging information. $debugText is what you want to print, $debugVal * is the level at which you want to print the information. * * @param string $debugText * @param string $debugVal * @access private */ function printDebug( $debugText, $debugVal ) { global $wgLDAPDebug; if ($wgLDAPDebug > $debugVal) { echo $debugText . "
"; } } /** * Binds as $userdn with $password. This can be called with only the ldap * connection resource for an anonymous bind. * * @param resourse $ldapconn * @param string $userdn * @param string $password * @return bool * @access private */ function bindAs( $ldapconn, $userdn=null, $password=null ) { //Let's see if the user can authenticate. if ($userdn == null || $password == null) { $bind = @ldap_bind($ldapconn); } else { $bind = @ldap_bind($ldapconn, $userdn, $password); } if (!$bind) { $this->printDebug("Failed to bind as $userdn",1); $this->printDebug("with password: $password",3); return false; } return true; } /** * Returns true if smartcard authentication is allowed, and the user is * authenticating using the smartcard domain. * * @return bool * @access private */ function useSmartcardAuth() { global $wgLDAPUseSmartcardAuth, $wgLDAPSmartcardDomain; return $wgLDAPUseSmartcardAuth && $_SESSION['wsDomain'] == $wgLDAPSmartcardDomain; } /** * Returns a string which has the chars *, (, ), \ & NUL escaped to LDAP compliant * syntax as per RFC 2254 * Thanks and credit to Iain Colledge for the research and function. * * @param string $string * @return string * @access private */ function getLdapEscapedString ($string) { // Make the string LDAP compliant by escaping *, (, ) , \ & NUL return str_replace(array("*","(",")","\\","\x00"),array("\\2a","\\28","\\29","\\5c","\\00"),$string); } } /** * Add extension information to Special:Version */ $wgExtensionCredits['other'][] = array( 'name' => 'LDAP Authentication Plugin', 'version' => '1.1e', 'author' => 'Ryan Lane', 'description' => 'LDAP Authentication plugin with support for multiple LDAP authentication methods', 'url' => 'http://meta.wikimedia.org/wiki/LDAP_Authentication' ); // The following was derived from the SSL Authentication plugin // http://www.mediawiki.org/wiki/SSL_authentication /** * Sets up the SSL authentication piece of the LDAP plugin. * * @access public */ function AutoAuthSetup() { global $wgLDAPSSLUsername; global $wgHooks; global $wgAuth; global $wgLDAPAutoAuthMethod; $wgAuth = new LdapAuthenticationPlugin(); $wgAuth->printDebug("Entering AutoAuthSetup.",1); //We may add quite a few different auto authenticate methods in the //future, let's make it easy to support. switch($wgLDAPAutoAuthMethod) { case "smartcard": $wgAuth->printDebug("Allowing smartcard authentication.",1); $wgAuth->printDebug("wgLDAPSSLUsername = $wgLDAPSSLUsername",2); if($wgLDAPSSLUsername != null) { $wgAuth->printDebug("wgLDAPSSLUsername is not null, adding hooks.",1); $wgHooks['AutoAuthenticate'][] = 'SSLAuth'; /* Hook for magical authN */ $wgHooks['PersonalUrls'][] = 'NoLogout'; /* Disallow logout link */ } break; default: $wgAuth->printDebug("Not using any AutoAuthentication methods .",1); } } /* No logout link in MW */ function NoLogout(&$personal_urls, $title) { $personal_urls['logout'] = null; } /** * Does the SSL authentication piece of the LDAP plugin. * * @access public */ function SSLAuth(&$user) { global $wgLDAPSSLUsername; global $wgUser; global $wgAuth; $wgAuth->printDebug("Entering SSLAuth.",1); //Give us a user, see if we're around $tmpuser = User::LoadFromSession(); //They already with us? If so, quit this function. if($tmpuser->isLoggedIn()) { $wgAuth->printDebug("User is already logged in.",1); return; } //Let regular authentication plugins configure themselves for auto //authentication chaining $wgAuth->autoAuthSetup(); //The user hasn't already been authenticated, let's check them $wgAuth->printDebug("User is not logged in, we need to authenticate",1); $authenticated = $wgAuth->authenticate($wgLDAPSSLUsername); if (!$authenticated) { //If the user doesn't exist in LDAP, there isn't much reason to //go any further. $wgAuth->printDebug("User wasn't found in LDAP, exiting.",1); return; } //We need the username that MediaWiki will always use, *not* the one we //get from LDAP. $mungedUsername = $wgAuth->getCanonicalName($wgLDAPSSLUsername); $wgAuth->printDebug("User exists in LDAP; finding the user by name in MediaWiki.",1); //Is the user already in the database? $tmpuser = User::newFromName($mungedUsername); if ( $tmpuser == null ) { $wgAuth->printDebug("Username is not a valid MediaWiki username.",1); return; } //If exists, log them in if($tmpuser->getID() != 0) { $wgAuth->printDebug("User exists in local database, logging in.",1); $wgUser = &$tmpuser; $wgAuth->updateUser($wgUser); $wgUser->setCookies(); $wgUser->setupSession(); return; } $wgAuth->printDebug("User does not exist in local database; creating.",1); //Require SpecialUserlogin so that we can get a loginForm require_once('SpecialUserlogin.php'); //This section contains a silly hack for MW global $wgLang; global $wgContLang; global $wgRequest; if(!isset($wgLang)) { $wgLang = $wgContLang; $wgLangUnset = true; } $wgAuth->printDebug("Creating LoginForm.",1); //This creates our form that'll let us create a new user in the database $lf = new LoginForm($wgRequest); //The user we'll be creating... $wgUser = &$tmpuser; $wgUser->setName($wgContLang->ucfirst($mungedUsername)); $wgAuth->printDebug("Creating User.",1); //Create the user $lf->initUser($wgUser); //Initialize the user $wgUser->setupSession(); $wgUser->setCookies(); } ?>