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

Ldap::search()   B

Complexity

Conditions 6
Paths 7

Size

Total Lines 47
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
cc 6
eloc 27
c 4
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
        return LdapObject::create(
60
            $extension,
61
            [
62
                'connection_string' => $connection_strings,
63
                'encryption' => $encryption,
64
                'version' => $version,
65
                'debug' => $debug,
66
                'options' => $options,
67
            ]
68
        );
69
    }
70
71
72
    /**
73
     * Bind to an LDAP-server
74
     *
75
     * @param \Symfony\Component\Ldap\Ldap $ldapObject
76
     * @param string $username
77
     * @param string|null $password  Null for passwordless logon
78
     * @throws \SimpleSAML\Error\Exception if none of the LDAP-servers could be contacted
79
     */
80
    public function bind(LdapObject $ldapObject, string $username, ?string $password): void
81
    {
82
        try {
83
            $ldapObject->bind($username, strval($password));
84
        } catch (InvalidCredentialsException $e) {
85
            throw new Error\Error('WRONGUSERPASS');
86
        }
87
    }
88
89
90
    /**
91
     * Search the LDAP-directory for a specific object
92
     *
93
     * @param \Symfony\Component\Ldap\Ldap $ldap
94
     * @param array $searchBase
95
     * @param string $filter
96
     * @param array $options
97
     * @param boolean $allowMissing
98
     * @return \Symfony\Component\Ldap\Entry|null The result of the search or null if none found
99
     * @throws \SimpleSAML\Error\Exception if more than one entry was found
100
     * @throws \SimpleSAML\Error\Exception if the object cannot be found using the given search base and filter
101
     */
102
    public function search(
103
        LdapObject $ldap,
104
        array $searchBase,
105
        string $filter,
106
        array $options,
107
        bool $allowMissing
108
    ): ?Entry {
109
        $entry = null;
110
111
        foreach ($searchBase as $base) {
112
            $query = $ldap->query($base, $filter, $options);
113
            $result = $query->execute()->toArray();
114
115
            if (count($result) > 1) {
116
                throw new Error\Exception(
117
                    sprintf(
118
                        "Library - LDAP search(): Found %d entries searching base '%s' for '%s'",
119
                        count($result),
120
                        $base,
121
                        $filter,
122
                    )
123
                );
124
            } elseif (count($result) === 1) {
125
                $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

125
                $entry = array_pop(/** @scrutinizer ignore-type */ $result);
Loading history...
126
                break;
127
            } else {
128
                Logger::debug(
129
                    sprintf(
130
                        "Library - LDAP search(): Found no entries searching base '%s' for '%s'",
131
                        $base,
132
                        $filter,
133
                    )
134
                );
135
            }
136
        }
137
138
        if ($entry === null && $allowMissing === false) {
139
            throw new Error\Exception(
140
                sprintf(
141
                    "Object not found using search base [%s] and filter '%s'",
142
                    implode(', ', $searchBase),
143
                    $filter
144
                )
145
            );
146
        }
147
148
        return $entry;
149
    }
150
151
152
    /**
153
     * Search the LDAP-directory for any object matching the search filter
154
     *
155
     * @param \Symfony\Component\Ldap\Ldap $ldap
156
     * @param array $searchBase
157
     * @param string $filter
158
     * @param array $options
159
     * @param boolean $allowMissing
160
     * @return \Symfony\Component\Ldap\Entry[] The result of the search
161
     * @throws \SimpleSAML\Error\Exception if more than one entry was found
162
     * @throws \SimpleSAML\Error\Exception if the object cannot be found using the given search base and filter
163
     */
164
    public function searchForMultiple(
165
        LdapObject $ldap,
166
        array $searchBase,
167
        string $filter,
168
        array $options,
169
        bool $allowMissing
170
    ): array {
171
        $results = [];
172
173
        foreach ($searchBase as $base) {
174
            $query = $ldap->query($base, $filter, $options);
175
            $result = $query->execute()->toArray();
176
            $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

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