Passed
Pull Request — master (#28)
by Tim
03:14
created

Ldap::search()   B

Complexity

Conditions 6
Paths 7

Size

Total Lines 47
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 5
Bugs 0 Features 0
Metric Value
cc 6
eloc 27
c 5
b 0
f 0
nc 7
nop 5
dl 0
loc 47
rs 8.8657
1
<?php
2
3
declare(strict_types=1);
4
5
namespace SimpleSAML\Module\ldap\Utils;
6
7
use SimpleSAML\Assert\Assert;
8
use SimpleSAML\Error;
9
use SimpleSAML\Logger;
10
use SimpleSAML\Utils;
11
use Symfony\Component\Ldap\Entry;
12
use Symfony\Component\Ldap\Exception\ConnectionException;
13
use Symfony\Component\Ldap\Exception\InvalidCredentialsException;
14
use Symfony\Component\Ldap\Ldap as LdapObject;
15
16
use function array_pop;
17
use function count;
18
use function dechex;
19
use function is_array;
20
use function is_iterable;
21
use function ord;
22
use function sprintf;
23
use function strlen;
24
use function strval;
25
use function substr;
26
use function str_replace;
27
28
/**
29
 * LDAP utilities
30
 *
31
 * @package simplesamlphp/simplesamlphp-module-ldap
32
 */
33
34
class Ldap
35
{
36
    /**
37
     * Create Ldap resource objects
38
     *
39
     * @param string $connection_strings
40
     * @param string $encryption
41
     * @param int $version
42
     * @param string $extension
43
     * @param bool $debug
44
     * @param array $options
45
     * @return \Symfony\Component\Ldap\Ldap
46
     */
47
    public function create(
48
        string $connection_strings,
49
        string $encryption = 'ssl',
50
        int $version = 3,
51
        string $extension = 'ext_ldap',
52
        bool $debug = false,
53
        array $options = ['referrals' => false, 'network_timeout' => 3]
54
    ): LdapObject {
55
        foreach (explode(' ', $connection_strings) as $connection_string) {
56
            Assert::regex($connection_string, '#^ldap[s]?:\/\/#');
57
        }
58
59
        Logger::debug(sprintf(
60
            "Setting up LDAP connection: host='%s', encryption=%s, version=%d, debug=%s, timeout=%d, referrals=%s.",
61
            $connection_strings,
62
            $encryption,
63
            $version,
64
            var_export($debug, true),
65
            $options['timeout'] ?? ini_get('default_socket_timeout'),
66
            var_export($options['referrals'] ?? false, true),
67
        ));
68
69
        return LdapObject::create(
70
            $extension,
71
            [
72
                'connection_string' => $connection_strings,
73
                'encryption' => $encryption,
74
                'version' => $version,
75
                'debug' => $debug,
76
                'options' => $options,
77
            ]
78
        );
79
    }
80
81
82
    /**
83
     * Bind to an LDAP-server
84
     *
85
     * @param \Symfony\Component\Ldap\Ldap $ldapObject
86
     * @param string $username
87
     * @param string|null $password  Null for passwordless logon
88
     * @throws \SimpleSAML\Error\Exception if none of the LDAP-servers could be contacted
89
     */
90
    public function bind(LdapObject $ldapObject, string $username, ?string $password): void
91
    {
92
        try {
93
            $ldapObject->bind($username, strval($password));
94
        } catch (InvalidCredentialsException $e) {
95
            throw new Error\Error('WRONGUSERPASS');
96
        }
97
98
        Logger::debug(sprintf("LDAP bind(): Bind successful for DN '%s'.", $username));
99
    }
100
101
102
    /**
103
     * Search the LDAP-directory for a specific object
104
     *
105
     * @param \Symfony\Component\Ldap\Ldap $ldap
106
     * @param array $searchBase
107
     * @param string $filter
108
     * @param array $options
109
     * @param boolean $allowMissing
110
     * @return \Symfony\Component\Ldap\Entry|null The result of the search or null if none found
111
     * @throws \SimpleSAML\Error\Exception if more than one entry was found
112
     * @throws \SimpleSAML\Error\Exception if the object cannot be found using the given search base and filter
113
     */
114
    public function search(
115
        LdapObject $ldap,
116
        array $searchBase,
117
        string $filter,
118
        array $options,
119
        bool $allowMissing
120
    ): ?Entry {
121
        $entry = null;
122
123
        foreach ($searchBase as $base) {
124
            $query = $ldap->query($base, $filter, $options);
125
            $result = $query->execute()->toArray();
126
127
            if (count($result) > 1) {
128
                throw new Error\Exception(
129
                    sprintf(
130
                        "LDAP search(): Found %d entries searching base '%s' for '%s'",
131
                        count($result),
132
                        $base,
133
                        $filter,
134
                    )
135
                );
136
            } elseif (count($result) === 1) {
137
                $entry = array_pop($result);
0 ignored issues
show
Bug introduced by
$result of type Symfony\Component\Ldap\Adapter\list is incompatible with the type array expected by parameter $array of array_pop(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

137
                $entry = array_pop(/** @scrutinizer ignore-type */ $result);
Loading history...
138
                break;
139
            } else {
140
                Logger::debug(
141
                    sprintf(
142
                        "LDAP search(): Found no entries searching base '%s' for '%s'",
143
                        $base,
144
                        $filter,
145
                    )
146
                );
147
            }
148
        }
149
150
        if ($entry === null && $allowMissing === false) {
151
            throw new Error\Exception(
152
                sprintf(
153
                    "Object not found using search base [%s] and filter '%s'",
154
                    implode(', ', $searchBase),
155
                    $filter
156
                )
157
            );
158
        }
159
160
        return $entry;
161
    }
162
163
164
    /**
165
     * Search the LDAP-directory for any object matching the search filter
166
     *
167
     * @param \Symfony\Component\Ldap\Ldap $ldap
168
     * @param array $searchBase
169
     * @param string $filter
170
     * @param array $options
171
     * @param boolean $allowMissing
172
     * @return \Symfony\Component\Ldap\Entry[] The result of the search
173
     * @throws \SimpleSAML\Error\Exception if more than one entry was found
174
     * @throws \SimpleSAML\Error\Exception if the object cannot be found using the given search base and filter
175
     */
176
    public function searchForMultiple(
177
        LdapObject $ldap,
178
        array $searchBase,
179
        string $filter,
180
        array $options,
181
        bool $allowMissing
182
    ): array {
183
        $results = [];
184
185
        foreach ($searchBase as $base) {
186
            $query = $ldap->query($base, $filter, $options);
187
            $result = $query->execute()->toArray();
188
            $results = array_merge($results, $result);
0 ignored issues
show
Bug introduced by
$result of type Symfony\Component\Ldap\Adapter\list is incompatible with the type array expected by parameter $arrays of array_merge(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

188
            $results = array_merge($results, /** @scrutinizer ignore-type */ $result);
Loading history...
189
190
            Logger::debug(sprintf(
191
                "Library - LDAP search(): Found %d entries searching base '%s' for '%s'",
192
                count($result),
193
                $base,
194
                $filter,
195
            ));
196
        }
197
198
        if (empty($results) && ($allowMissing === false)) {
199
            throw new Error\Exception(
200
                sprintf(
201
                    "No Objects found using search base [%s] and filter '%s'",
202
                    implode(', ', $searchBase),
203
                    $filter
204
                )
205
            );
206
        }
207
208
        return $results;
209
    }
210
211
212
    /**
213
     * Escapes the given VALUES according to RFC 2254 so that they can be safely used in LDAP filters.
214
     *
215
     * Any control characters with an ASCII code < 32 as well as the characters with special meaning in
216
     * LDAP filters "*", "(", ")", and "\" (the backslash) are converted into the representation of a
217
     * backslash followed by two hex digits representing the hexadecimal value of the character.
218
     *
219
     * @param string|string[] $values Array of values to escape
220
     * @param bool $singleValue
221
     * @return string|string[] Array $values, but escaped
222
     */
223
    public function escapeFilterValue($values = [], bool $singleValue = true)
224
    {
225
        // Parameter validation
226
        $arrayUtils = new Utils\Arrays();
227
        $values = $arrayUtils->arrayize($values);
228
229
        foreach ($values as $key => $val) {
230
            if ($val === null) {
231
                $val = '\0'; // apply escaped "null" if string is empty
232
            } else {
233
                // Escaping of filter meta characters
234
                $val = str_replace('\\', '\5c', $val);
235
                $val = str_replace('*', '\2a', $val);
236
                $val = str_replace('(', '\28', $val);
237
                $val = str_replace(')', '\29', $val);
238
239
                // ASCII < 32 escaping
240
                $val = $this->asc2hex32($val);
241
            }
242
243
            $values[$key] = $val;
244
        }
245
246
        if ($singleValue) {
247
            return $values[0];
248
        }
249
250
        return $values;
251
    }
252
253
254
    /**
255
     * Converts all ASCII chars < 32 to "\HEX"
256
     *
257
     * @param string $string String to convert
258
     * @return string
259
     */
260
    public function asc2hex32(string $string): string
261
    {
262
        for ($i = 0; $i < strlen($string); $i++) {
263
            $char = substr($string, $i, 1);
264
265
            if (ord($char) < 32) {
266
                $hex = str_pad(dechex(ord($char)), 2, '0', STR_PAD_LEFT);
267
                $string = str_replace($char, '\\' . $hex, $string);
268
            }
269
        }
270
271
        return $string;
272
    }
273
}
274