1
0
Fork 0
mirror of https://github.com/YunoHost-Apps/mediawiki_ynh.git synced 2024-09-03 19:46:05 +02:00
mediawiki_ynh/sources/LdapAuthentication/LdapAuthentication.php

2104 lines
67 KiB
PHP

<?php
# Copyright (C) 2004 Ryan Lane <http://www.mediawiki.org/wiki/User:Ryan_lane>
#
# 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 */
}
}