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

Ldap   A

Complexity

Total Complexity 25

Size/Duplication

Total Lines 241
Duplicated Lines 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
wmc 25
eloc 86
c 4
b 0
f 0
dl 0
loc 241
rs 10

6 Methods

Rating   Name   Duplication   Size   Complexity  
A searchForMultiple() 0 33 5
A create() 0 26 2
A asc2hex32() 0 16 4
B search() 0 49 7
A escapeFilterValue() 0 28 4
A bind() 0 12 3
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);
0 ignored issues
show
Bug introduced by
It seems like $result can also be of type Symfony\Component\Ldap\Adapter\list; however, parameter $array of array_pop() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

133
                $entry = array_pop(/** @scrutinizer ignore-type */ $result);
Loading history...
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
        $results = [];
181
182
        foreach ($searchBase as $base) {
183
            $query = $ldap->query($base, $filter, $options);
184
            $result = $query->execute();
185
            $results = array_merge($results, is_array($result) ? $result : $result->toArray());
0 ignored issues
show
Bug introduced by
It seems like is_array($result) ? $result : $result->toArray() can also be of type Symfony\Component\Ldap\Adapter\list; however, parameter $arrays of array_merge() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

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