Commit 6601b013 authored by Thomas Löffler's avatar Thomas Löffler
Browse files

Add 'extensions/t3o_ldap/' from commit 'dbaaae85'

git-subtree-dir: extensions/t3o_ldap
git-subtree-mainline: c37ac5ef
git-subtree-split: dbaaae85
parents c37ac5ef dbaaae85
<?php
namespace T3o\T3oLdap\Connectors;
/*
* (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.
*/
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Saltedpasswords\Utility\SaltedPasswordsUtility;
/**
* 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
*/
class Ldap
{
/**
* LDAP server as IP, hostname or complete URI
*
* @var string
*/
private $ldapServer = '';
/**
* LDAP server port (0/389/636)
*
* @var int
*/
private $ldapServerPort = 0;
/**
* LDAP version (defaults sto 3)
*
* @var int
*/
private $ldapProtocolVersion = 3;
/**
* LDAP admin DN to bind for directory updates
*
* @var string
*/
private $ldapBindDn = '';
/**
* Bind password for administrative LDAP bind
*
* @var string
*/
private $ldapBindPassword = '';
/**
* LDAP connection resource
*
* @var null
*/
private $ldapConnection = null;
/**
* LDAP base DN used to find users in LDAP. May be overridden in
* extension manager configuration
*
* @var string
*/
private $ldapBaseDnForPasswordChanges = 'ou=people,dc=typo3,dc=org';
/**
* TYPO3 extension configuration array
*
* @var array
*/
private $extensionConfiguration = [];
/**
* Last LDAP error in this class
*
* @var string
*/
private $lastLdapError = '';
/**
* LDAP constructor.
*/
public function __construct()
{
// Disable certificate checks on LDAP TLS
putenv('LDAPTLS_REQCERT=never');
// TODO Move to TypoScript configuration object if more than one LDAP server is required per installation
$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']);
// Connect and bind
$this->createLdapConnection();
$this->ldapBind($this->ldapConnection, $this->ldapBindDn, $this->ldapBindPassword);
}
/**
* 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
*/
public function testLdapPassword($dn, $password)
{
$ret = false;
if ($this->createLdapConnection() === true) {
if ($this->ldapBind($this->ldapConnection, $dn, $password) === true) {
$ret = true;
}
} else {
GeneralUtility::sysLog('Keine LDAP-Bind mit Nutzerdaten moeglich: ' . ldap_error($this->ldapConnection),
't3o_ldap',
GeneralUtility::SYSLOG_SEVERITY_ERROR);
}
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
*/
public function setLdapPasswords($username, $values)
{
$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) {
GeneralUtility::sysLog(ldap_error($this->ldapConnection), 't3o_ldap',
GeneralUtility::SYSLOG_SEVERITY_ERROR);
}
} else {
GeneralUtility::sysLog('Unable to bind to LDAP using: ' . ldap_error($this->ldapConnection), 't3o_ldap',
GeneralUtility::SYSLOG_SEVERITY_ERROR);
}
} else {
GeneralUtility::sysLog('No active LDAP connection available', 't3o_ldap',
GeneralUtility::SYSLOG_SEVERITY_ERROR);
}
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
*/
private function ldapBind($ldapConnection, $dn, $password)
{
$ret = false;
try {
// Bind to LDAP server
$ldapBind = @ldap_bind($ldapConnection, $dn, $password);
// Verify binding
if ($ldapBind) {
$ret = true;
} else {
throw new \RuntimeException('Could not bind to LDAP connection: ' . ldap_error($ldapConnection),
1453993540);
}
} catch (\RuntimeException $e) {
GeneralUtility::sysLog($e->getMessage(), 't3o_ldap', GeneralUtility::SYSLOG_SEVERITY_ERROR);
}
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
*/
private function updateLdapAttribute($dn, $attribute, $attributeValues, $multiValue = false)
{
$ret = false;
if (trim($dn) !== '') {
$attributes = [];
if (is_array($attributeValues)) {
foreach ($attributeValues AS $attributeValue) {
$attributes[$attribute][] = $attributeValue;
}
} else {
$attributes[$attribute] = $attributeValues;
}
$ret = ldap_mod_replace($this->ldapConnection, trim($dn), $attributes);
}
return $ret;
}
/**
* Create the LDAP connection and set in global scope on success. Return false on failure.
* Errors are logged to syslog.
*
* @return bool
*/
private function createLdapConnection()
{
$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 {
throw new \RuntimeException('Could not create LDAP connection: ' . ldap_error($this->ldapConnection),
1453993539);
}
} catch (\RuntimeException $e) {
GeneralUtility::sysLog($e->getMessage(), 't3o_ldap', GeneralUtility::SYSLOG_SEVERITY_ERROR);
}
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
*/
private function getDnForUserName($username)
{
$dn = 'uid=' . $username . ',' . $this->ldapBaseDnForPasswordChanges;
return $dn;
}
/**
* Check if a user exists in LDAP
*
* @param String $username The username
* @return bool
*/
public function userExists($username)
{
$ret = false;
$dn = $this->getDnForUserName($username);
$filter = '(|(objectClass=typo3Person))';
$attributes = ['sn', 'email', 'ou'];
$searchResult = @ldap_search($this->ldapConnection, $dn, $filter, $attributes);
if ($searchResult) {
$info = ldap_get_entries($this->ldapConnection, $searchResult);
if (intval($info['count']) > 0) {
$ret = true;
}
}
return $ret;
}
/**
* Update a user in LDAP
*
* @param \In2code\Femanager\Domain\Model\User $user The user data array
* @return bool
*/
public function updateUser(\In2code\Femanager\Domain\Model\User $user)
{
$ret = false;
$dn = $this->getDnForUserName($user->getUsername());
$ldapUserObject = $this->buildLdapUserArray($user);
$res = ldap_modify($this->ldapConnection, $dn, $ldapUserObject);
if ($res === true) {
// 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
*/
public function enableUser($username)
{
$ret = false;
$dn = $this->getDnForUserName($username);
$ldapUserObject = [
'active' => true
];
$res = ldap_modify($this->ldapConnection, $dn, $ldapUserObject);
if ($res === true) {
// TODO $this->updateFeUserLastLdapUpdateTimestamp($feUserUid);
$ret = true;
} else {
$this->setLastLdapError(ldap_error($this->ldapConnection));
}
return $ret;
}
/**
* Delete a user in LDAP
*
* @param string $username The username to delete in LDAP
* @return bool
*/
public function deleteUser($username)
{
$dn = $this->getDnForUserName($username);
return ldap_delete($this->ldapConnection, $dn);
}
/**
* Create a user in LDAP
*
* @param \In2code\Femanager\Domain\Model\User $user The user model
* @param string $password Clear text user password
* @return bool
*/
public function createUser(\In2code\Femanager\Domain\Model\User $user, $password = '')
{
$ret = false;
$dn = $this->getDnForUserName($user->getUsername());
$ldapUserObject = $this->buildLdapUserArray($user);
$res = ldap_add($this->ldapConnection, $dn, $ldapUserObject);
if ($res === true) {
$this->updateFeUserLastLdapUpdateTimestamp($user->getUid());
$ret = true;
} else {
$this->setLastLdapError(ldap_error($this->ldapConnection));
}
return $ret;
}
/**
* Build the array for LDAP insert or updates.
*
* @param \In2code\Femanager\Domain\Model\User $user
* @return array
*/
private function buildLdapUserArray(\In2code\Femanager\Domain\Model\User $user)
{
$ldapUserArray = [
'objectclass' => [
0 => 'top',
1 => 'person',
2 => 'typo3Person',
3 => 'inetOrgPerson'
]
];
$nameParts = GeneralUtility::trimExplode(' ', $user->getName());
$lastName = array_pop($nameParts);
$firstName = implode(' ', $nameParts);
if (trim($user->getName()) !== '') {
$ldapUserArray['cn'] = $user->getName();
}
if (trim($user->getName()) !== '') {
$ldapUserArray['displayName'] = trim($user->getName());
}
if (trim($firstName) !== '') {
$ldapUserArray['givenName'] = trim($firstName);
}
if (trim($lastName) !== '') {
$ldapUserArray['sn'] = trim($lastName);
}
if (trim($user->getAddress()) !== '') {
$ldapUserArray['street'] = trim($user->getAddress());
}
if (trim($user->getZip()) !== '') {
$ldapUserArray['postalCode'] = trim($user->getZip());
}
if (trim($user->getCity()) !== '') {
$ldapUserArray['l'] = trim($user->getCity());
}
if (trim($user->getCountry()) !== '') {
$countryDetails = $this->getCountryDetailsByCountryName($user->getCountry());
if ($countryDetails !== false) {
if (trim($countryDetails['cn_iso_2']) !== '') {
$ldapUserArray['c'] = trim($countryDetails['cn_iso_2']);
}
if (trim($countryDetails['cn_short_en']) !== '') {
$ldapUserArray['co'] = trim($countryDetails['cn_short_en']);
}
}
}
$url = filter_var($user->getWww(), FILTER_VALIDATE_URL);
if ($url !== false) {
$ldapUserArray['labeledURI'] = $url;
}
$email = filter_var($user->getEmail(), FILTER_VALIDATE_EMAIL);
if ($email !== false) {
$ldapUserArray['mail'] = $email;
}
if (trim($user->getTelephone()) !== '') {
$ldapUserArray['homePhone'] = trim($user->getTelephone());
}
if (trim($user->getFax()) !== '') {
$ldapUserArray['facsimileTelephoneNumber'] = trim($user->getFax());
}
// update terms and conditions data
if ($user->isTerms() === true) {
$ldapUserArray['conditionsAccepted'] = 1;
}
if ($user->getTermsDateOfAcceptance() !== null && $user->getTermsDateOfAcceptance()->getTimestamp() > 0) {
$ldapUserArray['conditionsDate'] = $user->getTermsDateOfAcceptance()->getTimestamp();
}
$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());
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());
}
if ($myProfileUser->getTermsVersion() !== '') {
$ldapUserArray['conditionsVersion'] = $myProfileUser->getTermsVersion();
}
// If the password is not salted, it has been submitted and must be included in the LDAP update
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');
}
// 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())) {
$ldapUserArray['userPassword'][] = $myProfileUser->getHashSha1();
$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();
}
if (trim($ldapUserArray['sn']) === '') {
$ldapUserArray['sn'] = $user->getUsername();
}
if (trim($ldapUserArray['cn']) === '') {
$ldapUserArray['cn'] = $user->getUsername();
}
if (trim($ldapUserArray['uid']) === '') {
$ldapUserArray['uid'] = $user->getUsername();
}
return $ldapUserArray;
}
/**
* Check a given String for salting.
*
* @param String $passwordString The password string
* @return bool
*/
private function isSaltedPassword($passwordString)
{
$ret = false;
if ($passwordString !== '') {
if (SaltedPasswordsUtility::isUsageEnabled('FE')) {
$objSalt = GeneralUtility::makeInstance(SaltedPasswordsUtility::getDefaultSaltingHashingMethod('FE'));
if (is_object($objSalt)) {
if ($objSalt->isValidSaltedPW($passwordString)) {
$ret = true;
}
}
}
}
return $ret;
}
/**
* Update the last modified in LDAP timestamp of a user
*
* @todo Rebuild it with Doctrine DBAL
* @param $feUserUid
* @return mixed
*/
private function updateFeUserLastLdapUpdateTimestamp($feUserUid)
{
return $GLOBALS['TYPO3_DB']->exec_UPDATEquery(
'fe_users',
'uid = ' . intval($feUserUid),
[
'tx_t3oldap_lastupdate_ts' => $GLOBALS['EXEC_TIME']
]
);
}
/**
* @return string
*/
public function getLastLdapError()
{
return $this->lastLdapError;
}
/**
* @param string $lastLdapError
* @return void
*/
public function setLastLdapError($lastLdapError)
{
$this->lastLdapError = $lastLdapError;
}
/**
* @todo Rebuild it with Doctrine DBAL
* @param $countryName
* @return bool
*/
private function getCountryDetailsByCountryName($countryName)
{
$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';
$result = $GLOBALS['TYPO3_DB']->exec_SELECTquery($selectFields, $fromTable, $whereClause, $groupBy, $orderBy,
$limit);
if ($result) {
if ($GLOBALS['TYPO3_DB']->sql_num_rows($result) == 1) {