Ldap.php 19.8 KB
Newer Older
1
<?php
2

3
4
namespace T3o\T3oLdap\Connectors;

5
6
7
8
9
10
11
12
13
14
15
/*
 * (c) 2016 by mehrwert intermediale kommunikation GmbH
 *
 * It is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License, either version 2
 * of the License, or any later version.
 *
 * For the full copyright and license information, please read the
 * LICENSE.txt file that was distributed with this source code.
 */

16
use TYPO3\CMS\Core\Utility\GeneralUtility;
17
use TYPO3\CMS\Saltedpasswords\Utility\SaltedPasswordsUtility;
18

19
20
21
22
23
24
25
26
27
/**
 * LDAP connector class to update accounts and passwords for LDAP user
 * identified by DN. Passwords may be a multivalue attribute hashed by
 * mechanisms defined in the PasswordHashing class. Currently CRYPT,
 * SHA1 and MD5 are used.
 *
 * @package Typo3\Ldap\Connectors
 * @since 1.0.0
 */
28
29
class Ldap
{
30
31
32

    /**
     * LDAP server as IP, hostname or complete URI
33
     *
34
35
36
37
38
39
     * @var string
     */
    private $ldapServer = '';

    /**
     * LDAP server port (0/389/636)
40
     *
41
42
43
44
45
46
     * @var int
     */
    private $ldapServerPort = 0;

    /**
     * LDAP version (defaults sto 3)
47
     *
48
49
50
51
52
53
     * @var int
     */
    private $ldapProtocolVersion = 3;

    /**
     * LDAP admin DN to bind for directory updates
54
     *
55
56
57
58
59
60
     * @var string
     */
    private $ldapBindDn = '';

    /**
     * Bind password for administrative LDAP bind
61
     *
62
63
64
65
66
67
     * @var string
     */
    private $ldapBindPassword = '';

    /**
     * LDAP connection resource
68
     *
69
70
71
72
73
74
75
     * @var null
     */
    private $ldapConnection = null;

    /**
     * LDAP base DN used to find users in LDAP. May be overridden in
     * extension manager configuration
76
     *
77
78
79
80
81
82
     * @var string
     */
    private $ldapBaseDnForPasswordChanges = 'ou=people,dc=typo3,dc=org';

    /**
     * TYPO3 extension configuration array
83
     *
84
85
     * @var array
     */
86
    private $extensionConfiguration = [];
87

88
89
    /**
     * Last LDAP error in this class
90
     *
91
92
93
94
     * @var string
     */
    private $lastLdapError = '';

95
96
97
98
99
100
101
102
    /**
     * LDAP constructor.
     */
    public function __construct()
    {
        // Disable certificate checks on LDAP TLS
        putenv('LDAPTLS_REQCERT=never');

103
        // TODO Move to TypoScript configuration object if more than one LDAP server is required per installation
104
105
106
107
108
109
110
        $this->extensionConfiguration = unserialize($GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf']['t3o_ldap']);
        $this->ldapServer = trim($this->extensionConfiguration['ldapServer']);
        $this->ldapServerPort = intval($this->extensionConfiguration['ldapServerPort']);
        $this->ldapProtocolVersion = intval($this->extensionConfiguration['ldapProtocolVersion']);
        $this->ldapBindDn = trim($this->extensionConfiguration['ldapBindDn']);
        $this->ldapBindPassword = $this->extensionConfiguration['ldapBindPassword'];
        $this->ldapBaseDnForPasswordChanges = trim($this->extensionConfiguration['ldapBaseDnForPasswordChanges']);
111
112
113

        // Connect and bind
        $this->createLdapConnection();
114
        $this->ldapBind($this->ldapConnection, $this->ldapBindDn, $this->ldapBindPassword);
115
116
117
118
119
120
121
122
123
124
    }

    /**
     * Test a LDAP bind using given dn and password. Returns true on success
     * and false on bind failure. Errors are logged to syslog.
     *
     * @param string $dn Complete bind DN
     * @param string $password Password to bind with
     * @return bool
     */
125
126
    public function testLdapPassword($dn, $password)
    {
127
128
129
130
131
132
        $ret = false;
        if ($this->createLdapConnection() === true) {
            if ($this->ldapBind($this->ldapConnection, $dn, $password) === true) {
                $ret = true;
            }
        } else {
133
134
            GeneralUtility::sysLog('Keine LDAP-Bind mit Nutzerdaten moeglich: ' . ldap_error($this->ldapConnection),
                't3o_ldap',
135
                GeneralUtility::SYSLOG_SEVERITY_ERROR);
136
        }
137

138
139
140
141
142
143
144
145
146
147
148
149
        return $ret;
    }

    /**
     * Create LDAP connection and bind with admin credentials. Update passwords for
     * a given user (by $username) for all available mechanisms. Errors are logged
     * to syslog.
     *
     * @param string $username Username for bind
     * @param array $values The password array
     * @return bool
     */
150
151
    public function setLdapPasswords($username, $values)
    {
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166

        $ret = false;

        // Create LDAP connection
        if ($this->createLdapConnection() === true) {
            // Try to bind as admin
            if ($this->ldapBind($this->ldapConnection, $this->ldapBindDn, $this->ldapBindPassword) === true) {

                $dn = $this->getDnForUserName($username);

                // TODO Check if user exists and create if not exists?

                // Finally try to update passwords
                $result = $this->updateLdapAttribute($dn, 'userPassword', $values, true);
                if ($result === false) {
167
168
                    GeneralUtility::sysLog(ldap_error($this->ldapConnection), 't3o_ldap',
                        GeneralUtility::SYSLOG_SEVERITY_ERROR);
169
170
                }
            } else {
171
172
                GeneralUtility::sysLog('Unable to bind to LDAP using: ' . ldap_error($this->ldapConnection), 't3o_ldap',
                    GeneralUtility::SYSLOG_SEVERITY_ERROR);
173
174
            }
        } else {
175
176
            GeneralUtility::sysLog('No active LDAP connection available', 't3o_ldap',
                GeneralUtility::SYSLOG_SEVERITY_ERROR);
177
        }
178

179
180
181
182
183
184
185
186
187
188
189
        return $ret;
    }

    /**
     * Bind to the LDAP directory with the given credentials. Errors are logged to syslog.
     *
     * @param resource $ldapConnection
     * @param String $dn Complete bind DN for LDAP entry to bind with
     * @param String $password The password to use for bind
     * @return bool
     */
190
191
    private function ldapBind($ldapConnection, $dn, $password)
    {
192
193
194
        $ret = false;
        try {
            // Bind to LDAP server
195
            $ldapBind = @ldap_bind($ldapConnection, $dn, $password);
196
197
198
199
            // Verify binding
            if ($ldapBind) {
                $ret = true;
            } else {
200
201
                throw new \RuntimeException('Could not bind to LDAP connection: ' . ldap_error($ldapConnection),
                    1453993540);
202
            }
203
204
        } catch (\RuntimeException $e) {
            GeneralUtility::sysLog($e->getMessage(), 't3o_ldap', GeneralUtility::SYSLOG_SEVERITY_ERROR);
205
        }
206

207
208
209
210
211
212
213
214
215
216
217
218
        return $ret;
    }

    /**
     * Update an attribute for the given DN. Errors are logged to syslog.
     *
     * @param String $dn Complete DN for LDAP entry to update attributes for
     * @param string $attribute The name of the attribute
     * @param string|array $attributeValues String or array (for multivalue attributes)
     * @param bool $multiValue Whether or not the attribute should be treated as single or multivalue
     * @return bool
     */
219
220
    private function updateLdapAttribute($dn, $attribute, $attributeValues, $multiValue = false)
    {
221
222
        $ret = false;
        if (trim($dn) !== '') {
223
224
            $attributes = [];
            if (is_array($attributeValues)) {
225
226
227
228
229
230
231
232
                foreach ($attributeValues AS $attributeValue) {
                    $attributes[$attribute][] = $attributeValue;
                }
            } else {
                $attributes[$attribute] = $attributeValues;
            }
            $ret = ldap_mod_replace($this->ldapConnection, trim($dn), $attributes);
        }
233

234
235
236
237
238
239
240
241
242
        return $ret;
    }

    /**
     * Create the LDAP connection and set in global scope on success. Return false on failure.
     * Errors are logged to syslog.
     *
     * @return bool
     */
243
244
    private function createLdapConnection()
    {
245
246
247
248
249
250
251
252
253
254
255
256
        $ret = false;
        $port = intval($this->ldapServerPort);
        try {
            $this->ldapConnection = @ldap_connect($this->ldapServer, ($port > 0 ? $port : null));
            if ($this->ldapConnection) {
                // Set protocol version
                if (ldap_set_option($this->ldapConnection, LDAP_OPT_PROTOCOL_VERSION, $this->ldapProtocolVersion)) {
                    if (ldap_set_option($this->ldapConnection, LDAP_OPT_REFERRALS, 0)) {
                        $ret = true;
                    }
                }
            } else {
257
258
                throw new \RuntimeException('Could not create LDAP connection: ' . ldap_error($this->ldapConnection),
                    1453993539);
259
            }
260
261
        } catch (\RuntimeException $e) {
            GeneralUtility::sysLog($e->getMessage(), 't3o_ldap', GeneralUtility::SYSLOG_SEVERITY_ERROR);
262
        }
263

264
265
266
267
268
269
270
271
272
273
        return $ret;
    }

    /**
     * Wrap with base DN to provide a valid DN to identify the user in the
     * directory service.
     *
     * @param string $username The username to wrap with the base DN
     * @return string
     */
274
275
    private function getDnForUserName($username)
    {
276
        $dn = 'uid=' . $username . ',' . $this->ldapBaseDnForPasswordChanges;
277

278
279
280
281
282
283
284
285
286
        return $dn;
    }

    /**
     * Check if a user exists in LDAP
     *
     * @param String $username The username
     * @return bool
     */
287
288
    public function userExists($username)
    {
289
290
291
292
293

        $ret = false;

        $dn = $this->getDnForUserName($username);
        $filter = '(|(objectClass=typo3Person))';
294
        $attributes = ['sn', 'email', 'ou'];
295
296
297
298
299
300
        $searchResult = @ldap_search($this->ldapConnection, $dn, $filter, $attributes);
        if ($searchResult) {
            $info = ldap_get_entries($this->ldapConnection, $searchResult);
            if (intval($info['count']) > 0) {
                $ret = true;
            }
301
302
303
304
305
306
307
308
        }

        return $ret;
    }

    /**
     * Update a user in LDAP
     *
309
     * @param \In2code\Femanager\Domain\Model\User $user The user data array
310
311
     * @return bool
     */
312
    public function updateUser(\In2code\Femanager\Domain\Model\User $user)
313
    {
314
315

        $ret = false;
316
        $dn = $this->getDnForUserName($user->getUsername());
317

318
        $ldapUserObject = $this->buildLdapUserArray($user);
319

320
        $res = ldap_modify($this->ldapConnection, $dn, $ldapUserObject);
321

322
        if ($res === true) {
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
            // TODO $this->updateFeUserLastLdapUpdateTimestamp($feUserUid);
            $ret = true;
        } else {
            $this->setLastLdapError(ldap_error($this->ldapConnection));
        }

        return $ret;
    }

    /**
     * Enable a user in LDAP
     *
     * @param string $username Username for DN
     * @return bool
     * @todo Requires refactoring for proper syntax of LDAP enabled/disabled
     */
339
340
    public function enableUser($username)
    {
341
342
343

        $ret = false;
        $dn = $this->getDnForUserName($username);
344
        $ldapUserObject = [
345
            'active' => true
346
347
348
349
        ];
        $res = ldap_modify($this->ldapConnection, $dn, $ldapUserObject);

        if ($res === true) {
350
351
            // TODO $this->updateFeUserLastLdapUpdateTimestamp($feUserUid);
            $ret = true;
352
353
        } else {
            $this->setLastLdapError(ldap_error($this->ldapConnection));
354
355
356
357
358
359
360
361
        }

        return $ret;
    }

    /**
     * Delete a user in LDAP
     *
362
     * @param string $username The username to delete in LDAP
363
364
     * @return bool
     */
365
366
    public function deleteUser($username)
    {
367
        $dn = $this->getDnForUserName($username);
368

369
370
371
372
373
374
        return ldap_delete($this->ldapConnection, $dn);
    }

    /**
     * Create a user in LDAP
     *
375
     * @param \In2code\Femanager\Domain\Model\User $user The user model
376
     * @param string $password Clear text user password
377
378
     * @return bool
     */
379
    public function createUser(\In2code\Femanager\Domain\Model\User $user, $password = '')
380
    {
381
382

        $ret = false;
383

384
        $dn = $this->getDnForUserName($user->getUsername());
385

386
        $ldapUserObject = $this->buildLdapUserArray($user);
387
        $res = ldap_add($this->ldapConnection, $dn, $ldapUserObject);
388

389
        if ($res === true) {
390
            $this->updateFeUserLastLdapUpdateTimestamp($user->getUid());
391
            $ret = true;
392
393
        } else {
            $this->setLastLdapError(ldap_error($this->ldapConnection));
394
395
396
397
398
399
400
401
        }

        return $ret;
    }

    /**
     * Build the array for LDAP insert or updates.
     *
402
     * @param \In2code\Femanager\Domain\Model\User $user
403
404
     * @return array
     */
405
    private function buildLdapUserArray(\In2code\Femanager\Domain\Model\User $user)
406
    {
407

408
        $ldapUserArray = [
409
            'objectclass' => [
410
411
412
413
                0 => 'top',
                1 => 'person',
                2 => 'typo3Person',
                3 => 'inetOrgPerson'
414
415
            ]
        ];
416
417
418
        $nameParts = GeneralUtility::trimExplode(' ', $user->getName());
        $lastName = array_pop($nameParts);
        $firstName = implode(' ', $nameParts);
419

420
421
        if (trim($user->getName()) !== '') {
            $ldapUserArray['cn'] = $user->getName();
422
        }
423
424
        if (trim($user->getName()) !== '') {
            $ldapUserArray['displayName'] = trim($user->getName());
425
        }
426
427
        if (trim($firstName) !== '') {
            $ldapUserArray['givenName'] = trim($firstName);
428
        }
429
430
        if (trim($lastName) !== '') {
            $ldapUserArray['sn'] = trim($lastName);
431
        }
432
433
        if (trim($user->getAddress()) !== '') {
            $ldapUserArray['street'] = trim($user->getAddress());
434
        }
435
436
        if (trim($user->getZip()) !== '') {
            $ldapUserArray['postalCode'] = trim($user->getZip());
437
        }
438
439
        if (trim($user->getCity()) !== '') {
            $ldapUserArray['l'] = trim($user->getCity());
440
        }
441
442
        if (trim($user->getCountry()) !== '') {
            $countryDetails = $this->getCountryDetailsByCountryName($user->getCountry());
443
444
            if ($countryDetails !== false) {
                if (trim($countryDetails['cn_iso_2']) !== '') {
445
                    $ldapUserArray['c'] = trim($countryDetails['cn_iso_2']);
446
447
                }
                if (trim($countryDetails['cn_short_en']) !== '') {
448
                    $ldapUserArray['co'] = trim($countryDetails['cn_short_en']);
449
450
451
452
                }
            }
        }

453
        $url = filter_var($user->getWww(), FILTER_VALIDATE_URL);
454
        if ($url !== false) {
455
            $ldapUserArray['labeledURI'] = $url;
456
        }
457
        $email = filter_var($user->getEmail(), FILTER_VALIDATE_EMAIL);
458
        if ($email !== false) {
459
            $ldapUserArray['mail'] = $email;
460
        }
461
462
        if (trim($user->getTelephone()) !== '') {
            $ldapUserArray['homePhone'] = trim($user->getTelephone());
463
        }
464
465
        if (trim($user->getFax()) !== '') {
            $ldapUserArray['facsimileTelephoneNumber'] = trim($user->getFax());
466
467
        }

468
        // update terms and conditions data
469
        if ($user->isTerms() === true) {
470
            $ldapUserArray['conditionsAccepted'] = 1;
471
        }
472
        if ($user->getTermsDateOfAcceptance() !== null && $user->getTermsDateOfAcceptance()->getTimestamp() > 0) {
473
            $ldapUserArray['conditionsDate'] = $user->getTermsDateOfAcceptance()->getTimestamp();
474
        }
475
476
477
478
479

        $objectManager = GeneralUtility::makeInstance(\TYPO3\CMS\Extbase\Object\ObjectManager::class);
        $myProfileRepository = $objectManager->get(\T3o\T3omy\Domain\Repository\MyProfileRepository::class);
        /** @var \T3o\T3omy\Domain\Model\MyProfile $myProfileUser */
        $myProfileUser = $myProfileRepository->findByUid($user->getUid());
480
481
482
483
        if ($myProfileUser === null) {
            // if no user is found, we try to find disabled users. This is needed, if we confirm new users via backend
            $myProfileUser = $myProfileRepository->findDisabledByUid($user->getUid());
        }
484
485
        if ($myProfileUser->getTermsVersion() !== '') {
            $ldapUserArray['conditionsVersion'] = $myProfileUser->getTermsVersion();
486
487
        }

488
        // If the password is not salted, it has been submitted and must be included in the LDAP update
489
490
491
492
493
494
        if ($this->isSaltedPassword($user->getPassword()) === false) {
            /** @var \T3o\T3oLdap\Utility\PasswordHashing $passwordHashing */
            $passwordHashing = GeneralUtility::makeInstance(\T3o\T3oLdap\Utility\PasswordHashing::class);
            $ldapUserArray['userPassword'][] = $passwordHashing->getPasswordHash($user->getPassword(), 'sha1');
            $ldapUserArray['userPassword'][] = $passwordHashing->getPasswordHash($user->getPassword(), 'crypt');
            $ldapUserArray['userPassword'][] = $passwordHashing->getPasswordHash($user->getPassword(), 'md5');
495
496
        }

497
498
        // if hash fields are filled, store them into ldap user and remove them afterwards
        if ($this->isSaltedPassword($user->getPassword()) && ($myProfileUser->getHashMd5() || $myProfileUser->getHashSha1() || $myProfileUser->getHashCrypt())) {
499
            $ldapUserArray['userPassword'][] = $myProfileUser->getHashSha1();
500
501
502
503
504
505
506
507
508
509
            $ldapUserArray['userPassword'][] = $myProfileUser->getHashCrypt();
            $ldapUserArray['userPassword'][] = $myProfileUser->getHashMd5();

            $myProfileUser->setHashCrypt('');
            $myProfileUser->setHashMd5('');
            $myProfileUser->setHashSha1('');
            $myProfileRepository->update($myProfileUser);
            GeneralUtility::makeInstance(\TYPO3\CMS\Extbase\Persistence\Generic\PersistenceManager::class)->persistAll();
        }

510
511
        if (trim($ldapUserArray['sn']) === '') {
            $ldapUserArray['sn'] = $user->getUsername();
512
        }
513
514
        if (trim($ldapUserArray['cn']) === '') {
            $ldapUserArray['cn'] = $user->getUsername();
515
        }
516
517
        if (trim($ldapUserArray['uid']) === '') {
            $ldapUserArray['uid'] = $user->getUsername();
518
519
        }

520
        return $ldapUserArray;
521
522
523
524
525
526
527
528
    }

    /**
     * Check a given String for salting.
     *
     * @param String $passwordString The password string
     * @return bool
     */
529
530
    private function isSaltedPassword($passwordString)
    {
531
532
        $ret = false;
        if ($passwordString !== '') {
533
534
            if (SaltedPasswordsUtility::isUsageEnabled('FE')) {
                $objSalt = GeneralUtility::makeInstance(SaltedPasswordsUtility::getDefaultSaltingHashingMethod('FE'));
535
536
537
538
539
540
541
                if (is_object($objSalt)) {
                    if ($objSalt->isValidSaltedPW($passwordString)) {
                        $ret = true;
                    }
                }
            }
        }
542

543
544
545
546
547
548
        return $ret;
    }

    /**
     * Update the last modified in LDAP timestamp of a user
     *
549
     * @todo Rebuild it with Doctrine DBAL
550
551
552
     * @param $feUserUid
     * @return mixed
     */
553
554
    private function updateFeUserLastLdapUpdateTimestamp($feUserUid)
    {
555
556
557
558
        return $GLOBALS['TYPO3_DB']->exec_UPDATEquery(
            'fe_users',
            'uid = ' . intval($feUserUid),
            [
559
                'tx_t3oldap_lastupdate_ts' => $GLOBALS['EXEC_TIME']
560
561
            ]
        );
562
563
    }

564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
    /**
     * @return string
     */
    public function getLastLdapError()
    {
        return $this->lastLdapError;
    }

    /**
     * @param string $lastLdapError
     * @return void
     */
    public function setLastLdapError($lastLdapError)
    {
        $this->lastLdapError = $lastLdapError;
    }

581
    /**
582
     * @todo Rebuild it with Doctrine DBAL
583
584
585
     * @param $countryName
     * @return bool
     */
586
587
    private function getCountryDetailsByCountryName($countryName)
    {
588
589
590
591
592
593
594
595
596
        $ret = false;

        $whereClause = 'cn_short_en LIKE ' . $GLOBALS['TYPO3_DB']->fullQuoteStr($countryName, 'static_countries');
        $selectFields = 'uid, cn_iso_2, cn_short_en';
        $fromTable = 'static_countries';
        $groupBy = '';
        $orderBy = '';
        $limit = '1';

597
598
        $result = $GLOBALS['TYPO3_DB']->exec_SELECTquery($selectFields, $fromTable, $whereClause, $groupBy, $orderBy,
            $limit);
599
600
        if ($result) {
            if ($GLOBALS['TYPO3_DB']->sql_num_rows($result) == 1) {
601
602
603
604
605
606
607
608
609
                $ret = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($result);
                $GLOBALS['TYPO3_DB']->sql_free_result($result);
            }
        }

        return $ret;

    }

610
611
612
613
614
615
616
617
618
619
    /**
     * Destroy the LDAP connection
     */
    public function __destruct()
    {
        if ($this->ldapConnection) {
            ldap_close($this->ldapConnection);
        }
    }

620
}