Passed
Pull Request — master (#28)
by Tim
02:30
created

Ldap::search()   B

Complexity

Conditions 7
Paths 12

Size

Total Lines 49
Code Lines 29

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 29
c 1
b 0
f 0
dl 0
loc 49
rs 8.5226
cc 7
nc 12
nop 5
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\Ldap as LdapObject;
14
15
use function array_pop;
16
use function count;
17
use function dechex;
18
use function is_array;
19
use function ord;
20
use function sprintf;
21
use function strlen;
22
use function strval;
23
use function substr;
24
use function str_replace;
25
26
/**
27
 * LDAP utilities
28
 *
29
 * @package simplesamlphp/simplesamlphp-module-ldap
30
 */
31
32
class Ldap
33
{
34
    /**
35
     * Create Ldap resource objects
36
     *
37
     * @param array $connection_strings
38
     * @param string $encryption
39
     * @param int $version
40
     * @param string $extension
41
     * @param bool $debug
42
     * @param array $options
43
     * @return \Symfony\Component\Ldap\Ldap[]
44
     */
45
    public function create(
46
        array $connection_strings,
47
        string $encryption = 'ssl',
48
        int $version = 3,
49
        string $extension = 'ext_ldap',
50
        bool $debug = false,
51
        array $options = ['referrals' => false, 'network_timeout' => 3]
52
    ): array {
53
        $ldapServers = [];
54
55
        foreach ($connection_strings as $connection_string) {
56
            Assert::regex($connection_string, '#^ldap[s]?:\/\/#');
57
58
            $ldapServers[] = LdapObject::create(
59
                $extension,
60
                [
61
                    'connection_string' => $connection_string,
62
                    'encryption' => $encryption,
63
                    'version' => $version,
64
                    'debug' => $debug,
65
                    'options' => $options,
66
                ]
67
            );
68
        }
69
70
        return $ldapServers;
71
    }
72
73
74
    /**
75
     * Bind to an LDAP-server
76
     *
77
     * @param \Symfony\Component\Ldap\Ldap[] $ldapServers
78
     * @param string $username
79
     * @param string|null $password  Null for passwordless logon
80
     * @throws \SimpleSAML\Error\Exception if none of the LDAP-servers could be contacted
81
     */
82
    public function bind(array $ldapServers, string $username, ?string $password): LdapObject
83
    {
84
        foreach ($ldapServers as $ldap) {
85
            try {
86
                $ldap->bind($username, strval($password));
87
                return $ldap;
88
            } catch (ConnectionException $e) {
89
                // Try next server
90
            }
91
        }
92
93
        throw new Error\Exception("Unable to bind to any of the configured LDAP servers.");
94
    }
95
96
97
    /**
98
     * Search the LDAP-directory for a specific object
99
     *
100
     * @param \Symfony\Component\Ldap\Ldap $ldap
101
     * @param array $searchBase
102
     * @param string $filter
103
     * @param array $options
104
     * @param boolean $allowMissing
105
     * @return \Symfony\Component\Ldap\Entry|null The result of the search or null if none found
106
     * @throws \SimpleSAML\Error\Exception if more than one entry was found
107
     * @throws \SimpleSAML\Error\Exception if the object cannot be found using the given search base and filter
108
     */
109
    public function search(
110
        LdapObject $ldap,
111
        array $searchBase,
112
        string $filter,
113
        array $options,
114
        bool $allowMissing
115
    ): ?Entry {
116
        $entry = null;
117
118
        foreach ($searchBase as $base) {
119
            $query = $ldap->query($base, $filter, $options);
120
            $result = $query->execute();
121
            $result = is_array($result) ? $result : $result->toArray();
122
123
            if (count($result) > 1) {
124
                throw new Error\Exception(
125
                    sprintf(
126
                        "Library - LDAP search(): Found %d entries searching base '%s' for '%s'",
127
                        count($result),
128
                        $base,
129
                        $filter,
130
                    )
131
                );
132
            } elseif (count($result) === 1) {
133
                $entry = array_pop($result);
134
                break;
135
            } else {
136
                Logger::debug(
137
                    sprintf(
138
                        "Library - LDAP search(): Found no entries searching base '%s' for '%s'",
139
                        count($result),
140
                        $base,
141
                        $filter,
142
                    )
143
                );
144
            }
145
        }
146
147
        if ($entry === null && $allowMissing === false) {
148
            throw new Error\Exception(
149
                sprintf(
150
                    "Object not found using search base [%s] and filter '%s'",
151
                    implode(', ', $searchBase),
152
                    $filter
153
                )
154
            );
155
        }
156
157
        return $entry;
158
    }
159
160
161
    /**
162
     * Search the LDAP-directory for any object matching the search filter
163
     *
164
     * @param \Symfony\Component\Ldap\Ldap $ldap
165
     * @param array $searchBase
166
     * @param string $filter
167
     * @param array $options
168
     * @param boolean $allowMissing
169
     * @return \Symfony\Component\Ldap\Entry[] The result of the search
170
     * @throws \SimpleSAML\Error\Exception if more than one entry was found
171
     * @throws \SimpleSAML\Error\Exception if the object cannot be found using the given search base and filter
172
     */
173
    public function searchForMultiple(
174
        LdapObject $ldap,
175
        array $searchBase,
176
        string $filter,
177
        array $options,
178
        bool $allowMissing
179
    ): array {
180
        $entry = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $entry is dead and can be removed.
Loading history...
181
182
        $results = [];
183
        foreach ($searchBase as $base) {
184
            $query = $ldap->query($base, $filter, $options);
185
            $result = $query->execute();
186
            $results = array_merge($results, is_array($result) ? $result : $result->toArray());
187
188
            sprintf(
189
                "Library - LDAP search(): Found %d entries searching base '%s' for '%s'",
190
                count($result),
191
                $base,
192
                $filter,
193
            );
194
        }
195
196
        if (empty($results) && ($allowMissing === false)) {
197
            throw new Error\Exception(
198
                sprintf(
199
                    "No Objects found using search base [%s] and filter '%s'",
200
                    implode(', ', $searchBase),
201
                    $filter
202
                )
203
            );
204
        }
205
206
        return $results;
207
    }
208
209
210
    /**
211
     * Escapes the given VALUES according to RFC 2254 so that they can be safely used in LDAP filters.
212
     *
213
     * Any control characters with an ACII code < 32 as well as the characters with special meaning in
214
     * LDAP filters "*", "(", ")", and "\" (the backslash) are converted into the representation of a
215
     * backslash followed by two hex digits representing the hexadecimal value of the character.
216
     *
217
     * @param string|array $values Array of values to escape
218
     * @param bool $singleValue
219
     * @return string|array Array $values, but escaped
220
     */
221
    public function escapeFilterValue($values = [], bool $singleValue = true)
222
    {
223
        // Parameter validation
224
        $arrayUtils = new Utils\Arrays();
225
        $values = $arrayUtils->arrayize($values);
226
227
        foreach ($values as $key => $val) {
228
            if ($val === null) {
229
                $val = '\0'; // apply escaped "null" if string is empty
230
            } else {
231
                // Escaping of filter meta characters
232
                $val = str_replace('\\', '\5c', $val);
233
                $val = str_replace('*', '\2a', $val);
234
                $val = str_replace('(', '\28', $val);
235
                $val = str_replace(')', '\29', $val);
236
237
                // ASCII < 32 escaping
238
                $val = $this->asc2hex32($val);
239
            }
240
241
            $values[$key] = $val;
242
        }
243
244
        if ($singleValue) {
245
            return $values[0];
246
        }
247
248
        return $values;
249
    }
250
251
252
    /**
253
     * Converts all ASCII chars < 32 to "\HEX"
254
     *
255
     * @param string $string String to convert
256
     * @return string
257
     */
258
    public function asc2hex32(string $string): string
259
    {
260
        for ($i = 0; $i < strlen($string); $i++) {
261
            $char = substr($string, $i, 1);
262
263
            if (ord($char) < 32) {
264
                $hex = dechex(ord($char));
265
                if (strlen($hex) == 1) {
266
                    $hex = '0' . $hex;
267
                }
268
269
                $string = str_replace($char, '\\' . $hex, $string);
270
            }
271
        }
272
273
        return $string;
274
    }
275
}
276