/** * @test * @dataProvider usernameSsoProvider */ public function validateUserSupportsSSO($filter, $username, $expected) { $GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf']['ig_ldap_sso_auth'] = serialize(array('enableFESSO' => 1)); \Causal\IgLdapSsoAuth\Library\Configuration::initialize('fe', new \Causal\IgLdapSsoAuth\Domain\Model\Configuration()); $result = $this->fixture->validateUser($username, NULL, 'cn=read-only-admin,dc=example,dc=com', $filter); $this->assertEquals($expected, $result); }
/** * Hooks into \TYPO3\CMS\Core\DataHandling\DataHandler after records have been saved to the database. * * @param string $operation * @param string $table * @param mixed $id * @param array $fieldArray * @param \TYPO3\CMS\Core\DataHandling\DataHandler $pObj * @return void */ public function processDatamap_afterDatabaseOperations($operation, $table, $id, array $fieldArray, \TYPO3\CMS\Core\DataHandling\DataHandler $pObj) { if ($table !== 'tx_igldapssoauth_config') { // Early return return; } if ($operation === 'new' && !is_numeric($id)) { $id = $pObj->substNEWwithIDs[$id]; } $row = \TYPO3\CMS\Backend\Utility\BackendUtility::getRecord($table, $id); if ($row['group_membership'] == Configuration::GROUP_MEMBERSHIP_FROM_MEMBER) { $warningMessageKeys = array(); if (!empty($row['be_users_basedn']) && !empty($row['be_groups_basedn'])) { // Check backend mapping $mapping = Configuration::parseMapping($row['be_users_mapping']); if (!isset($mapping['usergroup'])) { $warningMessageKeys[] = 'tx_igldapssoauth_config.group_membership.fe.missingUsergroupMapping'; } } if (!empty($row['fe_users_basedn']) && !empty($row['fe_groups_basedn'])) { // Check frontend mapping $mapping = Configuration::parseMapping($row['fe_users_mapping']); if (!isset($mapping['usergroup'])) { $warningMessageKeys[] = 'tx_igldapssoauth_config.group_membership.be.missingUsergroupMapping'; } } foreach ($warningMessageKeys as $key) { /** @var \TYPO3\CMS\Core\Messaging\FlashMessage $flashMessage */ $flashMessage = GeneralUtility::makeInstance('TYPO3\\CMS\\Core\\Messaging\\FlashMessage', $this->getLanguageService()->sL('LLL:EXT:ig_ldap_sso_auth/Resources/Private/Language/locallang_db.xlf:' . $key, TRUE), '', \TYPO3\CMS\Core\Messaging\FlashMessage::WARNING, TRUE); /** @var $flashMessageService \TYPO3\CMS\Core\Messaging\FlashMessageService */ $flashMessageService = GeneralUtility::makeInstance('TYPO3\\CMS\\Core\\Messaging\\FlashMessageService'); /** @var $defaultFlashMessageQueue \TYPO3\CMS\Core\Messaging\FlashMessageQueue */ $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier(); $defaultFlashMessageQueue->enqueue($flashMessage); } } }
/** * Authenticates a user (Check various conditions for the user that might invalidate its * authentication, e.g., password match, domain, IP, etc.). * * @param array $user Data of user. * @return int|FALSE */ public function authUser(array $user) { if (!Configuration::isInitialized()) { // Early return since LDAP is not configured return static::STATUS_AUTHENTICATION_FAILURE_CONTINUE; } if (TYPO3_MODE === 'BE') { $status = Configuration::getValue('BEfailsafe') ? static::STATUS_AUTHENTICATION_FAILURE_CONTINUE : static::STATUS_AUTHENTICATION_FAILURE_BREAK; } else { $status = static::STATUS_AUTHENTICATION_FAILURE_CONTINUE; } $enableFrontendSso = TYPO3_MODE === 'FE' && (bool) $this->config['enableFESSO'] && !empty($_SERVER['REMOTE_USER']); if (($this->login['uident'] && $this->login['uname'] || $enableFrontendSso) && !empty($user['tx_igldapssoauth_dn'])) { if (isset($user['tx_igldapssoauth_from'])) { $status = static::STATUS_AUTHENTICATION_SUCCESS_BREAK; } elseif (TYPO3_MODE === 'BE' && Configuration::getValue('BEfailsafe')) { return static::STATUS_AUTHENTICATION_FAILURE_CONTINUE; } else { // Failed login attempt (wrong password) - write that to the log! static::getLogger()->warning('Password not accepted: ' . array('username' => $this->login['uname'], 'remote' => sprintf('%s (%s)', $this->authInfo['REMOTE_ADDR'], $this->authInfo['REMOTE_HOST']))); $status = static::STATUS_AUTHENTICATION_FAILURE_BREAK; } // Checking the domain (lockToDomain) if ($status && $user['lockToDomain'] && $user['lockToDomain'] != $this->authInfo['HTTP_HOST']) { // Lock domain didn't match, so error: static::getLogger()->error(sprintf('Locked domain "%s" did not match "%s"', $user['lockToDomain'], $this->authInfo['HTTP_HOST']), array('username' => $user[$this->db_user['username_column']], 'remote' => sprintf('%s (%s)', $this->authInfo['REMOTE_ADDR'], $this->authInfo['REMOTE_HOST']))); $status = static::STATUS_AUTHENTICATION_FAILURE_BREAK; } } return $status; }
/** * @test */ public function mappingWithTypoScriptIsExtended() { $mapping = <<<EOT \t\t\tname = <sn>, <givenName> \t\t\tname.wrap = | EOT; $mapping = Configuration::parseMapping($mapping); $result = Configuration::hasExtendedMapping($mapping); $this->assertTrue($result); }
/** * Merges a field from LDAP into a TYPO3 record. * * @param array $ldap * @param array $typo3 * @param string $field * @param string $value * @return array Modified $typo3 array * @throws \UnexpectedValueException */ protected static function mergeSimple(array $ldap, array $typo3, $field, $value) { // Standard marker or custom function if (preg_match("`{([^\$]*)}`", $value, $matches)) { switch ($value) { case '{DATE}': $mappedValue = $GLOBALS['EXEC_TIME']; break; case '{RAND}': $mappedValue = rand(); break; default: $mappedValue = ''; $parameters = explode(';', $matches[1]); $hookParameters = array(); foreach ($parameters as $parameter) { list($parameterKey, $parameterValue) = explode('|', $parameter, 2); $hookParameters[trim($parameterKey)] = $parameterValue; } if (empty($hookParameters['hookName'])) { throw new \UnexpectedValueException(sprintf('Custom marker hook parameter "hookName" is undefined: %s', $matches[0]), 1430138379); } $hookName = $hookParameters['hookName']; $ldapAttributes = Configuration::getLdapAttributes(array($value)); // hook for processing user information once inserted or updated in the database if (is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['ig_ldap_sso_auth']['extraMergeField']) && !empty($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['ig_ldap_sso_auth']['extraMergeField'][$hookName])) { $_procObj = GeneralUtility::getUserObj($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['ig_ldap_sso_auth']['extraMergeField'][$hookName]); if (!is_callable(array($_procObj, 'extraMerge'))) { throw new \UnexpectedValueException(sprintf('Custom marker hook "%s" does not have a method "extraMerge"', $hookName), 1430140817); } $mappedValue = $_procObj->extraMerge($field, $typo3, $ldap, $ldapAttributes, $hookParameters); } break; } // LDAP attribute } elseif (preg_match("`<([^\$]*)>`", $value, $attribute)) { if ($field === 'tx_igldapssoauth_dn' || $field === 'title' && $value === '<dn>') { $mappedValue = $ldap[strtolower($attribute[1])]; } else { $mappedValue = static::replaceLdapMarkers($value, $ldap); } // Constant } else { $mappedValue = $value; } // If field exists in TYPO3, set it to the mapped value if (array_key_exists($field, $typo3)) { $typo3[$field] = $mappedValue; // Otherwise, it is some extra value, which we store in a special sub-array // This may be data that is meant to be mapped onto other database tables } else { if (!isset($typo3['__extraData'])) { $typo3['__extraData'] = array(); } $typo3['__extraData'][$field] = $mappedValue; } return $typo3; }
/** * Processes the username according to current configuration. * * @param string $username * @return string */ public static function setUsername($username) { if (Configuration::getValue('forceLowerCaseUsername')) { // Possible enhancement: use \TYPO3\CMS\Core\Charset\CharsetConverter::conv_case instead $username = strtolower($username); } return $username; }
/** * @test */ public function parentGroupIsNotMerged() { $mapping = <<<EOT \t\t\tpid = 1 \t\t\ttitle = <cn> \t\t\tdescription = LDAP Group <cn> \t\t\tparentGroup = <memberOf> EOT; $expected = $this->typo3GroupFixture; $expected['pid'] = '1'; $expected['title'] = 'Scientists'; $expected['description'] = 'LDAP Group Scientists'; $mapping = Configuration::parseMapping($mapping); $group = Authentication::merge($this->ldapGroupFixture, $this->typo3GroupFixture, $mapping); $this->assertEquals($expected, $group); }
/** * Returns the corresponding DN if a given user is provided, otherwise FALSE. * * @param string $username * @param string $password User's password. If NULL password will not be checked * @param string $baseDn * @param string $filter * @return bool|string */ public function validateUser($username = NULL, $password = NULL, $baseDn = NULL, $filter = NULL) { // If user found on ldap server. if ($this->ldapUtility->search($baseDn, str_replace('{USERNAME}', $username, $filter), array('dn'))) { // Validate with password. if ($password !== NULL) { // Bind DN of user with password. if (empty($password)) { $this->lastBindDiagnostic = 'Empty password provided!'; return FALSE; } elseif ($this->ldapUtility->bind($this->ldapUtility->getDn(), $password)) { $dn = $this->ldapUtility->getDn(); // Restore last LDAP binding $config = Configuration::getLdapConfiguration(); $this->ldapUtility->bind($config['binddn'], $config['password']); $this->lastBindDiagnostic = ''; return $dn; } else { $status = $this->ldapUtility->getStatus(); $this->lastBindDiagnostic = $status['bind']['diagnostic']; return FALSE; // Password does not match } // If enable, SSO authentication without password } elseif ($password === NULL && Configuration::getValue('SSOAuthentication')) { return $this->ldapUtility->getDn(); } else { // User invalid. Authentication failed. return FALSE; } } return FALSE; }
/** * Imports a given user to the TYPO3 database. * * @param array $user Local user information * @param array $ldapUser LDAP user information * @param string $restoreBehavior How to restore users (only for update) * @return array Modified user data * @throws ImportUsersException */ public function import($user, $ldapUser, $restoreBehavior = 'both') { // Store the extra data for later restore and remove it if (isset($user['__extraData'])) { $extraData = $user['__extraData']; unset($user['__extraData']); } if (empty($user['uid'])) { // Set other necessary information for a new user // First make sure to be acting in the right context Configuration::setMode($this->context); $user['username'] = Typo3UserRepository::setUsername($user['username']); $user['password'] = Typo3UserRepository::setRandomPassword(); $typo3Groups = Authentication::getUserGroups($ldapUser, $this->configuration, $this->groupTable); if ($typo3Groups === NULL) { // Required LDAP groups are missing: quit! return $user; } $user = Typo3UserRepository::setUserGroups($user, $typo3Groups); $user = Typo3UserRepository::add($this->userTable, $user); $this->usersAdded++; } else { // Restore user that may have been previously deleted or disabled, depending on chosen behavior // (default to both undelete and re-enable) switch ($restoreBehavior) { case 'enable': $user[$GLOBALS['TCA'][$this->userTable]['ctrl']['enablecolumns']['disabled']] = 0; break; case 'undelete': $user[$GLOBALS['TCA'][$this->userTable]['ctrl']['delete']] = 0; break; case 'nothing': break; default: $user[$GLOBALS['TCA'][$this->userTable]['ctrl']['enablecolumns']['disabled']] = 0; $user[$GLOBALS['TCA'][$this->userTable]['ctrl']['delete']] = 0; } $typo3Groups = Authentication::getUserGroups($ldapUser, $this->configuration, $this->groupTable); $user = Typo3UserRepository::setUserGroups($user, $typo3Groups === NULL ? array() : $typo3Groups); $success = Typo3UserRepository::update($this->userTable, $user); if ($success) { $this->usersUpdated++; } } // Restore the extra data and trigger a signal if (isset($extraData)) { $user['__extraData'] = $extraData; // Hook for processing the extra data if (is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['ig_ldap_sso_auth']['extraDataProcessing'])) { foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['ig_ldap_sso_auth']['extraDataProcessing'] as $className) { /** @var $postProcessor \Causal\IgLdapSsoAuth\Utility\ExtraDataProcessorInterface */ $postProcessor = GeneralUtility::getUserObj($className); if ($postProcessor instanceof \Causal\IgLdapSsoAuth\Utility\ExtraDataProcessorInterface) { $postProcessor->processExtraData($this->userTable, $user); } else { throw new ImportUsersException(sprintf('Invalid post-processing class %s. It must implement the \\Causal\\IgLdapSsoAuth\\Utility\\ExtraDataProcessorInterface interface', $className), 1414136057); } } } } return $user; }
/** * Checks the LDAP connection and prepares a Flash message if unavailable. * * @return bool */ protected function checkLdapConnection() { try { $success = $this->ldap->connect(Configuration::getLdapConfiguration()); } catch (\Causal\IgLdapSsoAuth\Exception\UnresolvedPhpDependencyException $e) { // Possible known exception: 1409566275, LDAP extension is not available for PHP $this->addFlashMessage($e->getMessage(), 'Error ' . $e->getCode(), \TYPO3\CMS\Core\Messaging\FlashMessage::ERROR); return FALSE; } return $success; }
/** * Performs the synchronization of LDAP users according to selected parameters. * * @return boolean Returns TRUE on successful execution, FALSE on error * @throws ImportUsersException */ public function execute() { // Assemble a list of configuration and contexts for import /** @var \Causal\IgLdapSsoAuth\Domain\Repository\ConfigurationRepository $configurationRepository */ $configurationRepository = GeneralUtility::makeInstance('Causal\\IgLdapSsoAuth\\Domain\\Repository\\ConfigurationRepository'); if (empty($this->configuration)) { $ldapConfigurations = $configurationRepository->findAll(); } else { $configuration = $configurationRepository->findByUid($this->configuration); $ldapConfigurations = array(); if ($configuration !== NULL) { $ldapConfigurations[] = $configuration; } } if ($this->context === 'both') { $executionContexts = array('fe', 'be'); } else { $executionContexts = array($this->context); } $mode = $this->getMode(); // Start a database transaction with all our changes // Syntax is compatible with MySQL, Oracle, MSSQL and PostgreSQL $this->getDatabaseConnection()->sql_query('START TRANSACTION'); // Loop on each configuration and context and import the related users $failures = 0; foreach ($ldapConfigurations as $configuration) { foreach ($executionContexts as $aContext) { /** @var \Causal\IgLdapSsoAuth\Utility\UserImportUtility $importUtility */ $importUtility = GeneralUtility::makeInstance('Causal\\IgLdapSsoAuth\\Utility\\UserImportUtility', $configuration, $aContext); $config = $importUtility->getConfiguration(); if (empty($config['users']['filter'])) { // Current context is not configured for this LDAP configuration record static::getLogger()->debug(sprintf('Configuration record %s is not configured for context "%s"', $configuration->getUid(), $aContext)); unset($importUtility); continue; } // Start by connecting to the designated LDAP/AD server $success = Ldap::getInstance()->connect(Configuration::getLdapConfiguration()); // Proceed with import if successful if (!$success) { $failures++; unset($importUtility); continue; } $ldapUsers = $importUtility->fetchLdapUsers(); // Consider that fetching no users from LDAP is an error if (count($ldapUsers) === 0) { static::getLogger()->error(sprintf('No users (%s) found for configuration record %s', $aContext, $configuration->getUid())); $failures++; } else { // Disable or delete users, according to settings if ($this->missingUsersHandling === 'disable') { static::getLogger()->debug(sprintf('Disabling users (%s) for configuration record %s', $aContext, $configuration->getUid())); $importUtility->disableUsers(); } elseif ($this->missingUsersHandling === 'delete') { static::getLogger()->debug(sprintf('Deleting users (%s) for configuration record %s', $aContext, $configuration->getUid())); $importUtility->deleteUsers(); } // Proceed with import (handle partial result sets until every LDAP record has been returned) do { $typo3Users = $importUtility->fetchTypo3Users($ldapUsers); // Loop on all users and import them foreach ($ldapUsers as $index => $aUser) { if ($mode === 'sync' && empty($typo3Users[$index]['uid'])) { // New LDAP user => skip it since only existing TYPO3 users should get synchronized continue; } // Merge LDAP and TYPO3 information $user = Authentication::merge($aUser, $typo3Users[$index], $config['users']['mapping']); // Import the user using information from LDAP $importUtility->import($user, $aUser, $this->restoredUsersHandling); } static::getLogger()->info(sprintf('Configuration record %s: processed %s LDAP users (%s)', $configuration->getUid(), count($ldapUsers), $aContext)); // Free memory before going on $typo3Users = NULL; $ldapUsers = NULL; $ldapUsers = $importUtility->hasMoreLdapUsers() ? $importUtility->fetchLdapUsers(TRUE) : array(); } while (count($ldapUsers) > 0); } // Clean up unset($importUtility); Ldap::getInstance()->disconnect(); } } // If some failures were registered, rollback the whole transaction and report error if ($failures > 0) { $this->getDatabaseConnection()->sql_query('ROLLBACK'); $message = 'Some or all imports failed. Synchronisation was aborted. Check your settings or your network connection'; static::getLogger()->error($message); throw new ImportUsersException($message, 1410774015); } else { // Everything went fine, commit the changes $this->getDatabaseConnection()->sql_query('COMMIT'); } return TRUE; }