# # 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., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # http://www.gnu.org/copyleft/gpl.html /** * LdapAuthentication plugin. LDAP Authentication and authorization integration with MediaWiki. * * @file * @ingroup MediaWiki */ # # LdapAuthentication.php # # Info available at http://www.mediawiki.org/wiki/Extension:LDAP_Authentication # Support is available at http://www.mediawiki.org/wiki/Extension_talk:LDAP_Authentication # if ( !defined( 'MEDIAWIKI' ) ) exit; $wgLDAPDomainNames = array(); $wgLDAPServerNames = array(); $wgLDAPUseLocal = false; $wgLDAPEncryptionType = array(); $wgLDAPOptions = array(); $wgLDAPPort = array(); $wgLDAPSearchStrings = array(); $wgLDAPProxyAgent = array(); $wgLDAPProxyAgentPassword = array(); $wgLDAPSearchAttributes = array(); $wgLDAPBaseDNs = array(); $wgLDAPGroupBaseDNs = array(); $wgLDAPUserBaseDNs = array(); $wgLDAPWriterDN = array(); $wgLDAPWriterPassword = array(); $wgLDAPWriteLocation = array(); $wgLDAPAddLDAPUsers = array(); $wgLDAPUpdateLDAP = array(); $wgLDAPPasswordHash = array(); $wgLDAPMailPassword = array(); $wgLDAPPreferences = array(); $wgLDAPDisableAutoCreate = array(); $wgLDAPDebug = 0; $wgLDAPGroupUseFullDN = array(); $wgLDAPLowerCaseUsername = array(); $wgLDAPGroupUseRetrievedUsername = array(); $wgLDAPGroupObjectclass = array(); $wgLDAPGroupAttribute = array(); $wgLDAPGroupNameAttribute = array(); $wgLDAPGroupsUseMemberOf = array(); $wgLDAPUseLDAPGroups = array(); $wgLDAPLocallyManagedGroups = array(); $wgLDAPGroupsPrevail = array(); $wgLDAPRequiredGroups = array(); $wgLDAPExcludedGroups = array(); $wgLDAPGroupSearchNestedGroups = array(); $wgLDAPAuthAttribute = array(); $wgLDAPAutoAuthUsername = ""; $wgLDAPAutoAuthDomain = ""; $wgPasswordResetRoutes['domain'] = true; $wgLDAPActiveDirectory = array(); define( "LDAPAUTHVERSION", "2.0f" ); /** * Add extension information to Special:Version */ $wgExtensionCredits['other'][] = array( 'path' => __FILE__, 'name' => 'LDAP Authentication Plugin', 'version' => LDAPAUTHVERSION, 'author' => 'Ryan Lane', 'descriptionmsg' => 'ldapauthentication-desc', 'url' => 'https://www.mediawiki.org/wiki/Extension:LDAP_Authentication', ); $dir = dirname( __FILE__ ) . '/'; $wgExtensionMessagesFiles['LdapAuthentication'] = $dir . 'LdapAuthentication.i18n.php'; # Schema changes $wgHooks['LoadExtensionSchemaUpdates'][] = 'efLdapAuthenticationSchemaUpdates'; $wgRedactedFunctionArguments['LdapAuthenticationPlugin::ldap_bind'] = 2; $wgRedactedFunctionArguments['LdapAuthenticationPlugin::authenticate'] = 2; $wgRedactedFunctionArguments['LdapAuthenticationPlugin::getPasswordHash'] = 0; $wgRedactedFunctionArguments['LdapAuthenticationPlugin::bindAs'] = 1; $wgRedactedFunctionArguments['LdapAuthenticationPlugin::setOrDefaultPrivate'] = 0; /** * @param $updater DatabaseUpdater * @return bool */ function efLdapAuthenticationSchemaUpdates( $updater ) { $base = dirname( __FILE__ ); switch ( $updater->getDB()->getType() ) { case 'mysql': $updater->addExtensionTable( 'ldap_domains', "$base/schema/ldap-mysql.sql" ); break; case 'postgres': $updater->addExtensionTable( 'ldap_domains', "$base/schema/ldap-postgres.sql" ); break; } return true; } // constants for search base define( "GROUPDN", 0 ); define( "USERDN", 1 ); define( "DEFAULTDN", 2 ); // constants for error reporting define( "NONSENSITIVE", 1 ); define( "SENSITIVE", 2 ); define( "HIGHLYSENSITIVE", 3 ); class LdapAuthenticationPlugin extends AuthPlugin { // ldap connection resource var $ldapconn; // preferences var $email, $lang, $realname, $nickname, $externalid; // username pulled from ldap var $LDAPUsername; // userdn pulled from ldap var $userdn; // groups pulled from ldap var $userLDAPGroups; var $allLDAPGroups; // boolean to test for failed auth var $authFailed; // boolean to test for fetched user info var $fetchedUserInfo; // the user's entry and all attributes var $userInfo; // the user we are currently bound as var $boundAs; /** * Wrapper for ldap_connect * @param null $hostname * @param int $port * @return resource|false */ public static function ldap_connect( $hostname=null, $port=389 ) { wfSuppressWarnings(); $ret = ldap_connect( $hostname, $port ); wfRestoreWarnings(); return $ret; } /** * Wrapper for ldap_bind * @param $ldapconn * @param null $dn * @param null $password * @return bool */ public static function ldap_bind( $ldapconn, $dn=null, $password=null ) { wfSuppressWarnings(); $ret = ldap_bind( $ldapconn, $dn, $password ); wfRestoreWarnings(); return $ret; } /** * Wrapper for ldap_unbind * @param $ldapconn * @return bool */ public static function ldap_unbind( $ldapconn ) { if ( $ldapconn ) { wfSuppressWarnings(); $ret = ldap_unbind( $ldapconn ); wfRestoreWarnings(); } else { $ret = false; } return $ret; } /** * Wrapper for ldap_modify * @param $ldapconn * @param $dn * @param $entry * @return bool */ public static function ldap_modify( $ldapconn, $dn, $entry ) { wfSuppressWarnings(); $ret = ldap_modify( $ldapconn, $dn, $entry ); wfRestoreWarnings(); return $ret; } /** * Wrapper for ldap_add * @param $ldapconn * @param $dn * @param $entry * @return bool */ public static function ldap_add( $ldapconn, $dn, $entry ) { wfSuppressWarnings(); $ret = ldap_add( $ldapconn, $dn, $entry ); wfRestoreWarnings(); return $ret; } /** * Wrapper for ldap_delete * @param $ldapconn * @param $dn * @return bool */ public static function ldap_delete( $ldapconn, $dn ) { wfSuppressWarnings(); $ret = ldap_delete( $ldapconn, $dn ); wfRestoreWarnings(); return $ret; } /** * Wrapper for ldap_search * @param $ldapconn * @param $basedn * @param $filter * @param array|null $attributes * @param null $attrsonly * @param null $sizelimit * @param null $timelimit * @param null $deref * @return resource */ public static function ldap_search( $ldapconn, $basedn, $filter, $attributes=array(), $attrsonly=null, $sizelimit=null, $timelimit=null, $deref=null ) { wfSuppressWarnings(); $ret = ldap_search( $ldapconn, $basedn, $filter, $attributes, $attrsonly, $sizelimit, $timelimit, $deref ); wfRestoreWarnings(); return $ret; } /** * Wrapper for ldap_read * @param $ldapconn * @param $basedn * @param $filter * @param array|null $attributes * @param null $attrsonly * @param null $sizelimit * @param null $timelimit * @param null $deref * @return resource */ public static function ldap_read( $ldapconn, $basedn, $filter, $attributes=array(), $attrsonly=null, $sizelimit=null, $timelimit=null, $deref=null ) { wfSuppressWarnings(); $ret = ldap_read( $ldapconn, $basedn, $filter, $attributes, $attrsonly, $sizelimit, $timelimit, $deref ); wfRestoreWarnings(); return $ret; } /** * Wrapper for ldap_list * @param $ldapconn * @param $basedn * @param $filter * @param array|null $attributes * @param null $attrsonly * @param null $sizelimit * @param null $timelimit * @param null $deref * @return \resource */ public static function ldap_list( $ldapconn, $basedn, $filter, $attributes=array(), $attrsonly=null, $sizelimit=null, $timelimit=null, $deref=null ) { wfSuppressWarnings(); $ret = ldap_list( $ldapconn, $basedn, $filter, $attributes, $attrsonly, $sizelimit, $timelimit, $deref ); wfRestoreWarnings(); return $ret; } /** * Wrapper for ldap_get_entries * @param $ldapconn * @param $resultid * @return array */ public static function ldap_get_entries( $ldapconn, $resultid ) { wfSuppressWarnings(); $ret = ldap_get_entries( $ldapconn, $resultid ); wfRestoreWarnings(); return $ret; } /** * Wrapper for ldap_count_entries * @param $ldapconn * @param $resultid * @return int */ public static function ldap_count_entries( $ldapconn, $resultid ) { wfSuppressWarnings(); $ret = ldap_count_entries( $ldapconn, $resultid ); wfRestoreWarnings(); return $ret; } /** * Wrapper for ldap_errno * @param $ldapconn * @return int */ public static function ldap_errno( $ldapconn ) { wfSuppressWarnings(); $ret = ldap_errno( $ldapconn ); wfRestoreWarnings(); return $ret; } /** * Get configuration defined by admin, or return default value * * @param string $preference * @param string $domain * @return mixed */ public function getConf( $preference, $domain='' ) { # Global preferences switch ( $preference ) { case 'DomainNames': global $wgLDAPDomainNames; return $wgLDAPDomainNames; case 'UseLocal': global $wgLDAPUseLocal; return $wgLDAPUseLocal; case 'AutoAuthUsername': global $wgLDAPAutoAuthUsername; return $wgLDAPAutoAuthUsername; case 'AutoAuthDomain': global $wgLDAPAutoAuthDomain; return $wgLDAPAutoAuthDomain; } # Domain specific preferences if ( !$domain ) { $domain = $this->getDomain(); } switch ( $preference ) { case 'ServerNames': global $wgLDAPServerNames; return self::setOrDefault( $wgLDAPServerNames, $domain ); case 'EncryptionType': global $wgLDAPEncryptionType; return self::setOrDefault( $wgLDAPEncryptionType, $domain, 'tls' ); case 'Options': global $wgLDAPOptions; return self::setOrDefault( $wgLDAPOptions, $domain, array() ); case 'Port': global $wgLDAPPort; if ( isset( $wgLDAPPort[$domain] ) ) { $this->printDebug( "Using non-standard port: " . $wgLDAPPort[$domain], SENSITIVE ); return (string)$wgLDAPPort[$domain]; } elseif ( $this->getConf( 'EncryptionType' ) == 'ssl' ) { return "636"; } else { return "389"; } case 'SearchString': global $wgLDAPSearchStrings; return self::setOrDefault( $wgLDAPSearchStrings, $domain ); case 'ProxyAgent': global $wgLDAPProxyAgent; return self::setOrDefault( $wgLDAPProxyAgent, $domain ); case 'ProxyAgentPassword': global $wgLDAPProxyAgentPassword; return self::setOrDefaultPrivate( $wgLDAPProxyAgentPassword, $domain ); case 'SearchAttribute': global $wgLDAPSearchAttributes; return self::setOrDefault( $wgLDAPSearchAttributes, $domain ); case 'BaseDN': global $wgLDAPBaseDNs; return self::setOrDefault( $wgLDAPBaseDNs, $domain ); case 'GroupBaseDN': global $wgLDAPGroupBaseDNs; return self::setOrDefault( $wgLDAPGroupBaseDNs, $domain ); case 'UserBaseDN': global $wgLDAPUserBaseDNs; return self::setOrDefault( $wgLDAPUserBaseDNs, $domain ); case 'WriterDN': global $wgLDAPWriterDN; return self::setOrDefault( $wgLDAPWriterDN, $domain ); case 'WriterPassword': global $wgLDAPWriterPassword; return self::setOrDefaultPrivate( $wgLDAPWriterPassword, $domain ); case 'WriteLocation': global $wgLDAPWriteLocation; return self::setOrDefault( $wgLDAPWriteLocation, $domain ); case 'AddLDAPUsers': global $wgLDAPAddLDAPUsers; return self::setOrDefault( $wgLDAPAddLDAPUsers, $domain, false ); case 'UpdateLDAP': global $wgLDAPUpdateLDAP; return self::setOrDefault( $wgLDAPUpdateLDAP, $domain, false ); case 'PasswordHash': global $wgLDAPPasswordHash; return self::setOrDefaultPrivate( $wgLDAPPasswordHash, $domain, 'clear' ); case 'MailPassword': global $wgLDAPMailPassword; return self::setOrDefaultPrivate( $wgLDAPMailPassword, $domain, false ); case 'Preferences': global $wgLDAPPreferences; return self::setOrDefault( $wgLDAPPreferences, $domain, array() ); case 'DisableAutoCreate': global $wgLDAPDisableAutoCreate; return self::setOrDefault( $wgLDAPDisableAutoCreate, $domain, false ); case 'GroupUseFullDN': global $wgLDAPGroupUseFullDN; return self::setOrDefault( $wgLDAPGroupUseFullDN, $domain, false ); case 'LowerCaseUsername': global $wgLDAPLowerCaseUsername; // Default set to true for backwards compatibility with // versions < 2.0a return self::setOrDefault( $wgLDAPLowerCaseUsername, $domain, true ); case 'GroupUseRetrievedUsername': global $wgLDAPGroupUseRetrievedUsername; return self::setOrDefault( $wgLDAPGroupUseRetrievedUsername, $domain, false ); case 'GroupObjectclass': global $wgLDAPGroupObjectclass; return self::setOrDefault( $wgLDAPGroupObjectclass, $domain ); case 'GroupAttribute': global $wgLDAPGroupAttribute; return self::setOrDefault( $wgLDAPGroupAttribute, $domain ); case 'GroupNameAttribute': global $wgLDAPGroupNameAttribute; return self::setOrDefault( $wgLDAPGroupNameAttribute, $domain ); case 'GroupsUseMemberOf': global $wgLDAPGroupsUseMemberOf; return self::setOrDefault( $wgLDAPGroupsUseMemberOf, $domain, false ); case 'UseLDAPGroups': global $wgLDAPUseLDAPGroups; return self::setOrDefault( $wgLDAPUseLDAPGroups, $domain, false ); case 'LocallyManagedGroups': global $wgLDAPLocallyManagedGroups; return self::setOrDefault( $wgLDAPLocallyManagedGroups, $domain, array() ); case 'GroupsPrevail': global $wgLDAPGroupsPrevail; return self::setOrDefault( $wgLDAPGroupsPrevail, $domain, false ); case 'RequiredGroups': global $wgLDAPRequiredGroups; return self::setOrDefault( $wgLDAPRequiredGroups, $domain, array() ); case 'ExcludedGroups': global $wgLDAPExcludedGroups; return self::setOrDefault( $wgLDAPExcludedGroups, $domain, array() ); case 'GroupSearchNestedGroups': global $wgLDAPGroupSearchNestedGroups; return self::setOrDefault( $wgLDAPGroupSearchNestedGroups, $domain, false ); case 'AuthAttribute': global $wgLDAPAuthAttribute; return self::setOrDefault( $wgLDAPAuthAttribute, $domain ); case 'ActiveDirectory': global $wgLDAPActiveDirectory; return self::setOrDefault( $wgLDAPActiveDirectory, $domain, false ); } return ''; } /** * Returns the item from $array at index $key if it is set, * else, it returns $default * * @param $array array * @param $key * @param $default mixed * @return mixed */ private static function setOrDefault( $array, $key, $default = '' ) { return isset( $array[$key] ) ? $array[$key] : $default; } /** * Returns the item from $array at index $key if it is set, * else, it returns $default * * Use for sensitive data * * @param $array array * @param $key * @param $default mixed * @return mixed */ private static function setOrDefaultPrivate( $array, $key, $default = '' ) { return isset( $array[$key] ) ? $array[$key] : $default; } /** * 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 */ public function userExists( $username ) { $this->printDebug( "Entering userExists", NONSENSITIVE ); // 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 auto authentication. if ( !$this->getConf( 'AddLDAPUsers' ) && !$this->useAutoAuth() ) { return true; } $ret = false; if ( $this->connect() ) { $searchstring = $this->getSearchString( $username ); // If we are using auto authentication, and we got // anything back, then the user exists. if ( $this->useAutoAuth() && $searchstring != '' ) { $ret = true; } else { // Search for the entry. $entry = LdapAuthenticationPlugin::ldap_read( $this->ldapconn, $searchstring, "objectclass=*" ); if ( $entry ) { $this->printDebug( "Found a matching user in LDAP", NONSENSITIVE ); $ret = true; } else { $this->printDebug( "Did not find a matching user in LDAP", NONSENSITIVE ); } } // getSearchString is going to bind, but will not unbind LdapAuthenticationPlugin::ldap_unbind( $this->ldapconn ); } return $ret; } /** * Connect to LDAP * @param string $domain * @return bool */ public function connect( $domain='' ) { $this->printDebug( "Entering Connect", NONSENSITIVE ); if ( !function_exists( 'ldap_connect' ) ) { $this->printDebug( "It looks like you are missing LDAP support; please ensure you have either compiled LDAP " . "support in, or have enabled the module. If the authentication is working for you, the plugin isn't properly " . "detecting the LDAP module, and you can safely ignore this message.", NONSENSITIVE ); return false; } // Set the server string depending on whether we use ssl or not $encryptionType = $this->getConf( 'EncryptionType', $domain ); switch( $encryptionType ) { case "ldapi": $this->printDebug( "Using ldapi", SENSITIVE ); $serverpre = "ldapi://"; break; case "ssl": $this->printDebug( "Using SSL", SENSITIVE ); $serverpre = "ldaps://"; break; default: $this->printDebug( "Using TLS or not using encryption.", SENSITIVE ); $serverpre = "ldap://"; } // Make a space separated list of server strings with the connection type // string added. $servers = ""; $tmpservers = $this->getConf( 'ServerNames', $domain ); $tok = strtok( $tmpservers, " " ); while ( $tok ) { $servers = $servers . " " . $serverpre . $tok . ":" . $this->getConf( 'Port', $domain ); $tok = strtok( " " ); } $servers = rtrim( $servers ); $this->printDebug( "Using servers: $servers", SENSITIVE ); // Connect and set options $this->ldapconn = LdapAuthenticationPlugin::ldap_connect( $servers ); if ( !$this->ldapconn ) { $this->printDebug( "PHP's LDAP connect method returned null, this likely implies a misconfiguration of the plugin.", NONSENSITIVE ); return false; } ldap_set_option( $this->ldapconn, LDAP_OPT_PROTOCOL_VERSION, 3 ); ldap_set_option( $this->ldapconn, LDAP_OPT_REFERRALS, 0 ); foreach ( $this->getConf( 'Options' ) as $key => $value ) { if ( !ldap_set_option( $this->ldapconn, constant( $key ), $value ) ) { $this->printDebug( "Can't set option to LDAP! Option code and value: " . $key . "=" . $value, 1 ); } } // TLS needs to be started after the connection resource is available if ( $encryptionType == "tls" ) { $this->printDebug( "Using TLS", SENSITIVE ); if ( !ldap_start_tls( $this->ldapconn ) ) { $this->printDebug( "Failed to start TLS.", SENSITIVE ); return false; } } $this->printDebug( "PHP's LDAP connect method returned true (note, this does not imply it connected to the server).", NONSENSITIVE ); return true; } /** * 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 */ public function authenticate( $username, $password = '' ) { $this->printDebug( "Entering authenticate for username $username", NONSENSITIVE ); // We don't handle local authentication if ( 'local' == $this->getDomain() ) { $this->printDebug( "User is using a local domain", SENSITIVE ); return false; } // Mediawiki munges the username before authenticate is called, // this can mess with authentication, group pulling/restriction, // preference pulling, etc. Let's allow the admin to use // a lowercased username if needed. if ( $this->getConf( 'LowerCaseUsername') ) { $username = strtolower( $username ); } // If the user is using auto 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 auto-authentication method. if ( $this->useAutoAuth() && $this->getConf( 'AutoAuthUsername' ) != $username ) { $this->printDebug( "The username provided ($username) doesn't match the username provided by the webserver (" . $this->getConf( 'AutoAuthUsername' ) . "). The user is probably trying to log in to the auto-authentication domain with password authentication via the wiki. Denying access.", SENSITIVE ); 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. // auto-authentication is handled by the webserver; a blank password // here is wanted. if ( '' == $password && !$this->useAutoAuth() ) { $this->printDebug( "User used a blank password", NONSENSITIVE ); return false; } if ( $this->connect() ) { $this->userdn = $this->getSearchString( $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 ( '' == $this->userdn ) { $this->printDebug( "User DN is blank", NONSENSITIVE ); LdapAuthenticationPlugin::ldap_unbind( $this->ldapconn ); $this->markAuthFailed(); return false; } // If we are using password authentication, we need to bind as the // user to make sure the password is correct. if ( !$this->useAutoAuth() ) { $this->printDebug( "Binding as the user", NONSENSITIVE ); $bind = $this->bindAs( $this->userdn, $password ); if ( !$bind ) { $this->markAuthFailed(); return false; } $result = true; wfRunHooks( 'ChainAuth', array( $username, $password, &$result ) ); if ( $result == false ) { return false; } $this->printDebug( "Bound successfully", NONSENSITIVE ); $ss = $this->getConf( 'SearchString' ); if ( $ss ) { 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. $this->userdn = $this->getUserDN( $username ); $this->printDebug( "Fetched UserDN: $this->userdn", NONSENSITIVE ); } else { // Now that we are bound, we can pull the user's info. $this->getUserInfo(); } } } // Ensure the user's entry has the required auth attribute $aa = $this->getConf( 'AuthAttribute' ); if ( $aa ) { $this->printDebug( "Checking for auth attributes: $aa", NONSENSITIVE ); $filter = "(" . $aa . ")"; $attributes = array( "dn" ); $entry = LdapAuthenticationPlugin::ldap_read( $this->ldapconn, $this->userdn, $filter, $attributes ); $info = LdapAuthenticationPlugin::ldap_get_entries( $this->ldapconn, $entry ); if ( $info["count"] < 1 ) { $this->printDebug( "Failed auth attribute check", NONSENSITIVE ); LdapAuthenticationPlugin::ldap_unbind( $this->ldapconn ); $this->markAuthFailed(); return false; } } $this->getGroups( $username ); if ( !$this->checkGroups() ) { LdapAuthenticationPlugin::ldap_unbind( $this->ldapconn ); $this->markAuthFailed(); return false; } $this->getPreferences(); LdapAuthenticationPlugin::ldap_unbind( $this->ldapconn ); } else { $this->markAuthFailed(); return false; } $this->printDebug( "Authentication passed", NONSENSITIVE ); // We made it this far; the user authenticated and didn't fail any checks, so he/she gets in. return true; } function markAuthFailed() { $this->authFailed = true; } /** * Modify options in the login template. * * @param UserLoginTemplate $template * @param $type */ public function modifyUITemplate( &$template, &$type ) { $this->printDebug( "Entering modifyUITemplate", NONSENSITIVE ); $template->set( 'create', $this->getConf( 'AddLDAPUsers' ) ); $template->set( 'usedomain', true ); $template->set( 'useemail', $this->getConf( 'MailPassword' ) ); $template->set( 'canreset', $this->getConf( 'MailPassword' ) ); $template->set( 'domainnames', $this->domainList() ); wfRunHooks( 'LDAPModifyUITemplate', array( &$template ) ); } /** * @return array */ function domainList() { $tempDomArr = $this->getConf( 'DomainNames' ); if ( $this->getConf( 'UseLocal' ) ) { $this->printDebug( "Allowing the local domain, adding it to the list.", NONSENSITIVE ); array_push( $tempDomArr, 'local' ); } if ( $this->getConf( 'AutoAuthDomain' ) ) { $this->printDebug( "Allowing auto-authentication login, removing the domain from the list.", NONSENSITIVE ); // There is no reason for people to log in directly to the wiki if the are using an // auto-authentication domain. If they try to, they are probably up to something fishy. unset( $tempDomArr[array_search( $this->getConf( 'AutoAuthDomain' ), $tempDomArr )] ); } $domains = array(); foreach ( $tempDomArr as $tempDom ) { $domains["$tempDom"] = $tempDom; } return $domains; } /** * 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 */ public function autoCreate() { return !$this->getConf( 'DisableAutoCreate' ); } /** * Set the given password in LDAP. * Return true if successful. * * @param User $user * @param string $password * @return bool */ public function setPassword( $user, $password ) { $this->printDebug( "Entering setPassword", NONSENSITIVE ); if ( $this->getDomain() == 'local' ) { $this->printDebug( "User is using a local domain", NONSENSITIVE ); // We don't set local passwords, but we don't want the wiki // to send the user a failure. return true; } if ( !$this->getConf( 'UpdateLDAP' ) ) { $this->printDebug( "Wiki is set to not allow updates", NONSENSITIVE ); // We aren't allowing the user to change his/her own password return false; } $writer = $this->getConf( 'WriterDN' ); if ( !$writer ) { $this->printDebug( "Wiki doesn't have wgLDAPWriterDN set", NONSENSITIVE ); // We can't change a user's password without an account that is // allowed to do it. return false; } $pass = $this->getPasswordHash( $password ); if ( $this->connect() ) { $this->userdn = $this->getSearchString( $user->getName() ); $this->printDebug( "Binding as the writerDN", NONSENSITIVE ); $bind = $this->bindAs( $writer, $this->getConf( 'WriterPassword' ) ); 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. // This doesn't do anything. $password isn't by reference $password = ''; $success = LdapAuthenticationPlugin::ldap_modify( $this->ldapconn, $this->userdn, $values ); LdapAuthenticationPlugin::ldap_unbind( $this->ldapconn ); if ( $success ) { $this->printDebug( "Successfully modified the user's password", NONSENSITIVE ); return true; } $this->printDebug( "Failed to modify the user's password", NONSENSITIVE ); } return false; } /** * Update user information in LDAP * Return true if successful. * * @param User $user * @return bool */ public function updateExternalDB( $user ) { global $wgMemc; $this->printDebug( "Entering updateExternalDB", NONSENSITIVE ); if ( !$this->getConf( 'UpdateLDAP' ) || $this->getDomain() == 'local' ) { $this->printDebug( "Either the user is using a local domain, or the wiki isn't allowing updates", NONSENSITIVE ); // We don't handle local preferences, but we don't want the // wiki to return an error. return true; } $writer = $this->getConf( 'WriterDN' ); if ( !$writer ) { $this->printDebug( "The wiki doesn't have wgLDAPWriterDN set", NONSENSITIVE ); // 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->lang = $user->getOption( 'language' ); if ( $this->connect() ) { $this->userdn = $this->getSearchString( $user->getName() ); $this->printDebug( "Binding as the writerDN", NONSENSITIVE ); $bind = $this->bindAs( $writer, $this->getConf( 'WriterPassword' ) ); if ( !$bind ) { return false; } $values = array(); $prefs = $this->getConf( 'Preferences' ); foreach ( array_keys( $prefs ) as $key ) { $attr = strtolower( $prefs[$key] ); switch ( $key ) { case "email": if ( is_string( $this->email ) ) { $values[$attr] = $this->email; } break; case "nickname": if ( is_string( $this->nickname ) ) { $values[$attr] = $this->nickname; } break; case "realname": if ( is_string( $this->realname ) ) { $values[$attr] = $this->realname; } break; case "language": if ( is_string( $this->lang ) ) { $values[$attr] = $this->lang; } break; } } if ( count( $values ) && LdapAuthenticationPlugin::ldap_modify( $this->ldapconn, $this->userdn, $values ) ) { // We changed the user, we need to invalidate the memcache key $key = wfMemcKey( 'ldapauthentication', 'userinfo', $this->userdn ); $wgMemc->delete( $key ); $this->printDebug( "Successfully modified the user's attributes", NONSENSITIVE ); LdapAuthenticationPlugin::ldap_unbind( $this->ldapconn ); return true; } $this->printDebug( "Failed to modify the user's attributes", NONSENSITIVE ); LdapAuthenticationPlugin::ldap_unbind( $this->ldapconn ); } return false; } /** * Can the wiki create accounts in LDAP? * Return true if yes. * * @return bool */ public function canCreateAccounts() { return $this->getConf( 'AddLDAPUsers' ); } /** * Can the wiki change passwords in LDAP, or can the user * change passwords locally? * Return true if yes. * * @return bool */ public function allowPasswordChange() { $this->printDebug( "Entering allowPasswordChange", NONSENSITIVE ); // Local domains need to be able to change passwords if ( $this->getConf( 'UseLocal' ) && 'local' == $this->getDomain() ) { return true; } if ( $this->getConf( 'UpdateLDAP' ) || $this->getConf( 'MailPassword' ) ) { return true; } return false; } /** * Disallow MediaWiki from setting local passwords in the database, * unless UseLocal is true. Warning: if you set $wgLDAPUseLocal, * it will cause MediaWiki to leak LDAP passwords into the local database. */ public function allowSetLocalPassword() { return $this->getConf( 'UseLocal'); } /** * Add a user to LDAP. * Return true if successful. * * @param User $user * @param string $password * @param string $email * @param string $realname * @return bool */ public function addUser( $user, $password, $email = '', $realname = '' ) { $this->printDebug( "Entering addUser", NONSENSITIVE ); if ( !$this->getConf( 'AddLDAPUsers' ) || 'local' == $this->getDomain() ) { $this->printDebug( "Either the user is using a local domain, or the wiki isn't allowing users to be added to LDAP", NONSENSITIVE ); // Tell the wiki not to return an error. return true; } if ( $this->getConf( 'RequiredGroups' ) ) { $this->printDebug( "The wiki is requiring users to be in specific groups, and cannot add users as this would be a security hole.", NONSENSITIVE ); // 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; } $writer = $this->getConf( 'WriterDN' ); if ( !$writer ) { $this->printDebug( "The wiki doesn't have wgLDAPWriterDN set", NONSENSITIVE ); // 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(); if ( $this->getConf( 'LowerCaseUsername' ) ) { $username = strtolower( $username ); } $pass = $this->getPasswordHash( $password ); if ( $this->connect() ) { $writeloc = $this->getConf( 'WriteLocation' ); $this->userdn = $this->getSearchString( $username ); if ( '' == $this->userdn ) { $this->printDebug( "userdn is blank, attempting to use wgLDAPWriteLocation", NONSENSITIVE ); if ( $writeloc ) { $this->printDebug( "wgLDAPWriteLocation is set, using that", NONSENSITIVE ); $this->userdn = $this->getConf( 'SearchAttribute' ) . "=" . $username . "," . $writeloc; } else { $this->printDebug( "wgLDAPWriteLocation is not set, failing", NONSENSITIVE ); // getSearchString will bind, but will not unbind LdapAuthenticationPlugin::ldap_unbind( $this->ldapconn ); return false; } } $this->printDebug( "Binding as the writerDN", NONSENSITIVE ); $bind = $this->bindAs( $writer, $this->getConf( 'WriterPassword' ) ); if ( !$bind ) { $this->printDebug( "Failed to bind as the writerDN; add failed", NONSENSITIVE ); return false; } // Set up LDAP objectclasses and attributes // TODO: make objectclasses and attributes configurable $values["uid"] = $username; // sn is required for objectclass inetorgperson $values["sn"] = $username; $prefs = $this->getConf( 'Preferences' ); foreach ( array_keys( $prefs ) as $key ) { $attr = strtolower( $prefs[$key] ); switch ( $key ) { case "email": if ( is_string( $this->email ) ) { $values[$attr] = $this->email; } break; case "nickname": if ( is_string( $this->nickname ) ) { $values[$attr] = $this->nickname; } break; case "realname": if ( is_string( $this->realname ) ) { $values[$attr] = $this->realname; } break; case "language": if ( is_string( $this->lang ) ) { $values[$attr] = $this->lang; } break; } } if ( !array_key_exists( "cn", $values ) ) { $values["cn"] = $username; } $values["userpassword"] = $pass; $values["objectclass"] = array( "inetorgperson" ); $result = true; # Let other extensions modify the user object before creation wfRunHooks( 'LDAPSetCreationValues', array( $this, $username, &$values, $writeloc, &$this->userdn, &$result ) ); if ( !$result ) { $this->printDebug( "Failed to add user because LDAPSetCreationValues returned false", NONSENSITIVE ); LdapAuthenticationPlugin::ldap_unbind( $this->ldapconn ); return false; } $this->printDebug( "Adding user", NONSENSITIVE ); if ( LdapAuthenticationPlugin::ldap_add( $this->ldapconn, $this->userdn, $values ) ) { $this->printDebug( "Successfully added user", NONSENSITIVE ); LdapAuthenticationPlugin::ldap_unbind( $this->ldapconn ); return true; } $errno = LdapAuthenticationPlugin::ldap_errno( $this->ldapconn ); # Constraint violation, let's allow other plugins a chance to retry if ( $errno === 19 ) { $result = false; wfRunHooks( 'LDAPRetrySetCreationValues', array( $this, $username, &$values, $writeloc, &$this->userdn, &$result ) ); if ( $result && LdapAuthenticationPlugin::ldap_add( $this->ldapconn, $this->userdn, $values ) ) { $this->printDebug( "Successfully added user", NONSENSITIVE ); LdapAuthenticationPlugin::ldap_unbind( $this->ldapconn ); return true; } } $this->printDebug( "Failed to add user", NONSENSITIVE ); LdapAuthenticationPlugin::ldap_unbind( $this->ldapconn ); } return false; } /** * Set the domain this plugin is supposed to use when authenticating. * * @param string $domain */ public function setDomain( $domain ) { $this->printDebug( "Setting domain as: $domain", NONSENSITIVE ); $_SESSION['wsDomain'] = $domain; } /** * Get the user's domain * * @return string */ public function getDomain() { global $wgUser; $this->printDebug( "Entering getDomain", NONSENSITIVE ); # If there's only a single domain set, there's no reason # to bother with sessions, tokens, etc.. This works around # a number of bugs caused by supporting multiple domains. # The bugs will still exist when using multiple domains, # though. $domainNames = $this->getConf( 'DomainNames' ); if ( ( count( $domainNames ) === 1 ) && !$this->getConf( 'UseLocal' ) ) { return $domainNames[0]; } # First check if we already have a valid domain set if ( isset( $_SESSION['wsDomain'] ) && $_SESSION['wsDomain'] != 'invaliddomain' ) { $this->printDebug( "Pulling domain from session.", NONSENSITIVE ); return $_SESSION['wsDomain']; } # If the session domain isn't set, the user may have been logged # in with a token, check the user options. # If $wgUser isn't defined yet, it might be due to an LDAPAutoAuthDomain config. if ( isset( $wgUser ) && $wgUser->isLoggedIn() && $wgUser->getToken( false ) ) { $this->printDebug( "Pulling domain from user options.", NONSENSITIVE ); $domain = self::loadDomain( $wgUser ); if ( $domain ) { return $domain; } } # The user must be using an invalid domain $this->printDebug( "No domain found, returning invaliddomain", NONSENSITIVE ); return 'invaliddomain'; } /** * Check to see if the specific domain is a valid domain. * Return true if the domain is valid. * * @param string $domain * @return bool */ public function validDomain( $domain ) { $this->printDebug( "Entering validDomain", NONSENSITIVE ); if ( in_array( $domain, $this->getConf( 'DomainNames' ) ) || ( $this->getConf( 'UseLocal' ) && 'local' == $domain ) ) { $this->printDebug( "User is using a valid domain ($domain).", NONSENSITIVE ); return true; } $this->printDebug( "User is not using a valid domain ($domain).", NONSENSITIVE ); return false; } /** * When a user logs in, update user with information from LDAP. * * @param $user User * TODO: fix the setExternalID stuff */ public function updateUser( &$user ) { $this->printDebug( "Entering updateUser", NONSENSITIVE ); if ( $this->authFailed ) { $this->printDebug( "User didn't successfully authenticate, exiting.", NONSENSITIVE ); return; } if ( $this->getConf( 'Preferences' ) ) { $this->printDebug( "Setting user preferences.", NONSENSITIVE ); if ( is_string( $this->lang ) ) { $this->printDebug( "Setting language.", NONSENSITIVE ); $user->setOption( 'language', $this->lang ); } if ( is_string( $this->nickname ) ) { $this->printDebug( "Setting nickname.", NONSENSITIVE ); $user->setOption( 'nickname', $this->nickname ); } if ( is_string( $this->realname ) ) { $this->printDebug( "Setting realname.", NONSENSITIVE ); $user->setRealName( $this->realname ); } if ( is_string( $this->email ) ) { $this->printDebug( "Setting email.", NONSENSITIVE ); $user->setEmail( $this->email ); $user->confirmEmail(); } } if ( $this->getConf( 'UseLDAPGroups' ) ) { $this->printDebug( "Setting user groups.", NONSENSITIVE ); $this->setGroups( $user ); } # We must set a user option if we want token based logins to work if ( $user->getToken( false ) ) { $this->printDebug( "User has a token, setting domain in user options.", NONSENSITIVE ); self::saveDomain( $user, $_SESSION['wsDomain'] ); } # Let other extensions update the user wfRunHooks( 'LDAPUpdateUser', array( &$user ) ); $this->printDebug( "Saving user settings.", NONSENSITIVE ); $user->saveSettings(); } /** * When creating a user account, initialize user with information from LDAP. * TODO: fix setExternalID stuff * * @param User $user * @param bool $autocreate */ public function initUser( &$user, $autocreate = false ) { $this->printDebug( "Entering initUser", NONSENSITIVE ); if ( $this->authFailed ) { $this->printDebug( "User didn't successfully authenticate, exiting.", NONSENSITIVE ); return; } if ( 'local' == $this->getDomain() ) { $this->printDebug( "User is using a local domain", NONSENSITIVE ); return; } // The update user function does everything else we need done. $this->updateUser( $user ); // updateUser() won't necessarily save the user's settings $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 */ public function strict() { $this->printDebug( "Entering strict.", NONSENSITIVE ); if ( $this->getConf( 'UseLocal' ) || $this->getConf( 'MailPassword' ) ) { $this->printDebug( "Returning false in strict().", NONSENSITIVE ); return false; } $this->printDebug( "Returning true in strict().", NONSENSITIVE ); return true; } /** * Munge the username based on a scheme (lowercase, by default), by search attribute * otherwise. * * @param string $username * @return string */ public function getCanonicalName( $username ) { global $wgMemc; $this->printDebug( "Entering getCanonicalName", NONSENSITIVE ); if ( User::isIP( $username ) ) { $this->printDebug( "Username is an IP, not munging.", NONSENSITIVE ); return $username; } $key = wfMemcKey( 'ldapauthentication', 'canonicalname', $username ); $canonicalname = $username; if ( $username != '' ) { $this->printDebug( "Username is: $username", NONSENSITIVE ); if ( $this->getConf( 'LowerCaseUsername' ) ) { $canonicalname = ucfirst( strtolower( $canonicalname ) ); } else { # Fetch username, so that we can possibly use it. $userInfo = $wgMemc->get( $key ); if ( is_array( $userInfo ) ) { $this->printDebug( "Fetched userInfo from memcache.", NONSENSITIVE ); if ( $userInfo["username"] == $username ) { $this->printDebug( "Username matched a key in memcache, using the fetched name: " . $userInfo["canonicalname"], NONSENSITIVE ); return $userInfo["canonicalname"]; } } if ( $this->validDomain( $this->getDomain() ) && $this->connect() ) { // Try to pull the username from LDAP. In the case of straight binds, // try to fetch the username by search before bind. $this->userdn = $this->getUserDN( $username, true ); $hookSetUsername = $this->LDAPUsername; wfRunHooks( 'SetUsernameAttributeFromLDAP', array( &$hookSetUsername, $this->userInfo ) ); if ( is_string( $hookSetUsername ) ) { $this->printDebug( "Username munged by hook: $hookSetUsername", NONSENSITIVE ); $this->LDAPUsername = $hookSetUsername; } else { $this->printDebug( "Fetched username is not a string (check your hook code...). This message can be safely ignored if you do not have the SetUsernameAttributeFromLDAP hook defined.", NONSENSITIVE ); } } // We want to use the username returned by LDAP // if it exists if ( $this->LDAPUsername != '' ) { $canonicalname = ucfirst( $this->LDAPUsername ); $this->printDebug( "Using LDAPUsername: $canonicalname", NONSENSITIVE ); } $wgMemc->set( $key, array( "username" => $username, "canonicalname" => $canonicalname ), 3600 * 24 ); } } $this->printDebug( "Munged username: $canonicalname", NONSENSITIVE ); return $canonicalname; } /** * Configures the authentication plugin for use with auto-authentication * plugins. */ public function autoAuthSetup() { $this->setDomain( $this->getConf( 'AutoAuthDomain' ) ); } /** * Gets the searchstring for a user based upon settings for the domain. * Returns a full DN for a user. * * @param string $username * @return string * @access private */ function getSearchString( $username ) { $this->printDebug( "Entering getSearchString", NONSENSITIVE ); $ss = $this->getConf( 'SearchString' ); if ( $ss ) { // This is a straight bind $this->printDebug( "Doing a straight bind", NONSENSITIVE ); $userdn = str_replace( "USER-NAME", $username, $ss ); } else { $userdn = $this->getUserDN( $username, true ); } $this->printDebug( "userdn is: $userdn", SENSITIVE ); return $userdn; } /** * Gets the DN of a user based upon settings for the domain. * This function will set $this->LDAPUsername * * @param string $username * @param bool $bind * @param string $searchattr * @return string */ public function getUserDN( $username, $bind=false, $searchattr='' ) { $this->printDebug( "Entering getUserDN", NONSENSITIVE ); if ( $bind ) { // This is a proxy bind, or an anonymous bind with a search $proxyagent = $this->getConf( 'ProxyAgent'); if ( $proxyagent ) { // This is a proxy bind $this->printDebug( "Doing a proxy bind", NONSENSITIVE ); $bind = $this->bindAs( $proxyagent, $this->getConf( 'ProxyAgentPassword' ) ); } else { // This is an anonymous bind $this->printDebug( "Doing an anonymous bind", NONSENSITIVE ); $bind = $this->bindAs(); } if ( !$bind ) { $this->printDebug( "Failed to bind", NONSENSITIVE ); return ''; } } if ( ! $searchattr ) { $searchattr = $this->getConf( 'SearchAttribute' ); } // we need to do a subbase search for the entry $filter = "(" . $searchattr . "=" . $this->getLdapEscapedString( $username ) . ")"; $this->printDebug( "Created a regular filter: $filter", SENSITIVE ); // We explicitly put memberof here because it's an operational attribute in some servers. $attributes = array( "*", "memberof" ); $base = $this->getBaseDN( USERDN ); $this->printDebug( "Using base: $base", SENSITIVE ); $entry = LdapAuthenticationPlugin::ldap_search( $this->ldapconn, $base, $filter, $attributes ); if ( LdapAuthenticationPlugin::ldap_count_entries( $this->ldapconn, $entry ) == 0 ) { $this->printDebug( "Couldn't find an entry", NONSENSITIVE ); $this->fetchedUserInfo = false; return ''; } $this->userInfo = LdapAuthenticationPlugin::ldap_get_entries( $this->ldapconn, $entry ); $this->fetchedUserInfo = true; if ( isset( $this->userInfo[0][$searchattr] ) ) { $username = $this->userInfo[0][$searchattr][0]; $this->printDebug( "Setting the LDAPUsername based on fetched wgLDAPSearchAttributes: $username", NONSENSITIVE ); $this->LDAPUsername = $username; } $userdn = $this->userInfo[0]["dn"]; return $userdn; } /** * Load the current user's entry * * @return bool */ function getUserInfo() { // Don't fetch the same data more than once if ( $this->fetchedUserInfo ) { return true; } $userInfo = $this->getUserInfoStateless( $this->userdn ); if ( is_null( $userInfo ) ) { $this->fetchedUserInfo = false; } else { $this->fetchedUserInfo = true; $this->userInfo = $userInfo; } return $this->fetchedUserInfo; } /** * @param $userdn string * @return array|null */ function getUserInfoStateless( $userdn ) { global $wgMemc; $key = wfMemcKey( 'ldapauthentication', 'userinfo', $userdn ); $userInfo = $wgMemc->get( $key ); if ( !is_array( $userInfo ) ) { $entry = LdapAuthenticationPlugin::ldap_read( $this->ldapconn, $userdn, "objectclass=*", array( '*', 'memberof' ) ); $userInfo = LdapAuthenticationPlugin::ldap_get_entries( $this->ldapconn, $entry ); if ( $userInfo["count"] < 1 ) { return null; } $wgMemc->set( $key, $userInfo, 3600 * 24 ); } return $userInfo; } /** * Retrieve user preferences from LDAP */ private function getPreferences() { $this->printDebug( "Entering getPreferences", NONSENSITIVE ); // Retrieve preferences $prefs = $this->getConf( 'Preferences' ); if ( !$prefs ) { return null; } if ( !$this->getUserInfo() ) { $this->printDebug( "Failed to get preferences, the user's entry wasn't found.", NONSENSITIVE ); return null; } $this->printDebug( "Retrieving preferences", NONSENSITIVE ); foreach ( array_keys( $prefs ) as $key ) { $attr = strtolower( $prefs[$key] ); if ( !isset( $this->userInfo[0][$attr] ) ) { continue; } $value = $this->userInfo[0][$attr][0]; switch ( $key ) { case "email": $this->email = $value; $this->printDebug( "Retrieved email ($this->email) using attribute ($prefs[$key])", NONSENSITIVE ); break; case "language": $this->lang = $value; $this->printDebug( "Retrieved language ($this->lang) using attribute ($prefs[$key])", NONSENSITIVE ); break; case "nickname": $this->nickname = $value; $this->printDebug( "Retrieved nickname ($this->nickname) using attribute ($prefs[$key])", NONSENSITIVE ); break; case "realname": $this->realname = $value; $this->printDebug( "Retrieved realname ($this->realname) using attribute ($prefs[$key])", NONSENSITIVE ); break; } } } /** * Checks to see whether a user is in a required group. * * @return bool */ private function checkGroups() { $this->printDebug( "Entering checkGroups", NONSENSITIVE ); $excgroups = $this->getConf( 'ExcludedGroups' ); if ( $excgroups ) { $this->printDebug( "Checking for excluded group membership", NONSENSITIVE ); for ( $i = 0; $i < count( $excgroups ); $i++ ) { $excgroups[$i] = strtolower( $excgroups[$i] ); } $this->printDebug( "Excluded groups:", NONSENSITIVE, $excgroups ); foreach ( $this->userLDAPGroups["dn"] as $group ) { $this->printDebug( "Checking against: $group", NONSENSITIVE ); if ( in_array( $group, $excgroups ) ) { $this->printDebug( "Found user in an excluded group.", NONSENSITIVE ); return false; } } } $reqgroups = $this->getConf( 'RequiredGroups' ); if ( $reqgroups ) { $this->printDebug( "Checking for (new style) group membership", NONSENSITIVE ); for ( $i = 0; $i < count( $reqgroups ); $i++ ) { $reqgroups[$i] = strtolower( $reqgroups[$i] ); } $this->printDebug( "Required groups:", NONSENSITIVE, $reqgroups ); foreach ( $this->userLDAPGroups["dn"] as $group ) { $this->printDebug( "Checking against: $group", NONSENSITIVE ); if ( in_array( $group, $reqgroups ) ) { $this->printDebug( "Found user in a group.", NONSENSITIVE ); return true; } } $this->printDebug( "Couldn't find the user in any groups.", NONSENSITIVE ); return false; } // Ensure we return true if we aren't checking groups. return true; } /** * Function to get the user's groups. * @param string $username */ protected function getGroups( $username ) { $this->printDebug( "Entering getGroups", NONSENSITIVE ); // Ensure userLDAPGroups is set, no matter what $this->userLDAPGroups = array( "dn"=> array(), "short"=>array() ); // Find groups if ( $this->getConf( 'RequiredGroups' ) || $this->getConf( 'UseLDAPGroups' ) ) { $this->printDebug( "Retrieving LDAP group membership", NONSENSITIVE ); // Let's figure out what we should be searching for if ( $this->getConf( 'GroupUseFullDN' ) ) { $usertopass = $this->userdn; } else { if ( $this->getConf( 'GroupUseRetrievedUsername' ) && $this->LDAPUsername != '' ) { $usertopass = $this->LDAPUsername; } else { $usertopass = $username; } } if ( $this->getConf( 'GroupsUseMemberOf' ) ) { $this->printDebug( "Using memberOf", NONSENSITIVE ); if ( !$this->getUserInfo() ) { $this->printDebug( "Couldn't get the user's entry.", NONSENSITIVE ); } else if ( isset( $this->userInfo[0]["memberof"] ) ) { # The first entry is always a count $memberOfMembers = $this->userInfo[0]["memberof"]; array_shift( $memberOfMembers ); $groups = array( "dn"=> array(), "short"=>array() ); foreach( $memberOfMembers as $mem ) { array_push( $groups["dn"], strtolower( $mem ) ); // Get short name of group $memAttrs = explode( ',', strtolower( $mem ) ); if ( isset( $memAttrs[0] ) ) { $memAttrs = explode( '=', $memAttrs[0] ); if ( isset( $memAttrs[0] ) ) { array_push( $groups["short"], strtolower( $memAttrs[1] ) ); } } } $this->printDebug( "Got the following groups:", SENSITIVE, $groups["dn"] ); $this->userLDAPGroups = $groups; } else { $this->printDebug( "memberOf attribute isn't set", NONSENSITIVE ); } } else { $this->printDebug( "Searching for the groups", NONSENSITIVE ); $this->userLDAPGroups = $this->searchGroups( $usertopass ); if ( $this->getConf( 'GroupSearchNestedGroups' ) ) { $this->userLDAPGroups = $this->searchNestedGroups( $this->userLDAPGroups ); $this->printDebug( "Got the following nested groups:", SENSITIVE, $this->userLDAPGroups["dn"] ); } } // Only find all groups if the user has any groups; otherwise, we are // just wasting a search. if ( $this->getConf( 'GroupsPrevail' ) && count( $this->userLDAPGroups ) != 0 ) { $this->allLDAPGroups = $this->searchGroups( '*' ); } } } /** * Function to return an array of nested groups when given a group or list of groups. * $searchedgroups is used for tail recursion and shouldn't be provided * when called externally. * * @param $groups * @param array $searchedgroups * @return bool * @access private */ function searchNestedGroups( $groups, $searchedgroups = array( "dn" => array(), "short" => array() ) ) { $this->printDebug( "Entering searchNestedGroups", NONSENSITIVE ); // base case, no more groups left to check if ( count( $groups["dn"] ) == 0 ) { $this->printDebug( "No more groups to search.", NONSENSITIVE ); return $searchedgroups; } $this->printDebug( "Searching groups:", SENSITIVE, $groups["dn"] ); $groupstosearch = array( "short" => array(), "dn" => array() ); foreach ( $groups["dn"] as $group ) { $returnedgroups = $this->searchGroups( $group ); $this->printDebug( "Group $group is in the following groups:", SENSITIVE, $returnedgroups["dn"] ); foreach ( $returnedgroups["dn"] as $searchme ) { if ( in_array( $searchme, $searchedgroups["dn"] ) ) { // We already searched this, move on continue; } else { // We'll need to search this group's members now $this->printDebug( "Adding $searchme to the list of groups (1)", SENSITIVE ); $groupstosearch["dn"][] = $searchme; } } foreach ( $returnedgroups["short"] as $searchme ) { if ( in_array( $searchme, $searchedgroups["short"] ) ) { // We already searched this, move on continue; } else { $this->printDebug( "Adding $searchme to the list of groups (2)", SENSITIVE ); // We'll need to search this group's members now $groupstosearch["short"][] = $searchme; } } } $searchedgroups = array_merge_recursive( $groups, $searchedgroups ); return $this->searchNestedGroups( $groupstosearch, $searchedgroups ); } /** * Search groups for the supplied DN * * @param string $dn * @return array */ private function searchGroups( $dn ) { $this->printDebug( "Entering searchGroups", NONSENSITIVE ); $base = $this->getBaseDN( GROUPDN ); $objectclass = $this->getConf( 'GroupObjectclass' ); $attribute = $this->getConf( 'GroupAttribute' ); $nameattribute = $this->getConf( 'GroupNameAttribute' ); // We actually want to search for * not \2a, ensure we don't escape * $value = $dn; if ( $value != "*" ) { $value = $this->getLdapEscapedString( $value ); } $proxyagent = $this->getConf( 'ProxyAgent' ); if ( $proxyagent ) { // 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 proxyagent", NONSENSITIVE ); $this->bindAs( $proxyagent, $this->getConf( 'ProxyAgentPassword' ) ); } $groups = array( "short" => array(), "dn" => array() ); // AD does not include the primary group in the list of groups, we have to find it ourselves. if ( $dn != "*" && $this->getConf('ActiveDirectory')) { $PGfilter = "(&(distinguishedName=$value)(objectclass=user))"; $this->printDebug( "User Filter: $PGfilter", SENSITIVE ); $PGinfo = LdapAuthenticationPlugin::ldap_search( $this->ldapconn, $base, $PGfilter ); $PGentries = LdapAuthenticationPlugin::ldap_get_entries( $this->ldapconn, $PGinfo ); if ( $PGentries ) { $Usid = $PGentries[0]['objectsid'][0]; $PGrid = $PGentries[0]['primarygroupid'][0]; $PGsid = bin2hex( $Usid ); $PGSID = array(); for ( $i=0; $i < 56; $i += 2 ) { $PGSID[] = substr( $PGsid, $i, 2 ); } $dPGrid = dechex( $PGrid ); $dPGrid = str_pad( $dPGrid, 8, '0', STR_PAD_LEFT ); $PGRID = array(); for ( $i = 0; $i < 8; $i += 2 ) { array_push( $PGRID, substr( $dPGrid, $i, 2 ) ); } for ( $i = 24; $i < 28; $i++ ) { $PGSID[$i] = array_pop( $PGRID ); } $PGsid_string = ''; foreach ( $PGSID as $PGsid_bit ) { $PGsid_string .= "\\" . $PGsid_bit; } $PGfilter = "(&(objectSid=$PGsid_string)(objectclass=$objectclass))"; $this->printDebug( "Primary Group Filter: $PGfilter", SENSITIVE ); $info = LdapAuthenticationPlugin::ldap_search( $this->ldapconn, $base, $PGfilter ); $PGentries = LdapAuthenticationPlugin::ldap_get_entries( $this->ldapconn, $info ); array_shift( $PGentries ); $dnMember = strtolower( $PGentries[0]['dn'] ); $groups["dn"][] = $dnMember; // Get short name of group $memAttrs = explode( ',', strtolower( $dnMember ) ); if ( isset( $memAttrs[0] ) ) { $memAttrs = explode( '=', $memAttrs[0] ); if ( isset( $memAttrs[1] ) ) { $groups["short"][] = strtolower( $memAttrs[1] ); } } } } $filter = "(&($attribute=$value)(objectclass=$objectclass))"; $this->printDebug( "Search string: $filter", SENSITIVE ); $info = LdapAuthenticationPlugin::ldap_search( $this->ldapconn, $base, $filter ); if ( !$info ) { $this->printDebug( "No entries returned from search.", SENSITIVE ); // Return an array so that other functions // don't error out. return array( "short" => array(), "dn" => array() ); } $entries = LdapAuthenticationPlugin::ldap_get_entries( $this->ldapconn, $info ); if ( $entries ){ // 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 foreach ( $entries as $entry ) { $shortMember = strtolower( $entry[$nameattribute][0] ); $dnMember = strtolower( $entry['dn'] ); $groups["short"][] = $shortMember; $groups["dn"][] = $dnMember; } } $this->printDebug( "Returned groups:", SENSITIVE, $groups["dn"] ); return $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", NONSENSITIVE ); return in_array( strtolower( $group ), $this->userLDAPGroups["short"] ); } /** * 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", NONSENSITIVE ); return in_array( strtolower( $group ), $this->allLDAPGroups["short"] ); } /** * 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 ) { global $wgGroupPermissions; // TODO: this is *really* ugly code. clean it up! $this->printDebug( "Entering setGroups.", NONSENSITIVE ); # Add ldap groups as local groups if ( $this->getConf( 'GroupsPrevail' ) ) { $this->printDebug( "Adding all groups to wgGroupPermissions: ", SENSITIVE, $this->allLDAPGroups ); foreach ( $this->allLDAPGroups["short"] as $ldapgroup ) { if ( !array_key_exists( $ldapgroup, $wgGroupPermissions ) ) { $wgGroupPermissions[$ldapgroup] = array(); } } } # add groups permissions $localAvailGrps = $user->getAllGroups(); $localUserGrps = $user->getEffectiveGroups(); $defaultLocallyManagedGrps = array( 'bot', 'sysop', 'bureaucrat' ); $locallyManagedGrps = $this->getConf( 'LocallyManagedGroups' ); if ( $locallyManagedGrps ) { $locallyManagedGrps = array_unique( array_merge( $defaultLocallyManagedGrps, $locallyManagedGrps ) ); $this->printDebug( "Locally managed groups: ", SENSITIVE, $locallyManagedGrps ); } else { $locallyManagedGrps = $defaultLocallyManagedGrps; $this->printDebug( "Locally managed groups is unset, using defaults: ", SENSITIVE, $locallyManagedGrps ); } $this->printDebug( "Available groups are: ", NONSENSITIVE, $localAvailGrps ); $this->printDebug( "Effective groups are: ", NONSENSITIVE, $localUserGrps ); # 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", NONSENSITIVE ); if ( ( !$this->hasLDAPGroup( $cGroup ) ) && ( !in_array( $cGroup, $locallyManagedGrps ) ) ) { $this->printDebug( "Removing user from: $cGroup", NONSENSITIVE ); # 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", NONSENSITIVE ); if ( $this->hasLDAPGroup( $cGroup ) ) { $this->printDebug( "Adding user to: $cGroup", NONSENSITIVE ); $user->addGroup( $cGroup ); } } } } /** * Returns a password that is created via the configured hash settings. * * @param string $password * @return string * @access private */ function getPasswordHash( $password ) { $this->printDebug( "Entering getPasswordHash", NONSENSITIVE ); // Set the password hashing based upon admin preference switch ( $this->getConf( 'PasswordHash' ) ) { case 'crypt': // https://bugs.php.net/bug.php?id=55439 if ( crypt( 'password', '$1$U7AjYB.O$' ) == '$1$U7AjYB.O' ) { die( 'The version of PHP in use has a broken crypt function. Please upgrade your installation of PHP, or use another encryption function for LDAP.' ); } $pass = '{CRYPT}' . crypt( $password ); break; case 'clear': $pass = $password; break; default: $pwd_sha = base64_encode( pack( 'H*', sha1( $password ) ) ); $pass = "{SHA}" . $pwd_sha; break; } 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 * @param Array|null $debugArr * @access private */ function printDebug( $debugText, $debugVal, $debugArr = null ) { if ( !function_exists( 'wfDebugLog' ) ) { return; } global $wgLDAPDebug; if ( $wgLDAPDebug >= $debugVal ) { if ( isset( $debugArr ) ) { $debugText = $debugText . " " . implode( "::", $debugArr ); } wfDebugLog( 'ldap', LDAPAUTHVERSION . ' ' . $debugText, false ); } } /** * Binds as $userdn with $password. This can be called with only the ldap * connection resource for an anonymous bind. * * @param string $userdn * @param string $password * @return bool * @access private */ function bindAs( $userdn = null, $password = null ) { // Let's see if the user can authenticate. if ( $userdn == null || $password == null ) { $bind = LdapAuthenticationPlugin::ldap_bind( $this->ldapconn ); } else { $bind = LdapAuthenticationPlugin::ldap_bind( $this->ldapconn, $userdn, $password ); } if ( !$bind ) { $this->printDebug( "Failed to bind as $userdn", NONSENSITIVE ); return false; } $this->boundAs = $userdn; return true; } /** * Returns true if auto-authentication is allowed, and the user is * authenticating using the auto-authentication domain. * * @return bool * @access private */ function useAutoAuth() { return $this->getDomain() == $this->getConf( 'AutoAuthDomain' ); } /** * 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( "\\5c", "\\28", "\\29", "\\2a", "\\00" ), $string ); } /** * Returns a basedn by the type of entry we are searching for. * * @param int $type * @return string * @access private */ function getBaseDN( $type ) { $this->printDebug( "Entering getBaseDN", NONSENSITIVE ); $ret = ''; switch( $type ) { case USERDN: $ret = $this->getConf( 'UserBaseDN' ); break; case GROUPDN: $ret = $this->getConf( 'GroupBaseDN' ); break; case DEFAULTDN: $ret = $this->getConf( 'BaseDN' ); if ( $ret ) { return $ret; } else { $this->printDebug( "basedn is not set.", NONSENSITIVE ); return ''; } } if ( $ret == '' ) { $this->printDebug( "basedn is not set for this type of entry, trying to get the default basedn.", NONSENSITIVE ); // We will never reach here if $type is self::DEFAULTDN, so to avoid code // code duplication, we'll get the default by re-calling the function. return $this->getBaseDN( DEFAULTDN ); } else { $this->printDebug( "basedn is $ret", NONSENSITIVE ); return $ret; } } /** * @param User $user * @return string */ static function loadDomain( $user ) { $user_id = $user->getId(); if ( $user_id != 0 ) { $dbr = wfGetDB( DB_SLAVE ); $row = $dbr->selectRow( 'ldap_domains', array( 'domain' ), array( 'user_id' => $user_id ), __METHOD__ ); if ( $row ) { return $row->domain; } } return null; } /** * @param User $user * @param string $domain * @return bool */ static function saveDomain( $user, $domain ) { $user_id = $user->getId(); if ( $user_id != 0 ) { $dbw = wfGetDB( DB_MASTER ); $olddomain = self::loadDomain( $user ); if ( $olddomain ) { return $dbw->update( 'ldap_domains', array( 'domain' => $domain ), array( 'user_id' => $user_id ), __METHOD__ ); } else { return $dbw->insert( 'ldap_domains', array( 'domain' => $domain, 'user_id' => $user_id ), __METHOD__ ); } } return false; } } // The auto-auth code was originally derived from the SSL Authentication plugin // http://www.mediawiki.org/wiki/SSL_authentication /** * Sets up the auto-authentication piece of the LDAP plugin. * * @access public */ function AutoAuthSetup() { global $wgHooks; global $wgAuth; $wgAuth = new LdapAuthenticationPlugin(); $wgAuth->printDebug( "Entering AutoAuthSetup.", NONSENSITIVE ); # We need both authentication username and domain (bug 34787) if ( $wgAuth->getConf("AutoAuthUsername") !== "" && $wgAuth->getConf("AutoAuthDomain") !== "" ) { $wgAuth->printDebug( "wgLDAPAutoAuthUsername and wgLDAPAutoAuthDomain is not null, adding hooks.", NONSENSITIVE ); $wgHooks['UserLoadAfterLoadFromSession'][] = 'LdapAutoAuthentication::Authenticate'; $wgHooks['PersonalUrls'][] = 'LdapAutoAuthentication::NoLogout'; /* Disallow logout link */ } }