ILIAS  release_5-3 Revision v5.3.23-19-g915713cf615
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 // 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
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
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 {
749 $authz_id = $this->authz_id;
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}
$result
An exception for terminatinating execution or to throw for unit testing.
static info($string)
Definition: Logger.php:201
static warning($string)
Definition: Logger.php:179
static error($string)
Definition: Logger.php:168
static debug($string)
Definition: Logger.php:213
static arrayize($data, $index=0)
Put a non-array variable into an array.
Definition: Arrays.php:24
bind($dn, $password, array $sasl_args=null)
Bind to LDAP with a specific DN and password.
Definition: LDAP.php:428
searchformultiple($bases, $filters, $attributes=array(), $and=true, $escape=true)
This method was created specifically for the ldap:AttributeAddUsersGroups->searchActiveDirectory() me...
Definition: LDAP.php:324
__construct($hostname, $enable_tls=true, $debug=false, $timeout=0, $port=389, $referrals=true)
Private constructor restricts instantiation to getInstance().
Definition: LDAP.php:57
makeException($description, $type=null)
Convenience method to create an LDAPException as well as log the description.
Definition: LDAP.php:126
whoami($searchBase, $searchAttributes)
ldap_exop_whoami accessor, if available.
Definition: LDAP.php:734
authzid_to_dn($searchBase, $searchAttributes, $authz_id)
Convert SASL authz_id into a DN.
Definition: LDAP.php:709
static escape_filter_value($values=array(), $singleValue=true)
Borrowed function from PEAR:LDAP.
Definition: LDAP.php:651
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 asc2hex32($string)
Borrowed function from PEAR:LDAP.
Definition: LDAP.php:691
getAttributes($dn, $attributes=null, $maxsize=null)
Search a given DN for attributes, and return the resulting associative array.
Definition: LDAP.php:525
searchfordn($base, $attribute, $value, $allowZeroHits=false, $searchFilter=null)
Search for a DN.
Definition: LDAP.php:280
search($base, $attribute, $value, $searchFilter=null)
Search for DN from a single base.
Definition: LDAP.php:206
setOption($option, $value)
Applies an LDAP option to the current connection.
Definition: LDAP.php:490
$key
Definition: croninfo.php:18
$i
Definition: disco.tpl.php:19
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
$error
Definition: Error.php:17
if($format !==null) $name
Definition: metadata.php:146
$debug
Definition: loganalyzer.php:16
Attribute-related utility methods.
defined( 'APPLICATION_ENV')||define( 'APPLICATION_ENV'
Definition: bootstrap.php:27
$type
$password
Definition: pwgen.php:17
$attributes
$results
Definition: svg-scanner.php:47