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

Ldap::bind()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 2
eloc 5
c 3
b 0
f 0
nc 2
nop 3
dl 0
loc 9
rs 10
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 string $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
        string $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
    ): LdapObject {
53
        foreach (explode(' ', $connection_strings) as $connection_string) {
54
            Assert::regex($connection_string, '#^ldap[s]?:\/\/#');
55
        }
56
57
        return LdapObject::create(
58
            $extension,
59
            [
60
                'connection_string' => $connection_strings,
61
                'encryption' => $encryption,
62
                'version' => $version,
63
                'debug' => $debug,
64
                'options' => $options,
65
            ]
66
        );
67
    }
68
69
70
    /**
71
     * Bind to an LDAP-server
72
     *
73
     * @param \Symfony\Component\Ldap\Ldap $ldapObject
74
     * @param string $username
75
     * @param string|null $password  Null for passwordless logon
76
     * @throws \SimpleSAML\Error\Exception if none of the LDAP-servers could be contacted
77
     */
78
    public function bind(LdapObject $ldapObject, string $username, ?string $password): void
79
    {
80
        try {
81
            $ldapObject->bind($username, strval($password));
82
        } catch (ConnectionException $e) {
83
            Logger::error(sprintf("LDAP bind failed:  %s", $e->getMessage()));
84
        }
85
86
        throw new Error\Exception("Unable to bind to any of the configured LDAP servers.");
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();
114
            $result = is_array($result) ? $result : $result->toArray();
115
116
            if (count($result) > 1) {
117
                throw new Error\Exception(
118
                    sprintf(
119
                        "Library - LDAP search(): Found %d entries searching base '%s' for '%s'",
120
                        count($result),
121
                        $base,
122
                        $filter,
123
                    )
124
                );
125
            } elseif (count($result) === 1) {
126
                $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

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

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