Completed
Pull Request — master (#7)
by Tim
03:08 queued 41s
created

Ldap::addAttributes()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 1
Metric Value
cc 2
eloc 5
c 2
b 0
f 1
nc 2
nop 2
dl 0
loc 10
rs 10
1
<?php
2
3
namespace SimpleSAML\Module\ldap\Auth;
4
5
use SimpleSAML\Error;
6
use SimpleSAML\Logger;
7
8
use Webmozart\Assert\Assert;
9
10
/**
11
 * Constants defining possible errors
12
 */
13
14
define('ERR_INTERNAL', 1);
15
define('ERR_NO_USER', 2);
16
define('ERR_WRONG_PW', 3);
17
define('ERR_AS_DATA_INCONSIST', 4);
18
define('ERR_AS_INTERNAL', 5);
19
define('ERR_AS_ATTRIBUTE', 6);
20
21
// not defined in earlier PHP versions
22
if (!defined('LDAP_OPT_DIAGNOSTIC_MESSAGE')) {
23
    define('LDAP_OPT_DIAGNOSTIC_MESSAGE', 0x0032);
24
}
25
26
/**
27
 * The LDAP class holds helper functions to access an LDAP database.
28
 *
29
 * @author Andreas Aakre Solberg, UNINETT AS. <[email protected]>
30
 * @author Anders Lund, UNINETT AS. <[email protected]>
31
 * @package SimpleSAMLphp
32
 */
33
34
class Ldap
35
{
36
    /**
37
     * LDAP link identifier.
38
     *
39
     * @var resource
40
     */
41
    protected $ldap;
42
43
    /**
44
     * LDAP user: authz_id if SASL is in use, binding dn otherwise
45
     *
46
     * @var string|null
47
     */
48
    protected $authz_id = null;
49
50
    /**
51
     * Timeout value, in seconds.
52
     *
53
     * @var int
54
     */
55
    protected $timeout = 0;
56
57
    /**
58
     * Private constructor restricts instantiation to getInstance().
59
     *
60
     * @param string $hostname
61
     * @param bool $enable_tls
62
     * @param bool $debug
63
     * @param int $timeout
64
     * @param int $port
65
     * @param bool $referrals
66
     * @psalm-suppress NullArgument
67
     */
68
    public function __construct(
69
        $hostname,
70
        $enable_tls = true,
71
        $debug = false,
72
        $timeout = 0,
73
        $port = 389,
74
        $referrals = true
75
    ) {
76
        // Debug
77
        Logger::debug('Library - LDAP __construct(): Setup LDAP with '.
78
            'host=\''.$hostname.
79
            '\', tls='.var_export($enable_tls, true).
80
            ', debug='.var_export($debug, true).
81
            ', timeout='.var_export($timeout, true).
82
            ', referrals='.var_export($referrals, true));
83
84
        /*
85
         * Set debug level before calling connect. Note that this passes
86
         * NULL to ldap_set_option, which is an undocumented feature.
87
         *
88
         * OpenLDAP 2.x.x or Netscape Directory SDK x.x needed for this option.
89
         */
90
        if ($debug && !ldap_set_option(null, LDAP_OPT_DEBUG_LEVEL, 7)) {
91
            Logger::warning('Library - LDAP __construct(): Unable to set debug level (LDAP_OPT_DEBUG_LEVEL) to 7');
92
        }
93
94
        /*
95
         * Prepare a connection for to this LDAP server. Note that this function
96
         * doesn't actually connect to the server.
97
         */
98
        $resource = @ldap_connect($hostname, $port);
99
        if ($resource === false) {
100
            throw $this->makeException(
101
                'Library - LDAP __construct(): Unable to connect to \''.$hostname.'\'',
102
                ERR_INTERNAL
103
            );
104
        }
105
        $this->ldap = $resource;
106
107
        // Enable LDAP protocol version 3
108
        if (!@ldap_set_option($this->ldap, LDAP_OPT_PROTOCOL_VERSION, 3)) {
109
            throw $this->makeException(
110
                'Library - LDAP __construct(): Failed to set LDAP Protocol version (LDAP_OPT_PROTOCOL_VERSION) to 3',
111
                ERR_INTERNAL
112
            );
113
        }
114
115
        // Set referral option
116
        if (!@ldap_set_option($this->ldap, LDAP_OPT_REFERRALS, $referrals)) {
117
            throw $this->makeException(
118
                'Library - LDAP __construct(): Failed to set LDAP Referrals (LDAP_OPT_REFERRALS) to '.$referrals,
119
                ERR_INTERNAL
120
            );
121
        }
122
123
        // Set timeouts, if supported
124
        // (OpenLDAP 2.x.x or Netscape Directory SDK x.x needed)
125
        $this->timeout = $timeout;
126
        if ($timeout > 0) {
127
            if (!@ldap_set_option($this->ldap, LDAP_OPT_NETWORK_TIMEOUT, $timeout)) {
128
                Logger::warning(
129
                    'Library - LDAP __construct(): Unable to set timeouts (LDAP_OPT_NETWORK_TIMEOUT) to '.$timeout
130
                );
131
            }
132
            if (!@ldap_set_option($this->ldap, LDAP_OPT_TIMELIMIT, $timeout)) {
133
                Logger::warning(
134
                    'Library - LDAP __construct(): Unable to set timeouts (LDAP_OPT_TIMELIMIT) to '.$timeout
135
                );
136
            }
137
        }
138
139
        // Enable TLS, if needed
140
        if (stripos($hostname, "ldaps:") === false && $enable_tls) {
141
            if (!@ldap_start_tls($this->ldap)) {
142
                throw $this->makeException('Library - LDAP __construct():'.
143
                    ' Unable to force TLS', ERR_INTERNAL);
144
            }
145
        }
146
    }
147
148
149
    /**
150
     * Convenience method to create an LDAPException as well as log the
151
     * description.
152
     *
153
     * @param string $description The exception's description
154
     * @param int|null $type The exception's type
155
     * @return \Exception
156
     */
157
    private function makeException($description, $type = null)
158
    {
159
        $errNo = @ldap_errno($this->ldap);
160
161
        // Decide exception type and return
162
        if ($type !== null) {
163
            if ($errNo !== 0) {
164
                // Only log real LDAP errors; not success
165
                Logger::error($description.'; cause: \''.ldap_error($this->ldap).'\' (0x'.dechex($errNo).')');
166
            } else {
167
                Logger::error($description);
168
            }
169
170
            switch ($type) {
171
                case ERR_INTERNAL:// 1 - ExInternal
172
                    return new Error\Exception($description, $errNo);
173
                case ERR_NO_USER:// 2 - ExUserNotFound
174
                    return new Error\UserNotFound($description, $errNo);
175
                case ERR_WRONG_PW:// 3 - ExInvalidCredential
176
                    return new Error\InvalidCredential($description, $errNo);
177
                case ERR_AS_DATA_INCONSIST:// 4 - ExAsDataInconsist
178
                    return new Error\AuthSource('ldap', $description);
179
                case ERR_AS_INTERNAL:// 5 - ExAsInternal
180
                    return new Error\AuthSource('ldap', $description);
181
            }
182
        } else {
183
            if ($errNo !== 0) {
184
                $description .= '; cause: \''.ldap_error($this->ldap).'\' (0x'.dechex($errNo).')';
185
                if (@ldap_get_option($this->ldap, LDAP_OPT_DIAGNOSTIC_MESSAGE, $extendedError)
186
                    && !empty($extendedError)
187
                ) {
188
                    $description .= '; additional: \''.$extendedError.'\'';
189
                }
190
            }
191
            switch ($errNo) {
192
                case 0x20://LDAP_NO_SUCH_OBJECT
193
                    Logger::warning($description);
194
                    return new Error\UserNotFound($description, $errNo);
195
                case 0x31://LDAP_INVALID_CREDENTIALS
196
                    Logger::info($description);
197
                    return new Error\InvalidCredential($description, $errNo);
198
                case -1://NO_SERVER_CONNECTION
199
                    Logger::error($description);
200
                    return new Error\AuthSource('ldap', $description);
201
                default:
202
                    Logger::error($description);
203
                    return new Error\AuthSource('ldap', $description);
204
            }
205
        }
206
        return new \Exception('Unknown LDAP error.');
207
    }
208
209
210
    /**
211
     * Search for DN from a single base.
212
     *
213
     * @param string $base
214
     * Indication of root of subtree to search
215
     * @param string|array $attribute
216
     * The attribute name(s) to search for.
217
     * @param string $value
218
     * The attribute value to search for.
219
     * Additional search filter
220
     * @param string|null $searchFilter
221
     * The scope of the search
222
     * @param string $scope
223
     * @return string
224
     * The DN of the resulting found element.
225
     * @throws Error\Exception if:
226
     * - Attribute parameter is wrong type
227
     * @throws Error\AuthSource if:
228
     * - Not able to connect to LDAP server
229
     * - False search result
230
     * - Count return false
231
     * - Searche found more than one result
232
     * - Failed to get first entry from result
233
     * - Failed to get DN for entry
234
     * @throws Error\UserNotFound if:
235
     * - Zero entries were found
236
     * @psalm-suppress TypeDoesNotContainType
237
     */
238
    private function search($base, $attribute, $value, $searchFilter = null, $scope = "subtree")
239
    {
240
        // Create the search filter
241
        /** @var array $attribute */
242
        $attribute = self::escape_filter_value($attribute, false);
243
244
        /** @var string $value */
245
        $value = self::escape_filter_value($value, true);
246
247
        $filter = '';
248
        foreach ($attribute as $attr) {
249
            $filter .= '('.$attr.'='.$value.')';
250
        }
251
        $filter = '(|'.$filter.')';
252
253
        // Append LDAP filters if defined
254
        if ($searchFilter !== null) {
255
            $filter = "(&".$filter."".$searchFilter.")";
256
        }
257
258
        // Search using generated filter
259
        Logger::debug('Library - LDAP search(): Searching base ('.$scope.') \''.$base.'\' for \''.$filter.'\'');
260
        if ($scope === 'base') {
261
            $result = @ldap_read($this->ldap, $base, $filter, [], 0, 0, $this->timeout, LDAP_DEREF_NEVER);
262
        } elseif ($scope === 'onelevel') {
263
            $result = @ldap_list($this->ldap, $base, $filter, [], 0, 0, $this->timeout, LDAP_DEREF_NEVER);
264
        } else {
265
            $result = @ldap_search($this->ldap, $base, $filter, [], 0, 0, $this->timeout, LDAP_DEREF_NEVER);
266
        }
267
268
        if ($result === false) {
269
            throw $this->makeException(
270
                'Library - LDAP search(): Failed search on base \''.$base.'\' for \''.$filter.'\''
271
            );
272
        }
273
274
        // Sanity checks on search results
275
        $count = @ldap_count_entries($this->ldap, $result);
276
        if ($count === false) {
277
            throw $this->makeException('Library - LDAP search(): Failed to get number of entries returned');
278
        } elseif ($count > 1) {
279
            // More than one entry is found. External error
280
            throw $this->makeException(
281
                'Library - LDAP search(): Found '.$count.' entries searching base \''.$base.'\' for \''.$filter.'\'',
282
                ERR_AS_DATA_INCONSIST
283
            );
284
        } elseif ($count === 0) {
285
            // No entry is fond => wrong username is given (or not registered in the catalogue). User error
286
            throw $this->makeException(
287
                'Library - LDAP search(): Found no entries searching base \''.$base.'\' for \''.$filter.'\'',
288
                ERR_NO_USER
289
            );
290
        }
291
292
293
        // Resolve the DN from the search result
294
        $entry = @ldap_first_entry($this->ldap, $result);
295
        if ($entry === false) {
296
            throw $this->makeException(
297
                'Library - LDAP search(): Unable to retrieve result after searching base \''.
298
                    $base.'\' for \''.$filter.'\''
299
            );
300
        }
301
        $dn = @ldap_get_dn($this->ldap, $entry);
302
        if ($dn === false) {
303
            throw $this->makeException(
304
                'Library - LDAP search(): Unable to get DN after searching base \''.$base.'\' for \''.$filter.'\''
305
            );
306
        }
307
        return $dn;
308
    }
309
310
311
    /**
312
     * Search for a DN.
313
     *
314
     * @param string|array $base
315
     * The base, or bases, which to search from.
316
     * @param string|array $attribute
317
     * The attribute name(s) searched for.
318
     * @param string $value
319
     * The attribute value searched for.
320
     * @param bool $allowZeroHits
321
     * Determines if the method will throw an exception if no hits are found.
322
     * Defaults to FALSE.
323
     * @param string|null $searchFilter
324
     * Additional searchFilter to be added to the (attribute=value) filter
325
     * @param string $scope
326
     * The scope of the search
327
     * @return string|null
328
     * The DN of the matching element, if found. If no element was found and
329
     * $allowZeroHits is set to FALSE, an exception will be thrown; otherwise
330
     * NULL will be returned.
331
     * @throws Error\AuthSource if:
332
     * - LDAP search encounter some problems when searching cataloge
333
     * - Not able to connect to LDAP server
334
     * @throws Error\UserNotFound if:
335
     * - $allowZeroHits is FALSE and no result is found
336
     *
337
     */
338
    public function searchfordn(
339
        $base,
340
        $attribute,
341
        $value,
342
        $allowZeroHits = false,
343
        $searchFilter = null,
344
        $scope = 'subtree'
345
    ) {
346
        // Traverse all search bases, returning DN if found
347
        $bases = \SimpleSAML\Utils\Arrays::arrayize($base);
348
        foreach ($bases as $current) {
349
            try {
350
                // Single base search
351
                $result = $this->search($current, $attribute, $value, $searchFilter, $scope);
352
353
                // We don't hawe to look any futher if user is found
354
                if (!empty($result)) {
355
                    return $result;
356
                }
357
                // If search failed, attempt the other base DNs
358
            } catch (Error\UserNotFound $e) {
359
                // Just continue searching
360
            }
361
        }
362
        // Decide what to do for zero entries
363
        Logger::debug('Library - LDAP searchfordn(): No entries found');
364
        if ($allowZeroHits) {
365
            // Zero hits allowed
366
            return null;
367
        } else {
368
            // Zero hits not allowed
369
            throw $this->makeException('Library - LDAP searchfordn(): LDAP search returned zero entries for'.
370
                ' filter \'('.join(' | ', \SimpleSAML\Utils\Arrays::arrayize($attribute)).' = '.$value.')\' on base(s) \'('.join(' & ', $bases).')\'', 2);
371
        }
372
    }
373
374
375
    /**
376
     * This method was created specifically for the ldap:AttributeAddUsersGroups->searchActiveDirectory()
377
     * method, but could be used for other LDAP search needs. It will search LDAP and return all the entries.
378
     *
379
     * @throws \Exception
380
     * @param string|array $bases
381
     * @param string|array $filters Array of 'attribute' => 'values' to be combined into the filter,
382
     *     or a raw filter string
383
     * @param string|array $attributes Array of attributes requested from LDAP
384
     * @param bool $and If multiple filters defined, then either bind them with & or |
385
     * @param bool $escape Weather to escape the filter values or not
386
     * @param string $scope The scope of the search
387
     * @return array
388
     */
389
    public function searchformultiple(
390
        $bases,
391
        $filters,
392
        $attributes = [],
393
        $and = true,
394
        $escape = true,
395
        $scope = 'subtree'
396
    ) {
397
        // Escape the filter values, if requested
398
        if ($escape) {
399
            $filters = $this->escape_filter_value($filters, false);
400
        }
401
402
        // Build search filter
403
        $filter = '';
404
        if (is_array($filters)) {
405
            foreach ($filters as $attribute => $value) {
406
                $filter .= "($attribute=$value)";
407
            }
408
            if (count($filters) > 1) {
409
                $filter = ($and ? '(&' : '(|').$filter.')';
410
            }
411
        } else {
412
            Assert::string($filters);
413
            $filter = $filters;
414
        }
415
416
        // Verify filter was created
417
        if ($filter == '' || $filter == '(=)') {
418
            throw $this->makeException('ldap:LdapConnection->search_manual : No search filters defined', ERR_INTERNAL);
419
        }
420
421
        // Verify at least one base was passed
422
        $bases = (array) $bases;
423
        if (empty($bases)) {
424
            throw $this->makeException('ldap:LdapConnection->search_manual : No base DNs were passed', ERR_INTERNAL);
425
        }
426
427
        $attributes = \SimpleSAML\Utils\Arrays::arrayize($attributes);
428
429
        // Search each base until result is found
430
        $result = false;
431
        foreach ($bases as $base) {
432
            if ($scope === 'base') {
433
                $result = @ldap_read($this->ldap, $base, $filter, $attributes, 0, 0, $this->timeout);
434
            } elseif ($scope === 'onelevel') {
435
                $result = @ldap_list($this->ldap, $base, $filter, $attributes, 0, 0, $this->timeout);
436
            } else {
437
                $result = @ldap_search($this->ldap, $base, $filter, $attributes, 0, 0, $this->timeout);
438
            }
439
440
            if ($result !== false && @ldap_count_entries($this->ldap, $result) > 0) {
441
                break;
442
            }
443
        }
444
445
        // Verify that a result was found in one of the bases
446
        if ($result === false) {
447
            throw $this->makeException(
448
                'ldap:LdapConnection->search_manual : Failed to search LDAP using base(s) ['.
449
                implode('; ', $bases).'] with filter ['.$filter.']. LDAP error ['.
450
                ldap_error($this->ldap).']'
451
            );
452
        } elseif (@ldap_count_entries($this->ldap, $result) < 1) {
453
            throw $this->makeException(
454
                'ldap:LdapConnection->search_manual : No entries found in LDAP using base(s) ['.
455
                implode('; ', $bases).'] with filter ['.$filter.']',
456
                ERR_NO_USER
457
            );
458
        }
459
460
        // Get all results
461
        $results = ldap_get_entries($this->ldap, $result);
462
        if ($results === false) {
463
            throw $this->makeException(
464
                'ldap:LdapConnection->search_manual : Unable to retrieve entries from search results'
465
            );
466
        }
467
468
        // parse each entry and process its attributes
469
        for ($i = 0; $i < $results['count']; $i++) {
470
            $entry = $results[$i];
471
472
            // iterate over the attributes of the entry
473
            for ($j = 0; $j < $entry['count']; $j++) {
474
                $name = $entry[$j];
475
                $attribute = $entry[$name];
476
477
                // decide whether to base64 encode or not
478
                for ($k = 0; $k < $attribute['count']; $k++) {
479
                    // base64 encode binary attributes
480
                    if (strtolower($name) === 'jpegphoto'
481
                        || strtolower($name) === 'objectguid'
482
                        || strtolower($name) === 'objectsid'
483
                        || strtolower($name) === 'ms-ds-consistencyguid'
484
                    ) {
485
                        $results[$i][$name][$k] = base64_encode($attribute[$k]);
486
                    }
487
                }
488
            }
489
        }
490
491
        // Remove the count and return
492
        unset($results['count']);
493
        return $results;
494
    }
495
496
497
    /**
498
     * Bind to LDAP with a specific DN and password. Simple wrapper around
499
     * ldap_bind() with some additional logging.
500
     *
501
     * @param string $dn
502
     * The DN used.
503
     * @param string $password
504
     * The password used.
505
     * @param array $sasl_args
506
     * Array of SASL options for SASL bind
507
     * @return bool
508
     * Returns TRUE if successful, FALSE if
509
     * LDAP_INVALID_CREDENTIALS, LDAP_X_PROXY_AUTHZ_FAILURE,
510
     * LDAP_INAPPROPRIATE_AUTH, LDAP_INSUFFICIENT_ACCESS
511
     * @throws Error\Exception on other errors
512
     */
513
    public function bind($dn, $password, array $sasl_args = null)
514
    {
515
        if ($sasl_args != null) {
516
            if (!function_exists('ldap_sasl_bind')) {
517
                $ex_msg = 'Library - missing SASL support';
518
                throw $this->makeException($ex_msg);
519
            }
520
521
            // SASL Bind, with error handling
522
            $authz_id = $sasl_args['authz_id'];
523
            $error = @ldap_sasl_bind(
524
                $this->ldap,
525
                $dn,
526
                $password,
527
                $sasl_args['mech'],
528
                $sasl_args['realm'],
529
                $sasl_args['authc_id'],
530
                $sasl_args['authz_id'],
531
                $sasl_args['props']
532
            );
533
        } else {
534
            // Simple Bind, with error handling
535
            $authz_id = $dn;
536
            $error = @ldap_bind($this->ldap, $dn, $password);
537
        }
538
539
        if ($error === true) {
540
            // Good
541
            $this->authz_id = $authz_id;
542
            Logger::debug('Library - LDAP bind(): Bind successful with DN \''.$dn.'\'');
543
            return true;
544
        }
545
546
        /* Handle errors
547
         * LDAP_INVALID_CREDENTIALS
548
         * LDAP_INSUFFICIENT_ACCESS */
549
        switch (ldap_errno($this->ldap)) {
550
            case 32: // LDAP_NO_SUCH_OBJECT
551
                // no break
552
            case 47: // LDAP_X_PROXY_AUTHZ_FAILURE
553
                // no break
554
            case 48: // LDAP_INAPPROPRIATE_AUTH
555
                // no break
556
            case 49: // LDAP_INVALID_CREDENTIALS
557
                // no break
558
            case 50: // LDAP_INSUFFICIENT_ACCESS
559
                return false;
560
            default:
561
                break;
562
        }
563
564
        // Bad
565
        throw $this->makeException('Library - LDAP bind(): Bind failed with DN \''.$dn.'\'');
566
    }
567
568
569
    /**
570
     * Applies an LDAP option to the current connection.
571
     *
572
     * @throws Exception
573
     * @param mixed $option
574
     * @param mixed $value
575
     * @return void
576
     */
577
    public function setOption($option, $value)
578
    {
579
        // Attempt to set the LDAP option
580
        if (!@ldap_set_option($this->ldap, $option, $value)) {
581
            throw $this->makeException(
582
                'ldap:LdapConnection->setOption : Failed to set LDAP option ['.
583
                $option.'] with the value ['.$value.'] error: '.ldap_error($this->ldap),
584
                ERR_INTERNAL
585
            );
586
        }
587
588
        // Log debug message
589
        Logger::debug(
590
            'ldap:LdapConnection->setOption : Set the LDAP option ['.
591
            $option.'] with the value ['.$value.']'
592
        );
593
    }
594
595
596
    /**
597
     * Search a given DN for attributes, and return the resulting associative
598
     * array.
599
     *
600
     * @param string $dn
601
     * The DN of an element.
602
     * @param string|array $attributes
603
     * The names of the attribute(s) to retrieve. Defaults to NULL; that is,
604
     * all available attributes. Note that this is not very effective.
605
     * @param int $maxsize
606
     * The maximum size of any attribute's value(s). If exceeded, the attribute
607
     * will not be returned.
608
     * @return array
609
     * The array of attributes and their values.
610
     * @see http://no.php.net/manual/en/function.ldap-read.php
611
     */
612
    public function getAttributes($dn, $attributes = null, $maxsize = null)
613
    {
614
        // Preparations, including a pretty debug message...
615
        $description = 'all attributes';
616
        if (is_array($attributes)) {
617
            $description = '\''.join(',', $attributes).'\'';
618
        } else {
619
            // Get all attributes...
620
            // TODO: Verify that this originally was the intended behaviour. Could $attributes be a string?
621
            $attributes = [];
622
        }
623
        Logger::debug('Library - LDAP getAttributes(): Getting '.$description.' from DN \''.$dn.'\'');
624
625
        // Attempt to get attributes
626
        // TODO: Should aliases be dereferenced?
627
        /** @var array $attributes */
628
        $result = @ldap_read($this->ldap, $dn, 'objectClass=*', $attributes, 0, 0, $this->timeout);
629
        if ($result === false) {
630
            throw $this->makeException('Library - LDAP getAttributes(): Failed to get attributes from DN \''.$dn.'\'');
631
        }
632
        $entry = @ldap_first_entry($this->ldap, $result);
633
        if ($entry === false) {
634
            throw $this->makeException('Library - LDAP getAttributes(): Could not get first entry from DN \''.$dn.'\'');
635
        }
636
        unset($attributes);
637
638
        /** @var array|false $attributes */
639
        $attributes = @ldap_get_attributes($this->ldap, $entry);
640
        if ($attributes === false) {
641
            throw $this->makeException(
642
                'Library - LDAP getAttributes(): Could not get attributes of first entry from DN \''.$dn.'\''
643
            );
644
        }
645
646
        // Parsing each found attribute into our result set
647
        $result = []; // Recycling $result... Possibly bad practice.
648
        for ($i = 0; $i < $attributes['count']; $i++) {
649
            // Ignore attributes that exceed the maximum allowed size
650
            $name = $attributes[$i];
651
            $attribute = $attributes[$name];
652
653
            // Deciding whether to base64 encode
654
            $values = [];
655
            for ($j = 0; $j < $attribute['count']; $j++) {
656
                $value = $attribute[$j];
657
658
                if (!empty($maxsize) && strlen($value) > $maxsize) {
659
                    // Ignoring and warning
660
                    Logger::warning('Library - LDAP getAttributes(): Attribute \''.
661
                        $name.'\' exceeded maximum allowed size by '.(strlen($value) - $maxsize));
662
                    continue;
663
                }
664
665
                // Base64 encode binary attributes
666
                if (strtolower($name) === 'jpegphoto'
667
                    || strtolower($name) === 'objectguid'
668
                    || strtolower($name) === 'objectsid'
669
                    || strtolower($name) === 'ms-ds-consistencyguid'
670
                ) {
671
                    $values[] = base64_encode($value);
672
                } else {
673
                    $values[] = $value;
674
                }
675
            }
676
677
            // Adding
678
            $result[$name] = $values;
679
        }
680
681
        // We're done
682
        Logger::debug('Library - LDAP getAttributes(): Found attributes \'('.join(',', array_keys($result)).')\'');
683
        return $result;
684
    }
685
686
687
    /**
688
     * Enter description here...
689
     *
690
     * @param array $config
691
     * @param string $username
692
     * @param string $password
693
     * @return array|false
694
     */
695
    public function validate($config, $username, $password = null)
696
    {
697
        /**
698
         * Escape any characters with a special meaning in LDAP. The following
699
         * characters have a special meaning (according to RFC 2253):
700
         * ',', '+', '"', '\', '<', '>', ';', '*'
701
         * These characters are escaped by prefixing them with '\'.
702
         */
703
        $username = addcslashes($username, ',+"\\<>;*');
704
705
        if (isset($config['priv_user_dn'])) {
706
            $this->bind($config['priv_user_dn'], $config['priv_user_pw']);
707
        }
708
        if (isset($config['dnpattern'])) {
709
            $dn = str_replace('%username%', $username, $config['dnpattern']);
710
        } else {
711
            /** @var string $dn */
712
            $dn = $this->searchfordn($config['searchbase'], $config['searchattributes'], $username, false);
713
        }
714
715
        if ($password !== null) {
716
            // checking users credentials ... assuming below that she may read her own attributes ...
717
            // escape characters with a special meaning, also in the password
718
            $password = addcslashes($password, ',+"\\<>;*');
719
            if (!$this->bind($dn, $password)) {
720
                Logger::info(
721
                    'Library - LDAP validate(): Failed to authenticate \''.$username.'\' using DN \''.$dn.'\''
722
                );
723
                return false;
724
            }
725
        }
726
727
        /**
728
         * Retrieve attributes from LDAP
729
         */
730
        $attributes = $this->getAttributes($dn, $config['attributes']);
731
        return $attributes;
732
    }
733
734
735
    /**
736
     * Borrowed function from PEAR:LDAP.
737
     *
738
     * Escapes the given VALUES according to RFC 2254 so that they can be safely used in LDAP filters.
739
     *
740
     * Any control characters with an ACII code < 32 as well as the characters with special meaning in
741
     * LDAP filters "*", "(", ")", and "\" (the backslash) are converted into the representation of a
742
     * backslash followed by two hex digits representing the hexadecimal value of the character.
743
     *
744
     * @static
745
     * @param string|array $values Array of values to escape
746
     * @param bool $singleValue
747
     * @return string|array Array $values, but escaped
748
     */
749
    public static function escape_filter_value($values = [], $singleValue = true)
750
    {
751
        // Parameter validation
752
        $values = \SimpleSAML\Utils\Arrays::arrayize($values);
753
754
        foreach ($values as $key => $val) {
755
            if ($val === null) {
756
                $val = '\0'; // apply escaped "null" if string is empty
757
            } else {
758
                // Escaping of filter meta characters
759
                $val = str_replace('\\', '\5c', $val);
760
                $val = str_replace('*', '\2a', $val);
761
                $val = str_replace('(', '\28', $val);
762
                $val = str_replace(')', '\29', $val);
763
764
                // ASCII < 32 escaping
765
                $val = self::asc2hex32($val);
766
            }
767
768
            $values[$key] = $val;
769
        }
770
        if ($singleValue) {
771
            return $values[0];
772
        }
773
        return $values;
774
    }
775
776
777
    /**
778
     * Borrowed function from PEAR:LDAP.
779
     *
780
     * Converts all ASCII chars < 32 to "\HEX"
781
     *
782
     * @param string $string String to convert
783
     *
784
     * @static
785
     * @return string
786
     */
787
    public static function asc2hex32($string)
788
    {
789
        for ($i = 0; $i < strlen($string); $i++) {
790
            $char = substr($string, $i, 1);
791
            if (ord($char) < 32) {
792
                $hex = dechex(ord($char));
793
                if (strlen($hex) == 1) {
794
                    $hex = '0'.$hex;
795
                }
796
                $string = str_replace($char, '\\'.$hex, $string);
797
            }
798
        }
799
        return $string;
800
    }
801
802
803
    /**
804
     * Convert SASL authz_id into a DN
805
     *
806
     * @param string $searchBase
807
     * @param array $searchAttributes
808
     * @param string $authz_id
809
     * @return string|null
810
     */
811
    private function authzidToDn($searchBase, $searchAttributes, $authz_id)
812
    {
813
        if (preg_match("/^dn:/", $authz_id)) {
814
            return preg_replace("/^dn:/", "", $authz_id);
815
        }
816
817
        if (preg_match("/^u:/", $authz_id)) {
818
            return $this->searchfordn(
819
                $searchBase,
820
                $searchAttributes,
821
                preg_replace("/^u:/", "", $authz_id)
822
            );
823
        }
824
        return $authz_id;
825
    }
826
827
828
    /**
829
     * ldap_exop_whoami accessor, if available. Use requested authz_id
830
     * otherwise.
831
     *
832
     * ldap_exop_whoami() has been provided as a third party patch that
833
     * waited several years to get its way upstream:
834
     * http://cvsweb.netbsd.org/bsdweb.cgi/pkgsrc/databases/php-ldap/files
835
     *
836
     * When it was integrated into PHP repository, the function prototype
837
     * was changed, The new prototype was used in third party patch for
838
     * PHP 7.0 and 7.1, hence the version test below.
839
     *
840
     * @param string $searchBase
841
     * @param array $searchAttributes
842
     * @throws \Exception
843
     * @return string
844
     */
845
    public function whoami($searchBase, $searchAttributes)
846
    {
847
        $authz_id = '';
848
        if (function_exists('ldap_exop_whoami')) {
849
            if (version_compare(phpversion(), '7', '<')) {
850
                /** @psalm-suppress TooManyArguments */
851
                if (ldap_exop_whoami($this->ldap, $authz_id) === false) {
852
                    throw $this->makeException('LDAP whoami exop failure');
853
                }
854
            } else {
855
                /** @var string|false $authz_id */
856
                $authz_id = ldap_exop_whoami($this->ldap);
857
                if ($authz_id === false) {
858
                    throw $this->makeException('LDAP whoami exop failure');
859
                }
860
            }
861
        } else {
862
            Assert::string($authz_id);
863
            /** @var string $authz_id */
864
            $authz_id = $this->authz_id;
865
        }
866
867
        $dn = $this->authzidToDn($searchBase, $searchAttributes, $authz_id);
868
869
        if (empty($dn)) {
870
            throw $this->makeException('Cannot figure userID');
871
        }
872
873
        return $dn;
874
    }
875
876
877
    /**
878
     * Set for a given DN attributes
879
     *
880
     * @param string $dn
881
     * The DN of an element.
882
     * @param array $attributes
883
     * The names and value of the attribute(s) to set using ldap_mod_replace structure;
884
     * @return bool
885
     * Result of operation
886
     */
887
    public function setAttributes($dn, array $attributes) {
888
      Logger::debug('Library - LDAP setAttributes(): Received arraydata:'.print_r($attributes, true));
889
890
      // Attempt to set attributes
891
      $result = @ldap_mod_replace($this->ldap, $dn, $attributes);
892
      if ($result === false) {
893
          throw $this->makeException('Library - LDAP setAttributes(): Failed to set attributes for DN \''.$dn.'\'. Bind necessary?');
894
      }
895
896
      return $result;
897
    }
898
899
900
    /**
901
      * Adds for a given DN attributes
902
      *
903
      * @param string $dn
904
      * The DN of an element.
905
      * @param array $attributes
906
      * The names and value of the attribute(s) to set using ldap_mod_add structure;
907
      * @return bool
908
      * Result of operation
909
      */
910
     public function addAttributes($dn, array $attributes) {
911
       Logger::debug('Library - LDAP addAttributes(): Received arraydata:'.print_r($attributes, true));
912
913
       // Attempt to add attributes
914
       $result = @ldap_mod_add($this->ldap, $dn, $attributes);
915
       if ($result === false) {
916
           throw $this->makeException('Library - LDAP addAttributes(): Failed to set attributes for DN \''.$dn.'\'. Bind necessary?');
917
       }
918
919
       return $result;
920
     }    
921
}
922