PageRenderTime 58ms CodeModel.GetById 15ms RepoModel.GetById 0ms app.codeStats 1ms

/auth/ldap/auth.php

https://bitbucket.org/ceu/moodle_demo
PHP | 2301 lines | 1460 code | 260 blank | 581 comment | 298 complexity | e430a2f4f283fbf57ba16176ca6fa223 MD5 | raw file
Possible License(s): BSD-3-Clause, LGPL-2.0, LGPL-2.1
  1. <?php
  2. /**
  3. * @author Martin Dougiamas
  4. * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
  5. * @package moodle multiauth
  6. *
  7. * Authentication Plugin: LDAP Authentication
  8. *
  9. * Authentication using LDAP (Lightweight Directory Access Protocol).
  10. *
  11. * 2006-08-28 File created.
  12. */
  13. if (!defined('MOODLE_INTERNAL')) {
  14. die('Direct access to this script is forbidden.'); /// It must be included from a Moodle page
  15. }
  16. // See http://support.microsoft.com/kb/305144 to interprete these values.
  17. if (!defined('AUTH_AD_ACCOUNTDISABLE')) {
  18. define('AUTH_AD_ACCOUNTDISABLE', 0x0002);
  19. }
  20. if (!defined('AUTH_AD_NORMAL_ACCOUNT')) {
  21. define('AUTH_AD_NORMAL_ACCOUNT', 0x0200);
  22. }
  23. if (!defined('AUTH_NTLMTIMEOUT')) { // timewindow for the NTLM SSO process, in secs...
  24. define('AUTH_NTLMTIMEOUT', 10);
  25. }
  26. require_once($CFG->libdir.'/authlib.php');
  27. /**
  28. * LDAP authentication plugin.
  29. */
  30. class auth_plugin_ldap extends auth_plugin_base {
  31. /**
  32. * Constructor with initialisation.
  33. */
  34. function auth_plugin_ldap() {
  35. $this->authtype = 'ldap';
  36. $this->config = get_config('auth/ldap');
  37. if (empty($this->config->ldapencoding)) {
  38. $this->config->ldapencoding = 'utf-8';
  39. }
  40. if (empty($this->config->user_type)) {
  41. $this->config->user_type = 'default';
  42. }
  43. $default = $this->ldap_getdefaults();
  44. //use defaults if values not given
  45. foreach ($default as $key => $value) {
  46. // watch out - 0, false are correct values too
  47. if (!isset($this->config->{$key}) or $this->config->{$key} == '') {
  48. $this->config->{$key} = $value[$this->config->user_type];
  49. }
  50. }
  51. // Hack prefix to objectclass
  52. if (empty($this->config->objectclass)) {
  53. // Can't send empty filter
  54. $this->config->objectclass='(objectClass=*)';
  55. } else if (stripos($this->config->objectclass, 'objectClass=') === 0) {
  56. // Value is 'objectClass=some-string-here', so just add ()
  57. // around the value (filter _must_ have them).
  58. $this->config->objectclass = '('.$this->config->objectclass.')';
  59. } else if (stripos($this->config->objectclass, '(') !== 0) {
  60. // Value is 'some-string-not-starting-with-left-parentheses',
  61. // which is assumed to be the objectClass matching value.
  62. // So build a valid filter with it.
  63. $this->config->objectclass = '(objectClass='.$this->config->objectclass.')';
  64. } else {
  65. // There is an additional possible value
  66. // '(some-string-here)', that can be used to specify any
  67. // valid filter string, to select subsets of users based
  68. // on any criteria. For example, we could select the users
  69. // whose objectClass is 'user' and have the
  70. // 'enabledMoodleUser' attribute, with something like:
  71. //
  72. // (&(objectClass=user)(enabledMoodleUser=1))
  73. //
  74. // This is only used in the functions that deal with the
  75. // whole potential set of users (currently sync_users()
  76. // and get_user_list() only).
  77. //
  78. // In this particular case we don't need to do anything,
  79. // so leave $this->config->objectclass as is.
  80. }
  81. }
  82. /**
  83. * Returns true if the username and password work and false if they are
  84. * wrong or don't exist.
  85. *
  86. * @param string $username The username (with system magic quotes)
  87. * @param string $password The password (with system magic quotes)
  88. *
  89. * @return bool Authentication success or failure.
  90. */
  91. function user_login($username, $password) {
  92. if (! function_exists('ldap_bind')) {
  93. print_error('auth_ldapnotinstalled','auth');
  94. return false;
  95. }
  96. if (!$username or !$password) { // Don't allow blank usernames or passwords
  97. return false;
  98. }
  99. $textlib = textlib_get_instance();
  100. $extusername = $textlib->convert(stripslashes($username), 'utf-8', $this->config->ldapencoding);
  101. $extpassword = $textlib->convert(stripslashes($password), 'utf-8', $this->config->ldapencoding);
  102. //
  103. // Before we connect to LDAP, check if this is an AD SSO login
  104. // if we succeed in this block, we'll return success early.
  105. //
  106. $key = sesskey();
  107. if (!empty($this->config->ntlmsso_enabled) && $key === $password) {
  108. $cf = get_cache_flags('auth/ldap/ntlmsess');
  109. // We only get the cache flag if we retrieve it before
  110. // it expires (AUTH_NTLMTIMEOUT seconds).
  111. if (!isset($cf[$key]) || $cf[$key] === '') {
  112. return false;
  113. }
  114. $sessusername = $cf[$key];
  115. if ($username === $sessusername) {
  116. unset($sessusername);
  117. unset($cf);
  118. // Check that the user is inside one of the configured LDAP contexts
  119. $validuser = false;
  120. $ldapconnection = $this->ldap_connect();
  121. if ($ldapconnection) {
  122. // if the user is not inside the configured contexts,
  123. // ldap_find_userdn returns false.
  124. if ($this->ldap_find_userdn($ldapconnection, $extusername)) {
  125. $validuser = true;
  126. }
  127. $this->ldap_close();
  128. }
  129. // Shortcut here - SSO confirmed
  130. return $validuser;
  131. }
  132. } // End SSO processing
  133. unset($key);
  134. $ldapconnection = $this->ldap_connect();
  135. if ($ldapconnection) {
  136. $ldap_user_dn = $this->ldap_find_userdn($ldapconnection, $extusername);
  137. //if ldap_user_dn is empty, user does not exist
  138. if (!$ldap_user_dn) {
  139. $this->ldap_close();
  140. return false;
  141. }
  142. // Try to bind with current username and password
  143. $ldap_login = @ldap_bind($ldapconnection, $ldap_user_dn, $extpassword);
  144. $this->ldap_close();
  145. if ($ldap_login) {
  146. return true;
  147. }
  148. }
  149. else {
  150. $this->ldap_close();
  151. print_error('auth_ldap_noconnect','auth','',$this->config->host_url);
  152. }
  153. return false;
  154. }
  155. /**
  156. * reads userinformation from ldap and return it in array()
  157. *
  158. * Read user information from external database and returns it as array().
  159. * Function should return all information available. If you are saving
  160. * this information to moodle user-table you should honor syncronization flags
  161. *
  162. * @param string $username username (with system magic quotes)
  163. *
  164. * @return mixed array with no magic quotes or false on error
  165. */
  166. function get_userinfo($username) {
  167. $textlib = textlib_get_instance();
  168. $extusername = $textlib->convert(stripslashes($username), 'utf-8', $this->config->ldapencoding);
  169. $ldapconnection = $this->ldap_connect();
  170. $attrmap = $this->ldap_attributes();
  171. $result = array();
  172. $search_attribs = array();
  173. foreach ($attrmap as $key=>$values) {
  174. if (!is_array($values)) {
  175. $values = array($values);
  176. }
  177. foreach ($values as $value) {
  178. if (!in_array($value, $search_attribs)) {
  179. array_push($search_attribs, $value);
  180. }
  181. }
  182. }
  183. $user_dn = $this->ldap_find_userdn($ldapconnection, $extusername);
  184. if (!$user_info_result = ldap_read($ldapconnection, $user_dn, $this->config->objectclass, $search_attribs)) {
  185. return false; // error!
  186. }
  187. $user_entry = $this->ldap_get_entries($ldapconnection, $user_info_result);
  188. if (empty($user_entry)) {
  189. return false; // entry not found
  190. }
  191. foreach ($attrmap as $key=>$values) {
  192. if (!is_array($values)) {
  193. $values = array($values);
  194. }
  195. $ldapval = NULL;
  196. foreach ($values as $value) {
  197. if ((moodle_strtolower($value) == 'dn') || (moodle_strtolower($value) == 'distinguishedname')) {
  198. $result[$key] = $user_dn;
  199. }
  200. if (!array_key_exists($value, $user_entry[0])) {
  201. continue; // wrong data mapping!
  202. }
  203. if (is_array($user_entry[0][$value])) {
  204. $newval = $textlib->convert($user_entry[0][$value][0], $this->config->ldapencoding, 'utf-8');
  205. } else {
  206. $newval = $textlib->convert($user_entry[0][$value], $this->config->ldapencoding, 'utf-8');
  207. }
  208. if (!empty($newval)) { // favour ldap entries that are set
  209. $ldapval = $newval;
  210. }
  211. }
  212. if (!is_null($ldapval)) {
  213. $result[$key] = $ldapval;
  214. }
  215. }
  216. $this->ldap_close();
  217. return $result;
  218. }
  219. /**
  220. * reads userinformation from ldap and return it in an object
  221. *
  222. * @param string $username username (with system magic quotes)
  223. * @return mixed object or false on error
  224. */
  225. function get_userinfo_asobj($username) {
  226. $user_array = $this->get_userinfo($username);
  227. if ($user_array == false) {
  228. return false; //error or not found
  229. }
  230. $user_array = truncate_userinfo($user_array);
  231. $user = new object();
  232. foreach ($user_array as $key=>$value) {
  233. $user->{$key} = $value;
  234. }
  235. return $user;
  236. }
  237. /**
  238. * returns all usernames from external database
  239. *
  240. * get_userlist returns all usernames from external database
  241. *
  242. * @return array
  243. */
  244. function get_userlist() {
  245. return $this->ldap_get_userlist("({$this->config->user_attribute}=*)");
  246. }
  247. /**
  248. * checks if user exists on external db
  249. *
  250. * @param string $username (with system magic quotes)
  251. */
  252. function user_exists($username) {
  253. $textlib = textlib_get_instance();
  254. $extusername = $textlib->convert(stripslashes($username), 'utf-8', $this->config->ldapencoding);
  255. //returns true if given username exist on ldap
  256. $users = $this->ldap_get_userlist("({$this->config->user_attribute}=".$this->filter_addslashes($extusername).")");
  257. return count($users);
  258. }
  259. /**
  260. * Creates a new user on external database.
  261. * By using information in userobject
  262. * Use user_exists to prevent dublicate usernames
  263. *
  264. * @param mixed $userobject Moodle userobject (with system magic quotes)
  265. * @param mixed $plainpass Plaintext password (with system magic quotes)
  266. */
  267. function user_create($userobject, $plainpass) {
  268. $textlib = textlib_get_instance();
  269. $extusername = $textlib->convert(stripslashes($userobject->username), 'utf-8', $this->config->ldapencoding);
  270. $extpassword = $textlib->convert(stripslashes($plainpass), 'utf-8', $this->config->ldapencoding);
  271. switch ($this->config->passtype) {
  272. case 'md5':
  273. $extpassword = '{MD5}' . base64_encode(pack('H*', md5($extpassword)));
  274. break;
  275. case 'sha1':
  276. $extpassword = '{SHA}' . base64_encode(pack('H*', sha1($extpassword)));
  277. break;
  278. case 'plaintext':
  279. default:
  280. break; // plaintext
  281. }
  282. $ldapconnection = $this->ldap_connect();
  283. $attrmap = $this->ldap_attributes();
  284. $newuser = array();
  285. foreach ($attrmap as $key => $values) {
  286. if (!is_array($values)) {
  287. $values = array($values);
  288. }
  289. foreach ($values as $value) {
  290. if (!empty($userobject->$key) ) {
  291. $newuser[$value] = $textlib->convert(stripslashes($userobject->$key), 'utf-8', $this->config->ldapencoding);
  292. }
  293. }
  294. }
  295. //Following sets all mandatory and other forced attribute values
  296. //User should be creted as login disabled untill email confirmation is processed
  297. //Feel free to add your user type and send patches to paca@sci.fi to add them
  298. //Moodle distribution
  299. switch ($this->config->user_type) {
  300. case 'edir':
  301. $newuser['objectClass'] = array("inetOrgPerson","organizationalPerson","person","top");
  302. $newuser['uniqueId'] = $extusername;
  303. $newuser['logindisabled'] = "TRUE";
  304. $newuser['userpassword'] = $extpassword;
  305. $uadd = ldap_add($ldapconnection, $this->config->user_attribute.'="'.$this->ldap_addslashes($userobject->username).','.$this->config->create_context.'"', $newuser);
  306. break;
  307. case 'ad':
  308. // User account creation is a two step process with AD. First you
  309. // create the user object, then you set the password. If you try
  310. // to set the password while creating the user, the operation
  311. // fails.
  312. // Passwords in Active Directory must be encoded as Unicode
  313. // strings (UCS-2 Little Endian format) and surrounded with
  314. // double quotes. See http://support.microsoft.com/?kbid=269190
  315. if (!function_exists('mb_convert_encoding')) {
  316. print_error ('auth_ldap_no_mbstring', 'auth');
  317. }
  318. // First create the user account, and mark it as disabled.
  319. $newuser['objectClass'] = array('top','person','user','organizationalPerson');
  320. $newuser['sAMAccountName'] = $extusername;
  321. $newuser['userAccountControl'] = AUTH_AD_NORMAL_ACCOUNT |
  322. AUTH_AD_ACCOUNTDISABLE;
  323. $userdn = 'cn=' . $this->ldap_addslashes($extusername) .
  324. ',' . $this->config->create_context;
  325. if (!ldap_add($ldapconnection, $userdn, $newuser)) {
  326. print_error ('auth_ldap_ad_create_req', 'auth');
  327. }
  328. // Now set the password
  329. unset($newuser);
  330. $newuser['unicodePwd'] = mb_convert_encoding('"' . $extpassword . '"',
  331. "UCS-2LE", "UTF-8");
  332. if(!ldap_modify($ldapconnection, $userdn, $newuser)) {
  333. // Something went wrong: delete the user account and error out
  334. ldap_delete ($ldapconnection, $userdn);
  335. print_error ('auth_ldap_ad_create_req', 'auth');
  336. }
  337. $uadd = true;
  338. break;
  339. default:
  340. print_error('auth_ldap_unsupportedusertype','auth','',$this->config->user_type);
  341. }
  342. $this->ldap_close();
  343. return $uadd;
  344. }
  345. function can_reset_password() {
  346. return !empty($this->config->stdchangepassword);
  347. }
  348. function can_signup() {
  349. return (!empty($this->config->auth_user_create) and !empty($this->config->create_context));
  350. }
  351. /**
  352. * Sign up a new user ready for confirmation.
  353. * Password is passed in plaintext.
  354. *
  355. * @param object $user new user object (with system magic quotes)
  356. * @param boolean $notify print notice with link and terminate
  357. */
  358. function user_signup($user, $notify=true) {
  359. global $CFG;
  360. require_once($CFG->dirroot.'/user/profile/lib.php');
  361. if ($this->user_exists($user->username)) {
  362. print_error('auth_ldap_user_exists', 'auth');
  363. }
  364. $plainslashedpassword = $user->password;
  365. unset($user->password);
  366. if (! $this->user_create($user, $plainslashedpassword)) {
  367. print_error('auth_ldap_create_error', 'auth');
  368. }
  369. if (! ($user->id = insert_record('user', $user)) ) {
  370. print_error('auth_emailnoinsert', 'auth');
  371. }
  372. /// Save any custom profile field information
  373. profile_save_data($user);
  374. $this->update_user_record($user->username);
  375. update_internal_user_password($user, $plainslashedpassword);
  376. $user = get_record('user', 'id', $user->id);
  377. events_trigger('user_created', $user);
  378. if (! send_confirmation_email($user)) {
  379. print_error('auth_emailnoemail', 'auth');
  380. }
  381. if ($notify) {
  382. global $CFG;
  383. $emailconfirm = get_string('emailconfirm');
  384. $navlinks = array();
  385. $navlinks[] = array('name' => $emailconfirm, 'link' => null, 'type' => 'misc');
  386. $navigation = build_navigation($navlinks);
  387. print_header($emailconfirm, $emailconfirm, $navigation);
  388. notice(get_string('emailconfirmsent', '', $user->email), "$CFG->wwwroot/index.php");
  389. } else {
  390. return true;
  391. }
  392. }
  393. /**
  394. * Returns true if plugin allows confirming of new users.
  395. *
  396. * @return bool
  397. */
  398. function can_confirm() {
  399. return $this->can_signup();
  400. }
  401. /**
  402. * Confirm the new user as registered.
  403. *
  404. * @param string $username (with system magic quotes)
  405. * @param string $confirmsecret (with system magic quotes)
  406. */
  407. function user_confirm($username, $confirmsecret) {
  408. $user = get_complete_user_data('username', $username);
  409. if (!empty($user)) {
  410. if ($user->confirmed) {
  411. return AUTH_CONFIRM_ALREADY;
  412. } else if ($user->auth != 'ldap') {
  413. return AUTH_CONFIRM_ERROR;
  414. } else if ($user->secret == stripslashes($confirmsecret)) { // They have provided the secret key to get in
  415. if (!$this->user_activate($username)) {
  416. return AUTH_CONFIRM_FAIL;
  417. }
  418. if (!set_field("user", "confirmed", 1, "id", $user->id)) {
  419. return AUTH_CONFIRM_FAIL;
  420. }
  421. if (!set_field("user", "firstaccess", time(), "id", $user->id)) {
  422. return AUTH_CONFIRM_FAIL;
  423. }
  424. return AUTH_CONFIRM_OK;
  425. }
  426. } else {
  427. return AUTH_CONFIRM_ERROR;
  428. }
  429. }
  430. /**
  431. * return number of days to user password expires
  432. *
  433. * If userpassword does not expire it should return 0. If password is already expired
  434. * it should return negative value.
  435. *
  436. * @param mixed $username username (with system magic quotes)
  437. * @return integer
  438. */
  439. function password_expire($username) {
  440. $result = 0;
  441. $textlib = textlib_get_instance();
  442. $extusername = $textlib->convert(stripslashes($username), 'utf-8', $this->config->ldapencoding);
  443. $ldapconnection = $this->ldap_connect();
  444. $user_dn = $this->ldap_find_userdn($ldapconnection, $extusername);
  445. $search_attribs = array($this->config->expireattr);
  446. $sr = ldap_read($ldapconnection, $user_dn, '(objectClass=*)', $search_attribs);
  447. if ($sr) {
  448. $info = $this->ldap_get_entries($ldapconnection, $sr);
  449. if (!empty ($info) and !empty($info[0][$this->config->expireattr][0])) {
  450. $expiretime = $this->ldap_expirationtime2unix($info[0][$this->config->expireattr][0], $ldapconnection, $user_dn);
  451. if ($expiretime != 0) {
  452. $now = time();
  453. if ($expiretime > $now) {
  454. $result = ceil(($expiretime - $now) / DAYSECS);
  455. }
  456. else {
  457. $result = floor(($expiretime - $now) / DAYSECS);
  458. }
  459. }
  460. }
  461. } else {
  462. error_log("ldap: password_expire did't find expiration time.");
  463. }
  464. //error_log("ldap: password_expire user $user_dn expires in $result days!");
  465. return $result;
  466. }
  467. /**
  468. * syncronizes user fron external db to moodle user table
  469. *
  470. * Sync is now using username attribute.
  471. *
  472. * Syncing users removes or suspends users that dont exists anymore in external db.
  473. * Creates new users and updates coursecreator status of users.
  474. *
  475. * @param int $bulk_insert_records will insert $bulkinsert_records per insert statement
  476. * valid only with $unsafe. increase to a couple thousand for
  477. * blinding fast inserts -- but test it: you may hit mysqld's
  478. * max_allowed_packet limit.
  479. * @param bool $do_updates will do pull in data updates from ldap if relevant
  480. */
  481. function sync_users ($bulk_insert_records = 1000, $do_updates = true) {
  482. global $CFG;
  483. $textlib = textlib_get_instance();
  484. $droptablesql = array(); /// sql commands to drop the table (because session scope could be a problem for
  485. /// some persistent drivers like ODBTP (mssql) or if this function is invoked
  486. /// from within a PHP application using persistent connections
  487. $temptable = $CFG->prefix . 'extuser';
  488. $createtemptablesql = '';
  489. // configure a temp table
  490. print "Configuring temp table\n";
  491. switch (strtolower($CFG->dbfamily)) {
  492. case 'mysql':
  493. $droptablesql[] = 'DROP TEMPORARY TABLE ' . $temptable; // sql command to drop the table (because session scope could be a problem)
  494. $createtemptablesql = 'CREATE TEMPORARY TABLE ' . $temptable . ' (username VARCHAR(64), PRIMARY KEY (username)) ENGINE=MyISAM';
  495. break;
  496. case 'postgres':
  497. $droptablesql[] = 'DROP TABLE ' . $temptable; // sql command to drop the table (because session scope could be a problem)
  498. $bulk_insert_records = 1; // no support for multiple sets of values
  499. $createtemptablesql = 'CREATE TEMPORARY TABLE '. $temptable . ' (username VARCHAR(64), PRIMARY KEY (username))';
  500. break;
  501. case 'mssql':
  502. $temptable = '#'. $temptable; /// MSSQL temp tables begin with #
  503. $droptablesql[] = 'DROP TABLE ' . $temptable; // sql command to drop the table (because session scope could be a problem)
  504. $bulk_insert_records = 1; // no support for multiple sets of values
  505. $createtemptablesql = 'CREATE TABLE ' . $temptable . ' (username VARCHAR(64), PRIMARY KEY (username))';
  506. break;
  507. case 'oracle':
  508. $droptablesql[] = 'TRUNCATE TABLE ' . $temptable; // oracle requires truncate before being able to drop a temp table
  509. $droptablesql[] = 'DROP TABLE ' . $temptable; // sql command to drop the table (because session scope could be a problem)
  510. $bulk_insert_records = 1; // no support for multiple sets of values
  511. $createtemptablesql = 'CREATE GLOBAL TEMPORARY TABLE '.$temptable.' (username VARCHAR(64), PRIMARY KEY (username)) ON COMMIT PRESERVE ROWS';
  512. break;
  513. }
  514. execute_sql_arr($droptablesql, true, false); /// Drop temp table to avoid persistence problems later
  515. echo "Creating temp table $temptable\n";
  516. if(! execute_sql($createtemptablesql, false) ){
  517. print "Failed to create temporary users table - aborting\n";
  518. exit;
  519. }
  520. print "Connecting to ldap...\n";
  521. $ldapconnection = $this->ldap_connect();
  522. if (!$ldapconnection) {
  523. $this->ldap_close();
  524. print get_string('auth_ldap_noconnect','auth',$this->config->host_url);
  525. exit;
  526. }
  527. ////
  528. //// get user's list from ldap to sql in a scalable fashion
  529. ////
  530. // prepare some data we'll need
  531. $filter = '(&('.$this->config->user_attribute.'=*)'.$this->config->objectclass.')';
  532. $contexts = explode(";",$this->config->contexts);
  533. if (!empty($this->config->create_context)) {
  534. array_push($contexts, $this->config->create_context);
  535. }
  536. $fresult = array();
  537. foreach ($contexts as $context) {
  538. $context = trim($context);
  539. if (empty($context)) {
  540. continue;
  541. }
  542. begin_sql();
  543. if ($this->config->search_sub) {
  544. //use ldap_search to find first user from subtree
  545. $ldap_result = ldap_search($ldapconnection, $context,
  546. $filter,
  547. array($this->config->user_attribute));
  548. } else {
  549. //search only in this context
  550. $ldap_result = ldap_list($ldapconnection, $context,
  551. $filter,
  552. array($this->config->user_attribute));
  553. }
  554. if ($entry = ldap_first_entry($ldapconnection, $ldap_result)) {
  555. do {
  556. $value = ldap_get_values_len($ldapconnection, $entry, $this->config->user_attribute);
  557. $value = $textlib->convert($value[0], $this->config->ldapencoding, 'utf-8');
  558. // usernames are __always__ lowercase.
  559. array_push($fresult, moodle_strtolower($value));
  560. if (count($fresult) >= $bulk_insert_records) {
  561. $this->ldap_bulk_insert($fresult, $temptable);
  562. $fresult = array();
  563. }
  564. } while ($entry = ldap_next_entry($ldapconnection, $entry));
  565. }
  566. unset($ldap_result); // free mem
  567. // insert any remaining users and release mem
  568. if (count($fresult)) {
  569. $this->ldap_bulk_insert($fresult, $temptable);
  570. $fresult = array();
  571. }
  572. commit_sql();
  573. }
  574. /// preserve our user database
  575. /// if the temp table is empty, it probably means that something went wrong, exit
  576. /// so as to avoid mass deletion of users; which is hard to undo
  577. $count = get_record_sql('SELECT COUNT(username) AS count, 1 FROM ' . $temptable);
  578. $count = $count->{'count'};
  579. if ($count < 1) {
  580. print "Did not get any users from LDAP -- error? -- exiting\n";
  581. exit;
  582. } else {
  583. print "Got $count records from LDAP\n\n";
  584. }
  585. /// User removal
  586. // find users in DB that aren't in ldap -- to be removed!
  587. // this is still not as scalable (but how often do we mass delete?)
  588. if (!empty($this->config->removeuser)) {
  589. $sql = "SELECT u.id, u.username, u.email, u.auth
  590. FROM {$CFG->prefix}user u
  591. LEFT JOIN $temptable e ON u.username = e.username
  592. WHERE u.auth='ldap'
  593. AND u.deleted=0
  594. AND e.username IS NULL";
  595. $remove_users = get_records_sql($sql);
  596. if (!empty($remove_users)) {
  597. print "User entries to remove: ". count($remove_users) . "\n";
  598. foreach ($remove_users as $user) {
  599. if ($this->config->removeuser == 2) {
  600. if (delete_user($user)) {
  601. echo "\t"; print_string('auth_dbdeleteuser', 'auth', array($user->username, $user->id)); echo "\n";
  602. } else {
  603. echo "\t"; print_string('auth_dbdeleteusererror', 'auth', $user->username); echo "\n";
  604. }
  605. } else if ($this->config->removeuser == 1) {
  606. $updateuser = new object();
  607. $updateuser->id = $user->id;
  608. $updateuser->auth = 'nologin';
  609. if (update_record('user', $updateuser)) {
  610. echo "\t"; print_string('auth_dbsuspenduser', 'auth', array($user->username, $user->id)); echo "\n";
  611. } else {
  612. echo "\t"; print_string('auth_dbsuspendusererror', 'auth', $user->username); echo "\n";
  613. }
  614. }
  615. }
  616. } else {
  617. print "No user entries to be removed\n";
  618. }
  619. unset($remove_users); // free mem!
  620. }
  621. /// Revive suspended users
  622. if (!empty($this->config->removeuser) and $this->config->removeuser == 1) {
  623. $sql = "SELECT u.id, u.username
  624. FROM $temptable e, {$CFG->prefix}user u
  625. WHERE e.username=u.username
  626. AND u.auth='nologin'";
  627. $revive_users = get_records_sql($sql);
  628. if (!empty($revive_users)) {
  629. print "User entries to be revived: ". count($revive_users) . "\n";
  630. begin_sql();
  631. foreach ($revive_users as $user) {
  632. $updateuser = new object();
  633. $updateuser->id = $user->id;
  634. $updateuser->auth = 'ldap';
  635. if (update_record('user', $updateuser)) {
  636. echo "\t"; print_string('auth_dbreviveduser', 'auth', array($user->username, $user->id)); echo "\n";
  637. } else {
  638. echo "\t"; print_string('auth_dbrevivedusererror', 'auth', $user->username); echo "\n";
  639. }
  640. }
  641. commit_sql();
  642. } else {
  643. print "No user entries to be revived\n";
  644. }
  645. unset($revive_users);
  646. }
  647. /// User Updates - time-consuming (optional)
  648. if ($do_updates) {
  649. // narrow down what fields we need to update
  650. $all_keys = array_keys(get_object_vars($this->config));
  651. $updatekeys = array();
  652. foreach ($all_keys as $key) {
  653. if (preg_match('/^field_updatelocal_(.+)$/',$key, $match)) {
  654. // if we have a field to update it from
  655. // and it must be updated 'onlogin' we
  656. // update it on cron
  657. if ( !empty($this->config->{'field_map_'.$match[1]})
  658. and $this->config->{$match[0]} === 'onlogin') {
  659. array_push($updatekeys, $match[1]); // the actual key name
  660. }
  661. }
  662. }
  663. // print_r($all_keys); print_r($updatekeys);
  664. unset($all_keys); unset($key);
  665. } else {
  666. print "No updates to be done\n";
  667. }
  668. if ( $do_updates and !empty($updatekeys) ) { // run updates only if relevant
  669. $users = get_records_sql("SELECT u.username, u.id
  670. FROM {$CFG->prefix}user u
  671. WHERE u.deleted=0 AND u.auth='ldap'");
  672. if (!empty($users)) {
  673. print "User entries to update: ". count($users). "\n";
  674. $sitecontext = get_context_instance(CONTEXT_SYSTEM);
  675. if (!empty($this->config->creators) and !empty($this->config->memberattribute)
  676. and $roles = get_roles_with_capability('moodle/legacy:coursecreator', CAP_ALLOW)) {
  677. $creatorrole = array_shift($roles); // We can only use one, let's use the first one
  678. } else {
  679. $creatorrole = false;
  680. }
  681. begin_sql();
  682. $xcount = 0;
  683. $maxxcount = 100;
  684. foreach ($users as $user) {
  685. echo "\t"; print_string('auth_dbupdatinguser', 'auth', array($user->username, $user->id));
  686. if (!$this->update_user_record(addslashes($user->username), $updatekeys)) {
  687. echo " - ".get_string('skipped');
  688. }
  689. echo "\n";
  690. $xcount++;
  691. // update course creators if needed
  692. if ($creatorrole !== false) {
  693. if ($this->iscreator($user->username)) {
  694. role_assign($creatorrole->id, $user->id, 0, $sitecontext->id, 0, 0, 0, 'ldap');
  695. } else {
  696. role_unassign($creatorrole->id, $user->id, 0, $sitecontext->id, 'ldap');
  697. }
  698. }
  699. if ($xcount++ > $maxxcount) {
  700. commit_sql();
  701. begin_sql();
  702. $xcount = 0;
  703. }
  704. }
  705. commit_sql();
  706. unset($users); // free mem
  707. }
  708. } else { // end do updates
  709. print "No updates to be done\n";
  710. }
  711. /// User Additions
  712. // find users missing in DB that are in LDAP
  713. // note that get_records_sql wants at least 2 fields returned,
  714. // and gives me a nifty object I don't want.
  715. // note: we do not care about deleted accounts anymore, this feature was replaced by suspending to nologin auth plugin
  716. $sql = "SELECT e.username, e.username
  717. FROM $temptable e LEFT JOIN {$CFG->prefix}user u ON e.username = u.username
  718. WHERE u.id IS NULL";
  719. $add_users = get_records_sql($sql); // get rid of the fat
  720. if (!empty($add_users)) {
  721. print "User entries to add: ". count($add_users). "\n";
  722. $sitecontext = get_context_instance(CONTEXT_SYSTEM);
  723. if (!empty($this->config->creators) and !empty($this->config->memberattribute)
  724. and $roles = get_roles_with_capability('moodle/legacy:coursecreator', CAP_ALLOW)) {
  725. $creatorrole = array_shift($roles); // We can only use one, let's use the first one
  726. } else {
  727. $creatorrole = false;
  728. }
  729. begin_sql();
  730. foreach ($add_users as $user) {
  731. $user = $this->get_userinfo_asobj(addslashes($user->username));
  732. // prep a few params
  733. $user->modified = time();
  734. $user->confirmed = 1;
  735. $user->auth = 'ldap';
  736. $user->mnethostid = $CFG->mnet_localhost_id;
  737. // get_userinfo_asobj() might have replaced $user->username with the value
  738. // from the LDAP server (which can be mixed-case). Make sure it's lowercase
  739. $user->username = trim(moodle_strtolower($user->username));
  740. if (empty($user->lang)) {
  741. $user->lang = $CFG->lang;
  742. }
  743. $user = addslashes_recursive($user);
  744. if ($id = insert_record('user',$user)) {
  745. echo "\t"; print_string('auth_dbinsertuser', 'auth', array(stripslashes($user->username), $id)); echo "\n";
  746. $userobj = $this->update_user_record($user->username);
  747. if (!empty($this->config->forcechangepassword)) {
  748. set_user_preference('auth_forcepasswordchange', 1, $userobj->id);
  749. }
  750. // add course creators if needed
  751. if ($creatorrole !== false and $this->iscreator(stripslashes($user->username))) {
  752. role_assign($creatorrole->id, $id, 0, $sitecontext->id, 0, 0, 0, 'ldap');
  753. }
  754. } else {
  755. echo "\t"; print_string('auth_dbinsertusererror', 'auth', $user->username); echo "\n";
  756. }
  757. }
  758. commit_sql();
  759. unset($add_users); // free mem
  760. } else {
  761. print "No users to be added\n";
  762. }
  763. $this->ldap_close();
  764. return true;
  765. }
  766. /**
  767. * Update a local user record from an external source.
  768. * This is a lighter version of the one in moodlelib -- won't do
  769. * expensive ops such as enrolment.
  770. *
  771. * If you don't pass $updatekeys, there is a performance hit and
  772. * values removed from LDAP won't be removed from moodle.
  773. *
  774. * @param string $username username (with system magic quotes)
  775. */
  776. function update_user_record($username, $updatekeys = false) {
  777. global $CFG;
  778. //just in case check text case
  779. $username = trim(moodle_strtolower($username));
  780. // get the current user record
  781. $user = get_record('user', 'username', $username, 'mnethostid', $CFG->mnet_localhost_id);
  782. if (empty($user)) { // trouble
  783. error_log("Cannot update non-existent user: ".stripslashes($username));
  784. print_error('auth_dbusernotexist','auth','',$username);
  785. die;
  786. }
  787. // Protect the userid from being overwritten
  788. $userid = $user->id;
  789. if ($newinfo = $this->get_userinfo($username)) {
  790. $newinfo = truncate_userinfo($newinfo);
  791. if (empty($updatekeys)) { // all keys? this does not support removing values
  792. $updatekeys = array_keys($newinfo);
  793. }
  794. foreach ($updatekeys as $key) {
  795. if (isset($newinfo[$key])) {
  796. $value = $newinfo[$key];
  797. } else {
  798. $value = '';
  799. }
  800. if (!empty($this->config->{'field_updatelocal_' . $key})) {
  801. if ($user->{$key} != $value) { // only update if it's changed
  802. set_field('user', $key, addslashes($value), 'id', $userid);
  803. }
  804. }
  805. }
  806. } else {
  807. return false;
  808. }
  809. return get_record_select('user', "id = $userid AND deleted = 0");
  810. }
  811. /**
  812. * Bulk insert in SQL's temp table
  813. * @param array $users is an array of usernames
  814. */
  815. function ldap_bulk_insert($users, $temptable) {
  816. // bulk insert -- superfast with $bulk_insert_records
  817. $sql = 'INSERT INTO ' . $temptable . ' (username) VALUES ';
  818. // make those values safe
  819. $users = addslashes_recursive($users);
  820. // join and quote the whole lot
  821. $sql = $sql . "('" . implode("'),('", $users) . "')";
  822. print "\t+ " . count($users) . " users\n";
  823. execute_sql($sql, false);
  824. }
  825. /**
  826. * Activates (enables) user in external db so user can login to external db
  827. *
  828. * @param mixed $username username (with system magic quotes)
  829. * @return boolen result
  830. */
  831. function user_activate($username) {
  832. $textlib = textlib_get_instance();
  833. $extusername = $textlib->convert(stripslashes($username), 'utf-8', $this->config->ldapencoding);
  834. $ldapconnection = $this->ldap_connect();
  835. $userdn = $this->ldap_find_userdn($ldapconnection, $extusername);
  836. switch ($this->config->user_type) {
  837. case 'edir':
  838. $newinfo['loginDisabled']="FALSE";
  839. break;
  840. case 'ad':
  841. // We need to unset the ACCOUNTDISABLE bit in the
  842. // userAccountControl attribute ( see
  843. // http://support.microsoft.com/kb/305144 )
  844. $sr = ldap_read($ldapconnection, $userdn, '(objectClass=*)',
  845. array('userAccountControl'));
  846. $info = ldap_get_entries($ldapconnection, $sr);
  847. $newinfo['userAccountControl'] = $info[0]['userAccountControl'][0]
  848. & (~AUTH_AD_ACCOUNTDISABLE);
  849. break;
  850. default:
  851. error ('auth: ldap user_activate() does not support selected usertype:"'.$this->config->user_type.'" (..yet)');
  852. }
  853. $result = ldap_modify($ldapconnection, $userdn, $newinfo);
  854. $this->ldap_close();
  855. return $result;
  856. }
  857. /**
  858. * Disables user in external db so user can't login to external db
  859. *
  860. * @param mixed $username username
  861. * @return boolean result
  862. */
  863. /* function user_disable($username) {
  864. $textlib = textlib_get_instance();
  865. $extusername = $textlib->convert(stripslashes($username), 'utf-8', $this->config->ldapencoding);
  866. $ldapconnection = $this->ldap_connect();
  867. $userdn = $this->ldap_find_userdn($ldapconnection, $extusername);
  868. switch ($this->config->user_type) {
  869. case 'edir':
  870. $newinfo['loginDisabled']="TRUE";
  871. break;
  872. case 'ad':
  873. // We need to set the ACCOUNTDISABLE bit in the
  874. // userAccountControl attribute ( see
  875. // http://support.microsoft.com/kb/305144 )
  876. $sr = ldap_read($ldapconnection, $userdn, '(objectClass=*)',
  877. array('userAccountControl'));
  878. $info = auth_ldap_get_entries($ldapconnection, $sr);
  879. $newinfo['userAccountControl'] = $info[0]['userAccountControl'][0]
  880. | AUTH_AD_ACCOUNTDISABLE;
  881. break;
  882. default:
  883. error ('auth: ldap user_disable() does not support selected usertype (..yet)');
  884. }
  885. $result = ldap_modify($ldapconnection, $userdn, $newinfo);
  886. $this->ldap_close();
  887. return $result;
  888. }*/
  889. /**
  890. * Returns true if user should be coursecreator.
  891. *
  892. * @param mixed $username username (without system magic quotes)
  893. * @return boolean result
  894. */
  895. function iscreator($username) {
  896. if (empty($this->config->creators) or empty($this->config->memberattribute)) {
  897. return null;
  898. }
  899. $textlib = textlib_get_instance();
  900. $extusername = $textlib->convert($username, 'utf-8', $this->config->ldapencoding);
  901. return (boolean)$this->ldap_isgroupmember($extusername, $this->config->creators);
  902. }
  903. /**
  904. * Called when the user record is updated.
  905. * Modifies user in external database. It takes olduser (before changes) and newuser (after changes)
  906. * conpares information saved modified information to external db.
  907. *
  908. * @param mixed $olduser Userobject before modifications (without system magic quotes)
  909. * @param mixed $newuser Userobject new modified userobject (without system magic quotes)
  910. * @return boolean result
  911. *
  912. */
  913. function user_update($olduser, $newuser) {
  914. global $USER;
  915. if (isset($olduser->username) and isset($newuser->username) and $olduser->username != $newuser->username) {
  916. error_log("ERROR:User renaming not allowed in LDAP");
  917. return false;
  918. }
  919. if (isset($olduser->auth) and $olduser->auth != 'ldap') {
  920. return true; // just change auth and skip update
  921. }
  922. $attrmap = $this->ldap_attributes();
  923. // Before doing anything else, make sure really need to update anything
  924. // in the external LDAP server.
  925. $update_external = false;
  926. foreach ($attrmap as $key => $ldapkeys) {
  927. if (!empty($this->config->{'field_updateremote_'.$key})) {
  928. $update_external = true;
  929. break;
  930. }
  931. }
  932. if (!$update_external) {
  933. return true;
  934. }
  935. $textlib = textlib_get_instance();
  936. $extoldusername = $textlib->convert($olduser->username, 'utf-8', $this->config->ldapencoding);
  937. $ldapconnection = $this->ldap_connect();
  938. $search_attribs = array();
  939. foreach ($attrmap as $key => $values) {
  940. if (!is_array($values)) {
  941. $values = array($values);
  942. }
  943. foreach ($values as $value) {
  944. if (!in_array($value, $search_attribs)) {
  945. array_push($search_attribs, $value);
  946. }
  947. }
  948. }
  949. $user_dn = $this->ldap_find_userdn($ldapconnection, $extoldusername);
  950. $user_info_result = ldap_read($ldapconnection, $user_dn,
  951. $this->config->objectclass, $search_attribs);
  952. if ($user_info_result) {
  953. $user_entry = $this->ldap_get_entries($ldapconnection, $user_info_result);
  954. if (empty($user_entry)) {
  955. $error = 'ldap: Could not find user while updating externally. '.
  956. 'Details follow: search base: \''.$user_dn.'\'; search filter: \''.
  957. $this->config->objectclass.'\'; search attributes: ';
  958. foreach ($search_attribs as $attrib) {
  959. $error .= $attrib.' ';
  960. }
  961. error_log($error);
  962. return false; // old user not found!
  963. } else if (count($user_entry) > 1) {
  964. error_log('ldap: Strange! More than one user record found in ldap. Only using the first one.');
  965. return false;
  966. }
  967. $user_entry = $user_entry[0];
  968. //error_log(var_export($user_entry) . 'fpp' );
  969. foreach ($attrmap as $key => $ldapkeys) {
  970. // only process if the moodle field ($key) has changed and we
  971. // are set to update LDAP with it
  972. if (isset($olduser->$key) and isset($newuser->$key)
  973. and $olduser->$key !== $newuser->$key
  974. and !empty($this->config->{'field_updateremote_'. $key})) {
  975. // for ldap values that could be in more than one
  976. // ldap key, we will do our best to match
  977. // where they came from
  978. $ambiguous = true;
  979. $changed = false;
  980. if (!is_array($ldapkeys)) {
  981. $ldapkeys = array($ldapkeys);
  982. }
  983. if (count($ldapkeys) < 2) {
  984. $ambiguous = false;
  985. }
  986. $nuvalue = $textlib->convert($newuser->$key, 'utf-8', $this->config->ldapencoding);
  987. empty($nuvalue) ? $nuvalue = array() : $nuvalue;
  988. $ouvalue = $textlib->convert($olduser->$key, 'utf-8', $this->config->ldapencoding);
  989. foreach ($ldapkeys as $ldapkey) {
  990. $ldapkey = $ldapkey;
  991. $ldapvalue = $user_entry[$ldapkey][0];
  992. if (!$ambiguous) {
  993. // skip update if the values already match
  994. if ($nuvalue !== $ldapvalue) {
  995. //this might fail due to schema validation
  996. if (@ldap_modify($ldapconnection, $user_dn, array($ldapkey => $nuvalue))) {
  997. continue;
  998. } else {
  999. error_log('Error updating LDAP record. Error code: '
  1000. . ldap_errno($ldapconnection) . '; Error string : '
  1001. . ldap_err2str(ldap_errno($ldapconnection))
  1002. . "\nKey ($key) - old moodle value: '$ouvalue' new value: '$nuvalue'");
  1003. continue;
  1004. }
  1005. }
  1006. } else {
  1007. // ambiguous
  1008. // value empty before in Moodle (and LDAP) - use 1st ldap candidate field
  1009. // no need to guess
  1010. if ($ouvalue === '') { // value empty before - use 1st ldap candidate
  1011. //this might fail due to schema validation
  1012. if (@ldap_modify($ldapconnection, $user_dn, array($ldapkey => $nuvalue))) {
  1013. $changed = true;
  1014. continue;
  1015. } else {
  1016. error_log('Error updating LDAP record. Error code: '
  1017. . ldap_errno($ldapconnection) . '; Error string : '
  1018. . ldap_err2str(ldap_errno($ldapconnection))
  1019. . "\nKey ($key) - old moodle value: '$ouvalue' new value: '$nuvalue'");
  1020. continue;
  1021. }
  1022. }
  1023. // we found which ldap key to update!
  1024. if ($ouvalue !== '' and $ouvalue === $ldapvalue ) {
  1025. //this might fail due to schema validation
  1026. if (@ldap_modify($ldapconnection, $user_dn, array($ldapkey => $nuvalue))) {
  1027. $changed = true;
  1028. continue;
  1029. } else {
  1030. error_log('Error updating LDAP record. Error code: '
  1031. . ldap_errno($ldapconnection) . '; Error string : '
  1032. . ldap_err2str(ldap_errno($ldapconnection))
  1033. . "\nKey ($key) - old moodle value: '$ouvalue' new value: '$nuvalue'");
  1034. continue;
  1035. }
  1036. }
  1037. }
  1038. }
  1039. if ($ambiguous and !$changed) {
  1040. error_log("Failed to update LDAP with ambiguous field $key".
  1041. " old moodle value: '" . $ouvalue .
  1042. "' new value '" . $nuvalue );
  1043. }
  1044. }
  1045. }
  1046. } else {
  1047. error_log("ERROR:No user found in LDAP");
  1048. $this->ldap_close();
  1049. return false;
  1050. }
  1051. $this->ldap_close();
  1052. return true;
  1053. }
  1054. /**
  1055. * changes userpassword in external db
  1056. *
  1057. * called when the user password is updated.
  1058. * changes userpassword in external db
  1059. *
  1060. * @param object $user User table object (with system magic quotes)
  1061. * @param string $newpassword Plaintext password (with system magic quotes)
  1062. * @return boolean result
  1063. *
  1064. */
  1065. function user_update_password($user, $newpassword) {
  1066. /// called when the user password is updated -- it assumes it is called by an admin
  1067. /// or that you've otherwise checked the user's credentials
  1068. /// IMPORTANT: $newpassword must be cleartext, not crypted/md5'ed
  1069. global $USER;
  1070. $result = false;
  1071. $username = $user->username;
  1072. $textlib = textlib_get_instance();
  1073. $extusername = $textlib->convert(stripslashes($username), 'utf-8', $this->config->ldapencoding);
  1074. $extpassword = $textlib->convert(stripslashes($newpassword), 'utf-8', $this->config->ldapencoding);
  1075. switch ($this->config->passtype) {
  1076. case 'md5':
  1077. $extpassword = '{MD5}' . base64_encode(pack('H*', md5($extpassword)));
  1078. break;
  1079. case 'sha1':
  1080. $extpassword = '{SHA}' . base64_encode(pack('H*', sha1($extpassword)));
  1081. break;
  1082. case 'plaintext':
  1083. default:
  1084. break; // plaintext
  1085. }
  1086. $ldapconnection = $this->ldap_connect();
  1087. $user_dn = $this->ldap_find_userdn($ldapconnection, $extusername);
  1088. if (!$user_dn) {
  1089. error_log('LDAP Error in user_update_password(). No DN for: ' . stripslashes($user->username));
  1090. return false;
  1091. }
  1092. switch ($this->config->user_type) {
  1093. case 'edir':
  1094. //Change password
  1095. $result = ldap_modify($ldapconnection, $user_dn, array('userPassword' => $extpassword));
  1096. if (!$result) {
  1097. error_log('LDAP Error in user_update_password(). Error code: '
  1098. . ldap_errno($ldapconnection) . '; Error string : '
  1099. . ldap_err2str(ldap_errno($ldapconnection)));
  1100. }
  1101. //Update password expiration time, grace logins count
  1102. $search_attribs = array($this->config->expireattr, 'passwordExpirationInterval','loginGraceLimit' );
  1103. $sr = ldap_read($ldapconnection, $user_dn, '(objectClass=*)', $search_attribs);
  1104. if ($sr) {
  1105. $info=$this->ldap_get_entries($ldapconnection, $sr);
  1106. $newattrs = array();
  1107. if (!empty($info[0][$this->config->expireattr][0])) {
  1108. //Set expiration time only if passwordExpirationInterval is defined
  1109. if (!empty($info[0]['passwordExpirationInterval'][0])) {
  1110. $expirationtime = time() + $info[0]['passwordExpirationInterval'][0];
  1111. $ldapexpirationtime = $this->ldap_unix2expirationtime($expirationtime);
  1112. $newattrs['passwordExpirationTime'] = $ldapexpirationtime;
  1113. }
  1114. //set gracelogin count
  1115. if (!empty($info[0]['loginGraceLimit'][0])) {
  1116. $newattrs['loginGraceRemaining']= $info[0]['loginGraceLimit'][0];
  1117. }
  1118. //Store attribute changes to ldap
  1119. $result = ldap_modify($ldapconnection, $user_dn, $newattrs);
  1120. if (!$result) {
  1121. error_log('LDAP Error in user_update_password() when modifying expirationtime and/or gracelogins. Error code: '
  1122. . ldap_errno($ldapconnection) . '; Error string : '
  1123. . ldap_err2str(ldap_errno($ldapconnection)));
  1124. }
  1125. }
  1126. }
  1127. else {
  1128. error_log('LDAP Error in user_update_password() when reading password expiration time. Error code: '
  1129. . ldap_errno($ldapconnection) . '; Error string : '
  1130. . ldap_err2str(ldap_errno($ldapconnection)));
  1131. }
  1132. break;
  1133. case 'ad':
  1134. // Passwords in Active Directory must be encoded as Unicode
  1135. // strings (UCS-2 Little Endian format) and surrounded with
  1136. // double quotes. See http://support.microsoft.com/?kbid=269190
  1137. if (!function_exists('mb_convert_encoding')) {
  1138. error_log ('You need the mbstring extension to change passwords in Active Directory');
  1139. return false;
  1140. }
  1141. $extpassword = mb_convert_encoding('"'.$extpassword.'"', "UCS-2LE", $this->config->ldapencoding);
  1142. $result = ldap_modify($ldapconnection, $user_dn, array('unicodePwd' => $extpassword));
  1143. if (!$result) {
  1144. error_log('LDAP Error in user_update_password(). Error code: '
  1145. . ldap_errno($ldapconnection) . '; Error string : '
  1146. . ldap_err2str(ldap_errno($ldapconnection)));
  1147. }
  1148. break;
  1149. default:
  1150. $usedconnection = &$ldapconnection;
  1151. // send ldap the password in cleartext, it will md5 it itself
  1152. $result = ldap_modify($ldapconnection, $user_dn, array('userPassword' => $extpassword));
  1153. if (!$result) {
  1154. error_log('LDAP Error in user_update_password(). Error code: '
  1155. . ldap_errno($ldapconnection) . '; Error string : '
  1156. . ldap_err2str(ldap_errno($ldapconnection)));
  1157. }
  1158. }
  1159. $this->ldap_close();
  1160. return $result;
  1161. }
  1162. //PRIVATE FUNCTIONS starts
  1163. //private functions are named as ldap_*
  1164. /**
  1165. * returns predefined usertypes
  1166. *
  1167. * @return array of predefined usertypes
  1168. */
  1169. function ldap_suppported_usertypes() {
  1170. $types = array();
  1171. $types['edir']='Novell Edirectory';
  1172. $types['rfc2307']='posixAccount (rfc2307)';
  1173. $types['rfc2307bis']='posixAccount (rfc2307bis)';
  1174. $types['samba']='sambaSamAccount (v.3.0.7)';
  1175. $types['ad']='MS ActiveDirectory';
  1176. $types['default']=get_string('default');
  1177. return $types;
  1178. }
  1179. /**
  1180. * Initializes needed variables for ldap-module
  1181. *
  1182. * Uses names defined in ldap_supported_usertypes.
  1183. * $default is first defined as:
  1184. * $default['pseudoname'] = array(
  1185. * 'typename1' => 'value',
  1186. * 'typename2' => 'value'
  1187. * ....
  1188. * );
  1189. *
  1190. * @return array of default values
  1191. */
  1192. function ldap_getdefaults() {
  1193. $default['objectclass'] = array(
  1194. 'edir' => 'User',
  1195. 'rfc2307' => 'posixAccount',
  1196. 'rfc2307bis' => 'posixAccount',
  1197. 'samba' => 'sambaSamAccount',
  1198. 'ad' => 'user',
  1199. 'default' => '*'
  1200. );
  1201. $default['user_attribute'] = array(
  1202. 'edir' => 'cn',
  1203. 'rfc2307' => 'uid',
  1204. 'rfc2307bis' => 'uid',
  1205. 'samba' => 'uid',
  1206. 'ad' => 'cn',
  1207. 'default' => 'cn'
  1208. );
  1209. $default['memberattribute'] = array(
  1210. 'edir' => 'member',
  1211. 'rfc2307' => 'member',
  1212. 'rfc2307bis' => 'member',
  1213. 'samba' => 'member',
  1214. 'ad' => 'member',
  1215. 'default' => 'member'
  1216. );
  1217. $default['memberattribute_isdn'] = array(
  1218. 'edir' => '1',
  1219. 'rfc2307' => '0',
  1220. 'rfc2307bis' => '1',
  1221. 'samba' => '0', //is this right?
  1222. 'ad' => '1',
  1223. 'default' => '0'
  1224. );
  1225. $default['expireattr'] = array (
  1226. 'edir' => 'passwordExpirationTime',
  1227. 'rfc2307' => 'shadowExpire',
  1228. 'rfc2307bis' => 'shadowExpire',
  1229. 'samba' => '', //No support yet
  1230. 'ad' => 'pwdLastSet',
  1231. 'default' => ''
  1232. );
  1233. return $default;
  1234. }
  1235. /**
  1236. * return binaryfields of selected usertype
  1237. *
  1238. *
  1239. * @return array
  1240. */
  1241. function ldap_getbinaryfields () {
  1242. $binaryfields = array (
  1243. 'edir' => array('guid'),
  1244. 'rfc2307' => array(),
  1245. 'rfc2307bis' => array(),
  1246. 'samba' => array(),
  1247. 'ad' => array(),
  1248. 'default' => array()
  1249. );
  1250. if (!empty($this->config->user_type)) {
  1251. return $binaryfields[$this->config->user_type];
  1252. }
  1253. else {
  1254. return $binaryfields['default'];
  1255. }
  1256. }
  1257. function ldap_isbinary ($field) {
  1258. if (empty($field)) {
  1259. return false;
  1260. }
  1261. return array_search($field, $this->ldap_getbinaryfields());
  1262. }
  1263. /**
  1264. * take expirationtime and return it as unixseconds
  1265. *
  1266. * takes expriration timestamp as readed from ldap
  1267. * returns it as unix seconds
  1268. * depends on $this->config->user_type variable
  1269. *
  1270. * @param mixed time Time stamp readed from ldap as it is.
  1271. * @param string $ldapconnection Just needed for Active Directory.
  1272. * @param string $user_dn User distinguished name for the user we are checking password expiration (just needed for Active Directory).
  1273. * @return timestamp
  1274. */
  1275. function ldap_expirationtime2unix ($time, $ldapconnection, $user_dn) {
  1276. $result = false;
  1277. switch ($this->config->user_type) {
  1278. case 'edir':
  1279. $yr=substr($time,0,4);
  1280. $mo=substr($time,4,2);
  1281. $dt=substr($time,6,2);
  1282. $hr=substr($time,8,2);
  1283. $min=substr($time,10,2);
  1284. $sec=substr($time,12,2);
  1285. $result = mktime($hr,$min,$sec,$mo,$dt,$yr);
  1286. break;
  1287. case 'rfc2307':
  1288. case 'rfc2307bis':
  1289. $result = $time * DAYSECS; //The shadowExpire contains the number of DAYS between 01/01/1970 and the actual expiration date
  1290. break;
  1291. case 'ad':
  1292. $result = $this->ldap_get_ad_pwdexpire($time, $ldapconnection, $user_dn);
  1293. break;
  1294. default:
  1295. print_error('auth_ldap_usertypeundefined', 'auth');
  1296. }
  1297. return $result;
  1298. }
  1299. /**
  1300. * takes unixtime and return it formated for storing in ldap
  1301. *
  1302. * @param integer unix time stamp
  1303. */
  1304. function ldap_unix2expirationtime($time) {
  1305. $result = false;
  1306. switch ($this->config->user_type) {
  1307. case 'edir':
  1308. $result=date('YmdHis', $time).'Z';
  1309. break;
  1310. case 'rfc2307':
  1311. case 'rfc2307bis':
  1312. $result = $time ; //Already in correct format
  1313. break;
  1314. default:
  1315. print_error('auth_ldap_usertypeundefined2', 'auth');
  1316. }
  1317. return $result;
  1318. }
  1319. /**
  1320. * checks if user belong to specific group(s)
  1321. * or is in a subtree.
  1322. *
  1323. * Returns true if user belongs group in grupdns string OR
  1324. * if the DN of the user is in a subtree pf the DN provided
  1325. * as "group"
  1326. *
  1327. * @param mixed $username username
  1328. * @param mixed $groupdns string of group dn separated by ;
  1329. *
  1330. */
  1331. function ldap_isgroupmember($extusername='', $groupdns='') {
  1332. // Takes username and groupdn(s) , separated by ;
  1333. // Returns true if user is member of any given groups
  1334. $ldapconnection = $this->ldap_connect();
  1335. if (empty($extusername) or empty($groupdns)) {
  1336. return false;
  1337. }
  1338. if ($this->config->memberattribute_isdn) {
  1339. $memberuser = $this->ldap_find_userdn($ldapconnection, $extusername);
  1340. } else {
  1341. $memberuser = $extusername;
  1342. }
  1343. if (empty($memberuser)) {
  1344. return false;
  1345. }
  1346. $groups = explode(";",$groupdns);
  1347. $result = false;
  1348. foreach ($groups as $group) {
  1349. $group = trim($group);
  1350. if (empty($group)) {
  1351. continue;
  1352. }
  1353. // check cheaply if the user's DN sits in a subtree
  1354. // of the "group" DN provided. Granted, this isn't
  1355. // a proper LDAP group, but it's a popular usage.
  1356. if (strpos(strrev(strtolower($memberuser)), strrev(strtolower($group)))===0) {
  1357. $result = true;
  1358. break;
  1359. }
  1360. //echo "Checking group $group for member $username\n";
  1361. $search = ldap_read($ldapconnection, $group, '('.$this->config->memberattribute.'='.$this->filter_addslashes($memberuser).')', array($this->config->memberattribute));
  1362. if (!empty($search) and ldap_count_entries($ldapconnection, $search)) {
  1363. $info = $this->ldap_get_entries($ldapconnection, $search);
  1364. if (count($info) > 0 ) {
  1365. // user is member of group
  1366. $result = true;
  1367. break;
  1368. }
  1369. }
  1370. }
  1371. return $result;
  1372. }
  1373. /**
  1374. * connects to ldap server
  1375. *
  1376. * Tries connect to specified ldap servers.
  1377. * Returns connection result or error.
  1378. *
  1379. * @return connection result
  1380. */
  1381. function ldap_connect($binddn='',$bindpwd='') {
  1382. // Cache ldap connections (they are expensive to set up
  1383. // and can drain the TCP/IP ressources on the server if we
  1384. // are syncing a lot of users (as we try to open a new connection
  1385. // to get the user details). This is the least invasive way
  1386. // to reuse existing connections without greater code surgery.
  1387. if(!empty($this->ldapconnection)) {
  1388. $this->ldapconns++;
  1389. return $this->ldapconnection;
  1390. }
  1391. //Select bind password, With empty values use
  1392. //ldap_bind_* variables or anonymous bind if ldap_bind_* are empty
  1393. if ($binddn == '' and $bindpwd == '') {
  1394. if (!empty($this->config->bind_dn)) {
  1395. $binddn = $this->config->bind_dn;
  1396. }
  1397. if (!empty($this->config->bind_pw)) {
  1398. $bindpwd = $this->config->bind_pw;
  1399. }
  1400. }
  1401. $urls = explode(";",$this->config->host_url);
  1402. foreach ($urls as $server) {
  1403. $server = trim($server);
  1404. if (empty($server)) {
  1405. continue;
  1406. }
  1407. $connresult = ldap_connect($server);
  1408. //ldap_connect returns ALWAYS true
  1409. if (!empty($this->config->version)) {
  1410. ldap_set_option($connresult, LDAP_OPT_PROTOCOL_VERSION, $this->config->version);
  1411. }
  1412. // Fix MDL-10921
  1413. if ($this->config->user_type == 'ad') {
  1414. ldap_set_option($connresult, LDAP_OPT_REFERRALS, 0);
  1415. }
  1416. if (!empty($binddn)) {
  1417. //bind with search-user
  1418. //$debuginfo .= 'Using bind user'.$binddn.'and password:'.$bindpwd;
  1419. $bindresult=ldap_bind($connresult, $binddn,$bindpwd);
  1420. }
  1421. else {
  1422. //bind anonymously
  1423. $bindresult=@ldap_bind($connresult);
  1424. }
  1425. if (!empty($this->config->opt_deref)) {
  1426. ldap_set_option($connresult, LDAP_OPT_DEREF, $this->config->opt_deref);
  1427. }
  1428. if ($bindresult) {
  1429. // Set the connection counter so we can call PHP's ldap_close()
  1430. // when we call $this->ldap_close() for the last 'open' connection.
  1431. $this->ldapconns = 1;
  1432. $this->ldapconnection = $connresult;
  1433. return $connresult;
  1434. }
  1435. $debuginfo .= "<br/>Server: '$server' <br/> Connection: '$connresult'<br/> Bind result: '$bindresult'</br>";
  1436. }
  1437. //If any of servers are alive we have already returned connection
  1438. print_error('auth_ldap_noconnect_all','auth','', $debuginfo);
  1439. return false;
  1440. }
  1441. /**
  1442. * disconnects from a ldap server
  1443. *
  1444. */
  1445. function ldap_close() {
  1446. $this->ldapconns--;
  1447. if($this->ldapconns == 0) {
  1448. @ldap_close($this->ldapconnection);
  1449. unset($this->ldapconnection);
  1450. }
  1451. }
  1452. /**
  1453. * retuns dn of username
  1454. *
  1455. * Search specified contexts for username and return user dn
  1456. * like: cn=username,ou=suborg,o=org
  1457. *
  1458. * @param mixed $ldapconnection $ldapconnection result
  1459. * @param mixed $username username (external encoding no slashes)
  1460. *
  1461. */
  1462. function ldap_find_userdn ($ldapconnection, $extusername) {
  1463. //default return value
  1464. $ldap_user_dn = FALSE;
  1465. //get all contexts and look for first matching user
  1466. $ldap_contexts = explode(";",$this->config->contexts);
  1467. if (!empty($this->config->create_context)) {
  1468. array_push($ldap_contexts, $this->config->create_context);
  1469. }
  1470. foreach ($ldap_contexts as $context) {
  1471. $context = trim($context);
  1472. if (empty($context)) {
  1473. continue;
  1474. }
  1475. if ($this->config->search_sub) {
  1476. //use ldap_search to find first user from subtree
  1477. $ldap_result = ldap_search($ldapconnection, $context, "(".$this->config->user_attribute."=".$this->filter_addslashes($extusername).")",array($this->config->user_attribute));
  1478. }
  1479. else {
  1480. //search only in this context
  1481. $ldap_result = ldap_list($ldapconnection, $context, "(".$this->config->user_attribute."=".$this->filter_addslashes($extusername).")",array($this->config->user_attribute));
  1482. }
  1483. $entry = ldap_first_entry($ldapconnection,$ldap_result);
  1484. if ($entry) {
  1485. $ldap_user_dn = ldap_get_dn($ldapconnection, $entry);
  1486. break ;
  1487. }
  1488. }
  1489. return $ldap_user_dn;
  1490. }
  1491. /**
  1492. * retuns user attribute mappings between moodle and ldap
  1493. *
  1494. * @return array
  1495. */
  1496. function ldap_attributes () {
  1497. $moodleattributes = array();
  1498. foreach ($this->userfields as $field) {
  1499. if (!empty($this->config->{"field_map_$field"})) {
  1500. $moodleattributes[$field] = $this->config->{"field_map_$field"};
  1501. if (preg_match('/,/',$moodleattributes[$field])) {
  1502. $moodleattributes[$field] = explode(',', $moodleattributes[$field]); // split ?
  1503. }
  1504. }
  1505. }
  1506. $moodleattributes['username'] = $this->config->user_attribute;
  1507. return $moodleattributes;
  1508. }
  1509. /**
  1510. * return all usernames from ldap
  1511. *
  1512. * @return array
  1513. */
  1514. function ldap_get_userlist($filter="*") {
  1515. /// returns all users from ldap servers
  1516. $fresult = array();
  1517. $ldapconnection = $this->ldap_connect();
  1518. if ($filter=="*") {
  1519. $filter = '(&('.$this->config->user_attribute.'=*)'.$this->config->objectclass.')';
  1520. }
  1521. $contexts = explode(";",$this->config->contexts);
  1522. if (!empty($this->config->create_context)) {
  1523. array_push($contexts, $this->config->create_context);
  1524. }
  1525. foreach ($contexts as $context) {
  1526. $context = trim($context);
  1527. if (empty($context)) {
  1528. continue;
  1529. }
  1530. if ($this->config->search_sub) {
  1531. //use ldap_search to find first user from subtree
  1532. $ldap_result = ldap_search($ldapconnection, $context,$filter,array($this->config->user_attribute));
  1533. }
  1534. else {
  1535. //search only in this context
  1536. $ldap_result = ldap_list($ldapconnection, $context,
  1537. $filter,
  1538. array($this->config->user_attribute));
  1539. }
  1540. $users = $this->ldap_get_entries($ldapconnection, $ldap_result);
  1541. //add found users to list
  1542. for ($i=0;$i<count($users);$i++) {
  1543. array_push($fresult, ($users[$i][$this->config->user_attribute][0]) );
  1544. }
  1545. }
  1546. return $fresult;
  1547. }
  1548. /**
  1549. * return entries from ldap
  1550. *
  1551. * Returns values like ldap_get_entries but is
  1552. * binary compatible and return all attributes as array
  1553. *
  1554. * @return array ldap-entries
  1555. */
  1556. function ldap_get_entries($conn, $searchresult) {
  1557. //Returns values like ldap_get_entries but is
  1558. //binary compatible
  1559. $i=0;
  1560. $fresult=array();
  1561. $entry = ldap_first_entry($conn, $searchresult);
  1562. do {
  1563. $attributes = @ldap_get_attributes($conn, $entry);
  1564. for ($j=0; $j<$attributes['count']; $j++) {
  1565. $values = ldap_get_values_len($conn, $entry,$attributes[$j]);
  1566. if (is_array($values)) {
  1567. $fresult[$i][$attributes[$j]] = $values;
  1568. }
  1569. else {
  1570. $fresult[$i][$attributes[$j]] = array($values);
  1571. }
  1572. }
  1573. $i++;
  1574. }
  1575. while ($entry = @ldap_next_entry($conn, $entry));
  1576. //were done
  1577. return ($fresult);
  1578. }
  1579. function prevent_local_passwords() {
  1580. return !empty($this->config->preventpassindb);
  1581. }
  1582. /**
  1583. * Returns true if this authentication plugin is 'internal'.
  1584. *
  1585. * @return bool
  1586. */
  1587. function is_internal() {
  1588. return false;
  1589. }
  1590. /**
  1591. * Returns true if this authentication plugin can change the user's
  1592. * password.
  1593. *
  1594. * @return bool
  1595. */
  1596. function can_change_password() {
  1597. return !empty($this->config->stdchangepassword) or !empty($this->config->changepasswordurl);
  1598. }
  1599. /**
  1600. * Returns the URL for changing the user's pw, or empty if the default can
  1601. * be used.
  1602. *
  1603. * @return string url
  1604. */
  1605. function change_password_url() {
  1606. if (empty($this->config->stdchangepassword)) {
  1607. return $this->config->changepasswordurl;
  1608. } else {
  1609. return '';
  1610. }
  1611. }
  1612. /**
  1613. * Will get called before the login page is shown, if NTLM SSO
  1614. * is enabled, and the user is in the right network, we'll redirect
  1615. * to the magic NTLM page for SSO...
  1616. *
  1617. */
  1618. function loginpage_hook() {
  1619. global $CFG, $SESSION;
  1620. // HTTPS is potentially required
  1621. httpsrequired();
  1622. if (($_SERVER['REQUEST_METHOD'] === 'GET' // Only on initial GET of loginpage
  1623. || ($_SERVER['REQUEST_METHOD'] === 'POST'
  1624. && (get_referer() != strip_querystring(qualified_me()))))
  1625. // Or when POSTed from another place
  1626. // See MDL-14071
  1627. && !empty($this->config->ntlmsso_enabled) // SSO enabled
  1628. && !empty($this->config->ntlmsso_subnet) // have a subnet to test for
  1629. && empty($_GET['authldap_skipntlmsso']) // haven't failed it yet
  1630. && (isguestuser() || !isloggedin()) // guestuser or not-logged-in users
  1631. && address_in_subnet(getremoteaddr(), $this->config->ntlmsso_subnet)) {
  1632. // First, let's remember where we were trying to get to before we got here
  1633. if (empty($SESSION->wantsurl)) {
  1634. $SESSION->wantsurl = (array_key_exists('HTTP_REFERER', $_SERVER) &&
  1635. $_SERVER['HTTP_REFERER'] != $CFG->wwwroot &&
  1636. $_SERVER['HTTP_REFERER'] != $CFG->wwwroot.'/' &&
  1637. $_SERVER['HTTP_REFERER'] != $CFG->httpswwwroot.'/login/' &&
  1638. $_SERVER['HTTP_REFERER'] != $CFG->httpswwwroot.'/login/index.php')
  1639. ? $_SERVER['HTTP_REFERER'] : NULL;
  1640. }
  1641. // Now start the whole NTLM machinery.
  1642. if(!empty($this->config->ntlmsso_ie_fastpath)) {
  1643. // Shortcut for IE browsers: skip the attempt page at all
  1644. if(check_browser_version('MSIE')) {
  1645. $sesskey = sesskey();
  1646. redirect($CFG->wwwroot.'/auth/ldap/ntlmsso_magic.php?sesskey='.$sesskey);
  1647. } else {
  1648. redirect($CFG->httpswwwroot.'/login/index.php?authldap_skipntlmsso=1');
  1649. }
  1650. } else {
  1651. redirect($CFG->wwwroot.'/auth/ldap/ntlmsso_attempt.php');
  1652. }
  1653. }
  1654. // No NTLM SSO, Use the normal login page instead.
  1655. // If $SESSION->wantsurl is empty and we have a 'Referer:' header, the login
  1656. // page insists on redirecting us to that page after user validation. If
  1657. // we clicked on the redirect link at the ntlmsso_finish.php page instead
  1658. // of waiting for the redirection to happen, then we have a 'Referer:' header
  1659. // we don't want to use at all. As we can't get rid of it, just point
  1660. // $SESSION->wantsurl to $CFG->wwwroot (after all, we came from there).
  1661. if (empty($SESSION->wantsurl)
  1662. && (get_referer() == $CFG->httpswwwroot.'/auth/ldap/ntlmsso_finish.php')) {
  1663. $SESSION->wantsurl = $CFG->wwwroot;
  1664. }
  1665. }
  1666. /**
  1667. * To be called from a page running under NTLM's
  1668. * "Integrated Windows Authentication".
  1669. *
  1670. * If successful, it will set a special "cookie" (not an HTTP cookie!)
  1671. * in cache_flags under the "auth/ldap/ntlmsess" "plugin" and return true.
  1672. * The "cookie" will be picked up by ntlmsso_finish() to complete the
  1673. * process.
  1674. *
  1675. * On failure it will return false for the caller to display an appropriate
  1676. * error message (probably saying that Integrated Windows Auth isn't enabled!)
  1677. *
  1678. * NOTE that this code will execute under the OS user credentials,
  1679. * so we MUST avoid dealing with files -- such as session files.
  1680. * (The caller should set $nomoodlecookie before including config.php)
  1681. *
  1682. */
  1683. function ntlmsso_magic($sesskey) {
  1684. if (isset($_SERVER['REMOTE_USER']) && !empty($_SERVER['REMOTE_USER'])) {
  1685. // HTTP __headers__ seem to be sent in ISO-8859-1 encoding
  1686. // (according to my reading of RFC-1945, RFC-2616 and RFC-2617 and
  1687. // my local tests), so we need to convert the REMOTE_USER value
  1688. // (i.e., what we got from the HTTP WWW-Authenticate header) into UTF-8
  1689. $textlib = textlib_get_instance();
  1690. $username = $textlib->convert($_SERVER['REMOTE_USER'], 'iso-8859-1', 'utf-8');
  1691. $username = substr(strrchr($username, '\\'), 1); //strip domain info
  1692. $username = moodle_strtolower($username); //compatibility hack
  1693. set_cache_flag('auth/ldap/ntlmsess', $sesskey, $username, AUTH_NTLMTIMEOUT);
  1694. return true;
  1695. }
  1696. return false;
  1697. }
  1698. /**
  1699. * Find the session set by ntlmsso_magic(), validate it and
  1700. * call authenticate_user_login() to authenticate the user through
  1701. * the auth machinery.
  1702. *
  1703. * It is complemented by a similar check in user_login().
  1704. *
  1705. * If it succeeds, it never returns.
  1706. *
  1707. */
  1708. function ntlmsso_finish() {
  1709. global $CFG, $USER, $SESSION;
  1710. $key = sesskey();
  1711. $cf = get_cache_flags('auth/ldap/ntlmsess');
  1712. if (!isset($cf[$key]) || $cf[$key] === '') {
  1713. return false;
  1714. }
  1715. $username = $cf[$key];
  1716. // Here we want to trigger the whole authentication machinery
  1717. // to make sure no step is bypassed...
  1718. $user = authenticate_user_login($username, $key);
  1719. if ($user) {
  1720. add_to_log(SITEID, 'user', 'login', "view.php?id=$USER->id&course=".SITEID,
  1721. $user->id, 0, $user->id);
  1722. $USER = complete_user_login($user);
  1723. // Cleanup the key to prevent reuse...
  1724. // and to allow re-logins with normal credentials
  1725. unset_cache_flag('auth/ldap/ntlmsess', $key);
  1726. /// Redirection
  1727. if (user_not_fully_set_up($USER)) {
  1728. $urltogo = $CFG->wwwroot.'/user/edit.php';
  1729. // We don't delete $SESSION->wantsurl yet, so we get there later
  1730. } else if (isset($SESSION->wantsurl) and (strpos($SESSION->wantsurl, $CFG->wwwroot) === 0)) {
  1731. $urltogo = $SESSION->wantsurl; /// Because it's an address in this site
  1732. unset($SESSION->wantsurl);
  1733. } else {
  1734. // no wantsurl stored or external - go to homepage
  1735. $urltogo = $CFG->wwwroot.'/';
  1736. unset($SESSION->wantsurl);
  1737. }
  1738. redirect($urltogo);
  1739. }
  1740. // Should never reach here.
  1741. return false;
  1742. }
  1743. /**
  1744. * Sync roles for this user
  1745. *
  1746. * @param $user object user object (without system magic quotes)
  1747. */
  1748. function sync_roles($user) {
  1749. $iscreator = $this->iscreator($user->username);
  1750. if ($iscreator === null) {
  1751. return; //nothing to sync - creators not configured
  1752. }
  1753. if ($roles = get_roles_with_capability('moodle/legacy:coursecreator', CAP_ALLOW)) {
  1754. $creatorrole = array_shift($roles); // We can only use one, let's use the first one
  1755. $systemcontext = get_context_instance(CONTEXT_SYSTEM);
  1756. if ($iscreator) { // Following calls will not create duplicates
  1757. role_assign($creatorrole->id, $user->id, 0, $systemcontext->id, 0, 0, 0, 'ldap');
  1758. } else {
  1759. //unassign only if previously assigned by this plugin!
  1760. role_unassign($creatorrole->id, $user->id, 0, $systemcontext->id, 'ldap');
  1761. }
  1762. }
  1763. }
  1764. /**
  1765. * Prints a form for configuring this authentication plugin.
  1766. *
  1767. * This function is called from admin/auth.php, and outputs a full page with
  1768. * a form for configuring this plugin.
  1769. *
  1770. * @param array $page An object containing all the data for this page.
  1771. */
  1772. function config_form($config, $err, $user_fields) {
  1773. include 'config.html';
  1774. }
  1775. /**
  1776. * Processes and stores configuration data for this authentication plugin.
  1777. */
  1778. function process_config($config) {
  1779. // set to defaults if undefined
  1780. if (!isset($config->host_url))
  1781. { $config->host_url = ''; }
  1782. if (empty($config->ldapencoding))
  1783. { $config->ldapencoding = 'utf-8'; }
  1784. if (!isset($config->contexts))
  1785. { $config->contexts = ''; }
  1786. if (!isset($config->user_type))
  1787. { $config->user_type = 'default'; }
  1788. if (!isset($config->user_attribute))
  1789. { $config->user_attribute = ''; }
  1790. if (!isset($config->search_sub))
  1791. { $config->search_sub = ''; }
  1792. if (!isset($config->opt_deref))
  1793. { $config->opt_deref = ''; }
  1794. if (!isset($config->preventpassindb))
  1795. { $config->preventpassindb = 0; }
  1796. if (!isset($config->bind_dn))
  1797. {$config->bind_dn = ''; }
  1798. if (!isset($config->bind_pw))
  1799. {$config->bind_pw = ''; }
  1800. if (!isset($config->version))
  1801. {$config->version = '2'; }
  1802. if (!isset($config->objectclass))
  1803. {$config->objectclass = ''; }
  1804. if (!isset($config->memberattribute))
  1805. {$config->memberattribute = ''; }
  1806. if (!isset($config->memberattribute_isdn))
  1807. {$config->memberattribute_isdn = ''; }
  1808. if (!isset($config->creators))
  1809. {$config->creators = ''; }
  1810. if (!isset($config->create_context))
  1811. {$config->create_context = ''; }
  1812. if (!isset($config->expiration))
  1813. {$config->expiration = ''; }
  1814. if (!isset($config->expiration_warning))
  1815. {$config->expiration_warning = '10'; }
  1816. if (!isset($config->expireattr))
  1817. {$config->expireattr = ''; }
  1818. if (!isset($config->gracelogins))
  1819. {$config->gracelogins = ''; }
  1820. if (!isset($config->graceattr))
  1821. {$config->graceattr = ''; }
  1822. if (!isset($config->auth_user_create))
  1823. {$config->auth_user_create = ''; }
  1824. if (!isset($config->forcechangepassword))
  1825. {$config->forcechangepassword = 0; }
  1826. if (!isset($config->stdchangepassword))
  1827. {$config->stdchangepassword = 0; }
  1828. if (!isset($config->passtype))
  1829. {$config->passtype = 'plaintext'; }
  1830. if (!isset($config->changepasswordurl))
  1831. {$config->changepasswordurl = ''; }
  1832. if (!isset($config->removeuser))
  1833. {$config->removeuser = 0; }
  1834. if (!isset($config->ntlmsso_enabled))
  1835. {$config->ntlmsso_enabled = 0; }
  1836. if (!isset($config->ntlmsso_subnet))
  1837. {$config->ntlmsso_subnet = ''; }
  1838. if (!isset($config->ntlmsso_ie_fastpath))
  1839. {$config->ntlmsso_ie_fastpath = 0; }
  1840. // save settings
  1841. set_config('host_url', $config->host_url, 'auth/ldap');
  1842. set_config('ldapencoding', $config->ldapencoding, 'auth/ldap');
  1843. set_config('host_url', $config->host_url, 'auth/ldap');
  1844. set_config('contexts', $config->contexts, 'auth/ldap');
  1845. set_config('user_type', $config->user_type, 'auth/ldap');
  1846. set_config('user_attribute', $config->user_attribute, 'auth/ldap');
  1847. set_config('search_sub', $config->search_sub, 'auth/ldap');
  1848. set_config('opt_deref', $config->opt_deref, 'auth/ldap');
  1849. set_config('preventpassindb', $config->preventpassindb, 'auth/ldap');
  1850. set_config('bind_dn', $config->bind_dn, 'auth/ldap');
  1851. set_config('bind_pw', $config->bind_pw, 'auth/ldap');
  1852. set_config('version', $config->version, 'auth/ldap');
  1853. set_config('objectclass', trim($config->objectclass), 'auth/ldap');
  1854. set_config('memberattribute', $config->memberattribute, 'auth/ldap');
  1855. set_config('memberattribute_isdn', $config->memberattribute_isdn, 'auth/ldap');
  1856. set_config('creators', $config->creators, 'auth/ldap');
  1857. set_config('create_context', $config->create_context, 'auth/ldap');
  1858. set_config('expiration', $config->expiration, 'auth/ldap');
  1859. set_config('expiration_warning', $config->expiration_warning, 'auth/ldap');
  1860. set_config('expireattr', $config->expireattr, 'auth/ldap');
  1861. set_config('gracelogins', $config->gracelogins, 'auth/ldap');
  1862. set_config('graceattr', $config->graceattr, 'auth/ldap');
  1863. set_config('auth_user_create', $config->auth_user_create, 'auth/ldap');
  1864. set_config('forcechangepassword', $config->forcechangepassword, 'auth/ldap');
  1865. set_config('stdchangepassword', $config->stdchangepassword, 'auth/ldap');
  1866. set_config('passtype', $config->passtype, 'auth/ldap');
  1867. set_config('changepasswordurl', $config->changepasswordurl, 'auth/ldap');
  1868. set_config('removeuser', $config->removeuser, 'auth/ldap');
  1869. set_config('ntlmsso_enabled', (int)$config->ntlmsso_enabled, 'auth/ldap');
  1870. set_config('ntlmsso_subnet', $config->ntlmsso_subnet, 'auth/ldap');
  1871. set_config('ntlmsso_ie_fastpath', (int)$config->ntlmsso_ie_fastpath, 'auth/ldap');
  1872. return true;
  1873. }
  1874. /**
  1875. * Quote control characters in texts used in ldap filters - see RFC 4515/2254
  1876. *
  1877. * @param string
  1878. */
  1879. function filter_addslashes($text) {
  1880. $text = str_replace('\\', '\\5c', $text);
  1881. $text = str_replace(array('*', '(', ')', "\0"),
  1882. array('\\2a', '\\28', '\\29', '\\00'), $text);
  1883. return $text;
  1884. }
  1885. /**
  1886. * The order of the special characters in these arrays _IS IMPORTANT_.
  1887. * Make sure '\\5C' (and '\\') are the first elements of the arrays.
  1888. * Otherwise we'll double replace '\' with '\5C' which is Bad(tm)
  1889. */
  1890. var $LDAP_DN_QUOTED_SPECIAL_CHARS = array('\\5c','\\20','\\22','\\23','\\2b','\\2c','\\3b','\\3c','\\3d','\\3e','\\00');
  1891. var $LDAP_DN_SPECIAL_CHARS = array('\\', ' ', '"', '#', '+', ',', ';', '<', '=', '>', "\0");
  1892. /**
  1893. * Quote control characters in distinguished names used in ldap - See RFC 4514/2253
  1894. *
  1895. * @param string
  1896. * @return string
  1897. */
  1898. function ldap_addslashes($text) {
  1899. $text = str_replace ($this->LDAP_DN_SPECIAL_CHARS,
  1900. $this->LDAP_DN_QUOTED_SPECIAL_CHARS,
  1901. $text);
  1902. return $text;
  1903. }
  1904. /**
  1905. * Get password expiration time for a given user from Active Directory
  1906. *
  1907. * @param string $pwdlastset The time last time we changed the password.
  1908. * @param resource $lcapconn The open LDAP connection.
  1909. * @param string $user_dn The distinguished name of the user we are checking.
  1910. *
  1911. * @return string $unixtime
  1912. */
  1913. function ldap_get_ad_pwdexpire($pwdlastset, $ldapconn, $user_dn){
  1914. define ('ROOTDSE', '');
  1915. // UF_DONT_EXPIRE_PASSWD value taken from MSDN directly
  1916. define ('UF_DONT_EXPIRE_PASSWD', 0x00010000);
  1917. global $CFG;
  1918. if (!function_exists('bcsub')) {
  1919. error_log ('You need the BCMath extension to use grace logins with Active Directory');
  1920. return 0;
  1921. }
  1922. // If UF_DONT_EXPIRE_PASSWD flag is set in user's
  1923. // userAccountControl attribute, the password doesn't expire.
  1924. $sr = ldap_read($ldapconn, $user_dn, '(objectClass=*)',
  1925. array('userAccountControl'));
  1926. if (!$sr) {
  1927. error_log("ldap: error getting userAccountControl for $user_dn");
  1928. // don't expire password, as we are not sure it has to be
  1929. // expired or not.
  1930. return 0;
  1931. }
  1932. $info = $this->ldap_get_entries($ldapconn, $sr);
  1933. $useraccountcontrol = $info[0]['userAccountControl'][0];
  1934. if ($useraccountcontrol & UF_DONT_EXPIRE_PASSWD) {
  1935. // password doesn't expire.
  1936. return 0;
  1937. }
  1938. // If pwdLastSet is zero, the user must change his/her password now
  1939. // (unless UF_DONT_EXPIRE_PASSWD flag is set, but we already
  1940. // tested this above)
  1941. if ($pwdlastset === '0') {
  1942. // password has expired
  1943. return -1;
  1944. }
  1945. // ----------------------------------------------------------------
  1946. // Password expiration time in Active Directory is the composition of
  1947. // two values:
  1948. //
  1949. // - User's pwdLastSet attribute, that stores the last time
  1950. // the password was changed.
  1951. //
  1952. // - Domain's maxPwdAge attribute, that sets how long
  1953. // passwords last in this domain.
  1954. //
  1955. // We already have the first value (passed in as a parameter). We
  1956. // need to get the second one. As we don't know the domain DN, we
  1957. // have to query rootDSE's defaultNamingContext attribute to get
  1958. // it. Then we have to query that DN's maxPwdAge attribute to get
  1959. // the real value.
  1960. //
  1961. // Once we have both values, we just need to combine them. But MS
  1962. // chose to use a different base and unit for time measurements.
  1963. // So we need to convert the values to Unix timestamps (see
  1964. // details below).
  1965. // ----------------------------------------------------------------
  1966. $sr = ldap_read($ldapconn, ROOTDSE, '(objectClass=*)',
  1967. array('defaultNamingContext'));
  1968. if (!$sr) {
  1969. error_log("ldap: error querying rootDSE for Active Directory");
  1970. return 0;
  1971. }
  1972. $info = $this->ldap_get_entries($ldapconn, $sr);
  1973. $domaindn = $info[0]['defaultNamingContext'][0];
  1974. $sr = ldap_read ($ldapconn, $domaindn, '(objectClass=*)',
  1975. array('maxPwdAge'));
  1976. $info = $this->ldap_get_entries($ldapconn, $sr);
  1977. $maxpwdage = $info[0]['maxPwdAge'][0];
  1978. // ----------------------------------------------------------------
  1979. // MSDN says that "pwdLastSet contains the number of 100 nanosecond
  1980. // intervals since January 1, 1601 (UTC), stored in a 64 bit integer".
  1981. //
  1982. // According to Perl's Date::Manip, the number of seconds between
  1983. // this date and Unix epoch is 11644473600. So we have to
  1984. // substract this value to calculate a Unix time, once we have
  1985. // scaled pwdLastSet to seconds. This is the script used to
  1986. // calculate the value shown above:
  1987. //
  1988. // #!/usr/bin/perl -w
  1989. //
  1990. // use Date::Manip;
  1991. //
  1992. // $date1 = ParseDate ("160101010000 UTC");
  1993. // $date2 = ParseDate ("197001010000 UTC");
  1994. // $delta = DateCalc($date1, $date2, \$err);
  1995. // $secs = Delta_Format($delta, 0, "%st");
  1996. // print "$secs \n";
  1997. //
  1998. // MSDN also says that "maxPwdAge is stored as a large integer that
  1999. // represents the number of 100 nanosecond intervals from the time
  2000. // the password was set before the password expires." We also need
  2001. // to scale this to seconds. Bear in mind that this value is stored
  2002. // as a _negative_ quantity (at least in my AD domain).
  2003. //
  2004. // As a last remark, if the low 32 bits of maxPwdAge are equal to 0,
  2005. // the maximum password age in the domain is set to 0, which means
  2006. // passwords do not expire (see
  2007. // http://msdn2.microsoft.com/en-us/library/ms974598.aspx)
  2008. //
  2009. // As the quantities involved are too big for PHP integers, we
  2010. // need to use BCMath functions to work with arbitrary precision
  2011. // numbers.
  2012. // ----------------------------------------------------------------
  2013. // If the low order 32 bits are 0, then passwords do not expire in
  2014. // the domain. Just do '$maxpwdage mod 2^32' and check the result
  2015. // (2^32 = 4294967296)
  2016. if (bcmod ($maxpwdage, 4294967296) === '0') {
  2017. return 0;
  2018. }
  2019. // Add up pwdLastSet and maxPwdAge to get password expiration
  2020. // time, in MS time units. Remember maxPwdAge is stored as a
  2021. // _negative_ quantity, so we need to substract it in fact.
  2022. $pwdexpire = bcsub ($pwdlastset, $maxpwdage);
  2023. // Scale the result to convert it to Unix time units and return
  2024. // that value.
  2025. return bcsub( bcdiv($pwdexpire, '10000000'), '11644473600');
  2026. }
  2027. }
  2028. ?>