mirror of
https://github.com/moodle/moodle.git
synced 2025-01-18 22:08:20 +01:00
3e5f4b870e
We need to specify a valid user id in the call to role_assign(). And we only have to make the call if the user has been added successfully, not otherwise. Also make sure we lowercase the memberuser and group distinguished names before comparing them. Depending on the LDAP server we can get mixed case values for the DNs, and the user may have specified the creators group/ou name in a different case. By the way, this has been broken for ages (since the auth cleanup in 1.8, in 2007!). It's a bit strange nobody noticed before :-O Credit goes to Joe Chryst.
367 lines
13 KiB
PHP
367 lines
13 KiB
PHP
<?php
|
||
|
||
/**
|
||
* ldaplib.php - LDAP functions & data library
|
||
*
|
||
* Library file of miscellaneous general-purpose LDAP functions and
|
||
* data structures, useful for both ldap authentication (or ldap based
|
||
* authentication like CAS) and enrolment plugins.
|
||
*
|
||
* @author I<>aki Arenaza
|
||
* @package core
|
||
* @subpackage lib
|
||
* @copyright 1999 onwards Martin Dougiamas http://dougiamas.com
|
||
* @copyright 2010 onwards I<>aki Arenaza
|
||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||
*/
|
||
|
||
defined('MOODLE_INTERNAL') || die();
|
||
|
||
// rootDSE is defined as the root of the directory data tree on a directory server.
|
||
if (!defined('ROOTDSE')) {
|
||
define ('ROOTDSE', '');
|
||
}
|
||
|
||
/**
|
||
* Returns predefined user types
|
||
*
|
||
* @return array of predefined user types
|
||
*/
|
||
function ldap_supported_usertypes() {
|
||
$types = array();
|
||
$types['edir'] = 'Novell Edirectory';
|
||
$types['rfc2307'] = 'posixAccount (rfc2307)';
|
||
$types['rfc2307bis'] = 'posixAccount (rfc2307bis)';
|
||
$types['samba'] = 'sambaSamAccount (v.3.0.7)';
|
||
$types['ad'] = 'MS ActiveDirectory';
|
||
$types['default'] = get_string('default');
|
||
return $types;
|
||
}
|
||
|
||
/**
|
||
* Initializes needed variables for ldap-module
|
||
*
|
||
* Uses names defined in ldap_supported_usertypes.
|
||
* $default is first defined as:
|
||
* $default['pseudoname'] = array(
|
||
* 'typename1' => 'value',
|
||
* 'typename2' => 'value'
|
||
* ....
|
||
* );
|
||
*
|
||
* @return array of default values
|
||
*/
|
||
function ldap_getdefaults() {
|
||
// All the values have to be written in lowercase, even if the
|
||
// standard LDAP attributes are mixed-case
|
||
$default['objectclass'] = array(
|
||
'edir' => 'user',
|
||
'rfc2307' => 'posixaccount',
|
||
'rfc2307bis' => 'posixaccount',
|
||
'samba' => 'sambasamaccount',
|
||
'ad' => 'user',
|
||
'default' => '*'
|
||
);
|
||
$default['user_attribute'] = array(
|
||
'edir' => 'cn',
|
||
'rfc2307' => 'uid',
|
||
'rfc2307bis' => 'uid',
|
||
'samba' => 'uid',
|
||
'ad' => 'cn',
|
||
'default' => 'cn'
|
||
);
|
||
$default['memberattribute'] = array(
|
||
'edir' => 'member',
|
||
'rfc2307' => 'member',
|
||
'rfc2307bis' => 'member',
|
||
'samba' => 'member',
|
||
'ad' => 'member',
|
||
'default' => 'member'
|
||
);
|
||
$default['memberattribute_isdn'] = array(
|
||
'edir' => '1',
|
||
'rfc2307' => '0',
|
||
'rfc2307bis' => '1',
|
||
'samba' => '0', // is this right?
|
||
'ad' => '1',
|
||
'default' => '0'
|
||
);
|
||
$default['expireattr'] = array (
|
||
'edir' => 'passwordexpirationtime',
|
||
'rfc2307' => 'shadowexpire',
|
||
'rfc2307bis' => 'shadowexpire',
|
||
'samba' => '', // No support yet
|
||
'ad' => 'pwdlastset',
|
||
'default' => ''
|
||
);
|
||
return $default;
|
||
}
|
||
|
||
/**
|
||
* Checks if user belongs to specific group(s) or is in a subtree.
|
||
*
|
||
* Returns true if user belongs to a group in grupdns string OR if the
|
||
* DN of the user is in a subtree of the DN provided as "group"
|
||
*
|
||
* @param mixed $ldapconnection A valid LDAP connection.
|
||
* @param string $userid LDAP user id (dn/cn/uid/...) to test membership for.
|
||
* @param array $group_dns arrary of group dn
|
||
* @param string $member_attrib the name of the membership attribute.
|
||
* @return boolean
|
||
*
|
||
*/
|
||
function ldap_isgroupmember($ldapconnection, $userid, $group_dns, $member_attrib) {
|
||
if (empty($ldapconnection) || empty($userid) || empty($group_dns) || empty($member_attrib)) {
|
||
return false;
|
||
}
|
||
|
||
$result = false;
|
||
foreach ($group_dns as $group) {
|
||
$group = trim($group);
|
||
if (empty($group)) {
|
||
continue;
|
||
}
|
||
|
||
// Check cheaply if the user's DN sits in a subtree of the
|
||
// "group" DN provided. Granted, this isn't a proper LDAP
|
||
// group, but it's a popular usage.
|
||
if (stripos(strrev(strtolower($userid)), strrev(strtolower($group))) === 0) {
|
||
$result = true;
|
||
break;
|
||
}
|
||
|
||
$search = ldap_read($ldapconnection, $group,
|
||
'('.$member_attrib.'='.ldap_filter_addslashes($userid).')',
|
||
array($member_attrib));
|
||
|
||
if (!empty($search) && ldap_count_entries($ldapconnection, $search)) {
|
||
$info = ldap_get_entries_moodle($ldapconnection, $search);
|
||
if (count($info) > 0 ) {
|
||
// User is member of group
|
||
$result = true;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
return $result;
|
||
}
|
||
|
||
/**
|
||
* Tries connect to specified ldap servers. Returns a valid LDAP
|
||
* connection or false.
|
||
*
|
||
* @param string $host_url
|
||
* @param integer $ldap_version either 2 (LDAPv2) or 3 (LDAPv3).
|
||
* @param string $user_type the configured user type for this connection.
|
||
* @param string $bind_dn the binding user dn. If an emtpy string, anonymous binding is used.
|
||
* @param string $bind_pw the password for the binding user. Ignored for anonymous bindings.
|
||
* @param boolean $opt_deref whether to set LDAP_OPT_DEREF on this connection or not.
|
||
* @param string &$debuginfo the debugging information in case the connection fails.
|
||
* @return mixed connection result or false.
|
||
*/
|
||
function ldap_connect_moodle($host_url, $ldap_version, $user_type, $bind_dn, $bind_pw, $opt_deref, &$debuginfo) {
|
||
if (empty($host_url) || empty($ldap_version) || empty($user_type)) {
|
||
$debuginfo = 'No LDAP Host URL, Version or User Type specified in your LDAP settings';
|
||
return false;
|
||
}
|
||
|
||
$debuginfo = '';
|
||
$urls = explode(';', $host_url);
|
||
foreach ($urls as $server) {
|
||
$server = trim($server);
|
||
if (empty($server)) {
|
||
continue;
|
||
}
|
||
|
||
$connresult = ldap_connect($server); // ldap_connect returns ALWAYS true
|
||
|
||
if (!empty($ldap_version)) {
|
||
ldap_set_option($connresult, LDAP_OPT_PROTOCOL_VERSION, $ldap_version);
|
||
}
|
||
|
||
// Fix MDL-10921
|
||
if ($user_type === 'ad') {
|
||
ldap_set_option($connresult, LDAP_OPT_REFERRALS, 0);
|
||
}
|
||
|
||
if (!empty($opt_deref)) {
|
||
ldap_set_option($connresult, LDAP_OPT_DEREF, $opt_deref);
|
||
}
|
||
|
||
if (!empty($bind_dn)) {
|
||
$bindresult = @ldap_bind($connresult, $bind_dn, $bind_pw);
|
||
} else {
|
||
// Bind anonymously
|
||
$bindresult = @ldap_bind($connresult);
|
||
}
|
||
|
||
if ($bindresult) {
|
||
return $connresult;
|
||
}
|
||
|
||
$debuginfo .= "Server: '$server', Connection: '$connresult', Bind result: '$bindresult'\n";
|
||
}
|
||
|
||
// If any of servers were alive we have already returned connection.
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* Search specified contexts for username and return the user dn like:
|
||
* cn=username,ou=suborg,o=org
|
||
*
|
||
* @param mixed $ldapconnection a valid LDAP connection.
|
||
* @param mixed $username username (external LDAP encoding, no db slashes).
|
||
* @param array $contexts contexts to look for the user.
|
||
* @param string $objectclass objectlass of the user (in LDAP filter syntax).
|
||
* @param string $search_attrib the attribute use to look for the user.
|
||
* @param boolean $search_sub whether to search subcontexts or not.
|
||
* @return mixed the user dn (external LDAP encoding, no db slashes) or false
|
||
*
|
||
*/
|
||
function ldap_find_userdn($ldapconnection, $username, $contexts, $objectclass, $search_attrib, $search_sub) {
|
||
if (empty($ldapconnection) || empty($username) || empty($contexts) || empty($objectclass) || empty($search_attrib)) {
|
||
return false;
|
||
}
|
||
|
||
// Default return value
|
||
$ldap_user_dn = false;
|
||
|
||
// Get all contexts and look for first matching user
|
||
foreach ($contexts as $context) {
|
||
$context = trim($context);
|
||
if (empty($context)) {
|
||
continue;
|
||
}
|
||
|
||
if ($search_sub) {
|
||
$ldap_result = ldap_search($ldapconnection, $context,
|
||
'(&'.$objectclass.'('.$search_attrib.'='.ldap_filter_addslashes($username).'))',
|
||
array($search_attrib));
|
||
} else {
|
||
$ldap_result = ldap_list($ldapconnection, $context,
|
||
'(&'.$objectclass.'('.$search_attrib.'='.ldap_filter_addslashes($username).'))',
|
||
array($search_attrib));
|
||
}
|
||
|
||
$entry = ldap_first_entry($ldapconnection, $ldap_result);
|
||
if ($entry) {
|
||
$ldap_user_dn = ldap_get_dn($ldapconnection, $entry);
|
||
break;
|
||
}
|
||
}
|
||
|
||
return $ldap_user_dn;
|
||
}
|
||
|
||
/**
|
||
* Returns values like ldap_get_entries but is binary compatible and
|
||
* returns all attributes as array.
|
||
*
|
||
* @param mixed $ldapconnection A valid LDAP connection
|
||
* @param mixed $searchresult A search result from ldap_search, ldap_list, etc.
|
||
* @return array ldap-entries with lower-cased attributes as indexes
|
||
*/
|
||
function ldap_get_entries_moodle($ldapconnection, $searchresult) {
|
||
if (empty($ldapconnection) || empty($searchresult)) {
|
||
return array();
|
||
}
|
||
|
||
$i = 0;
|
||
$result = array();
|
||
$entry = ldap_first_entry($ldapconnection, $searchresult);
|
||
if (!$entry) {
|
||
return array();
|
||
}
|
||
do {
|
||
$attributes = array_change_key_case(ldap_get_attributes($ldapconnection, $entry), CASE_LOWER);
|
||
for ($j = 0; $j < $attributes['count']; $j++) {
|
||
$values = ldap_get_values_len($ldapconnection, $entry, $attributes[$j]);
|
||
if (is_array($values)) {
|
||
$result[$i][$attributes[$j]] = $values;
|
||
} else {
|
||
$result[$i][$attributes[$j]] = array($values);
|
||
}
|
||
}
|
||
$i++;
|
||
} while ($entry = ldap_next_entry($ldapconnection, $entry));
|
||
|
||
return ($result);
|
||
}
|
||
|
||
/**
|
||
* Quote control characters in texts used in LDAP filters - see RFC 4515/2254
|
||
*
|
||
* @param string filter string to quote
|
||
* @return string the filter string quoted
|
||
*/
|
||
function ldap_filter_addslashes($text) {
|
||
$text = str_replace('\\', '\\5c', $text);
|
||
$text = str_replace(array('*', '(', ')', "\0"),
|
||
array('\\2a', '\\28', '\\29', '\\00'), $text);
|
||
return $text;
|
||
}
|
||
|
||
if(!defined('LDAP_DN_SPECIAL_CHARS')) {
|
||
define('LDAP_DN_SPECIAL_CHARS', 0);
|
||
}
|
||
if(!defined('LDAP_DN_SPECIAL_CHARS_QUOTED_NUM')) {
|
||
define('LDAP_DN_SPECIAL_CHARS_QUOTED_NUM', 1);
|
||
}
|
||
if(!defined('LDAP_DN_SPECIAL_CHARS_QUOTED_ALPHA')) {
|
||
define('LDAP_DN_SPECIAL_CHARS_QUOTED_ALPHA', 2);
|
||
}
|
||
|
||
/**
|
||
* The order of the special characters in these arrays _IS IMPORTANT_.
|
||
* Make sure '\\5C' (and '\\') are the first elements of the arrays.
|
||
* Otherwise we'll double replace '\' with '\5C' which is Bad(tm)
|
||
*/
|
||
function ldap_get_dn_special_chars() {
|
||
return array (
|
||
LDAP_DN_SPECIAL_CHARS => array('\\', ' ', '"', '#', '+', ',', ';', '<', '=', '>', "\0"),
|
||
LDAP_DN_SPECIAL_CHARS_QUOTED_NUM => array('\\5c','\\20','\\22','\\23','\\2b','\\2c','\\3b','\\3c','\\3d','\\3e','\\00'),
|
||
LDAP_DN_SPECIAL_CHARS_QUOTED_ALPHA => array('\\\\','\\ ', '\\"', '\\#', '\\+', '\\,', '\\;', '\\<', '\\>', '\\=', '\\00'),
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Quote control characters in distinguished names used in LDAP - See RFC 4514/2253
|
||
*
|
||
* @param string The text to quote
|
||
* @return string The text quoted
|
||
*/
|
||
function ldap_addslashes($text) {
|
||
$special_dn_chars = ldap_get_dn_special_chars();
|
||
|
||
$text = str_replace ($special_dn_chars[LDAP_DN_SPECIAL_CHARS],
|
||
$special_dn_chars[LDAP_DN_SPECIAL_CHARS_QUOTED_NUM],
|
||
$text);
|
||
return $text;
|
||
}
|
||
|
||
/**
|
||
* Unquote control characters in distinguished names used in LDAP - See RFC 4514/2253
|
||
*
|
||
* @param string The text quoted
|
||
* @return string The text unquoted
|
||
*/
|
||
function ldap_stripslashes($text) {
|
||
$special_dn_chars = ldap_get_dn_special_chars();
|
||
|
||
// First unquote the simply backslashed special characters. If we
|
||
// do it the other way, we remove too many slashes.
|
||
$text = str_replace($special_dn_chars[LDAP_DN_SPECIAL_CHARS_QUOTED_ALPHA],
|
||
$special_dn_chars[LDAP_DN_SPECIAL_CHARS],
|
||
$text);
|
||
|
||
// Next unquote the 'numerically' quoted characters. We don't use
|
||
// LDAP_DN_SPECIAL_CHARS_QUOTED_NUM because the standard allows us
|
||
// to quote any character with this encoding, not just the special
|
||
// ones.
|
||
$text = preg_replace('/\\\([0-9A-Fa-f]{2})/e', "chr(hexdec('\\1'))", $text);
|
||
|
||
return $text;
|
||
}
|