1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace SimpleSAML\Module\ldap\Auth; |
4
|
|
|
|
5
|
|
|
use SimpleSAML\Error; |
6
|
|
|
use SimpleSAML\Logger; |
7
|
|
|
|
8
|
|
|
use Webmozart\Assert\Assert; |
9
|
|
|
|
10
|
|
|
/** |
11
|
|
|
* Constants defining possible errors |
12
|
|
|
*/ |
13
|
|
|
|
14
|
|
|
define('ERR_INTERNAL', 1); |
15
|
|
|
define('ERR_NO_USER', 2); |
16
|
|
|
define('ERR_WRONG_PW', 3); |
17
|
|
|
define('ERR_AS_DATA_INCONSIST', 4); |
18
|
|
|
define('ERR_AS_INTERNAL', 5); |
19
|
|
|
define('ERR_AS_ATTRIBUTE', 6); |
20
|
|
|
|
21
|
|
|
// not defined in earlier PHP versions |
22
|
|
|
if (!defined('LDAP_OPT_DIAGNOSTIC_MESSAGE')) { |
23
|
|
|
define('LDAP_OPT_DIAGNOSTIC_MESSAGE', 0x0032); |
24
|
|
|
} |
25
|
|
|
|
26
|
|
|
/** |
27
|
|
|
* The LDAP class holds helper functions to access an LDAP database. |
28
|
|
|
* |
29
|
|
|
* @author Andreas Aakre Solberg, UNINETT AS. <[email protected]> |
30
|
|
|
* @author Anders Lund, UNINETT AS. <[email protected]> |
31
|
|
|
* @package SimpleSAMLphp |
32
|
|
|
*/ |
33
|
|
|
|
34
|
|
|
class Ldap |
35
|
|
|
{ |
36
|
|
|
/** |
37
|
|
|
* LDAP link identifier. |
38
|
|
|
* |
39
|
|
|
* @var resource |
40
|
|
|
*/ |
41
|
|
|
protected $ldap; |
42
|
|
|
|
43
|
|
|
/** |
44
|
|
|
* LDAP user: authz_id if SASL is in use, binding dn otherwise |
45
|
|
|
* |
46
|
|
|
* @var string|null |
47
|
|
|
*/ |
48
|
|
|
protected $authz_id = null; |
49
|
|
|
|
50
|
|
|
/** |
51
|
|
|
* Timeout value, in seconds. |
52
|
|
|
* |
53
|
|
|
* @var int |
54
|
|
|
*/ |
55
|
|
|
protected $timeout = 0; |
56
|
|
|
|
57
|
|
|
/** |
58
|
|
|
* Private constructor restricts instantiation to getInstance(). |
59
|
|
|
* |
60
|
|
|
* @param string $hostname |
61
|
|
|
* @param bool $enable_tls |
62
|
|
|
* @param bool $debug |
63
|
|
|
* @param int $timeout |
64
|
|
|
* @param int $port |
65
|
|
|
* @param bool $referrals |
66
|
|
|
* @psalm-suppress NullArgument |
67
|
|
|
*/ |
68
|
|
|
public function __construct( |
69
|
|
|
$hostname, |
70
|
|
|
$enable_tls = true, |
71
|
|
|
$debug = false, |
72
|
|
|
$timeout = 0, |
73
|
|
|
$port = 389, |
74
|
|
|
$referrals = true |
75
|
|
|
) { |
76
|
|
|
// Debug |
77
|
|
|
Logger::debug('Library - LDAP __construct(): Setup LDAP with '. |
78
|
|
|
'host=\''.$hostname. |
79
|
|
|
'\', tls='.var_export($enable_tls, true). |
80
|
|
|
', debug='.var_export($debug, true). |
81
|
|
|
', timeout='.var_export($timeout, true). |
82
|
|
|
', referrals='.var_export($referrals, true)); |
83
|
|
|
|
84
|
|
|
/* |
85
|
|
|
* Set debug level before calling connect. Note that this passes |
86
|
|
|
* NULL to ldap_set_option, which is an undocumented feature. |
87
|
|
|
* |
88
|
|
|
* OpenLDAP 2.x.x or Netscape Directory SDK x.x needed for this option. |
89
|
|
|
*/ |
90
|
|
|
if ($debug && !ldap_set_option(null, LDAP_OPT_DEBUG_LEVEL, 7)) { |
91
|
|
|
Logger::warning('Library - LDAP __construct(): Unable to set debug level (LDAP_OPT_DEBUG_LEVEL) to 7'); |
92
|
|
|
} |
93
|
|
|
|
94
|
|
|
/* |
95
|
|
|
* Prepare a connection for to this LDAP server. Note that this function |
96
|
|
|
* doesn't actually connect to the server. |
97
|
|
|
*/ |
98
|
|
|
$resource = @ldap_connect($hostname, $port); |
99
|
|
|
if ($resource === false) { |
100
|
|
|
throw $this->makeException( |
101
|
|
|
'Library - LDAP __construct(): Unable to connect to \''.$hostname.'\'', |
102
|
|
|
ERR_INTERNAL |
103
|
|
|
); |
104
|
|
|
} |
105
|
|
|
$this->ldap = $resource; |
106
|
|
|
|
107
|
|
|
// Enable LDAP protocol version 3 |
108
|
|
|
if (!@ldap_set_option($this->ldap, LDAP_OPT_PROTOCOL_VERSION, 3)) { |
109
|
|
|
throw $this->makeException( |
110
|
|
|
'Library - LDAP __construct(): Failed to set LDAP Protocol version (LDAP_OPT_PROTOCOL_VERSION) to 3', |
111
|
|
|
ERR_INTERNAL |
112
|
|
|
); |
113
|
|
|
} |
114
|
|
|
|
115
|
|
|
// Set referral option |
116
|
|
|
if (!@ldap_set_option($this->ldap, LDAP_OPT_REFERRALS, $referrals)) { |
117
|
|
|
throw $this->makeException( |
118
|
|
|
'Library - LDAP __construct(): Failed to set LDAP Referrals (LDAP_OPT_REFERRALS) to '.$referrals, |
119
|
|
|
ERR_INTERNAL |
120
|
|
|
); |
121
|
|
|
} |
122
|
|
|
|
123
|
|
|
// Set timeouts, if supported |
124
|
|
|
// (OpenLDAP 2.x.x or Netscape Directory SDK x.x needed) |
125
|
|
|
$this->timeout = $timeout; |
126
|
|
|
if ($timeout > 0) { |
127
|
|
|
if (!@ldap_set_option($this->ldap, LDAP_OPT_NETWORK_TIMEOUT, $timeout)) { |
128
|
|
|
Logger::warning( |
129
|
|
|
'Library - LDAP __construct(): Unable to set timeouts (LDAP_OPT_NETWORK_TIMEOUT) to '.$timeout |
130
|
|
|
); |
131
|
|
|
} |
132
|
|
|
if (!@ldap_set_option($this->ldap, LDAP_OPT_TIMELIMIT, $timeout)) { |
133
|
|
|
Logger::warning( |
134
|
|
|
'Library - LDAP __construct(): Unable to set timeouts (LDAP_OPT_TIMELIMIT) to '.$timeout |
135
|
|
|
); |
136
|
|
|
} |
137
|
|
|
} |
138
|
|
|
|
139
|
|
|
// Enable TLS, if needed |
140
|
|
|
if (stripos($hostname, "ldaps:") === false && $enable_tls) { |
141
|
|
|
if (!@ldap_start_tls($this->ldap)) { |
142
|
|
|
throw $this->makeException('Library - LDAP __construct():'. |
143
|
|
|
' Unable to force TLS', ERR_INTERNAL); |
144
|
|
|
} |
145
|
|
|
} |
146
|
|
|
} |
147
|
|
|
|
148
|
|
|
|
149
|
|
|
/** |
150
|
|
|
* Convenience method to create an LDAPException as well as log the |
151
|
|
|
* description. |
152
|
|
|
* |
153
|
|
|
* @param string $description The exception's description |
154
|
|
|
* @param int|null $type The exception's type |
155
|
|
|
* @return \Exception |
156
|
|
|
*/ |
157
|
|
|
private function makeException($description, $type = null) |
158
|
|
|
{ |
159
|
|
|
$errNo = @ldap_errno($this->ldap); |
160
|
|
|
|
161
|
|
|
// Decide exception type and return |
162
|
|
|
if ($type !== null) { |
163
|
|
|
if ($errNo !== 0) { |
164
|
|
|
// Only log real LDAP errors; not success |
165
|
|
|
Logger::error($description.'; cause: \''.ldap_error($this->ldap).'\' (0x'.dechex($errNo).')'); |
166
|
|
|
} else { |
167
|
|
|
Logger::error($description); |
168
|
|
|
} |
169
|
|
|
|
170
|
|
|
switch ($type) { |
171
|
|
|
case ERR_INTERNAL:// 1 - ExInternal |
172
|
|
|
return new Error\Exception($description, $errNo); |
173
|
|
|
case ERR_NO_USER:// 2 - ExUserNotFound |
174
|
|
|
return new Error\UserNotFound($description, $errNo); |
175
|
|
|
case ERR_WRONG_PW:// 3 - ExInvalidCredential |
176
|
|
|
return new Error\InvalidCredential($description, $errNo); |
177
|
|
|
case ERR_AS_DATA_INCONSIST:// 4 - ExAsDataInconsist |
178
|
|
|
return new Error\AuthSource('ldap', $description); |
179
|
|
|
case ERR_AS_INTERNAL:// 5 - ExAsInternal |
180
|
|
|
return new Error\AuthSource('ldap', $description); |
181
|
|
|
} |
182
|
|
|
} else { |
183
|
|
|
if ($errNo !== 0) { |
184
|
|
|
$description .= '; cause: \''.ldap_error($this->ldap).'\' (0x'.dechex($errNo).')'; |
185
|
|
|
if (@ldap_get_option($this->ldap, LDAP_OPT_DIAGNOSTIC_MESSAGE, $extendedError) |
186
|
|
|
&& !empty($extendedError) |
187
|
|
|
) { |
188
|
|
|
$description .= '; additional: \''.$extendedError.'\''; |
189
|
|
|
} |
190
|
|
|
} |
191
|
|
|
switch ($errNo) { |
192
|
|
|
case 0x20://LDAP_NO_SUCH_OBJECT |
193
|
|
|
Logger::warning($description); |
194
|
|
|
return new Error\UserNotFound($description, $errNo); |
195
|
|
|
case 0x31://LDAP_INVALID_CREDENTIALS |
196
|
|
|
Logger::info($description); |
197
|
|
|
return new Error\InvalidCredential($description, $errNo); |
198
|
|
|
case -1://NO_SERVER_CONNECTION |
199
|
|
|
Logger::error($description); |
200
|
|
|
return new Error\AuthSource('ldap', $description); |
201
|
|
|
default: |
202
|
|
|
Logger::error($description); |
203
|
|
|
return new Error\AuthSource('ldap', $description); |
204
|
|
|
} |
205
|
|
|
} |
206
|
|
|
return new \Exception('Unknown LDAP error.'); |
207
|
|
|
} |
208
|
|
|
|
209
|
|
|
|
210
|
|
|
/** |
211
|
|
|
* Search for DN from a single base. |
212
|
|
|
* |
213
|
|
|
* @param string $base |
214
|
|
|
* Indication of root of subtree to search |
215
|
|
|
* @param string|array $attribute |
216
|
|
|
* The attribute name(s) to search for. |
217
|
|
|
* @param string $value |
218
|
|
|
* The attribute value to search for. |
219
|
|
|
* Additional search filter |
220
|
|
|
* @param string|null $searchFilter |
221
|
|
|
* The scope of the search |
222
|
|
|
* @param string $scope |
223
|
|
|
* @return string |
224
|
|
|
* The DN of the resulting found element. |
225
|
|
|
* @throws Error\Exception if: |
226
|
|
|
* - Attribute parameter is wrong type |
227
|
|
|
* @throws Error\AuthSource if: |
228
|
|
|
* - Not able to connect to LDAP server |
229
|
|
|
* - False search result |
230
|
|
|
* - Count return false |
231
|
|
|
* - Searche found more than one result |
232
|
|
|
* - Failed to get first entry from result |
233
|
|
|
* - Failed to get DN for entry |
234
|
|
|
* @throws Error\UserNotFound if: |
235
|
|
|
* - Zero entries were found |
236
|
|
|
* @psalm-suppress TypeDoesNotContainType |
237
|
|
|
*/ |
238
|
|
|
private function search($base, $attribute, $value, $searchFilter = null, $scope = "subtree") |
239
|
|
|
{ |
240
|
|
|
// Create the search filter |
241
|
|
|
/** @var array $attribute */ |
242
|
|
|
$attribute = self::escape_filter_value($attribute, false); |
243
|
|
|
|
244
|
|
|
/** @var string $value */ |
245
|
|
|
$value = self::escape_filter_value($value, true); |
246
|
|
|
|
247
|
|
|
$filter = ''; |
248
|
|
|
foreach ($attribute as $attr) { |
249
|
|
|
$filter .= '('.$attr.'='.$value.')'; |
250
|
|
|
} |
251
|
|
|
$filter = '(|'.$filter.')'; |
252
|
|
|
|
253
|
|
|
// Append LDAP filters if defined |
254
|
|
|
if ($searchFilter !== null) { |
255
|
|
|
$filter = "(&".$filter."".$searchFilter.")"; |
256
|
|
|
} |
257
|
|
|
|
258
|
|
|
// Search using generated filter |
259
|
|
|
Logger::debug('Library - LDAP search(): Searching base ('.$scope.') \''.$base.'\' for \''.$filter.'\''); |
260
|
|
|
if ($scope === 'base') { |
261
|
|
|
$result = @ldap_read($this->ldap, $base, $filter, [], 0, 0, $this->timeout, LDAP_DEREF_NEVER); |
262
|
|
|
} elseif ($scope === 'onelevel') { |
263
|
|
|
$result = @ldap_list($this->ldap, $base, $filter, [], 0, 0, $this->timeout, LDAP_DEREF_NEVER); |
264
|
|
|
} else { |
265
|
|
|
$result = @ldap_search($this->ldap, $base, $filter, [], 0, 0, $this->timeout, LDAP_DEREF_NEVER); |
266
|
|
|
} |
267
|
|
|
|
268
|
|
|
if ($result === false) { |
269
|
|
|
throw $this->makeException( |
270
|
|
|
'Library - LDAP search(): Failed search on base \''.$base.'\' for \''.$filter.'\'' |
271
|
|
|
); |
272
|
|
|
} |
273
|
|
|
|
274
|
|
|
// Sanity checks on search results |
275
|
|
|
$count = @ldap_count_entries($this->ldap, $result); |
276
|
|
|
if ($count === false) { |
277
|
|
|
throw $this->makeException('Library - LDAP search(): Failed to get number of entries returned'); |
278
|
|
|
} elseif ($count > 1) { |
279
|
|
|
// More than one entry is found. External error |
280
|
|
|
throw $this->makeException( |
281
|
|
|
'Library - LDAP search(): Found '.$count.' entries searching base \''.$base.'\' for \''.$filter.'\'', |
282
|
|
|
ERR_AS_DATA_INCONSIST |
283
|
|
|
); |
284
|
|
|
} elseif ($count === 0) { |
285
|
|
|
// No entry is fond => wrong username is given (or not registered in the catalogue). User error |
286
|
|
|
throw $this->makeException( |
287
|
|
|
'Library - LDAP search(): Found no entries searching base \''.$base.'\' for \''.$filter.'\'', |
288
|
|
|
ERR_NO_USER |
289
|
|
|
); |
290
|
|
|
} |
291
|
|
|
|
292
|
|
|
|
293
|
|
|
// Resolve the DN from the search result |
294
|
|
|
$entry = @ldap_first_entry($this->ldap, $result); |
295
|
|
|
if ($entry === false) { |
296
|
|
|
throw $this->makeException( |
297
|
|
|
'Library - LDAP search(): Unable to retrieve result after searching base \''. |
298
|
|
|
$base.'\' for \''.$filter.'\'' |
299
|
|
|
); |
300
|
|
|
} |
301
|
|
|
$dn = @ldap_get_dn($this->ldap, $entry); |
302
|
|
|
if ($dn === false) { |
303
|
|
|
throw $this->makeException( |
304
|
|
|
'Library - LDAP search(): Unable to get DN after searching base \''.$base.'\' for \''.$filter.'\'' |
305
|
|
|
); |
306
|
|
|
} |
307
|
|
|
return $dn; |
308
|
|
|
} |
309
|
|
|
|
310
|
|
|
|
311
|
|
|
/** |
312
|
|
|
* Search for a DN. |
313
|
|
|
* |
314
|
|
|
* @param string|array $base |
315
|
|
|
* The base, or bases, which to search from. |
316
|
|
|
* @param string|array $attribute |
317
|
|
|
* The attribute name(s) searched for. |
318
|
|
|
* @param string $value |
319
|
|
|
* The attribute value searched for. |
320
|
|
|
* @param bool $allowZeroHits |
321
|
|
|
* Determines if the method will throw an exception if no hits are found. |
322
|
|
|
* Defaults to FALSE. |
323
|
|
|
* @param string|null $searchFilter |
324
|
|
|
* Additional searchFilter to be added to the (attribute=value) filter |
325
|
|
|
* @param string $scope |
326
|
|
|
* The scope of the search |
327
|
|
|
* @return string|null |
328
|
|
|
* The DN of the matching element, if found. If no element was found and |
329
|
|
|
* $allowZeroHits is set to FALSE, an exception will be thrown; otherwise |
330
|
|
|
* NULL will be returned. |
331
|
|
|
* @throws Error\AuthSource if: |
332
|
|
|
* - LDAP search encounter some problems when searching cataloge |
333
|
|
|
* - Not able to connect to LDAP server |
334
|
|
|
* @throws Error\UserNotFound if: |
335
|
|
|
* - $allowZeroHits is FALSE and no result is found |
336
|
|
|
* |
337
|
|
|
*/ |
338
|
|
|
public function searchfordn( |
339
|
|
|
$base, |
340
|
|
|
$attribute, |
341
|
|
|
$value, |
342
|
|
|
$allowZeroHits = false, |
343
|
|
|
$searchFilter = null, |
344
|
|
|
$scope = 'subtree' |
345
|
|
|
) { |
346
|
|
|
// Traverse all search bases, returning DN if found |
347
|
|
|
$bases = \SimpleSAML\Utils\Arrays::arrayize($base); |
348
|
|
|
foreach ($bases as $current) { |
349
|
|
|
try { |
350
|
|
|
// Single base search |
351
|
|
|
$result = $this->search($current, $attribute, $value, $searchFilter, $scope); |
352
|
|
|
|
353
|
|
|
// We don't hawe to look any futher if user is found |
354
|
|
|
if (!empty($result)) { |
355
|
|
|
return $result; |
356
|
|
|
} |
357
|
|
|
// If search failed, attempt the other base DNs |
358
|
|
|
} catch (Error\UserNotFound $e) { |
359
|
|
|
// Just continue searching |
360
|
|
|
} |
361
|
|
|
} |
362
|
|
|
// Decide what to do for zero entries |
363
|
|
|
Logger::debug('Library - LDAP searchfordn(): No entries found'); |
364
|
|
|
if ($allowZeroHits) { |
365
|
|
|
// Zero hits allowed |
366
|
|
|
return null; |
367
|
|
|
} else { |
368
|
|
|
// Zero hits not allowed |
369
|
|
|
throw $this->makeException('Library - LDAP searchfordn(): LDAP search returned zero entries for'. |
370
|
|
|
' filter \'('.join(' | ', \SimpleSAML\Utils\Arrays::arrayize($attribute)).' = '.$value.')\' on base(s) \'('.join(' & ', $bases).')\'', 2); |
371
|
|
|
} |
372
|
|
|
} |
373
|
|
|
|
374
|
|
|
|
375
|
|
|
/** |
376
|
|
|
* This method was created specifically for the ldap:AttributeAddUsersGroups->searchActiveDirectory() |
377
|
|
|
* method, but could be used for other LDAP search needs. It will search LDAP and return all the entries. |
378
|
|
|
* |
379
|
|
|
* @throws \Exception |
380
|
|
|
* @param string|array $bases |
381
|
|
|
* @param string|array $filters Array of 'attribute' => 'values' to be combined into the filter, |
382
|
|
|
* or a raw filter string |
383
|
|
|
* @param string|array $attributes Array of attributes requested from LDAP |
384
|
|
|
* @param bool $and If multiple filters defined, then either bind them with & or | |
385
|
|
|
* @param bool $escape Weather to escape the filter values or not |
386
|
|
|
* @param string $scope The scope of the search |
387
|
|
|
* @return array |
388
|
|
|
*/ |
389
|
|
|
public function searchformultiple( |
390
|
|
|
$bases, |
391
|
|
|
$filters, |
392
|
|
|
$attributes = [], |
393
|
|
|
$and = true, |
394
|
|
|
$escape = true, |
395
|
|
|
$scope = 'subtree' |
396
|
|
|
) { |
397
|
|
|
// Escape the filter values, if requested |
398
|
|
|
if ($escape) { |
399
|
|
|
$filters = $this->escape_filter_value($filters, false); |
400
|
|
|
} |
401
|
|
|
|
402
|
|
|
// Build search filter |
403
|
|
|
$filter = ''; |
404
|
|
|
if (is_array($filters)) { |
405
|
|
|
foreach ($filters as $attribute => $value) { |
406
|
|
|
$filter .= "($attribute=$value)"; |
407
|
|
|
} |
408
|
|
|
if (count($filters) > 1) { |
409
|
|
|
$filter = ($and ? '(&' : '(|').$filter.')'; |
410
|
|
|
} |
411
|
|
|
} else { |
412
|
|
|
Assert::string($filters); |
413
|
|
|
$filter = $filters; |
414
|
|
|
} |
415
|
|
|
|
416
|
|
|
// Verify filter was created |
417
|
|
|
if ($filter == '' || $filter == '(=)') { |
418
|
|
|
throw $this->makeException('ldap:LdapConnection->search_manual : No search filters defined', ERR_INTERNAL); |
419
|
|
|
} |
420
|
|
|
|
421
|
|
|
// Verify at least one base was passed |
422
|
|
|
$bases = (array) $bases; |
423
|
|
|
if (empty($bases)) { |
424
|
|
|
throw $this->makeException('ldap:LdapConnection->search_manual : No base DNs were passed', ERR_INTERNAL); |
425
|
|
|
} |
426
|
|
|
|
427
|
|
|
$attributes = \SimpleSAML\Utils\Arrays::arrayize($attributes); |
428
|
|
|
|
429
|
|
|
// Search each base until result is found |
430
|
|
|
$result = false; |
431
|
|
|
foreach ($bases as $base) { |
432
|
|
|
if ($scope === 'base') { |
433
|
|
|
$result = @ldap_read($this->ldap, $base, $filter, $attributes, 0, 0, $this->timeout); |
434
|
|
|
} elseif ($scope === 'onelevel') { |
435
|
|
|
$result = @ldap_list($this->ldap, $base, $filter, $attributes, 0, 0, $this->timeout); |
436
|
|
|
} else { |
437
|
|
|
$result = @ldap_search($this->ldap, $base, $filter, $attributes, 0, 0, $this->timeout); |
438
|
|
|
} |
439
|
|
|
|
440
|
|
|
if ($result !== false && @ldap_count_entries($this->ldap, $result) > 0) { |
441
|
|
|
break; |
442
|
|
|
} |
443
|
|
|
} |
444
|
|
|
|
445
|
|
|
// Verify that a result was found in one of the bases |
446
|
|
|
if ($result === false) { |
447
|
|
|
throw $this->makeException( |
448
|
|
|
'ldap:LdapConnection->search_manual : Failed to search LDAP using base(s) ['. |
449
|
|
|
implode('; ', $bases).'] with filter ['.$filter.']. LDAP error ['. |
450
|
|
|
ldap_error($this->ldap).']' |
451
|
|
|
); |
452
|
|
|
} elseif (@ldap_count_entries($this->ldap, $result) < 1) { |
453
|
|
|
throw $this->makeException( |
454
|
|
|
'ldap:LdapConnection->search_manual : No entries found in LDAP using base(s) ['. |
455
|
|
|
implode('; ', $bases).'] with filter ['.$filter.']', |
456
|
|
|
ERR_NO_USER |
457
|
|
|
); |
458
|
|
|
} |
459
|
|
|
|
460
|
|
|
// Get all results |
461
|
|
|
$results = ldap_get_entries($this->ldap, $result); |
462
|
|
|
if ($results === false) { |
463
|
|
|
throw $this->makeException( |
464
|
|
|
'ldap:LdapConnection->search_manual : Unable to retrieve entries from search results' |
465
|
|
|
); |
466
|
|
|
} |
467
|
|
|
|
468
|
|
|
// parse each entry and process its attributes |
469
|
|
|
for ($i = 0; $i < $results['count']; $i++) { |
470
|
|
|
$entry = $results[$i]; |
471
|
|
|
|
472
|
|
|
// iterate over the attributes of the entry |
473
|
|
|
for ($j = 0; $j < $entry['count']; $j++) { |
474
|
|
|
$name = $entry[$j]; |
475
|
|
|
$attribute = $entry[$name]; |
476
|
|
|
|
477
|
|
|
// decide whether to base64 encode or not |
478
|
|
|
for ($k = 0; $k < $attribute['count']; $k++) { |
479
|
|
|
// base64 encode binary attributes |
480
|
|
|
if (strtolower($name) === 'jpegphoto' |
481
|
|
|
|| strtolower($name) === 'objectguid' |
482
|
|
|
|| strtolower($name) === 'objectsid' |
483
|
|
|
|| strtolower($name) === 'ms-ds-consistencyguid' |
484
|
|
|
) { |
485
|
|
|
$results[$i][$name][$k] = base64_encode($attribute[$k]); |
486
|
|
|
} |
487
|
|
|
} |
488
|
|
|
} |
489
|
|
|
} |
490
|
|
|
|
491
|
|
|
// Remove the count and return |
492
|
|
|
unset($results['count']); |
493
|
|
|
return $results; |
494
|
|
|
} |
495
|
|
|
|
496
|
|
|
|
497
|
|
|
/** |
498
|
|
|
* Bind to LDAP with a specific DN and password. Simple wrapper around |
499
|
|
|
* ldap_bind() with some additional logging. |
500
|
|
|
* |
501
|
|
|
* @param string $dn |
502
|
|
|
* The DN used. |
503
|
|
|
* @param string $password |
504
|
|
|
* The password used. |
505
|
|
|
* @param array $sasl_args |
506
|
|
|
* Array of SASL options for SASL bind |
507
|
|
|
* @return bool |
508
|
|
|
* Returns TRUE if successful, FALSE if |
509
|
|
|
* LDAP_INVALID_CREDENTIALS, LDAP_X_PROXY_AUTHZ_FAILURE, |
510
|
|
|
* LDAP_INAPPROPRIATE_AUTH, LDAP_INSUFFICIENT_ACCESS |
511
|
|
|
* @throws Error\Exception on other errors |
512
|
|
|
*/ |
513
|
|
|
public function bind($dn, $password, array $sasl_args = null) |
514
|
|
|
{ |
515
|
|
|
if ($sasl_args != null) { |
516
|
|
|
if (!function_exists('ldap_sasl_bind')) { |
517
|
|
|
$ex_msg = 'Library - missing SASL support'; |
518
|
|
|
throw $this->makeException($ex_msg); |
519
|
|
|
} |
520
|
|
|
|
521
|
|
|
// SASL Bind, with error handling |
522
|
|
|
$authz_id = $sasl_args['authz_id']; |
523
|
|
|
$error = @ldap_sasl_bind( |
524
|
|
|
$this->ldap, |
525
|
|
|
$dn, |
526
|
|
|
$password, |
527
|
|
|
$sasl_args['mech'], |
528
|
|
|
$sasl_args['realm'], |
529
|
|
|
$sasl_args['authc_id'], |
530
|
|
|
$sasl_args['authz_id'], |
531
|
|
|
$sasl_args['props'] |
532
|
|
|
); |
533
|
|
|
} else { |
534
|
|
|
// Simple Bind, with error handling |
535
|
|
|
$authz_id = $dn; |
536
|
|
|
$error = @ldap_bind($this->ldap, $dn, $password); |
537
|
|
|
} |
538
|
|
|
|
539
|
|
|
if ($error === true) { |
540
|
|
|
// Good |
541
|
|
|
$this->authz_id = $authz_id; |
542
|
|
|
Logger::debug('Library - LDAP bind(): Bind successful with DN \''.$dn.'\''); |
543
|
|
|
return true; |
544
|
|
|
} |
545
|
|
|
|
546
|
|
|
/* Handle errors |
547
|
|
|
* LDAP_INVALID_CREDENTIALS |
548
|
|
|
* LDAP_INSUFFICIENT_ACCESS */ |
549
|
|
|
switch (ldap_errno($this->ldap)) { |
550
|
|
|
case 32: // LDAP_NO_SUCH_OBJECT |
551
|
|
|
// no break |
552
|
|
|
case 47: // LDAP_X_PROXY_AUTHZ_FAILURE |
553
|
|
|
// no break |
554
|
|
|
case 48: // LDAP_INAPPROPRIATE_AUTH |
555
|
|
|
// no break |
556
|
|
|
case 49: // LDAP_INVALID_CREDENTIALS |
557
|
|
|
// no break |
558
|
|
|
case 50: // LDAP_INSUFFICIENT_ACCESS |
559
|
|
|
return false; |
560
|
|
|
default: |
561
|
|
|
break; |
562
|
|
|
} |
563
|
|
|
|
564
|
|
|
// Bad |
565
|
|
|
throw $this->makeException('Library - LDAP bind(): Bind failed with DN \''.$dn.'\''); |
566
|
|
|
} |
567
|
|
|
|
568
|
|
|
|
569
|
|
|
/** |
570
|
|
|
* Applies an LDAP option to the current connection. |
571
|
|
|
* |
572
|
|
|
* @throws Exception |
573
|
|
|
* @param mixed $option |
574
|
|
|
* @param mixed $value |
575
|
|
|
* @return void |
576
|
|
|
*/ |
577
|
|
|
public function setOption($option, $value) |
578
|
|
|
{ |
579
|
|
|
// Attempt to set the LDAP option |
580
|
|
|
if (!@ldap_set_option($this->ldap, $option, $value)) { |
581
|
|
|
throw $this->makeException( |
582
|
|
|
'ldap:LdapConnection->setOption : Failed to set LDAP option ['. |
583
|
|
|
$option.'] with the value ['.$value.'] error: '.ldap_error($this->ldap), |
584
|
|
|
ERR_INTERNAL |
585
|
|
|
); |
586
|
|
|
} |
587
|
|
|
|
588
|
|
|
// Log debug message |
589
|
|
|
Logger::debug( |
590
|
|
|
'ldap:LdapConnection->setOption : Set the LDAP option ['. |
591
|
|
|
$option.'] with the value ['.$value.']' |
592
|
|
|
); |
593
|
|
|
} |
594
|
|
|
|
595
|
|
|
|
596
|
|
|
/** |
597
|
|
|
* Search a given DN for attributes, and return the resulting associative |
598
|
|
|
* array. |
599
|
|
|
* |
600
|
|
|
* @param string $dn |
601
|
|
|
* The DN of an element. |
602
|
|
|
* @param string|array $attributes |
603
|
|
|
* The names of the attribute(s) to retrieve. Defaults to NULL; that is, |
604
|
|
|
* all available attributes. Note that this is not very effective. |
605
|
|
|
* @param int $maxsize |
606
|
|
|
* The maximum size of any attribute's value(s). If exceeded, the attribute |
607
|
|
|
* will not be returned. |
608
|
|
|
* @return array |
609
|
|
|
* The array of attributes and their values. |
610
|
|
|
* @see http://no.php.net/manual/en/function.ldap-read.php |
611
|
|
|
*/ |
612
|
|
|
public function getAttributes($dn, $attributes = null, $maxsize = null) |
613
|
|
|
{ |
614
|
|
|
// Preparations, including a pretty debug message... |
615
|
|
|
$description = 'all attributes'; |
616
|
|
|
if (is_array($attributes)) { |
617
|
|
|
$description = '\''.join(',', $attributes).'\''; |
618
|
|
|
} else { |
619
|
|
|
// Get all attributes... |
620
|
|
|
// TODO: Verify that this originally was the intended behaviour. Could $attributes be a string? |
621
|
|
|
$attributes = []; |
622
|
|
|
} |
623
|
|
|
Logger::debug('Library - LDAP getAttributes(): Getting '.$description.' from DN \''.$dn.'\''); |
624
|
|
|
|
625
|
|
|
// Attempt to get attributes |
626
|
|
|
// TODO: Should aliases be dereferenced? |
627
|
|
|
/** @var array $attributes */ |
628
|
|
|
$result = @ldap_read($this->ldap, $dn, 'objectClass=*', $attributes, 0, 0, $this->timeout); |
629
|
|
|
if ($result === false) { |
630
|
|
|
throw $this->makeException('Library - LDAP getAttributes(): Failed to get attributes from DN \''.$dn.'\''); |
631
|
|
|
} |
632
|
|
|
$entry = @ldap_first_entry($this->ldap, $result); |
633
|
|
|
if ($entry === false) { |
634
|
|
|
throw $this->makeException('Library - LDAP getAttributes(): Could not get first entry from DN \''.$dn.'\''); |
635
|
|
|
} |
636
|
|
|
unset($attributes); |
637
|
|
|
|
638
|
|
|
/** @var array|false $attributes */ |
639
|
|
|
$attributes = @ldap_get_attributes($this->ldap, $entry); |
640
|
|
|
if ($attributes === false) { |
641
|
|
|
throw $this->makeException( |
642
|
|
|
'Library - LDAP getAttributes(): Could not get attributes of first entry from DN \''.$dn.'\'' |
643
|
|
|
); |
644
|
|
|
} |
645
|
|
|
|
646
|
|
|
// Parsing each found attribute into our result set |
647
|
|
|
$result = []; // Recycling $result... Possibly bad practice. |
648
|
|
|
for ($i = 0; $i < $attributes['count']; $i++) { |
649
|
|
|
// Ignore attributes that exceed the maximum allowed size |
650
|
|
|
$name = $attributes[$i]; |
651
|
|
|
$attribute = $attributes[$name]; |
652
|
|
|
|
653
|
|
|
// Deciding whether to base64 encode |
654
|
|
|
$values = []; |
655
|
|
|
for ($j = 0; $j < $attribute['count']; $j++) { |
656
|
|
|
$value = $attribute[$j]; |
657
|
|
|
|
658
|
|
|
if (!empty($maxsize) && strlen($value) > $maxsize) { |
659
|
|
|
// Ignoring and warning |
660
|
|
|
Logger::warning('Library - LDAP getAttributes(): Attribute \''. |
661
|
|
|
$name.'\' exceeded maximum allowed size by '.(strlen($value) - $maxsize)); |
662
|
|
|
continue; |
663
|
|
|
} |
664
|
|
|
|
665
|
|
|
// Base64 encode binary attributes |
666
|
|
|
if (strtolower($name) === 'jpegphoto' |
667
|
|
|
|| strtolower($name) === 'objectguid' |
668
|
|
|
|| strtolower($name) === 'objectsid' |
669
|
|
|
|| strtolower($name) === 'ms-ds-consistencyguid' |
670
|
|
|
) { |
671
|
|
|
$values[] = base64_encode($value); |
672
|
|
|
} else { |
673
|
|
|
$values[] = $value; |
674
|
|
|
} |
675
|
|
|
} |
676
|
|
|
|
677
|
|
|
// Adding |
678
|
|
|
$result[$name] = $values; |
679
|
|
|
} |
680
|
|
|
|
681
|
|
|
// We're done |
682
|
|
|
Logger::debug('Library - LDAP getAttributes(): Found attributes \'('.join(',', array_keys($result)).')\''); |
683
|
|
|
return $result; |
684
|
|
|
} |
685
|
|
|
|
686
|
|
|
|
687
|
|
|
/** |
688
|
|
|
* Enter description here... |
689
|
|
|
* |
690
|
|
|
* @param array $config |
691
|
|
|
* @param string $username |
692
|
|
|
* @param string $password |
693
|
|
|
* @return array|false |
694
|
|
|
*/ |
695
|
|
|
public function validate($config, $username, $password = null) |
696
|
|
|
{ |
697
|
|
|
/** |
698
|
|
|
* Escape any characters with a special meaning in LDAP. The following |
699
|
|
|
* characters have a special meaning (according to RFC 2253): |
700
|
|
|
* ',', '+', '"', '\', '<', '>', ';', '*' |
701
|
|
|
* These characters are escaped by prefixing them with '\'. |
702
|
|
|
*/ |
703
|
|
|
$username = addcslashes($username, ',+"\\<>;*'); |
704
|
|
|
|
705
|
|
|
if (isset($config['priv_user_dn'])) { |
706
|
|
|
$this->bind($config['priv_user_dn'], $config['priv_user_pw']); |
707
|
|
|
} |
708
|
|
|
if (isset($config['dnpattern'])) { |
709
|
|
|
$dn = str_replace('%username%', $username, $config['dnpattern']); |
710
|
|
|
} else { |
711
|
|
|
/** @var string $dn */ |
712
|
|
|
$dn = $this->searchfordn($config['searchbase'], $config['searchattributes'], $username, false); |
713
|
|
|
} |
714
|
|
|
|
715
|
|
|
if ($password !== null) { |
716
|
|
|
// checking users credentials ... assuming below that she may read her own attributes ... |
717
|
|
|
// escape characters with a special meaning, also in the password |
718
|
|
|
$password = addcslashes($password, ',+"\\<>;*'); |
719
|
|
|
if (!$this->bind($dn, $password)) { |
720
|
|
|
Logger::info( |
721
|
|
|
'Library - LDAP validate(): Failed to authenticate \''.$username.'\' using DN \''.$dn.'\'' |
722
|
|
|
); |
723
|
|
|
return false; |
724
|
|
|
} |
725
|
|
|
} |
726
|
|
|
|
727
|
|
|
/** |
728
|
|
|
* Retrieve attributes from LDAP |
729
|
|
|
*/ |
730
|
|
|
$attributes = $this->getAttributes($dn, $config['attributes']); |
731
|
|
|
return $attributes; |
732
|
|
|
} |
733
|
|
|
|
734
|
|
|
|
735
|
|
|
/** |
736
|
|
|
* Borrowed function from PEAR:LDAP. |
737
|
|
|
* |
738
|
|
|
* Escapes the given VALUES according to RFC 2254 so that they can be safely used in LDAP filters. |
739
|
|
|
* |
740
|
|
|
* Any control characters with an ACII code < 32 as well as the characters with special meaning in |
741
|
|
|
* LDAP filters "*", "(", ")", and "\" (the backslash) are converted into the representation of a |
742
|
|
|
* backslash followed by two hex digits representing the hexadecimal value of the character. |
743
|
|
|
* |
744
|
|
|
* @static |
745
|
|
|
* @param string|array $values Array of values to escape |
746
|
|
|
* @param bool $singleValue |
747
|
|
|
* @return string|array Array $values, but escaped |
748
|
|
|
*/ |
749
|
|
|
public static function escape_filter_value($values = [], $singleValue = true) |
750
|
|
|
{ |
751
|
|
|
// Parameter validation |
752
|
|
|
$values = \SimpleSAML\Utils\Arrays::arrayize($values); |
753
|
|
|
|
754
|
|
|
foreach ($values as $key => $val) { |
755
|
|
|
if ($val === null) { |
756
|
|
|
$val = '\0'; // apply escaped "null" if string is empty |
757
|
|
|
} else { |
758
|
|
|
// Escaping of filter meta characters |
759
|
|
|
$val = str_replace('\\', '\5c', $val); |
760
|
|
|
$val = str_replace('*', '\2a', $val); |
761
|
|
|
$val = str_replace('(', '\28', $val); |
762
|
|
|
$val = str_replace(')', '\29', $val); |
763
|
|
|
|
764
|
|
|
// ASCII < 32 escaping |
765
|
|
|
$val = self::asc2hex32($val); |
766
|
|
|
} |
767
|
|
|
|
768
|
|
|
$values[$key] = $val; |
769
|
|
|
} |
770
|
|
|
if ($singleValue) { |
771
|
|
|
return $values[0]; |
772
|
|
|
} |
773
|
|
|
return $values; |
774
|
|
|
} |
775
|
|
|
|
776
|
|
|
|
777
|
|
|
/** |
778
|
|
|
* Borrowed function from PEAR:LDAP. |
779
|
|
|
* |
780
|
|
|
* Converts all ASCII chars < 32 to "\HEX" |
781
|
|
|
* |
782
|
|
|
* @param string $string String to convert |
783
|
|
|
* |
784
|
|
|
* @static |
785
|
|
|
* @return string |
786
|
|
|
*/ |
787
|
|
|
public static function asc2hex32($string) |
788
|
|
|
{ |
789
|
|
|
for ($i = 0; $i < strlen($string); $i++) { |
790
|
|
|
$char = substr($string, $i, 1); |
791
|
|
|
if (ord($char) < 32) { |
792
|
|
|
$hex = dechex(ord($char)); |
793
|
|
|
if (strlen($hex) == 1) { |
794
|
|
|
$hex = '0'.$hex; |
795
|
|
|
} |
796
|
|
|
$string = str_replace($char, '\\'.$hex, $string); |
797
|
|
|
} |
798
|
|
|
} |
799
|
|
|
return $string; |
800
|
|
|
} |
801
|
|
|
|
802
|
|
|
|
803
|
|
|
/** |
804
|
|
|
* Convert SASL authz_id into a DN |
805
|
|
|
* |
806
|
|
|
* @param string $searchBase |
807
|
|
|
* @param array $searchAttributes |
808
|
|
|
* @param string $authz_id |
809
|
|
|
* @return string|null |
810
|
|
|
*/ |
811
|
|
|
private function authzidToDn($searchBase, $searchAttributes, $authz_id) |
812
|
|
|
{ |
813
|
|
|
if (preg_match("/^dn:/", $authz_id)) { |
814
|
|
|
return preg_replace("/^dn:/", "", $authz_id); |
815
|
|
|
} |
816
|
|
|
|
817
|
|
|
if (preg_match("/^u:/", $authz_id)) { |
818
|
|
|
return $this->searchfordn( |
819
|
|
|
$searchBase, |
820
|
|
|
$searchAttributes, |
821
|
|
|
preg_replace("/^u:/", "", $authz_id) |
822
|
|
|
); |
823
|
|
|
} |
824
|
|
|
return $authz_id; |
825
|
|
|
} |
826
|
|
|
|
827
|
|
|
|
828
|
|
|
/** |
829
|
|
|
* ldap_exop_whoami accessor, if available. Use requested authz_id |
830
|
|
|
* otherwise. |
831
|
|
|
* |
832
|
|
|
* ldap_exop_whoami() has been provided as a third party patch that |
833
|
|
|
* waited several years to get its way upstream: |
834
|
|
|
* http://cvsweb.netbsd.org/bsdweb.cgi/pkgsrc/databases/php-ldap/files |
835
|
|
|
* |
836
|
|
|
* When it was integrated into PHP repository, the function prototype |
837
|
|
|
* was changed, The new prototype was used in third party patch for |
838
|
|
|
* PHP 7.0 and 7.1, hence the version test below. |
839
|
|
|
* |
840
|
|
|
* @param string $searchBase |
841
|
|
|
* @param array $searchAttributes |
842
|
|
|
* @throws \Exception |
843
|
|
|
* @return string |
844
|
|
|
*/ |
845
|
|
|
public function whoami($searchBase, $searchAttributes) |
846
|
|
|
{ |
847
|
|
|
$authz_id = ''; |
848
|
|
|
if (function_exists('ldap_exop_whoami')) { |
849
|
|
|
if (version_compare(phpversion(), '7', '<')) { |
850
|
|
|
/** @psalm-suppress TooManyArguments */ |
851
|
|
|
if (ldap_exop_whoami($this->ldap, $authz_id) === false) { |
852
|
|
|
throw $this->makeException('LDAP whoami exop failure'); |
853
|
|
|
} |
854
|
|
|
} else { |
855
|
|
|
/** @var string|false $authz_id */ |
856
|
|
|
$authz_id = ldap_exop_whoami($this->ldap); |
857
|
|
|
if ($authz_id === false) { |
858
|
|
|
throw $this->makeException('LDAP whoami exop failure'); |
859
|
|
|
} |
860
|
|
|
} |
861
|
|
|
} else { |
862
|
|
|
Assert::string($authz_id); |
863
|
|
|
/** @var string $authz_id */ |
864
|
|
|
$authz_id = $this->authz_id; |
865
|
|
|
} |
866
|
|
|
|
867
|
|
|
$dn = $this->authzidToDn($searchBase, $searchAttributes, $authz_id); |
868
|
|
|
|
869
|
|
|
if (empty($dn)) { |
870
|
|
|
throw $this->makeException('Cannot figure userID'); |
871
|
|
|
} |
872
|
|
|
|
873
|
|
|
return $dn; |
874
|
|
|
} |
875
|
|
|
|
876
|
|
|
|
877
|
|
|
/** |
878
|
|
|
* Set for a given DN attributes |
879
|
|
|
* |
880
|
|
|
* @param string $dn |
881
|
|
|
* The DN of an element. |
882
|
|
|
* @param array $attributes |
883
|
|
|
* The names and value of the attribute(s) to set using ldap_mod_replace structure; |
884
|
|
|
* @return bool |
885
|
|
|
* Result of operation |
886
|
|
|
*/ |
887
|
|
|
public function setAttributes($dn, array $attributes) { |
888
|
|
|
Logger::debug('Library - LDAP setAttributes(): Received arraydata:'.print_r($attributes, true)); |
889
|
|
|
|
890
|
|
|
// Attempt to set attributes |
891
|
|
|
$result = @ldap_mod_replace($this->ldap, $dn, $attributes); |
892
|
|
|
if ($result === false) { |
893
|
|
|
throw $this->makeException('Library - LDAP setAttributes(): Failed to set attributes for DN \''.$dn.'\'. Bind necessary?'); |
894
|
|
|
} |
895
|
|
|
|
896
|
|
|
return $result; |
897
|
|
|
} |
898
|
|
|
|
899
|
|
|
|
900
|
|
|
/** |
901
|
|
|
* Adds for a given DN attributes |
902
|
|
|
* |
903
|
|
|
* @param string $dn |
904
|
|
|
* The DN of an element. |
905
|
|
|
* @param array $attributes |
906
|
|
|
* The names and value of the attribute(s) to set using ldap_mod_add structure; |
907
|
|
|
* @return bool |
908
|
|
|
* Result of operation |
909
|
|
|
*/ |
910
|
|
|
public function addAttributes($dn, array $attributes) { |
911
|
|
|
Logger::debug('Library - LDAP addAttributes(): Received arraydata:'.print_r($attributes, true)); |
912
|
|
|
|
913
|
|
|
// Attempt to add attributes |
914
|
|
|
$result = @ldap_mod_add($this->ldap, $dn, $attributes); |
915
|
|
|
if ($result === false) { |
916
|
|
|
throw $this->makeException('Library - LDAP addAttributes(): Failed to set attributes for DN \''.$dn.'\'. Bind necessary?'); |
917
|
|
|
} |
918
|
|
|
|
919
|
|
|
return $result; |
920
|
|
|
} |
921
|
|
|
} |
922
|
|
|
|