ILIAS  release_5-4 Revision v5.4.26-12-gabc799a52e6
LDAP.php
Go to the documentation of this file.
1<?php
2
6define('ERR_INTERNAL', 1);
7define('ERR_NO_USER', 2);
8define('ERR_WRONG_PW', 3);
9define('ERR_AS_DATA_INCONSIST', 4);
10define('ERR_AS_INTERNAL', 5);
11define('ERR_AS_ATTRIBUTE', 6);
12
13// not defined in earlier PHP versions
14if (!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 public function __construct($hostname, $enable_tls = true, $debug = false, $timeout = 0, $port = 389, $referrals = true)
57 {
58 // Debug
59 SimpleSAML\Logger::debug('Library - LDAP __construct(): Setup LDAP with '.
60 'host=\''.$hostname.
61 '\', tls='.var_export($enable_tls, true).
62 ', debug='.var_export($debug, true).
63 ', timeout='.var_export($timeout, true).
64 ', referrals='.var_export($referrals, true));
65
66 /*
67 * Set debug level before calling connect. Note that this passes
68 * NULL to ldap_set_option, which is an undocumented feature.
69 *
70 * OpenLDAP 2.x.x or Netscape Directory SDK x.x needed for this option.
71 */
72 if ($debug && !ldap_set_option(null, LDAP_OPT_DEBUG_LEVEL, 7)) {
73 SimpleSAML\Logger::warning('Library - LDAP __construct(): Unable to set debug level (LDAP_OPT_DEBUG_LEVEL) to 7');
74 }
75
76 /*
77 * Prepare a connection for to this LDAP server. Note that this function
78 * doesn't actually connect to the server.
79 */
80 $this->ldap = @ldap_connect($hostname, $port);
81 if ($this->ldap === false) {
82 throw $this->makeException('Library - LDAP __construct(): Unable to connect to \''.$hostname.'\'', ERR_INTERNAL);
83 }
84
85 // Enable LDAP protocol version 3
86 if (!@ldap_set_option($this->ldap, LDAP_OPT_PROTOCOL_VERSION, 3)) {
87 throw $this->makeException('Library - LDAP __construct(): Failed to set LDAP Protocol version (LDAP_OPT_PROTOCOL_VERSION) to 3', ERR_INTERNAL);
88 }
89
90 // Set referral option
91 if (!@ldap_set_option($this->ldap, LDAP_OPT_REFERRALS, $referrals)) {
92 throw $this->makeException('Library - LDAP __construct(): Failed to set LDAP Referrals (LDAP_OPT_REFERRALS) to '.$referrals, ERR_INTERNAL);
93 }
94
95 // Set timeouts, if supported
96 // (OpenLDAP 2.x.x or Netscape Directory SDK x.x needed)
97 $this->timeout = $timeout;
98 if ($timeout > 0) {
99 if (!@ldap_set_option($this->ldap, LDAP_OPT_NETWORK_TIMEOUT, $timeout)) {
100 SimpleSAML\Logger::warning('Library - LDAP __construct(): Unable to set timeouts (LDAP_OPT_NETWORK_TIMEOUT) to '.$timeout);
101 }
102 if (!@ldap_set_option($this->ldap, LDAP_OPT_TIMELIMIT, $timeout)) {
103 SimpleSAML\Logger::warning('Library - LDAP __construct(): Unable to set timeouts (LDAP_OPT_TIMELIMIT) to '.$timeout);
104 }
105 }
106
107 // Enable TLS, if needed
108 if (stripos($hostname, "ldaps:") === false && $enable_tls) {
109 if (!@ldap_start_tls($this->ldap)) {
110 throw $this->makeException('Library - LDAP __construct(): Unable to force TLS', ERR_INTERNAL);
111 }
112 }
113 }
114
115
124 private function makeException($description, $type = null)
125 {
126 $errNo = 0x00;
127
128 // Log LDAP code and description, if possible
129 if (empty($this->ldap)) {
131 } else {
132 $errNo = @ldap_errno($this->ldap);
133 }
134
135 // Decide exception type and return
136 if ($type) {
137 if ($errNo !== 0) {
138 // Only log real LDAP errors; not success
139 SimpleSAML\Logger::error($description.'; cause: \''.ldap_error($this->ldap).'\' (0x'.dechex($errNo).')');
140 } else {
141 SimpleSAML\Logger::error($description);
142 }
143
144 switch ($type) {
145 case ERR_INTERNAL:// 1 - ExInternal
146 return new SimpleSAML_Error_Exception($description, $errNo);
147 case ERR_NO_USER:// 2 - ExUserNotFound
148 return new SimpleSAML_Error_UserNotFound($description, $errNo);
149 case ERR_WRONG_PW:// 3 - ExInvalidCredential
150 return new SimpleSAML_Error_InvalidCredential($description, $errNo);
151 case ERR_AS_DATA_INCONSIST:// 4 - ExAsDataInconsist
152 return new SimpleSAML_Error_AuthSource('ldap', $description);
153 case ERR_AS_INTERNAL:// 5 - ExAsInternal
154 return new SimpleSAML_Error_AuthSource('ldap', $description);
155 }
156 } else {
157 if ($errNo !== 0) {
158 $description .= '; cause: \''.ldap_error($this->ldap).'\' (0x'.dechex($errNo).')';
159 if (@ldap_get_option($this->ldap, LDAP_OPT_DIAGNOSTIC_MESSAGE, $extendedError) && !empty($extendedError)) {
160 $description .= '; additional: \''.$extendedError.'\'';
161 }
162 }
163 switch ($errNo) {
164 case 0x20://LDAP_NO_SUCH_OBJECT
167 case 0x31://LDAP_INVALID_CREDENTIALS
170 case -1://NO_SERVER_CONNECTION
172 return new SimpleSAML_Error_AuthSource('ldap', $description);
173 default:
175 return new SimpleSAML_Error_AuthSource('ldap', $description);
176 }
177 }
178 }
179
180
208 private function search($base, $attribute, $value, $searchFilter = null, $scope = "subtree")
209 {
210 // Create the search filter
211 $attribute = self::escape_filter_value($attribute, false);
212 $value = self::escape_filter_value($value, true);
213 $filter = '';
214 foreach ($attribute as $attr) {
215 $filter .= '('.$attr.'='.$value.')';
216 }
217 $filter = '(|'.$filter.')';
218
219 // Append LDAP filters if defined
220 if ($searchFilter != null) {
221 $filter = "(&".$filter."".$searchFilter.")";
222 }
223
224 // Search using generated filter
225 SimpleSAML\Logger::debug('Library - LDAP search(): Searching base ('.$scope.') \''.$base.'\' for \''.$filter.'\'');
226 if ($scope === 'base') {
227 $result = @ldap_read($this->ldap, $base, $filter, array(), 0, 0, $this->timeout, LDAP_DEREF_NEVER);
228 } else if ($scope === 'onelevel') {
229 $result = @ldap_list($this->ldap, $base, $filter, array(), 0, 0, $this->timeout, LDAP_DEREF_NEVER);
230 } else {
231 $result = @ldap_search($this->ldap, $base, $filter, array(), 0, 0, $this->timeout, LDAP_DEREF_NEVER);
232 }
233
234 if ($result === false) {
235 throw $this->makeException('Library - LDAP search(): Failed search on base \''.$base.'\' for \''.$filter.'\'');
236 }
237
238 // Sanity checks on search results
239 $count = @ldap_count_entries($this->ldap, $result);
240 if ($count === false) {
241 throw $this->makeException('Library - LDAP search(): Failed to get number of entries returned');
242 } elseif ($count > 1) {
243 // More than one entry is found. External error
244 throw $this->makeException('Library - LDAP search(): Found '.$count.' entries searching base \''.$base.'\' for \''.$filter.'\'', ERR_AS_DATA_INCONSIST);
245 } elseif ($count === 0) {
246 // No entry is fond => wrong username is given (or not registered in the catalogue). User error
247 throw $this->makeException('Library - LDAP search(): Found no entries searching base \''.$base.'\' for \''.$filter.'\'', ERR_NO_USER);
248 }
249
250
251 // Resolve the DN from the search result
252 $entry = @ldap_first_entry($this->ldap, $result);
253 if ($entry === false) {
254 throw $this->makeException('Library - LDAP search(): Unable to retrieve result after searching base \''.$base.'\' for \''.$filter.'\'');
255 }
256 $dn = @ldap_get_dn($this->ldap, $entry);
257 if ($dn === false) {
258 throw $this->makeException('Library - LDAP search(): Unable to get DN after searching base \''.$base.'\' for \''.$filter.'\'');
259 }
260 return $dn;
261 }
262
263
291 public function searchfordn($base, $attribute, $value, $allowZeroHits = false, $searchFilter = null, $scope = 'subtree')
292 {
293 // Traverse all search bases, returning DN if found
295 foreach ($bases as $current) {
296 try {
297 // Single base search
298 $result = $this->search($current, $attribute, $value, $searchFilter, $scope);
299
300 // We don't hawe to look any futher if user is found
301 if (!empty($result)) {
302 return $result;
303 }
304 // If search failed, attempt the other base DNs
305 } catch (SimpleSAML_Error_UserNotFound $e) {
306 // Just continue searching
307 }
308 }
309 // Decide what to do for zero entries
310 SimpleSAML\Logger::debug('Library - LDAP searchfordn(): No entries found');
311 if ($allowZeroHits) {
312 // Zero hits allowed
313 return null;
314 } else {
315 // Zero hits not allowed
316 throw $this->makeException('Library - LDAP searchfordn(): LDAP search returned zero entries for filter \'('.
317 join(' | ', $attribute).' = '.$value.')\' on base(s) \'('.join(' & ', $bases).')\'', 2);
318 }
319 }
320
321
335 public function searchformultiple($bases, $filters, $attributes = array(), $and = true, $escape = true, $scope = 'subtree')
336 {
337 // Escape the filter values, if requested
338 if ($escape) {
339 $filters = $this->escape_filter_value($filters, false);
340 }
341
342 // Build search filter
343 $filter = '';
344 if (is_array($filters)) {
345 foreach ($filters as $attribute => $value) {
346 $filter .= "($attribute=$value)";
347 }
348 if (count($filters) > 1) {
349 $filter = ($and ? '(&' : '(|').$filter.')';
350 }
351 } elseif (is_string($filters)) {
352 $filter = $filters;
353 }
354
355 // Verify filter was created
356 if ($filter == '' || $filter == '(=)') {
357 throw $this->makeException('ldap:LdapConnection->search_manual : No search filters defined', ERR_INTERNAL);
358 }
359
360 // Verify at least one base was passed
361 $bases = (array) $bases;
362 if (empty($bases)) {
363 throw $this->makeException('ldap:LdapConnection->search_manual : No base DNs were passed', ERR_INTERNAL);
364 }
365
366 // Search each base until result is found
367 $result = false;
368 foreach ($bases as $base) {
369 if ($scope === 'base') {
370 $result = @ldap_read($this->ldap, $base, $filter, $attributes, 0, 0, $this->timeout);
371 } else if ($scope === 'onelevel') {
372 $result = @ldap_list($this->ldap, $base, $filter, $attributes, 0, 0, $this->timeout);
373 } else {
374 $result = @ldap_search($this->ldap, $base, $filter, $attributes, 0, 0, $this->timeout);
375 }
376
377 if ($result !== false && @ldap_count_entries($this->ldap, $result) > 0) {
378 break;
379 }
380 }
381
382 // Verify that a result was found in one of the bases
383 if ($result === false) {
384 throw $this->makeException(
385 'ldap:LdapConnection->search_manual : Failed to search LDAP using base(s) ['.
386 implode('; ', $bases).'] with filter ['.$filter.']. LDAP error ['.
387 ldap_error($this->ldap).']'
388 );
389 } elseif (@ldap_count_entries($this->ldap, $result) < 1) {
390 throw $this->makeException(
391 'ldap:LdapConnection->search_manual : No entries found in LDAP using base(s) ['.
392 implode('; ', $bases).'] with filter ['.$filter.']',
394 );
395 }
396
397 // Get all results
398 $results = ldap_get_entries($this->ldap, $result);
399 if ($results === false) {
400 throw $this->makeException(
401 'ldap:LdapConnection->search_manual : Unable to retrieve entries from search results'
402 );
403 }
404
405 // parse each entry and process its attributes
406 for ($i = 0; $i < $results['count']; $i++) {
407 $entry = $results[$i];
408
409 // iterate over the attributes of the entry
410 for ($j = 0; $j < $entry['count']; $j++) {
411 $name = $entry[$j];
412 $attribute = $entry[$name];
413
414 // decide whether to base64 encode or not
415 for ($k = 0; $k < $attribute['count']; $k++) {
416 // base64 encode binary attributes
417 if (strtolower($name) === 'jpegphoto' || strtolower($name) === 'objectguid') {
418 $results[$i][$name][$k] = base64_encode($attribute[$k]);
419 }
420 }
421 }
422 }
423
424 // Remove the count and return
425 unset($results['count']);
426 return $results;
427 }
428
429
446 public function bind($dn, $password, array $sasl_args = null)
447 {
448 if ($sasl_args != null) {
449 if (!function_exists('ldap_sasl_bind')) {
450 $ex_msg = 'Library - missing SASL support';
451 throw $this->makeException($ex_msg);
452 }
453
454 // SASL Bind, with error handling
455 $authz_id = $sasl_args['authz_id'];
456 $error = @ldap_sasl_bind(
457 $this->ldap,
458 $dn,
459 $password,
460 $sasl_args['mech'],
461 $sasl_args['realm'],
462 $sasl_args['authc_id'],
463 $sasl_args['authz_id'],
464 $sasl_args['props']
465 );
466 } else {
467 // Simple Bind, with error handling
468 $authz_id = $dn;
469 $error = @ldap_bind($this->ldap, $dn, $password);
470 }
471
472 if ($error === true) {
473 // Good
474 $this->authz_id = $authz_id;
475 SimpleSAML\Logger::debug('Library - LDAP bind(): Bind successful with DN \''.$dn.'\'');
476 return true;
477 }
478
479 /* Handle errors
480 * LDAP_INVALID_CREDENTIALS
481 * LDAP_INSUFFICIENT_ACCESS */
482 switch (ldap_errno($this->ldap)) {
483 case 32: // LDAP_NO_SUCH_OBJECT
484 // no break
485 case 47: // LDAP_X_PROXY_AUTHZ_FAILURE
486 // no break
487 case 48: // LDAP_INAPPROPRIATE_AUTH
488 // no break
489 case 49: // LDAP_INVALID_CREDENTIALS
490 // no break
491 case 50: // LDAP_INSUFFICIENT_ACCESS
492 return false;
493 default:
494 break;
495 }
496
497 // Bad
498 throw $this->makeException('Library - LDAP bind(): Bind failed with DN \''.$dn.'\'');
499 }
500
501
510 public function setOption($option, $value)
511 {
512 // Attempt to set the LDAP option
513 if (!@ldap_set_option($this->ldap, $option, $value)) {
514 throw $this->makeException(
515 'ldap:LdapConnection->setOption : Failed to set LDAP option ['.
516 $option.'] with the value ['.$value.'] error: '.ldap_error($this->ldap),
518 );
519 }
520
521 // Log debug message
523 'ldap:LdapConnection->setOption : Set the LDAP option ['.
524 $option.'] with the value ['.$value.']'
525 );
526 }
527
528
545 public function getAttributes($dn, $attributes = null, $maxsize = null)
546 {
547 // Preparations, including a pretty debug message...
548 $description = 'all attributes';
549 if (is_array($attributes)) {
550 $description = '\''.join(',', $attributes).'\'';
551 } else {
552 // Get all attributes...
553 // TODO: Verify that this originally was the intended behaviour. Could $attributes be a string?
554 $attributes = array();
555 }
556 SimpleSAML\Logger::debug('Library - LDAP getAttributes(): Getting '.$description.' from DN \''.$dn.'\'');
557
558 // Attempt to get attributes
559 // TODO: Should aliases be dereferenced?
560 $result = @ldap_read($this->ldap, $dn, 'objectClass=*', $attributes, 0, 0, $this->timeout);
561 if ($result === false) {
562 throw $this->makeException('Library - LDAP getAttributes(): Failed to get attributes from DN \''.$dn.'\'');
563 }
564 $entry = @ldap_first_entry($this->ldap, $result);
565 if ($entry === false) {
566 throw $this->makeException('Library - LDAP getAttributes(): Could not get first entry from DN \''.$dn.'\'');
567 }
568 $attributes = @ldap_get_attributes($this->ldap, $entry); // Recycling $attributes... Possibly bad practice.
569 if ($attributes === false) {
570 throw $this->makeException('Library - LDAP getAttributes(): Could not get attributes of first entry from DN \''.$dn.'\'');
571 }
572
573 // Parsing each found attribute into our result set
574 $result = array(); // Recycling $result... Possibly bad practice.
575 for ($i = 0; $i < $attributes['count']; $i++) {
576 // Ignore attributes that exceed the maximum allowed size
578 $attribute = $attributes[$name];
579
580 // Deciding whether to base64 encode
581 $values = array();
582 for ($j = 0; $j < $attribute['count']; $j++) {
583 $value = $attribute[$j];
584
585 if (!empty($maxsize) && strlen($value) > $maxsize) {
586 // Ignoring and warning
587 SimpleSAML\Logger::warning('Library - LDAP getAttributes(): Attribute \''.
588 $name.'\' exceeded maximum allowed size by '.(strlen($value) - $maxsize));
589 continue;
590 }
591
592 // Base64 encode binary attributes
593 if (strtolower($name) === 'jpegphoto' || strtolower($name) === 'objectguid' || strtolower($name) === 'ms-ds-consistencyguid') {
594 $values[] = base64_encode($value);
595 } else {
596 $values[] = $value;
597 }
598 }
599
600 // Adding
601 $result[$name] = $values;
602 }
603
604 // We're done
605 SimpleSAML\Logger::debug('Library - LDAP getAttributes(): Found attributes \'('.join(',', array_keys($result)).')\'');
606 return $result;
607 }
608
609
618 public function validate($config, $username, $password = null)
619 {
620 /* Escape any characters with a special meaning in LDAP. The following
621 * characters have a special meaning (according to RFC 2253):
622 * ',', '+', '"', '\', '<', '>', ';', '*'
623 * These characters are escaped by prefixing them with '\'.
624 */
625 $username = addcslashes($username, ',+"\\<>;*');
626
627 if (isset($config['priv_user_dn'])) {
628 $this->bind($config['priv_user_dn'], $config['priv_user_pw']);
629 }
630 if (isset($config['dnpattern'])) {
631 $dn = str_replace('%username%', $username, $config['dnpattern']);
632 } else {
633 $dn = $this->searchfordn($config['searchbase'], $config['searchattributes'], $username);
634 }
635
636 if ($password !== null) { // checking users credentials ... assuming below that she may read her own attributes ...
637 // escape characters with a special meaning, also in the password
638 $password = addcslashes($password, ',+"\\<>;*');
639 if (!$this->bind($dn, $password)) {
640 SimpleSAML\Logger::info('Library - LDAP validate(): Failed to authenticate \''.$username.'\' using DN \''.$dn.'\'');
641 return false;
642 }
643 }
644
645 /*
646 * Retrieve attributes from LDAP
647 */
648 $attributes = $this->getAttributes($dn, $config['attributes']);
649 return $attributes;
650 }
651
652
666 public static function escape_filter_value($values = array(), $singleValue = true)
667 {
668 // Parameter validation
670
671 foreach ($values as $key => $val) {
672 // Escaping of filter meta characters
673 $val = str_replace('\\', '\5c', $val);
674 $val = str_replace('*', '\2a', $val);
675 $val = str_replace('(', '\28', $val);
676 $val = str_replace(')', '\29', $val);
677
678 // ASCII < 32 escaping
679 $val = self::asc2hex32($val);
680
681 if (null === $val) {
682 $val = '\0'; // apply escaped "null" if string is empty
683 }
684
685 $values[$key] = $val;
686 }
687 if ($singleValue) {
688 return $values[0];
689 }
690 return $values;
691 }
692
693
704 public static function asc2hex32($string)
705 {
706 for ($i = 0; $i < strlen($string); $i++) {
707 $char = substr($string, $i, 1);
708 if (ord($char) < 32) {
709 $hex = dechex(ord($char));
710 if (strlen($hex) == 1) {
711 $hex = '0'.$hex;
712 }
713 $string = str_replace($char, '\\'.$hex, $string);
714 }
715 }
716 return $string;
717 }
718
722 private function authzid_to_dn($searchBase, $searchAttributes, $authz_id)
723 {
724 if (preg_match("/^dn:/", $authz_id)) {
725 return preg_replace("/^dn:/", "", $authz_id);
726 }
727
728 if (preg_match("/^u:/", $authz_id)) {
729 return $this->searchfordn(
730 $searchBase,
731 $searchAttributes,
732 preg_replace("/^u:/", "", $authz_id)
733 );
734 }
735 return $authz_id;
736 }
737
750 public function whoami($searchBase, $searchAttributes)
751 {
752 $authz_id = '';
753 if (function_exists('ldap_exop_whoami')) {
754 if (version_compare(phpversion(), '7', '<')) {
755 if (ldap_exop_whoami($this->ldap, $authz_id) !== true) {
756 throw $this->makeException('LDAP whoami exop failure');
757 }
758 } else {
759 if (($authz_id = ldap_exop_whoami($this->ldap)) === false) {
760 throw $this->makeException('LDAP whoami exop failure');
761 }
762 }
763 } else {
764 $authz_id = $this->authz_id;
765 }
766
767 $dn = $this->authzid_to_dn($searchBase, $searchAttributes, $authz_id);
768
769 if (!isset($dn) || ($dn == '')) {
770 throw $this->makeException('Cannot figure userID');
771 }
772
773 return $dn;
774 }
775}
$result
An exception for terminatinating execution or to throw for unit testing.
static info($string)
Definition: Logger.php:199
static warning($string)
Definition: Logger.php:177
static error($string)
Definition: Logger.php:166
static debug($string)
Definition: Logger.php:211
static arrayize($data, $index=0)
Put a non-array variable into an array.
Definition: Arrays.php:24
search($base, $attribute, $value, $searchFilter=null, $scope="subtree")
Search for DN from a single base.
Definition: LDAP.php:208
bind($dn, $password, array $sasl_args=null)
Bind to LDAP with a specific DN and password.
Definition: LDAP.php:446
__construct($hostname, $enable_tls=true, $debug=false, $timeout=0, $port=389, $referrals=true)
Private constructor restricts instantiation to getInstance().
Definition: LDAP.php:56
makeException($description, $type=null)
Convenience method to create an LDAPException as well as log the description.
Definition: LDAP.php:124
whoami($searchBase, $searchAttributes)
ldap_exop_whoami accessor, if available.
Definition: LDAP.php:750
authzid_to_dn($searchBase, $searchAttributes, $authz_id)
Convert SASL authz_id into a DN.
Definition: LDAP.php:722
static escape_filter_value($values=array(), $singleValue=true)
Borrowed function from PEAR:LDAP.
Definition: LDAP.php:666
validate($config, $username, $password=null)
Enter description here...
Definition: LDAP.php:618
searchformultiple($bases, $filters, $attributes=array(), $and=true, $escape=true, $scope='subtree')
This method was created specifically for the ldap:AttributeAddUsersGroups->searchActiveDirectory() me...
Definition: LDAP.php:335
$authz_id
LDAP user: authz_id if SASL is in use, binding dn otherwise.
Definition: LDAP.php:37
searchfordn($base, $attribute, $value, $allowZeroHits=false, $searchFilter=null, $scope='subtree')
Search for a DN.
Definition: LDAP.php:291
static asc2hex32($string)
Borrowed function from PEAR:LDAP.
Definition: LDAP.php:704
getAttributes($dn, $attributes=null, $maxsize=null)
Search a given DN for attributes, and return the resulting associative array.
Definition: LDAP.php:545
setOption($option, $value)
Applies an LDAP option to the current connection.
Definition: LDAP.php:510
$password
Definition: cron.php:14
$key
Definition: croninfo.php:18
$i
Definition: disco.tpl.php:19
if(array_key_exists('yes', $_REQUEST)) $attributes
Definition: getconsent.php:85
font size
Definition: langcheck.php:162
const ERR_NO_USER
Definition: LDAP.php:7
const ERR_AS_DATA_INCONSIST
Definition: LDAP.php:9
const ERR_INTERNAL
Constants defining possible errors.
Definition: LDAP.php:6
$base
Definition: index.php:4
$config
Definition: bootstrap.php:15
Attribute-related utility methods.
$type
$results
Definition: svg-scanner.php:47
$values