Ldap.php 20.3 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\Database\ConnectionPool;
17
18
use TYPO3\CMS\Core\Utility\GeneralUtility;

19
20
21
22
23
24
25
26
/**
 * 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.
 *
 * @since 1.0.0
 */
27
class Ldap implements \Psr\Log\LoggerAwareInterface
28
{
29
    use \Psr\Log\LoggerAwareTrait;
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
    /**
     * LDAP constructor.
97
     * @throws \Exception
98
99
100
101
102
103
     */
    public function __construct()
    {
        // Disable certificate checks on LDAP TLS
        putenv('LDAPTLS_REQCERT=never');

104
        // TODO Move to TypoScript configuration object if more than one LDAP server is required per installation
105
106
107
108
109
        if (version_compare(TYPO3_version, '9.0', '<')) {
            $this->extensionConfiguration = unserialize($GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf']['t3o_ldap'] ?? '') ?? [];
        } else {
            $this->extensionConfiguration = $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['t3o_ldap'] ?? [];
        }
110
111
112
113
114
115
        $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']);
116

Thomas Löffler's avatar
Thomas Löffler committed
117
118
        $this->logger = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Log\LogManager::class)->getLogger(__CLASS__);

119
        // Connect and bind
120
121
122
123
124
125
126
127
128
129
130
131
        try {
            if ($this->createLdapConnection()) {
                $this->ldapBind($this->ldapConnection, $this->ldapBindDn, $this->ldapBindPassword);
            } else {
                throw new \Exception(
                    'createLdapConnection connection failed',
                    1553856512
                );
            }
        } catch (\Exception $e) {
            throw $e;
        }
132
133
134
135
136
137
138
139
140
141
    }

    /**
     * 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
     */
142
143
    public function testLdapPassword($dn, $password)
    {
144
145
146
147
148
149
        $ret = false;
        if ($this->createLdapConnection() === true) {
            if ($this->ldapBind($this->ldapConnection, $dn, $password) === true) {
                $ret = true;
            }
        } else {
150
            $this->logger->error('ldab_bind is not working with user data: ' . ldap_error($this->ldapConnection));
151
        }
152

153
154
155
156
157
158
159
160
161
162
163
164
        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
     */
165
166
    public function setLdapPasswords($username, $values)
    {
167
168
169
170
171
172
173
174
175
176
177
178
179
        $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) {
180
                    $this->logger->error(ldap_error($this->ldapConnection));
181
182
                }
            } else {
183
                $this->logger->error('Unable to bind to LDAP using: ' . ldap_error($this->ldapConnection));
184
185
            }
        } else {
186
            $this->logger->error('No active LDAP connection available');
187
        }
188

189
190
191
192
193
194
195
        return $ret;
    }

    /**
     * Bind to the LDAP directory with the given credentials. Errors are logged to syslog.
     *
     * @param resource $ldapConnection
196
197
     * @param string $dn Complete bind DN for LDAP entry to bind with
     * @param string $password The password to use for bind
198
199
     * @return bool
     */
200
201
    private function ldapBind($ldapConnection, $dn, $password)
    {
202
203
204
        $ret = false;
        try {
            // Bind to LDAP server
205
            $ldapBind = @ldap_bind($ldapConnection, $dn, $password);
206
207
208
209
            // Verify binding
            if ($ldapBind) {
                $ret = true;
            } else {
210
211
212
213
                throw new \RuntimeException(
                    'Could not bind to LDAP connection: ' . ldap_error($ldapConnection),
                    1453993540
                );
214
            }
215
        } catch (\RuntimeException $e) {
216
            $this->logger->error($e->getMessage());
217
        }
218

219
220
221
222
223
224
        return $ret;
    }

    /**
     * Update an attribute for the given DN. Errors are logged to syslog.
     *
225
     * @param string $dn Complete DN for LDAP entry to update attributes for
226
227
228
229
230
     * @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
     */
231
232
    private function updateLdapAttribute($dn, $attribute, $attributeValues, $multiValue = false)
    {
233
234
        $ret = false;
        if (trim($dn) !== '') {
235
236
            $attributes = [];
            if (is_array($attributeValues)) {
237
                foreach ($attributeValues as $attributeValue) {
238
239
240
241
242
243
244
                    $attributes[$attribute][] = $attributeValue;
                }
            } else {
                $attributes[$attribute] = $attributeValues;
            }
            $ret = ldap_mod_replace($this->ldapConnection, trim($dn), $attributes);
        }
245

246
247
248
249
250
251
252
253
        return $ret;
    }

    /**
     * Create the LDAP connection and set in global scope on success. Return false on failure.
     * Errors are logged to syslog.
     *
     * @return bool
254
     * @throws \Exception
255
     */
256
257
    private function createLdapConnection()
    {
258
259
260
        $ret = false;
        $port = intval($this->ldapServerPort);
        try {
261
262
263
264
265
266
267
268
            if (function_exists('ldap_connect')) {
                $this->ldapConnection = @ldap_connect($this->ldapServer, ($port > 0 ? $port : null));
            } else {
                throw new \Exception(
                    'LDAP PHP Extension is not available',
                    1553856513
                );
            }
269
270
271
272
273
274
275
276
            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 {
277
278
279
280
                throw new \RuntimeException(
                    'Could not create LDAP connection: ' . ldap_error($this->ldapConnection),
                    1453993539
                );
281
            }
282
        } catch (\RuntimeException $e) {
283
            $this->logger->error($e->getMessage());
284
        }
285

286
287
288
289
290
291
292
293
294
295
        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
     */
296
297
    private function getDnForUserName($username)
    {
298
        $dn = 'uid=' . $username . ',' . $this->ldapBaseDnForPasswordChanges;
299

300
301
302
303
304
305
        return $dn;
    }

    /**
     * Check if a user exists in LDAP
     *
306
     * @param string $username The username
307
308
     * @return bool
     */
309
310
    public function userExists($username)
    {
311
312
313
314
        $ret = false;

        $dn = $this->getDnForUserName($username);
        $filter = '(|(objectClass=typo3Person))';
315
        $attributes = ['sn', 'email', 'ou'];
316
317
318
319
320
321
        $searchResult = @ldap_search($this->ldapConnection, $dn, $filter, $attributes);
        if ($searchResult) {
            $info = ldap_get_entries($this->ldapConnection, $searchResult);
            if (intval($info['count']) > 0) {
                $ret = true;
            }
322
323
324
325
326
327
328
329
        }

        return $ret;
    }

    /**
     * Update a user in LDAP
     *
330
     * @param \In2code\Femanager\Domain\Model\User $user The user data array
331
332
     * @return bool
     */
333
    public function updateUser(\In2code\Femanager\Domain\Model\User $user)
334
    {
335
        $ret = false;
336
        $dn = $this->getDnForUserName($user->getUsername());
337

338
        $ldapUserObject = $this->buildLdapUserArray($user);
339

340
        $res = ldap_modify($this->ldapConnection, $dn, $ldapUserObject);
341

342
        if ($res === true) {
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
            // 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
     */
359
360
    public function enableUser($username)
    {
361
362
        $ret = false;
        $dn = $this->getDnForUserName($username);
363
        $ldapUserObject = [
364
            'active' => true
365
366
367
368
        ];
        $res = ldap_modify($this->ldapConnection, $dn, $ldapUserObject);

        if ($res === true) {
369
370
            // TODO $this->updateFeUserLastLdapUpdateTimestamp($feUserUid);
            $ret = true;
371
372
        } else {
            $this->setLastLdapError(ldap_error($this->ldapConnection));
373
374
375
376
377
378
379
380
        }

        return $ret;
    }

    /**
     * Delete a user in LDAP
     *
381
     * @param string $username The username to delete in LDAP
382
383
     * @return bool
     */
384
385
    public function deleteUser($username)
    {
386
        $dn = $this->getDnForUserName($username);
387

388
389
390
391
392
393
        return ldap_delete($this->ldapConnection, $dn);
    }

    /**
     * Create a user in LDAP
     *
394
     * @param \In2code\Femanager\Domain\Model\User $user The user model
395
     * @param string $password Clear text user password
396
397
     * @return bool
     */
398
    public function createUser(\In2code\Femanager\Domain\Model\User $user, $password = '')
399
    {
400
        $ret = false;
401

402
        $dn = $this->getDnForUserName($user->getUsername());
403

404
        $ldapUserObject = $this->buildLdapUserArray($user);
405
        $res = ldap_add($this->ldapConnection, $dn, $ldapUserObject);
406

407
        if ($res === true) {
408
            $this->updateFeUserLastLdapUpdateTimestamp($user->getUid());
409
            $ret = true;
410
411
        } else {
            $this->setLastLdapError(ldap_error($this->ldapConnection));
412
413
414
415
416
417
418
419
        }

        return $ret;
    }

    /**
     * Build the array for LDAP insert or updates.
     *
420
     * @param \In2code\Femanager\Domain\Model\User $user
421
422
     * @return array
     */
423
    private function buildLdapUserArray(\In2code\Femanager\Domain\Model\User $user)
424
    {
425
        $ldapUserArray = [
426
            'objectclass' => [
427
428
429
430
                0 => 'top',
                1 => 'person',
                2 => 'typo3Person',
                3 => 'inetOrgPerson'
431
432
            ]
        ];
433
434
435
        $nameParts = GeneralUtility::trimExplode(' ', $user->getName());
        $lastName = array_pop($nameParts);
        $firstName = implode(' ', $nameParts);
436

437
438
        if (trim($user->getName()) !== '') {
            $ldapUserArray['cn'] = $user->getName();
439
        }
440
441
        if (trim($user->getName()) !== '') {
            $ldapUserArray['displayName'] = trim($user->getName());
442
        }
443
444
        if (trim($firstName) !== '') {
            $ldapUserArray['givenName'] = trim($firstName);
445
        }
446
447
        if (trim($lastName) !== '') {
            $ldapUserArray['sn'] = trim($lastName);
448
        }
449
450
        if (trim($user->getAddress()) !== '') {
            $ldapUserArray['street'] = trim($user->getAddress());
451
        }
452
453
        if (trim($user->getZip()) !== '') {
            $ldapUserArray['postalCode'] = trim($user->getZip());
454
        }
455
456
        if (trim($user->getCity()) !== '') {
            $ldapUserArray['l'] = trim($user->getCity());
457
        }
458
459
        if (trim($user->getCountry()) !== '') {
            $countryDetails = $this->getCountryDetailsByCountryName($user->getCountry());
460
461
            if ($countryDetails !== false) {
                if (trim($countryDetails['cn_iso_2']) !== '') {
462
                    $ldapUserArray['c'] = trim($countryDetails['cn_iso_2']);
463
464
                }
                if (trim($countryDetails['cn_short_en']) !== '') {
465
                    $ldapUserArray['co'] = trim($countryDetails['cn_short_en']);
466
467
468
469
                }
            }
        }

470
        $url = filter_var($user->getWww(), FILTER_VALIDATE_URL);
471
        if ($url !== false) {
472
            $ldapUserArray['labeledURI'] = $url;
473
        }
474
        $email = filter_var($user->getEmail(), FILTER_VALIDATE_EMAIL);
475
        if ($email !== false) {
476
            $ldapUserArray['mail'] = $email;
477
        }
478
479
        if (trim($user->getTelephone()) !== '') {
            $ldapUserArray['homePhone'] = trim($user->getTelephone());
480
        }
481
482
        if (trim($user->getFax()) !== '') {
            $ldapUserArray['facsimileTelephoneNumber'] = trim($user->getFax());
483
484
        }

485
        // update terms and conditions data
486
        if ($user->isTerms() === true) {
487
            $ldapUserArray['conditionsAccepted'] = 1;
488
        }
489
        if ($user->getTermsDateOfAcceptance() !== null && $user->getTermsDateOfAcceptance()->getTimestamp() > 0) {
490
            $ldapUserArray['conditionsDate'] = $user->getTermsDateOfAcceptance()->getTimestamp();
491
        }
492
493
494
495
496

        $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());
497
498
499
500
        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());
        }
501
502
        if ($myProfileUser->getTermsVersion() !== '') {
            $ldapUserArray['conditionsVersion'] = $myProfileUser->getTermsVersion();
503
504
        }

505
        // If the password is not salted, it has been submitted and must be included in the LDAP update
506
507
508
509
510
511
        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');
512
513
        }

514
515
        // 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())) {
516
            $ldapUserArray['userPassword'][] = $myProfileUser->getHashSha1();
517
518
519
520
521
522
523
524
525
526
            $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();
        }

527
528
        if (trim($ldapUserArray['sn']) === '') {
            $ldapUserArray['sn'] = $user->getUsername();
529
        }
530
531
        if (trim($ldapUserArray['cn']) === '') {
            $ldapUserArray['cn'] = $user->getUsername();
532
        }
533
534
        if (trim($ldapUserArray['uid']) === '') {
            $ldapUserArray['uid'] = $user->getUsername();
535
536
        }

537
        return $ldapUserArray;
538
539
540
541
542
    }

    /**
     * Check a given String for salting.
     *
543
     * @param string $passwordString The password string
544
545
     * @return bool
     */
546
547
    private function isSaltedPassword($passwordString)
    {
548
549
        $ret = false;
        if ($passwordString !== '') {
550
551
552
553
554
555
            $saltedHashingMethods = \TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory::getRegisteredSaltedHashingMethods();
            foreach ($saltedHashingMethods as $saltedHashingMethod) {
                /** @var \TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashInterface $method */
                $method = GeneralUtility::makeInstance($saltedHashingMethod);
                if ($method->isAvailable() && $method->isValidSaltedPW($passwordString)) {
                    return true;
556
557
558
559
560
561
562
563
564
565
566
                }
            }
        }
    }

    /**
     * Update the last modified in LDAP timestamp of a user
     *
     * @param $feUserUid
     * @return mixed
     */
567
568
    private function updateFeUserLastLdapUpdateTimestamp($feUserUid)
    {
569
570
571
572
573
574
575
        return GeneralUtility::makeInstance(ConnectionPool::class)
            ->getConnectionForTable('fe_users')
            ->update(
                'fe_users',
                ['tx_t3oldap_lastupdate_ts' => $GLOBALS['EXEC_TIME']],
                ['uid' => (int)$feUserUid]
            );
576
577
    }

578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
    /**
     * @return string
     */
    public function getLastLdapError()
    {
        return $this->lastLdapError;
    }

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

594
595
596
597
    /**
     * @param $countryName
     * @return bool
     */
598
599
    private function getCountryDetailsByCountryName($countryName)
    {
600
601
        $ret = false;

602
        $selectFields = ['uid', 'cn_iso_2', 'cn_short_en'];
603
        $fromTable = 'static_countries';
604
605
606
607
608
609
610
611
612

        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($fromTable);
        $ret = $queryBuilder->select($selectFields)
            ->from($fromTable)
            ->where(
                $queryBuilder->expr()->eq('cn_short_en', $queryBuilder->createNamedParameter($countryName))
            )
            ->execute()
            ->fetch();
613
614
615
616

        return $ret;
    }

617
618
619
620
621
622
    /**
     * Destroy the LDAP connection
     */
    public function __destruct()
    {
        if ($this->ldapConnection) {
623
            ldap_unbind($this->ldapConnection);
624
625
        }
    }
626
}