ILIAS  release_5-3 Revision v5.3.23-19-g915713cf615
LDAP.php
Go to the documentation of this file.
1 <?php
2 
6 define('ERR_INTERNAL', 1);
7 define('ERR_NO_USER', 2);
8 define('ERR_WRONG_PW', 3);
9 define('ERR_AS_DATA_INCONSIST', 4);
10 define('ERR_AS_INTERNAL', 5);
11 define('ERR_AS_ATTRIBUTE', 6);
12 
13 // not defined in earlier PHP versions
14 if (!defined('LDAP_OPT_DIAGNOSTIC_MESSAGE')) {
15  define('LDAP_OPT_DIAGNOSTIC_MESSAGE', 0x0032);
16 }
17 
26 {
32  protected $ldap = null;
33 
37  protected $authz_id = null;
38 
44  protected $timeout = 0;
45 
56  // TODO: Flesh out documentation
57  public function __construct($hostname, $enable_tls = true, $debug = false, $timeout = 0, $port = 389, $referrals = true)
58  {
59 
60  // Debug
61  SimpleSAML\Logger::debug('Library - LDAP __construct(): Setup LDAP with ' .
62  'host=\'' . $hostname .
63  '\', tls=' . var_export($enable_tls, true) .
64  ', debug=' . var_export($debug, true) .
65  ', timeout=' . var_export($timeout, true) .
66  ', referrals=' . var_export($referrals, true));
67 
68  /*
69  * Set debug level before calling connect. Note that this passes
70  * NULL to ldap_set_option, which is an undocumented feature.
71  *
72  * OpenLDAP 2.x.x or Netscape Directory SDK x.x needed for this option.
73  */
74  if ($debug && !ldap_set_option(null, LDAP_OPT_DEBUG_LEVEL, 7)) {
75  SimpleSAML\Logger::warning('Library - LDAP __construct(): Unable to set debug level (LDAP_OPT_DEBUG_LEVEL) to 7');
76  }
77 
78  /*
79  * Prepare a connection for to this LDAP server. Note that this function
80  * doesn't actually connect to the server.
81  */
82  $this->ldap = @ldap_connect($hostname, $port);
83  if ($this->ldap === false) {
84  throw $this->makeException('Library - LDAP __construct(): Unable to connect to \'' . $hostname . '\'', ERR_INTERNAL);
85  }
86 
87  // Enable LDAP protocol version 3
88  if (!@ldap_set_option($this->ldap, LDAP_OPT_PROTOCOL_VERSION, 3)) {
89  throw $this->makeException('Library - LDAP __construct(): Failed to set LDAP Protocol version (LDAP_OPT_PROTOCOL_VERSION) to 3', ERR_INTERNAL);
90  }
91 
92  // Set referral option
93  if (!@ldap_set_option($this->ldap, LDAP_OPT_REFERRALS, $referrals)) {
94  throw $this->makeException('Library - LDAP __construct(): Failed to set LDAP Referrals (LDAP_OPT_REFERRALS) to '.$referrals, ERR_INTERNAL);
95  }
96 
97  // Set timeouts, if supported
98  // (OpenLDAP 2.x.x or Netscape Directory SDK x.x needed)
99  $this->timeout = $timeout;
100  if ($timeout > 0) {
101  if (!@ldap_set_option($this->ldap, LDAP_OPT_NETWORK_TIMEOUT, $timeout)) {
102  SimpleSAML\Logger::warning('Library - LDAP __construct(): Unable to set timeouts (LDAP_OPT_NETWORK_TIMEOUT) to ' . $timeout);
103  }
104  if (!@ldap_set_option($this->ldap, LDAP_OPT_TIMELIMIT, $timeout)) {
105  SimpleSAML\Logger::warning('Library - LDAP __construct(): Unable to set timeouts (LDAP_OPT_TIMELIMIT) to ' . $timeout);
106  }
107  }
108 
109  // Enable TLS, if needed
110  if (stripos($hostname, "ldaps:") === false and $enable_tls) {
111  if (!@ldap_start_tls($this->ldap)) {
112  throw $this->makeException('Library - LDAP __construct(): Unable to force TLS', ERR_INTERNAL);
113  }
114  }
115  }
116 
117 
126  private function makeException($description, $type = null)
127  {
128  $errNo = 0x00;
129 
130  // Log LDAP code and description, if possible
131  if (empty($this->ldap)) {
133  } else {
134  $errNo = @ldap_errno($this->ldap);
135  }
136 
137  // Decide exception type and return
138  if ($type) {
139  if ($errNo !== 0) {
140  // Only log real LDAP errors; not success
141  SimpleSAML\Logger::error($description . '; cause: \'' . ldap_error($this->ldap) . '\' (0x' . dechex($errNo) . ')');
142  } else {
143  SimpleSAML\Logger::error($description);
144  }
145 
146  switch ($type) {
147  case ERR_INTERNAL:// 1 - ExInternal
148  return new SimpleSAML_Error_Exception($description, $errNo);
149  case ERR_NO_USER:// 2 - ExUserNotFound
150  return new SimpleSAML_Error_UserNotFound($description, $errNo);
151  case ERR_WRONG_PW:// 3 - ExInvalidCredential
152  return new SimpleSAML_Error_InvalidCredential($description, $errNo);
153  case ERR_AS_DATA_INCONSIST:// 4 - ExAsDataInconsist
154  return new SimpleSAML_Error_AuthSource('ldap', $description);
155  case ERR_AS_INTERNAL:// 5 - ExAsInternal
156  return new SimpleSAML_Error_AuthSource('ldap', $description);
157  }
158  } else {
159  if ($errNo !== 0) {
160  $description .= '; cause: \'' . ldap_error($this->ldap) . '\' (0x' . dechex($errNo) . ')';
161  if (@ldap_get_option($this->ldap, LDAP_OPT_DIAGNOSTIC_MESSAGE, $extendedError) && !empty($extendedError)) {
162  $description .= '; additional: \'' . $extendedError . '\'';
163  }
164  }
165  switch ($errNo) {
166  case 0x20://LDAP_NO_SUCH_OBJECT
168  return new SimpleSAML_Error_UserNotFound($description, $errNo);
169  case 0x31://LDAP_INVALID_CREDENTIALS
172  case -1://NO_SERVER_CONNECTION
174  return new SimpleSAML_Error_AuthSource('ldap', $description);
175  default:
177  return new SimpleSAML_Error_AuthSource('ldap', $description);
178  }
179  }
180  }
181 
182 
206  private function search($base, $attribute, $value, $searchFilter = null)
207  {
208  // Create the search filter
209  $attribute = self::escape_filter_value($attribute, false);
210  $value = self::escape_filter_value($value);
211  $filter = '';
212  foreach ($attribute as $attr) {
213  $filter .= '(' . $attr . '=' . $value. ')';
214  }
215  $filter = '(|' . $filter . ')';
216 
217  // Append LDAP filters if defined
218  if ($searchFilter != null) {
219  $filter = "(&".$filter."".$searchFilter.")";
220  }
221 
222  // Search using generated filter
223  SimpleSAML\Logger::debug('Library - LDAP search(): Searching base \'' . $base . '\' for \'' . $filter . '\'');
224  // TODO: Should aliases be dereferenced?
225  $result = @ldap_search($this->ldap, $base, $filter, array(), 0, 0, $this->timeout);
226  if ($result === false) {
227  throw $this->makeException('Library - LDAP search(): Failed search on base \'' . $base . '\' for \'' . $filter . '\'');
228  }
229 
230  // Sanity checks on search results
231  $count = @ldap_count_entries($this->ldap, $result);
232  if ($count === false) {
233  throw $this->makeException('Library - LDAP search(): Failed to get number of entries returned');
234  } elseif ($count > 1) {
235  // More than one entry is found. External error
236  throw $this->makeException('Library - LDAP search(): Found ' . $count . ' entries searching base \'' . $base . '\' for \'' . $filter . '\'', ERR_AS_DATA_INCONSIST);
237  } elseif ($count === 0) {
238  // No entry is fond => wrong username is given (or not registered in the catalogue). User error
239  throw $this->makeException('Library - LDAP search(): Found no entries searching base \'' . $base . '\' for \'' . $filter . '\'', ERR_NO_USER);
240  }
241 
242 
243  // Resolve the DN from the search result
244  $entry = @ldap_first_entry($this->ldap, $result);
245  if ($entry === false) {
246  throw $this->makeException('Library - LDAP search(): Unable to retrieve result after searching base \'' . $base . '\' for \'' . $filter . '\'');
247  }
248  $dn = @ldap_get_dn($this->ldap, $entry);
249  if ($dn === false) {
250  throw $this->makeException('Library - LDAP search(): Unable to get DN after searching base \'' . $base . '\' for \'' . $filter . '\'');
251  }
252  // FIXME: Are we now sure, if no excepton has been thrown, that we are returning a DN?
253  return $dn;
254  }
255 
256 
280  public function searchfordn($base, $attribute, $value, $allowZeroHits = false, $searchFilter = null)
281  {
282  // Traverse all search bases, returning DN if found
284  $result = null;
285  foreach ($bases as $current) {
286  try {
287  // Single base search
288  $result = $this->search($current, $attribute, $value, $searchFilter);
289 
290  // We don't hawe to look any futher if user is found
291  if (!empty($result)) {
292  return $result;
293  }
294  // If search failed, attempt the other base DNs
295  } catch (SimpleSAML_Error_UserNotFound $e) {
296  // Just continue searching
297  }
298  }
299  // Decide what to do for zero entries
300  SimpleSAML\Logger::debug('Library - LDAP searchfordn(): No entries found');
301  if ($allowZeroHits) {
302  // Zero hits allowed
303  return null;
304  } else {
305  // Zero hits not allowed
306  throw $this->makeException('Library - LDAP searchfordn(): LDAP search returned zero entries for filter \'(' .
307  $attribute . ' = ' . $value . ')\' on base(s) \'(' . join(' & ', $bases) . ')\'', 2);
308  }
309  }
310 
311 
324  public function searchformultiple($bases, $filters, $attributes = array(), $and = true, $escape = true)
325  {
326  // Escape the filter values, if requested
327  if ($escape) {
328  $filters = $this->escape_filter_value($filters, false);
329  }
330 
331  // Build search filter
332  $filter = '';
333  if (is_array($filters)) {
334  foreach ($filters as $attribute => $value) {
335  $filter .= "($attribute=$value)";
336  }
337  if (count($filters) > 1) {
338  $filter = ($and ? '(&' : '(|') . $filter . ')';
339  }
340  } elseif (is_string($filters)) {
341  $filter = $filters;
342  }
343 
344  // Verify filter was created
345  if ($filter == '' || $filter == '(=)') {
346  throw $this->makeException('ldap:LdapConnection->search_manual : No search filters defined', ERR_INTERNAL);
347  }
348 
349  // Verify at least one base was passed
350  $bases = (array) $bases;
351  if (empty($bases)) {
352  throw $this->makeException('ldap:LdapConnection->search_manual : No base DNs were passed', ERR_INTERNAL);
353  }
354 
355  // Search each base until result is found
356  $result = false;
357  foreach ($bases as $base) {
358  $result = @ldap_search($this->ldap, $base, $filter, $attributes, 0, 0, $this->timeout);
359  if ($result !== false && @ldap_count_entries($this->ldap, $result) > 0) {
360  break;
361  }
362  }
363 
364  // Verify that a result was found in one of the bases
365  if ($result === false) {
366  throw $this->makeException(
367  'ldap:LdapConnection->search_manual : Failed to search LDAP using base(s) [' .
368  implode('; ', $bases) . '] with filter [' . $filter . ']. LDAP error [' .
369  ldap_error($this->ldap) . ']'
370  );
371  } elseif (@ldap_count_entries($this->ldap, $result) < 1) {
372  throw $this->makeException(
373  'ldap:LdapConnection->search_manual : No entries found in LDAP using base(s) [' .
374  implode('; ', $bases) . '] with filter [' . $filter . ']',
376  );
377  }
378 
379  // Get all results
380  $results = ldap_get_entries($this->ldap, $result);
381  if ($results === false) {
382  throw $this->makeException(
383  'ldap:LdapConnection->search_manual : Unable to retrieve entries from search results'
384  );
385  }
386 
387  // parse each entry and process its attributes
388  for ($i = 0; $i < $results['count']; $i++) {
389  $entry = $results[$i];
390 
391  // iterate over the attributes of the entry
392  for ($j = 0; $j < $entry['count']; $j++) {
393  $name = $entry[$j];
394  $attribute = $entry[$name];
395 
396  // decide whether to base64 encode or not
397  for ($k = 0; $k < $attribute['count']; $k++) {
398  // base64 encode binary attributes
399  if (strtolower($name) === 'jpegphoto' || strtolower($name) === 'objectguid') {
400  $results[$i][$name][$k] = base64_encode($attribute[$k]);
401  }
402  }
403  }
404  }
405 
406  // Remove the count and return
407  unset($results['count']);
408  return $results;
409  }
410 
411 
428  public function bind($dn, $password, array $sasl_args = null)
429  {
430  $authz_id = null;
431 
432  if ($sasl_args != null) {
433  if (!function_exists('ldap_sasl_bind')) {
434  $ex_msg = 'Library - missing SASL support';
435  throw $this->makeException($ex_msg);
436  }
437 
438  // SASL Bind, with error handling
439  $authz_id = $sasl_args['authz_id'];
440  $error = @ldap_sasl_bind($this->ldap, $dn, $password,
441  $sasl_args['mech'],
442  $sasl_args['realm'],
443  $sasl_args['authc_id'],
444  $sasl_args['authz_id'],
445  $sasl_args['props']);
446  } else {
447  // Simple Bind, with error handling
448  $authz_id = $dn;
449  $error = @ldap_bind($this->ldap, $dn, $password);
450  }
451 
452  if ($error === true) {
453  // Good
454  $this->authz_id = $authz_id;
455  SimpleSAML\Logger::debug('Library - LDAP bind(): Bind successful with DN \'' . $dn . '\'');
456  return true;
457  }
458 
459  /* Handle errors
460  * LDAP_INVALID_CREDENTIALS
461  * LDAP_INSUFFICIENT_ACCESS */
462  switch (ldap_errno($this->ldap)) {
463  case 32: // LDAP_NO_SUCH_OBJECT
464  // no break
465  case 47: // LDAP_X_PROXY_AUTHZ_FAILURE
466  // no break
467  case 48: // LDAP_INAPPROPRIATE_AUTH
468  // no break
469  case 49: // LDAP_INVALID_CREDENTIALS
470  // no break
471  case 50: // LDAP_INSUFFICIENT_ACCESS
472  return false;
473  default:
474  break;
475  }
476 
477  // Bad
478  throw $this->makeException('Library - LDAP bind(): Bind failed with DN \'' . $dn . '\'');
479  }
480 
481 
490  public function setOption($option, $value)
491  {
492  // Attempt to set the LDAP option
493  if (!@ldap_set_option($this->ldap, $option, $value)) {
494  throw $this->makeException(
495  'ldap:LdapConnection->setOption : Failed to set LDAP option [' .
496  $option . '] with the value [' . $value . '] error: ' . ldap_error($this->ldap),
498  );
499  }
500 
501  // Log debug message
503  'ldap:LdapConnection->setOption : Set the LDAP option [' .
504  $option . '] with the value [' . $value . ']'
505  );
506  }
507 
508 
525  public function getAttributes($dn, $attributes = null, $maxsize = null)
526  {
527  // Preparations, including a pretty debug message...
528  $description = 'all attributes';
529  if (is_array($attributes)) {
530  $description = '\'' . join(',', $attributes) . '\'';
531  } else {
532  // Get all attributes...
533  // TODO: Verify that this originally was the intended behaviour. Could $attributes be a string?
534  $attributes = array();
535  }
536  SimpleSAML\Logger::debug('Library - LDAP getAttributes(): Getting ' . $description . ' from DN \'' . $dn . '\'');
537 
538  // Attempt to get attributes
539  // TODO: Should aliases be dereferenced?
540  $result = @ldap_read($this->ldap, $dn, 'objectClass=*', $attributes, 0, 0, $this->timeout);
541  if ($result === false) {
542  throw $this->makeException('Library - LDAP getAttributes(): Failed to get attributes from DN \'' . $dn . '\'');
543  }
544  $entry = @ldap_first_entry($this->ldap, $result);
545  if ($entry === false) {
546  throw $this->makeException('Library - LDAP getAttributes(): Could not get first entry from DN \'' . $dn . '\'');
547  }
548  $attributes = @ldap_get_attributes($this->ldap, $entry); // Recycling $attributes... Possibly bad practice.
549  if ($attributes === false) {
550  throw $this->makeException('Library - LDAP getAttributes(): Could not get attributes of first entry from DN \'' . $dn . '\'');
551  }
552 
553  // Parsing each found attribute into our result set
554  $result = array(); // Recycling $result... Possibly bad practice.
555  for ($i = 0; $i < $attributes['count']; $i++) {
556 
557  // Ignore attributes that exceed the maximum allowed size
558  $name = $attributes[$i];
559  $attribute = $attributes[$name];
560 
561  // Deciding whether to base64 encode
562  $values = array();
563  for ($j = 0; $j < $attribute['count']; $j++) {
564  $value = $attribute[$j];
565 
566  if (!empty($maxsize) && strlen($value) >= $maxsize) {
567  // Ignoring and warning
568  SimpleSAML\Logger::warning('Library - LDAP getAttributes(): Attribute \'' .
569  $name . '\' exceeded maximum allowed size by ' + ($maxsize - strlen($value)));
570  continue;
571  }
572 
573  // Base64 encode binary attributes
574  if (strtolower($name) === 'jpegphoto' || strtolower($name) === 'objectguid') {
575  $values[] = base64_encode($value);
576  } else {
577  $values[] = $value;
578  }
579 
580  }
581 
582  // Adding
583  $result[$name] = $values;
584 
585  }
586 
587  // We're done
588  SimpleSAML\Logger::debug('Library - LDAP getAttributes(): Found attributes \'(' . join(',', array_keys($result)) . ')\'');
589  return $result;
590  }
591 
592 
601  // TODO: Documentation; only cleared up exception/log messages
602  public function validate($config, $username, $password = null)
603  {
604  /* Escape any characters with a special meaning in LDAP. The following
605  * characters have a special meaning (according to RFC 2253):
606  * ',', '+', '"', '\', '<', '>', ';', '*'
607  * These characters are escaped by prefixing them with '\'.
608  */
609  $username = addcslashes($username, ',+"\\<>;*');
610 
611  if (isset($config['priv_user_dn'])) {
612  $this->bind($config['priv_user_dn'], $config['priv_user_pw']);
613  }
614  if (isset($config['dnpattern'])) {
615  $dn = str_replace('%username%', $username, $config['dnpattern']);
616  } else {
617  $dn = $this->searchfordn($config['searchbase'], $config['searchattributes'], $username);
618  }
619 
620  if ($password !== null) { // checking users credentials ... assuming below that she may read her own attributes ...
621  // escape characters with a special meaning, also in the password
622  $password = addcslashes($password, ',+"\\<>;*');
623  if (!$this->bind($dn, $password)) {
624  SimpleSAML\Logger::info('Library - LDAP validate(): Failed to authenticate \''. $username . '\' using DN \'' . $dn . '\'');
625  return false;
626  }
627  }
628 
629  /*
630  * Retrieve attributes from LDAP
631  */
632  $attributes = $this->getAttributes($dn, $config['attributes']);
633  return $attributes;
634 
635  }
636 
637 
651  public static function escape_filter_value($values = array(), $singleValue = true)
652  {
653  // Parameter validation
654  if (!is_array($values)) {
655  $values = array($values);
656  }
657 
658  foreach ($values as $key => $val) {
659  // Escaping of filter meta characters
660  $val = str_replace('\\', '\5c', $val);
661  $val = str_replace('*', '\2a', $val);
662  $val = str_replace('(', '\28', $val);
663  $val = str_replace(')', '\29', $val);
664 
665  // ASCII < 32 escaping
666  $val = self::asc2hex32($val);
667 
668  if (null === $val) {
669  $val = '\0'; // apply escaped "null" if string is empty
670  }
671 
672  $values[$key] = $val;
673  }
674  if ($singleValue) {
675  return $values[0];
676  }
677  return $values;
678  }
679 
680 
691  public static function asc2hex32($string)
692  {
693  for ($i = 0; $i < strlen($string); $i++) {
694  $char = substr($string, $i, 1);
695  if (ord($char) < 32) {
696  $hex = dechex(ord($char));
697  if (strlen($hex) == 1) {
698  $hex = '0'.$hex;
699  }
700  $string = str_replace($char, '\\'.$hex, $string);
701  }
702  }
703  return $string;
704  }
705 
709  private function authzid_to_dn($searchBase, $searchAttributes, $authz_id)
710  {
711  if (preg_match("/^dn:/", $authz_id)) {
712  return preg_replace("/^dn:/", "", $authz_id);
713  }
714 
715  if (preg_match("/^u:/", $authz_id)) {
716  return $this->searchfordn($searchBase, $searchAttributes,
717  preg_replace("/^u:/", "", $authz_id));
718  }
719  return $authz_id;
720  }
721 
734  public function whoami($searchBase, $searchAttributes)
735  {
736  $authz_id = '';
737 
738  if (function_exists('ldap_exop_whoami')) {
739  if (version_compare(phpversion(), '7', '<')) {
740  if (ldap_exop_whoami($this->ldap, $authz_id) !== true) {
741  throw $this->makeException('LDAP whoami exop failure');
742  }
743  } else {
744  if (($authz_id = ldap_exop_whoami($this->ldap)) === false) {
745  throw $this->makeException('LDAP whoami exop failure');
746  }
747  }
748  } else {
750  }
751 
752  $dn = $this->authzid_to_dn($searchBase, $searchAttributes, $authz_id);
753 
754  if (!isset($dn) || ($dn == '')) {
755  throw $this->makeException('Cannot figure userID');
756  }
757 
758  return $dn;
759  }
760 }
File written to
searchfordn($base, $attribute, $value, $allowZeroHits=false, $searchFilter=null)
Search for a DN.
Definition: LDAP.php:280
static asc2hex32($string)
Borrowed function from PEAR:LDAP.
Definition: LDAP.php:691
searchformultiple($bases, $filters, $attributes=array(), $and=true, $escape=true)
This method was created specifically for the ldap:AttributeAddUsersGroups->searchActiveDirectory() me...
Definition: LDAP.php:324
$result
$type
static arrayize($data, $index=0)
Put a non-array variable into an array.
Definition: Arrays.php:24
const ERR_NO_USER
Definition: LDAP.php:7
search($base, $attribute, $value, $searchFilter=null)
Search for DN from a single base.
Definition: LDAP.php:206
authzid_to_dn($searchBase, $searchAttributes, $authz_id)
Convert SASL authz_id into a DN.
Definition: LDAP.php:709
static debug($string)
Definition: Logger.php:213
setOption($option, $value)
Applies an LDAP option to the current connection.
Definition: LDAP.php:490
$attributes
static escape_filter_value($values=array(), $singleValue=true)
Borrowed function from PEAR:LDAP.
Definition: LDAP.php:651
const ERR_AS_DATA_INCONSIST
Definition: LDAP.php:9
$base
Definition: index.php:4
$password
Definition: pwgen.php:17
$error
Definition: Error.php:17
Attribute-related utility methods.
if($format !==null) $name
Definition: metadata.php:146
static info($string)
Definition: Logger.php:201
validate($config, $username, $password=null)
Enter description here...
Definition: LDAP.php:602
$authz_id
LDAP user: authz_id if SASL is in use, binding dn otherwise.
Definition: LDAP.php:37
static warning($string)
Definition: Logger.php:179
makeException($description, $type=null)
Convenience method to create an LDAPException as well as log the description.
Definition: LDAP.php:126
static error($string)
Definition: Logger.php:168
Create styles array
The data for the language used.
getAttributes($dn, $attributes=null, $maxsize=null)
Search a given DN for attributes, and return the resulting associative array.
Definition: LDAP.php:525
__construct($hostname, $enable_tls=true, $debug=false, $timeout=0, $port=389, $referrals=true)
Private constructor restricts instantiation to getInstance().
Definition: LDAP.php:57
$debug
Definition: loganalyzer.php:16
$results
Definition: svg-scanner.php:47
$i
Definition: disco.tpl.php:19
whoami($searchBase, $searchAttributes)
ldap_exop_whoami accessor, if available.
Definition: LDAP.php:734
defined( 'APPLICATION_ENV')||define( 'APPLICATION_ENV'
Definition: bootstrap.php:27
bind($dn, $password, array $sasl_args=null)
Bind to LDAP with a specific DN and password.
Definition: LDAP.php:428
const ERR_INTERNAL
Constants defining possible errors.
Definition: LDAP.php:6
$key
Definition: croninfo.php:18
Set page orientation and size
Definition: 04printing.php:77