ILIAS  eassessment Revision 61809
 All Data Structures Namespaces Files Functions Variables Groups Pages
LDAP.php
Go to the documentation of this file.
1 <?php
2 /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4 foldmethod=marker: */
3 
29 require_once "Auth/Container.php";
33 require_once "PEAR.php";
34 
204 {
205 
206  // {{{ properties
207 
212  var $options = array();
213 
218  var $conn_id = false;
219 
220  // }}}
221 
222  // {{{ Auth_Container_LDAP() [constructor]
223 
230  function Auth_Container_LDAP($params)
231  {
232  if (false === extension_loaded('ldap')) {
233  return PEAR::raiseError('Auth_Container_LDAP: LDAP Extension not loaded',
234  41, PEAR_ERROR_DIE);
235  }
236 
237  $this->_setDefaults();
238 
239  if (is_array($params)) {
240  $this->_parseOptions($params);
241  }
242  }
243 
244  // }}}
245  // {{{ _prepare()
246 
256  function _prepare()
257  {
258  if (!$this->_isValidLink()) {
259  $res = $this->_connect();
260  if (PEAR::isError($res)) {
261  return $res;
262  }
263  }
264  return true;
265  }
266 
267  // }}}
268  // {{{ _connect()
269 
276  function _connect()
277  {
278  $this->log('Auth_Container_LDAP::_connect() called.', AUTH_LOG_DEBUG);
279  // connect
280  if (isset($this->options['url']) && $this->options['url'] != '') {
281  $this->log('Connecting with URL', AUTH_LOG_DEBUG);
282  $conn_params = array($this->options['url']);
283  } else {
284  $this->log('Connecting with host:port', AUTH_LOG_DEBUG);
285  $conn_params = array($this->options['host'], $this->options['port']);
286  }
287 
288  if (($this->conn_id = @call_user_func_array('ldap_connect', $conn_params)) === false) {
289  $this->log('Connection to server failed.', AUTH_LOG_DEBUG);
290  $this->log('LDAP ERROR: '.ldap_errno($this->conn_id).': '.ldap_error($this->conn_id), AUTH_LOG_DEBUG);
291  return PEAR::raiseError('Auth_Container_LDAP: Could not connect to server.', 41);
292  }
293  $this->log('Successfully connected to server', AUTH_LOG_DEBUG);
294 
295  // switch LDAP version
296  if (is_numeric($this->options['version']) && $this->options['version'] > 2) {
297  $this->log("Switching to LDAP version {$this->options['version']}", AUTH_LOG_DEBUG);
298  @ldap_set_option($this->conn_id, LDAP_OPT_PROTOCOL_VERSION, $this->options['version']);
299 
300  // start TLS if available
301  if (isset($this->options['start_tls']) && $this->options['start_tls']) {
302  $this->log("Starting TLS session", AUTH_LOG_DEBUG);
303  if (@ldap_start_tls($this->conn_id) === false) {
304  $this->log('Could not start TLS session', AUTH_LOG_DEBUG);
305  $this->log('LDAP ERROR: '.ldap_errno($this->conn_id).': '.ldap_error($this->conn_id), AUTH_LOG_DEBUG);
306  return PEAR::raiseError('Auth_Container_LDAP: Could not start tls.', 41);
307  }
308  }
309  }
310 
311  // switch LDAP referrals
312  if (is_bool($this->options['referrals'])) {
313  $this->log("Switching LDAP referrals to " . (($this->options['referrals']) ? 'true' : 'false'), AUTH_LOG_DEBUG);
314  if (@ldap_set_option($this->conn_id, LDAP_OPT_REFERRALS, $this->options['referrals']) === false) {
315  $this->log('Could not change LDAP referrals options', AUTH_LOG_DEBUG);
316  $this->log('LDAP ERROR: '.ldap_errno($this->conn_id).': '.ldap_error($this->conn_id), AUTH_LOG_DEBUG);
317  }
318  }
319 
320  // bind with credentials or anonymously
321  if (strlen($this->options['binddn']) && strlen($this->options['bindpw'])) {
322  $this->log('Binding with credentials', AUTH_LOG_DEBUG);
323  $bind_params = array($this->conn_id, $this->options['binddn'], $this->options['bindpw']);
324  } else {
325  $this->log('Binding anonymously', AUTH_LOG_DEBUG);
326  $bind_params = array($this->conn_id);
327  }
328 
329  // bind for searching
330  if ((@call_user_func_array('ldap_bind', $bind_params)) === false) {
331  $this->log('Bind failed', AUTH_LOG_DEBUG);
332  $this->log('LDAP ERROR: '.ldap_errno($this->conn_id).': '.ldap_error($this->conn_id), AUTH_LOG_DEBUG);
333  $this->_disconnect();
334  return PEAR::raiseError("Auth_Container_LDAP: Could not bind to LDAP server.", 41);
335  }
336  $this->log('Binding was successful', AUTH_LOG_DEBUG);
337 
338  return true;
339  }
340 
341  // }}}
342  // {{{ _disconnect()
343 
349  function _disconnect()
350  {
351  $this->log('Auth_Container_LDAP::_disconnect() called.', AUTH_LOG_DEBUG);
352  if ($this->_isValidLink()) {
353  $this->log('disconnecting from server');
354  @ldap_unbind($this->conn_id);
355  }
356  }
357 
358  // }}}
359  // {{{ _getBaseDN()
360 
366  function _getBaseDN()
367  {
368  $this->log('Auth_Container_LDAP::_getBaseDN() called.', AUTH_LOG_DEBUG);
369  $err = $this->_prepare();
370  if ($err !== true) {
371  return PEAR::raiseError($err->getMessage(), $err->getCode());
372  }
373 
374  if ($this->options['basedn'] == "" && $this->_isValidLink()) {
375  $this->log("basedn not set, searching via namingContexts.", AUTH_LOG_DEBUG);
376 
377  $result_id = @ldap_read($this->conn_id, "", "(objectclass=*)", array("namingContexts"));
378 
379  if (@ldap_count_entries($this->conn_id, $result_id) == 1) {
380 
381  $this->log("got result for namingContexts", AUTH_LOG_DEBUG);
382 
383  $entry_id = @ldap_first_entry($this->conn_id, $result_id);
384  $attrs = @ldap_get_attributes($this->conn_id, $entry_id);
385  $basedn = $attrs['namingContexts'][0];
386 
387  if ($basedn != "") {
388  $this->log("result for namingContexts was $basedn", AUTH_LOG_DEBUG);
389  $this->options['basedn'] = $basedn;
390  }
391  }
392  @ldap_free_result($result_id);
393  }
394 
395  // if base ist still not set, raise error
396  if ($this->options['basedn'] == "") {
397  return PEAR::raiseError("Auth_Container_LDAP: LDAP search base not specified!", 41);
398  }
399  return true;
400  }
401 
402  // }}}
403  // {{{ _isValidLink()
404 
411  function _isValidLink()
412  {
413  if (is_resource($this->conn_id)) {
414  if (get_resource_type($this->conn_id) == 'ldap link') {
415  return true;
416  }
417  }
418  return false;
419  }
420 
421  // }}}
422  // {{{ _setDefaults()
423 
429  function _setDefaults()
430  {
431  $this->options['url'] = '';
432  $this->options['host'] = 'localhost';
433  $this->options['port'] = '389';
434  $this->options['version'] = 2;
435  $this->options['referrals'] = true;
436  $this->options['binddn'] = '';
437  $this->options['bindpw'] = '';
438  $this->options['basedn'] = '';
439  $this->options['userdn'] = '';
440  $this->options['userscope'] = 'sub';
441  $this->options['userattr'] = 'uid';
442  $this->options['userfilter'] = '(objectClass=posixAccount)';
443  $this->options['attributes'] = array(''); // no attributes
444  $this->options['attrformat'] = 'AUTH'; // returns attribute like other Auth containers
445  $this->options['group'] = '';
446  $this->options['groupdn'] = '';
447  $this->options['groupscope'] = 'sub';
448  $this->options['groupattr'] = 'cn';
449  $this->options['groupfilter'] = '(objectClass=groupOfUniqueNames)';
450  $this->options['memberattr'] = 'uniqueMember';
451  $this->options['memberisdn'] = true;
452  $this->options['start_tls'] = false;
453  $this->options['debug'] = false;
454  $this->options['try_all'] = false; // Try all user ids returned not just the first one
455  }
456 
457  // }}}
458  // {{{ _parseOptions()
459 
466  function _parseOptions($array)
467  {
468  $array = $this->_setV12OptionsToV13($array);
469 
470  foreach ($array as $key => $value) {
471  if (array_key_exists($key, $this->options)) {
472  if ($key == 'attributes') {
473  if (is_array($value)) {
474  $this->options[$key] = $value;
475  } else {
476  $this->options[$key] = explode(',', $value);
477  }
478  } else {
479  $this->options[$key] = $value;
480  }
481  }
482  }
483  }
484 
485  // }}}
486  // {{{ _setV12OptionsToV13()
487 
496  function _setV12OptionsToV13($array)
497  {
498  if (isset($array['useroc']))
499  $array['userfilter'] = "(objectClass=".$array['useroc'].")";
500  if (isset($array['groupoc']))
501  $array['groupfilter'] = "(objectClass=".$array['groupoc'].")";
502  if (isset($array['scope']))
503  $array['userscope'] = $array['scope'];
504 
505  return $array;
506  }
507 
508  // }}}
509  // {{{ _scope2function()
510 
517  function _scope2function($scope)
518  {
519  switch($scope) {
520  case 'one':
521  $function = 'ldap_list';
522  break;
523  case 'base':
524  $function = 'ldap_read';
525  break;
526  default:
527  $function = 'ldap_search';
528  break;
529  }
530  return $function;
531  }
532 
533  // }}}
534  // {{{ fetchData()
535 
547  function fetchData($username, $password)
548  {
549  $this->log('Auth_Container_LDAP::fetchData() called.', AUTH_LOG_DEBUG);
550  $err = $this->_prepare();
551  if ($err !== true) {
552  return PEAR::raiseError($err->getMessage(), $err->getCode());
553  }
554 
555  $err = $this->_getBaseDN();
556  if ($err !== true) {
557  return PEAR::raiseError($err->getMessage(), $err->getCode());
558  }
559 
560  // UTF8 Encode username for LDAPv3
561  if (@ldap_get_option($this->conn_id, LDAP_OPT_PROTOCOL_VERSION, $ver) && $ver == 3) {
562  $this->log('UTF8 encoding username for LDAPv3', AUTH_LOG_DEBUG);
563  $username = utf8_encode($username);
564  }
565 
566  // make search filter
567  $filter = sprintf('(&(%s=%s)%s)',
568  $this->options['userattr'],
569  $this->_quoteFilterString($username),
570  $this->options['userfilter']);
571 
572  // make search base dn
573  $search_basedn = $this->options['userdn'];
574  if ($search_basedn != '' && substr($search_basedn, -1) != ',') {
575  $search_basedn .= ',';
576  }
577  $search_basedn .= $this->options['basedn'];
578 
579  // attributes
580  $searchAttributes = $this->options['attributes'];
581 
582  // make functions params array
583  $func_params = array($this->conn_id, $search_basedn, $filter, $searchAttributes);
584 
585  // search function to use
586  $func_name = $this->_scope2function($this->options['userscope']);
587 
588  $this->log("Searching with $func_name and filter $filter in $search_basedn", AUTH_LOG_DEBUG);
589 
590  // search
591  if (($result_id = @call_user_func_array($func_name, $func_params)) === false) {
592  $this->log('User not found', AUTH_LOG_DEBUG);
593  } elseif (@ldap_count_entries($this->conn_id, $result_id) >= 1) { // did we get some possible results?
594 
595  $this->log('User(s) found', AUTH_LOG_DEBUG);
596 
597  $first = true;
598  $entry_id = null;
599 
600  do {
601 
602  // then get the user dn
603  if ($first) {
604  $entry_id = @ldap_first_entry($this->conn_id, $result_id);
605  $first = false;
606  } else {
607  $entry_id = @ldap_next_entry($this->conn_id, $entry_id);
608  if ($entry_id === false)
609  break;
610  }
611  $user_dn = @ldap_get_dn($this->conn_id, $entry_id);
612 
613  // as the dn is not fetched as an attribute, we save it anyway
614  if (is_array($searchAttributes) && in_array('dn', $searchAttributes)) {
615  $this->log('Saving DN to AuthData', AUTH_LOG_DEBUG);
616  $this->_auth_obj->setAuthData('dn', $user_dn);
617  }
618 
619  // fetch attributes
620  if ($attributes = @ldap_get_attributes($this->conn_id, $entry_id)) {
621 
622  if (is_array($attributes) && isset($attributes['count']) &&
623  $attributes['count'] > 0) {
624 
625  // ldap_get_attributes() returns a specific multi dimensional array
626  // format containing all the attributes and where each array starts
627  // with a 'count' element providing the number of attributes in the
628  // entry, or the number of values for attribute. For compatibility
629  // reasons, it remains the default format returned by LDAP container
630  // setAuthData().
631  // The code below optionally returns attributes in another format,
632  // more compliant with other Auth containers, where each attribute
633  // element are directly set in the 'authData' list. This option is
634  // enabled by setting 'attrformat' to
635  // 'AUTH' in the 'options' array.
636  // eg. $this->options['attrformat'] = 'AUTH'
637 
638  if ( strtoupper($this->options['attrformat']) == 'AUTH' ) {
639  $this->log('Saving attributes to Auth data in AUTH format', AUTH_LOG_DEBUG);
640  unset ($attributes['count']);
641  foreach ($attributes as $attributeName => $attributeValue ) {
642  if (is_int($attributeName)) continue;
643  if (is_array($attributeValue) && isset($attributeValue['count'])) {
644  unset ($attributeValue['count']);
645  }
646  if (count($attributeValue)<=1) $attributeValue = $attributeValue[0];
647  $this->log('Storing additional field: '.$attributeName, AUTH_LOG_DEBUG);
648  $this->_auth_obj->setAuthData($attributeName, $attributeValue);
649  }
650  }
651  else
652  {
653  $this->log('Saving attributes to Auth data in LDAP format', AUTH_LOG_DEBUG);
654  $this->_auth_obj->setAuthData('attributes', $attributes);
655  }
656  }
657  }
658  @ldap_free_result($result_id);
659 
660  // need to catch an empty password as openldap seems to return TRUE
661  // if anonymous binding is allowed
662  if ($password != "") {
663  $this->log("Bind as $user_dn", AUTH_LOG_DEBUG);
664 
665  // try binding as this user with the supplied password
666  if (@ldap_bind($this->conn_id, $user_dn, $password)) {
667  $this->log('Bind successful', AUTH_LOG_DEBUG);
668 
669  // check group if appropiate
670  if (strlen($this->options['group'])) {
671  // decide whether memberattr value is a dn or the username
672  $this->log('Checking group membership', AUTH_LOG_DEBUG);
673  $return = $this->checkGroup(($this->options['memberisdn']) ? $user_dn : $username);
674  $this->_disconnect();
675  return $return;
676  } else {
677  $this->log('Authenticated', AUTH_LOG_DEBUG);
678  $this->_disconnect();
679  return true; // user authenticated
680  } // checkGroup
681  } // bind
682  } // non-empty password
683  } while ($this->options['try_all'] == true); // interate through entries
684  } // get results
685  // default
686  $this->log('NOT authenticated!', AUTH_LOG_DEBUG);
687  $this->_disconnect();
688  return false;
689  }
690 
691  // }}}
692  // {{{ checkGroup()
693 
704  function checkGroup($user)
705  {
706  $this->log('Auth_Container_LDAP::checkGroup() called.', AUTH_LOG_DEBUG);
707  $err = $this->_prepare();
708  if ($err !== true) {
709  return PEAR::raiseError($err->getMessage(), $err->getCode());
710  }
711 
712  // make filter
713  $filter = sprintf('(&(%s=%s)(%s=%s)%s)',
714  $this->options['groupattr'],
715  $this->options['group'],
716  $this->options['memberattr'],
717  $this->_quoteFilterString($user),
718  $this->options['groupfilter']);
719 
720  // make search base dn
721  $search_basedn = $this->options['groupdn'];
722  if ($search_basedn != '' && substr($search_basedn, -1) != ',') {
723  $search_basedn .= ',';
724  }
725  $search_basedn .= $this->options['basedn'];
726 
727  $func_params = array($this->conn_id, $search_basedn, $filter,
728  array($this->options['memberattr']));
729  $func_name = $this->_scope2function($this->options['groupscope']);
730 
731  $this->log("Searching with $func_name and filter $filter in $search_basedn", AUTH_LOG_DEBUG);
732 
733  // search
734  if (($result_id = @call_user_func_array($func_name, $func_params)) != false) {
735  if (@ldap_count_entries($this->conn_id, $result_id) == 1) {
736  @ldap_free_result($result_id);
737  $this->log('User is member of group', AUTH_LOG_DEBUG);
738  return true;
739  }
740  }
741  // default
742  $this->log('User is NOT member of group', AUTH_LOG_DEBUG);
743  return false;
744  }
745 
746  // }}}
747  // {{{ _quoteFilterString()
748 
755  function _quoteFilterString($filter_str)
756  {
757  $metas = array( '\\', '*', '(', ')', "\x00");
758  $quoted_metas = array('\\\\', '\*', '\(', '\)', "\\\x00");
759  return str_replace($metas, $quoted_metas, $filter_str);
760  }
761 
762  // }}}
763 
764 }
765 
766 ?>