Commit 0b8aac4c authored by Benni Mack's avatar Benni Mack
Browse files

[TASK] Migrate authentication logic into ApiUser class

This change adds an object called "ApiUser" which can be built with
a factory method "createFromSoapAccountData".

Using the ApiUser allows for easy access of the user data, and
covers both TSFE and LDAP dependencies which can later be exchanged
easily for other purposes via a possible ApiUserInterface.

The helper class usages are now reduced to continue removing
legacy code and TYPO3_DB calls.
parent fc579160
Pipeline #9111 passed with stages
in 6 minutes and 9 seconds
<?php
declare(strict_types = 1);
namespace T3o\Ter\Api;
/*
* This file is part of TYPO3 CMS-extension "ter", created by Benni Mack.
*
* 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.
*/
use Causal\IgLdapSsoAuth\Domain\Repository\ConfigurationRepository;
use Causal\IgLdapSsoAuth\Library\Authentication;
use T3o\Ter\Exception\UnauthorizedException;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* This class contains information about a user accessing the API (currently SOAP)
* and works with factory methods to build a user.
*
* The main methods to use is
* -> authenticate() which hides the LDAP implementation currently (which can be replaced later-on via tokens)
* -> isAuthenticated()
*/
class ApiUser
{
/**
* @var string
*/
protected $username;
/**
* Currently only used internally for authentication purposes with LDAP
*
* @var string
*/
protected $submittedPassword;
/**
* @var array
*/
protected $userGroups = [];
/**
* @var bool
*/
protected $authenticated = false;
public function __construct(string $username)
{
$this->username = $username;
}
/**
* Usually the SOAP API authenticates against username + password via a \stdClass object.
* For this reason, the method can be used to authenticate once the object is available.
* $user = ApiUser::createFromSoapAccountData($accountData);
* $user->authenticate();
*
* once the two lines are added, the code has a valid user to continue with an API request.
*
* @param object $accountData
* @return static
* @throws UnauthorizedException
*/
public static function createFromSoapAccountData(object $accountData)
{
if ($accountData->username === '' || $accountData->password === '') {
throw new UnauthorizedException('No user or no password submitted.', ResultCodes::ERROR_GENERAL_NOUSERORPASSWORD);
}
$user = new static($accountData->username);
$user->submittedPassword = $accountData->password;
return $user;
}
/**
* Fetches the user data, authenticates a user and loads the user groups
*/
public function authenticate(): void
{
// No need to re-authenticate for now
if ($this->authenticated) {
return;
}
if ($this->username === '' || $this->submittedPassword === '') {
throw new UnauthorizedException('No user or no password submitted.', ResultCodes::ERROR_GENERAL_NOUSERORPASSWORD);
}
// get raw user record from fe_users database
$userData = $this->getUserByUsername($this->username);
if ($userData) {
$this->userGroups = GeneralUtility::intExplode(',', $userData['usergroup']);
if (!$this->userIsAlreadyLoggedIn() && !$this->ldapValidationSucceeded()) {
$this->userGroups = [];
$this->authenticated = false;
throw new UnauthorizedException('Wrong password.', ResultCodes::ERROR_GENERAL_WRONGPASSWORD);
}
} else {
throw new UnauthorizedException('The specified user does not exist. You need to login first on extensions.typo3.org.', ResultCodes::ERROR_GENERAL_USERNOTFOUND);
}
$this->authenticated = true;
}
public function isAuthenticated(): bool
{
return $this->authenticated;
}
public function getUsername(): string
{
return $this->username;
}
/**
* Check if LDAP authenticates the credentials
*
* @return bool
*/
protected function ldapValidationSucceeded(): bool
{
if (!ExtensionManagementUtility::isLoaded('ig_ldap_sso_auth')) {
return false;
}
$configurationRepository = GeneralUtility::makeInstance(ConfigurationRepository::class);
$configurationRecords = $configurationRepository->findAll();
\Causal\IgLdapSsoAuth\Library\Configuration::initialize('fe', $configurationRecords[0]);
return (bool)Authentication::ldapAuthenticate($this->username, $this->submittedPassword);
}
/**
* We check whether a user is logged in by TYPO3
* because of a sent session cookie
*
* @return bool
*/
protected function userIsAlreadyLoggedIn(): bool
{
$usernameFromFrontend = $GLOBALS['TSFE']->fe_user->user['username'] ?? '';
return !empty($usernameFromFrontend) && $this->username === $usernameFromFrontend;
}
public function isAdministrator(): bool
{
$configuration = GeneralUtility::makeInstance(Configuration::class);
if (in_array($configuration->getAdministratorsGroupId(), $this->userGroups)) {
return true;
}
if (in_array($configuration->getSecurityUserGroupId(), $this->userGroups)) {
return true;
}
return false;
}
public function isReviewer(): bool
{
$groupId = GeneralUtility::makeInstance(Configuration::class)->getReviewersUserGroupId();
return in_array($groupId, $this->userGroups);
}
/**
* Internal method to fetch all information about a user
*
* @param string $username
* @return array|null
*/
protected function getUserByUsername(string $username = ''): ?array
{
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('fe_users');
$userRecord = $queryBuilder
->select('*')
->from('fe_users')
->where(
$queryBuilder->expr()->eq('username', $queryBuilder->createNamedParameter($username))
)
->execute()
->fetch();
return $userRecord ?: null;
}
}
......@@ -17,6 +17,7 @@
*
* @author Robert Lemke <robert@typo3.org>
*/
use T3o\Ter\Api\ApiUser;
use T3o\Ter\Api\ResultCodes;
use TYPO3\CMS\Core\Utility\GeneralUtility;
......@@ -89,7 +90,13 @@ class tx_ter_api
*/
public function login($accountData)
{
return $this->helperObj->checkValidUser($accountData);
try {
$user = ApiUser::createFromSoapAccountData($accountData);
$user->authenticate();
} catch (\T3o\Ter\Exception\UnauthorizedException $e) {
return false;
}
return $user->isAuthenticated();
}
/**
......@@ -121,15 +128,14 @@ class tx_ter_api
$extensionKey = strtolower($extensionInfoData->extensionKey);
$this->logger->info('Upload of extension ' . $extensionKey . ' (' . $extensionInfoData->version . ') by user ' . $accountData->username);
$uploadUserRecordArr = $this->helperObj->getValidUser($accountData);
$user = ApiUser::createFromSoapAccountData($accountData);
$user->authenticate();
$extensionKeyRecordArr = $this->helperObj->getExtensionKeyRecord($extensionKey);
if ($extensionKeyRecordArr == false) {
throw new \T3o\Ter\Exception\NotFoundException('Extension "' . $extensionKey . '" does not exist.', ResultCodes::ERROR_UPLOADEXTENSION_EXTENSIONDOESNTEXIST);
}
if (strtolower($extensionKeyRecordArr['ownerusername']) !== strtolower(
$accountData->username
) && $uploadUserRecordArr['admin'] !== true
) {
if (strtolower($extensionKeyRecordArr['ownerusername']) !== strtolower($user->getUsername()) && !$user->isAdministrator()) {
throw new \T3o\Ter\Exception\UnauthorizedException('Access denied.', ResultCodes::ERROR_UPLOADEXTENSION_ACCESSDENIED);
}
......@@ -157,7 +163,7 @@ class tx_ter_api
);
}
if (($typo3DependencyCheck = static::checkExtensionDependencyOnSupportedTypo3Version($extensionInfoData)) !== true && $uploadUserRecordArr['admin'] !== true) {
if (($typo3DependencyCheck = static::checkExtensionDependencyOnSupportedTypo3Version($extensionInfoData)) !== true && !$user->isAdministrator()) {
switch ($typo3DependencyCheck) {
case ResultCodes::ERROR_UPLOADEXTENSION_TYPO3DEPENDENCYINCORRECT:
$message = 'Extension does not have a dependency for a supported version of TYPO3. See http://typo3.org/news/article/announcing-ter-cleanup-process/ for how to fix this.';
......@@ -258,8 +264,9 @@ class tx_ter_api
{
$this->logger->info('Deletion of extension ' . $extensionKey . ' (' . $version . ') by user ' . $accountData->username);
$userRecordArr = $this->helperObj->getValidUser($accountData);
if ($userRecordArr['admin'] !== true) {
$user = ApiUser::createFromSoapAccountData($accountData);
$user->authenticate();
if (!$user->isAdministrator()) {
throw new \T3o\Ter\Exception\UnauthorizedException(
'Access denied. You must be administrator in order to delete extensions',
ResultCodes::ERROR_DELETEEXTENSION_ACCESS_DENIED
......@@ -419,12 +426,14 @@ class tx_ter_api
*/
public function deleteExtensionKey($accountData, $extensionKey)
{
$userRecordArr = $this->helperObj->getValidUser($accountData);
$user = ApiUser::createFromSoapAccountData($accountData);
$user->authenticate();
$extensionKeyRecordArr = $this->helperObj->getExtensionKeyRecord($extensionKey);
if (is_array($extensionKeyRecordArr)) {
if ($userRecordArr['admin'] !== true
&& strtolower($extensionKeyRecordArr['ownerusername']) !== strtolower($accountData->username)
if (!$user->isAdministrator()
&& strtolower($extensionKeyRecordArr['ownerusername']) !== strtolower($user->getUsername())
) {
throw new \T3o\Ter\Exception\UnauthorizedException('Access denied.', ResultCodes::ERROR_DELETEEXTENSIONKEY_ACCESSDENIED);
}
......@@ -487,12 +496,14 @@ class tx_ter_api
*/
public function modifyExtensionKey($accountData, $modifyExtensionKeyData)
{
$userRecordArr = $this->helperObj->getValidUser($accountData);
$user = ApiUser::createFromSoapAccountData($accountData);
$user->authenticate();
$extensionKeyRecordArr = $this->helperObj->getExtensionKeyRecord($modifyExtensionKeyData->extensionKey);
if (is_array($extensionKeyRecordArr)) {
if ($userRecordArr['admin'] !== true
&& strtolower($extensionKeyRecordArr['ownerusername']) !== strtolower($accountData->username)
if (!$user->isAdministrator()
&& strtolower($extensionKeyRecordArr['ownerusername']) !== strtolower($user->getUsername())
) {
throw new \T3o\Ter\Exception\UnauthorizedException('Access denied.', ResultCodes::ERROR_MODIFYEXTENSIONKEY_ACCESSDENIED);
}
......@@ -522,9 +533,9 @@ class tx_ter_api
*/
public function setReviewState($accountData, $setReviewStateData)
{
$userRecordArr = $this->helperObj->getValidUser($accountData);
$reviewersFrontendUsergroupUid = $this->apiConfiguration->getReviewersUserGroupId();
if (!GeneralUtility::inList($userRecordArr['usergroup'], $reviewersFrontendUsergroupUid)) {
$user = ApiUser::createFromSoapAccountData($accountData);
$user->authenticate();
if (!$user->isReviewer()) {
throw new \T3o\Ter\Exception\UnauthorizedException('Access denied.', ResultCodes::ERROR_SETREVIEWSTATE_ACCESSDENIED);
}
......
......@@ -17,7 +17,6 @@
*
* @author Robert Lemke <robert@typo3.org>
*/
use T3o\Ter\Api\ResultCodes;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;
......@@ -28,126 +27,6 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
*/
class tx_ter_helper
{
/**
* This verifies the given fe_users username/password.
* Either the fe_user row is returned or an exception is thrown.
*
* @param object $accountData : Account data information with username, password and upload password
* @return mixed If success, returns array of fe_users, otherwise error string.
* @access public
* @throws \T3o\Ter\Exception\UnauthorizedException
*/
public function getValidUser(object $accountData): ?array
{
if ($accountData->username === '' || $accountData->password === '') {
throw new \T3o\Ter\Exception\UnauthorizedException('No user or no password submitted.', ResultCodes::ERROR_GENERAL_NOUSERORPASSWORD);
}
$user = $this->getUserByUsername($accountData->username);
if ($user) {
if (!$this->userIsAlreadyLoggedIn($accountData) && !$this->ldapValidationSucceeded($accountData)) {
throw new \T3o\Ter\Exception\UnauthorizedException('Wrong password.', ResultCodes::ERROR_GENERAL_WRONGPASSWORD);
}
$user['admin'] = $this->userIsAdmin($user['usergroup']) || $this->userIsSecurityTeamMember($user['usergroup']);
} else {
throw new \T3o\Ter\Exception\UnauthorizedException('The specified user does not exist. You need to login first on extensions.typo3.org.', ResultCodes::ERROR_GENERAL_USERNOTFOUND);
}
return $user;
}
private function getUserByUsername(string $username = ''): ?array
{
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('fe_users');
$userRecord = $queryBuilder
->select('*')
->from('fe_users')
->where(
$queryBuilder->expr()->eq('username', $queryBuilder->createNamedParameter($username))
)
->execute()
->fetch();
return $userRecord ?: null;
}
/**
* @param string $userGroupList
* @return bool
*/
private function userIsAdmin(string $userGroupList): bool
{
$groupId = GeneralUtility::makeInstance(\T3o\Ter\Api\Configuration::class)->getAdministratorsGroupId();
return GeneralUtility::inList($userGroupList, $groupId);
}
/**
* @param string $userGroupList
* @return bool
*/
private function userIsSecurityTeamMember(string $userGroupList): bool
{
$groupId = GeneralUtility::makeInstance(\T3o\Ter\Api\Configuration::class)->getSecurityUserGroupId();
return GeneralUtility::inList($userGroupList, $groupId);
}
/**
* We check whether a user is logged in by TYPO3
* because of a sent session cookie
*
* @param $accountData
* @return bool
*/
private function userIsAlreadyLoggedIn(object $accountData): bool
{
/** @var \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController $tsfe */
$tsfe = $GLOBALS['TSFE'];
return !empty($tsfe->fe_user->user['username']) && $accountData->username === $tsfe->fe_user->user['username'];
}
/**
* Check if LDAP authenticates the credentials
*
* @param stdClass $accountData
* @return bool
*/
private function ldapValidationSucceeded(\stdClass $accountData): bool
{
if (!\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::isLoaded('ig_ldap_sso_auth')) {
return false;
}
/** @var \Causal\IgLdapSsoAuth\Domain\Repository\ConfigurationRepository $configurationRepository */
$configurationRepository = GeneralUtility::makeInstance(\Causal\IgLdapSsoAuth\Domain\Repository\ConfigurationRepository::class);
$configurationRecords = $configurationRepository->findAll();
\Causal\IgLdapSsoAuth\Library\Configuration::initialize('fe', $configurationRecords[0]);
return (bool)\Causal\IgLdapSsoAuth\Library\Authentication::ldapAuthenticate($accountData->username, $accountData->password);
}
/**
* Checks for correct account data without throwing an exception.
* It just returns TRUE / FALSE
*
* @param object $accountData
* @return bool
*/
public function checkValidUser(object $accountData): bool
{
if ($accountData->username === '' || $accountData->password === '') {
$success = false;
} else {
$user = $this->getUserByUsername($accountData->username);
$success = $user && $this->ldapValidationSucceeded($accountData);
}
return $success;
}
/**
* Checks if the given extension key is unique and not registered yet.
* Takes underscores into account, so the key "ter_ter" can't be registered
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment