TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Backend\Security;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Backend\Tree\TreeNode;
|
||||
use TYPO3\CMS\Backend\Tree\TreeNodeCollection;
|
||||
use TYPO3\CMS\Core\Attribute\AsEventListener;
|
||||
use TYPO3\CMS\Core\Database\Connection;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Http\ApplicationType;
|
||||
use TYPO3\CMS\Core\Tree\Event\ModifyTreeDataEvent;
|
||||
use TYPO3\CMS\Core\Tree\TableConfiguration\DatabaseTreeDataProvider;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* This event listener deals with tree data security which reacts on a PSR-14 event
|
||||
* on data object initialization.
|
||||
*
|
||||
* The aspect defines category mount points according to BE User permissions.
|
||||
*
|
||||
* @internal This class is TYPO3-internal hook and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
final class CategoryPermissionsAspect
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $categoryTableName = 'sys_category';
|
||||
|
||||
/**
|
||||
* The listener for the event in DatabaseTreeDataProvider, which only affects the TYPO3 Backend
|
||||
*/
|
||||
#[AsEventListener('backend-user-permissions')]
|
||||
public function addUserPermissionsToCategoryTreeData(ModifyTreeDataEvent $event): void
|
||||
{
|
||||
// Only evaluate this in the backend
|
||||
if (!($GLOBALS['TYPO3_REQUEST'] ?? null) instanceof ServerRequestInterface
|
||||
|| !ApplicationType::fromRequest($GLOBALS['TYPO3_REQUEST'])->isBackend()
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var DatabaseTreeDataProvider $dataProvider */
|
||||
$dataProvider = $event->getProvider();
|
||||
$treeData = $event->getTreeData();
|
||||
|
||||
if (!$GLOBALS['BE_USER']->isAdmin() && $dataProvider->getTableName() === $this->categoryTableName) {
|
||||
// Get User permissions related to category
|
||||
$categoryMountPoints = $GLOBALS['BE_USER']->getCategoryMountPoints();
|
||||
|
||||
// Backup child nodes to be processed.
|
||||
$treeNodeCollection = $treeData->getChildNodes();
|
||||
|
||||
if (!empty($categoryMountPoints) && $treeData->hasChildNodes()) {
|
||||
$shallRepopulateTree = false;
|
||||
|
||||
// Check the rootline against categoryMountPoints when tree was filtered
|
||||
foreach ($dataProvider->getStartingPoints() as $startingPoint) {
|
||||
if (!in_array($startingPoint, $categoryMountPoints)) {
|
||||
$shallRepopulateTree = true;
|
||||
break;
|
||||
}
|
||||
$uidsInRootline = $this->findUidsInRootline($startingPoint);
|
||||
if (empty(array_intersect($categoryMountPoints, $uidsInRootline))) {
|
||||
$shallRepopulateTree = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($shallRepopulateTree) {
|
||||
// First, remove all child nodes which must be analyzed to be considered as "secure".
|
||||
// The nodes were backed up in variable $treeNodeCollection beforehand.
|
||||
$treeData->removeChildNodes();
|
||||
|
||||
// Create an empty tree node collection to receive the secured nodes.
|
||||
$securedTreeNodeCollection = GeneralUtility::makeInstance(TreeNodeCollection::class);
|
||||
|
||||
foreach ($categoryMountPoints as $categoryMountPoint) {
|
||||
$treeNode = $this->lookUpCategoryMountPointInTreeNodes((int)$categoryMountPoint, $treeNodeCollection);
|
||||
if ($treeNode !== null) {
|
||||
$securedTreeNodeCollection->append($treeNode);
|
||||
}
|
||||
}
|
||||
|
||||
// Reset child nodes.
|
||||
$treeData->setChildNodes($securedTreeNodeCollection);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively look up for a category mount point within a tree.
|
||||
*/
|
||||
private function lookUpCategoryMountPointInTreeNodes(int $categoryMountPoint, TreeNodeCollection $treeNodeCollection): ?TreeNode
|
||||
{
|
||||
$result = null;
|
||||
|
||||
// If any User permission, recursively traverse the tree and set tree part as mount point
|
||||
foreach ($treeNodeCollection as $treeNode) {
|
||||
/** @var TreeNode $treeNode */
|
||||
if ((int)$treeNode->getId() === $categoryMountPoint) {
|
||||
$result = $treeNode;
|
||||
break;
|
||||
}
|
||||
|
||||
if ($treeNode->hasChildNodes()) {
|
||||
$node = $this->lookUpCategoryMountPointInTreeNodes($categoryMountPoint, $treeNode->getChildNodes());
|
||||
if ($node !== null) {
|
||||
$result = $node;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find parent uids in rootline
|
||||
*/
|
||||
private function findUidsInRootline(int $uid): array
|
||||
{
|
||||
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||
->getQueryBuilderForTable($this->categoryTableName);
|
||||
$row = $queryBuilder
|
||||
->select('parent')
|
||||
->from($this->categoryTableName)
|
||||
->where(
|
||||
$queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($uid, Connection::PARAM_INT))
|
||||
)
|
||||
->executeQuery()
|
||||
->fetchAssociative();
|
||||
|
||||
$parentUids = [];
|
||||
if ($row['parent'] > 0) {
|
||||
$parentUids = $this->findUidsInRootline($row['parent']);
|
||||
$parentUids[] = $row['parent'];
|
||||
}
|
||||
return $parentUids;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Backend\Security\ContentSecurityPolicy;
|
||||
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Symfony\Component\Uid\UuidV4;
|
||||
use TYPO3\CMS\Backend\Attribute\AsController;
|
||||
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
||||
use TYPO3\CMS\Core\Crypto\HashService;
|
||||
use TYPO3\CMS\Core\Http\JsonResponse;
|
||||
use TYPO3\CMS\Core\Http\NullResponse;
|
||||
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Event\InvestigateMutationsEvent;
|
||||
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\ModelService;
|
||||
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\MutationSuggestion;
|
||||
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\PolicyProvider;
|
||||
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Reporting\Report;
|
||||
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Reporting\ReportAttribute;
|
||||
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Reporting\ReportDemand;
|
||||
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Reporting\ReportRepository;
|
||||
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Reporting\ReportStatus;
|
||||
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Reporting\Resolution;
|
||||
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Reporting\ResolutionRepository;
|
||||
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Reporting\SummarizedReport;
|
||||
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\Scope;
|
||||
|
||||
/**
|
||||
* AJAX endpoint for the CSP backend module, providing access to persisted CSP reports & resolutions.
|
||||
* @internal This is a specific Backend Controller implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
#[AsController]
|
||||
readonly class CspAjaxController
|
||||
{
|
||||
public function __construct(
|
||||
protected ModelService $modelService,
|
||||
protected PolicyProvider $policyProvider,
|
||||
protected ReportRepository $reportRepository,
|
||||
protected ResolutionRepository $resolutionRepository,
|
||||
protected EventDispatcherInterface $eventDispatcher,
|
||||
protected HashService $hashService,
|
||||
) {}
|
||||
|
||||
public function handleRequest(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
if ($request->getMethod() === 'GET') {
|
||||
return (new NullResponse())->withStatus(400);
|
||||
}
|
||||
if (!$this->isSystemMaintainer()) {
|
||||
return (new NullResponse())->withStatus(403);
|
||||
}
|
||||
return $this->dispatchAction($request)
|
||||
?? (new NullResponse())->withStatus(500);
|
||||
}
|
||||
|
||||
protected function dispatchAction(ServerRequestInterface $request): ?ResponseInterface
|
||||
{
|
||||
$parsedBody = $request->getParsedBody();
|
||||
$hmac = $parsedBody['hmac'] ?? null;
|
||||
$action = $parsedBody['action'] ?? null;
|
||||
$summaries = $parsedBody['summaries'] ?? [];
|
||||
$scope = Scope::tryFrom($parsedBody['scope'] ?? '');
|
||||
$uuid = $parsedBody['uuid'] ?? null;
|
||||
if ($uuid !== null) {
|
||||
$uuid = Uuidv4::fromString($uuid);
|
||||
}
|
||||
if (!empty($parsedBody['suggestion'])) {
|
||||
$suggestion = $this->modelService->buildMutationSuggestionFromArray($parsedBody['suggestion']);
|
||||
}
|
||||
// reports
|
||||
if ($action === 'fetchReports') {
|
||||
return $this->fetchReportsAction($scope);
|
||||
}
|
||||
if ($action === 'muteReport' && is_array($summaries)) {
|
||||
return $this->muteReportAction(...$summaries);
|
||||
}
|
||||
if ($action === 'deleteReport' && is_array($summaries)) {
|
||||
return $this->deleteReportAction(...$summaries);
|
||||
}
|
||||
if ($action === 'deleteReports') {
|
||||
return $this->deleteReportsAction($scope);
|
||||
}
|
||||
if ($action === 'handleReport' && $uuid !== null) {
|
||||
return $this->handleReportAction($uuid);
|
||||
}
|
||||
if ($action === 'mutateReport'
|
||||
&& $scope !== null
|
||||
&& is_array($summaries)
|
||||
&& isset($suggestion)
|
||||
&& hash_equals($suggestion->hmac(), $hmac)
|
||||
) {
|
||||
return $this->mutateReportAction($scope, $suggestion, ...$summaries);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function fetchReportsAction(?Scope $scope): ResponseInterface
|
||||
{
|
||||
$demand = ReportDemand::create();
|
||||
$demand->scope = $scope;
|
||||
$reports = $this->reportRepository->findAllSummarized($demand);
|
||||
// @todo not sure whether this is a good idea performance-wise
|
||||
$reports = array_map(
|
||||
function (SummarizedReport $report): SummarizedReport {
|
||||
$event = $this->dispatchInvestigateMutationsEvent($report);
|
||||
if ($event->getMutationSuggestions() !== []) {
|
||||
$mutationHashes = array_map(
|
||||
static fn(MutationSuggestion $suggestion): string => $suggestion->hash(),
|
||||
$event->getMutationSuggestions()
|
||||
);
|
||||
$report = $report->withMutationHashes(...$mutationHashes)
|
||||
->withAttribute(ReportAttribute::fixable);
|
||||
}
|
||||
return $report;
|
||||
},
|
||||
$reports
|
||||
);
|
||||
return new JsonResponse($reports);
|
||||
}
|
||||
|
||||
protected function muteReportAction(string ...$summaries): ResponseInterface
|
||||
{
|
||||
$reports = $this->reportRepository->findBySummary(...$summaries);
|
||||
$uuids = array_map(static fn(Report $report): UuidV4 => $report->uuid, $reports);
|
||||
$this->reportRepository->updateStatus(ReportStatus::Muted, ...$uuids);
|
||||
return new JsonResponse(['uuids' => $uuids]);
|
||||
}
|
||||
|
||||
protected function deleteReportAction(string ...$summaries): ResponseInterface
|
||||
{
|
||||
$reports = $this->reportRepository->findBySummary(...$summaries);
|
||||
$reportUuids = $this->resolveReportUuids(...$reports);
|
||||
$this->reportRepository->updateStatus(ReportStatus::Deleted, ...$reportUuids);
|
||||
return new JsonResponse(['uuids' => $reportUuids]);
|
||||
}
|
||||
|
||||
protected function deleteReportsAction(?Scope $scope): ResponseInterface
|
||||
{
|
||||
$amount = $this->reportRepository->removeAll($scope);
|
||||
return new JsonResponse(['amount' => $amount]);
|
||||
}
|
||||
|
||||
protected function handleReportAction(UuidV4 $uuid): ResponseInterface
|
||||
{
|
||||
$report = $this->reportRepository->findByUuid($uuid);
|
||||
if ($report === null) {
|
||||
return new JsonResponse();
|
||||
}
|
||||
$event = $this->dispatchInvestigateMutationsEvent($report);
|
||||
$suggestions = $event->getMutationSuggestions();
|
||||
// reverse sort by priority (higher priorities take precedence)
|
||||
usort($suggestions, static fn(MutationSuggestion $a, MutationSuggestion $b) => $b->priority <=> $a->priority);
|
||||
return new JsonResponse($suggestions);
|
||||
}
|
||||
|
||||
protected function mutateReportAction(Scope $scope, MutationSuggestion $suggestion, string ...$initiators): ResponseInterface
|
||||
{
|
||||
$summary = $this->generateResolutionSummary($scope, $suggestion);
|
||||
$resolution = $this->resolutionRepository->findBySummary($summary);
|
||||
$reports = $this->reportRepository->findBySummary(...$initiators);
|
||||
if ($resolution !== null || $reports === []) {
|
||||
return new JsonResponse();
|
||||
}
|
||||
$resolution = new Resolution($summary, $scope, $suggestion->identifier, $suggestion->collection, ['initiators' => $initiators]);
|
||||
$this->resolutionRepository->add($resolution);
|
||||
$reportUuids = $this->resolveReportUuids(...$reports);
|
||||
$this->reportRepository->updateStatus(ReportStatus::Handled, ...$reportUuids);
|
||||
return new JsonResponse(['initiators' => $initiators, 'uuids' => $reportUuids]);
|
||||
}
|
||||
|
||||
protected function dispatchInvestigateMutationsEvent(Report $report): InvestigateMutationsEvent
|
||||
{
|
||||
// @todo for future versions, it might be considered to distinguish `enforce` and `report` in the database
|
||||
$policy = $this->policyProvider->provideFor($report->scope, $report->details->resolveDisposition());
|
||||
$event = new InvestigateMutationsEvent($policy, $report);
|
||||
$this->eventDispatcher->dispatch($event);
|
||||
return $event;
|
||||
}
|
||||
|
||||
protected function generateResolutionSummary(Scope $scope, MutationSuggestion $suggestion): string
|
||||
{
|
||||
return $this->hashService->hmac(
|
||||
json_encode([
|
||||
$scope,
|
||||
$suggestion->identifier,
|
||||
$suggestion->collection,
|
||||
]),
|
||||
self::class,
|
||||
);
|
||||
}
|
||||
|
||||
protected function resolveReportUuids(Report ...$reports): array
|
||||
{
|
||||
return array_map(static fn(Report $report): UuidV4 => $report->uuid, $reports);
|
||||
}
|
||||
|
||||
protected function isSystemMaintainer(): bool
|
||||
{
|
||||
$backendUser = $GLOBALS['BE_USER'] ?? null;
|
||||
return $backendUser instanceof BackendUserAuthentication && $backendUser->isSystemMaintainer();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Backend\Security\ContentSecurityPolicy;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Backend\Attribute\AsController;
|
||||
use TYPO3\CMS\Backend\Module\ModuleInterface;
|
||||
use TYPO3\CMS\Backend\Routing\UriBuilder;
|
||||
use TYPO3\CMS\Backend\Template\Components\ButtonBar;
|
||||
use TYPO3\CMS\Backend\Template\Components\ComponentFactory;
|
||||
use TYPO3\CMS\Backend\Template\ModuleTemplate;
|
||||
use TYPO3\CMS\Backend\Template\ModuleTemplateFactory;
|
||||
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
||||
use TYPO3\CMS\Core\Configuration\Features;
|
||||
use TYPO3\CMS\Core\Imaging\IconFactory;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Page\PageRenderer;
|
||||
use TYPO3\CMS\Core\Security\ContentSecurityPolicy\ScopeRepository;
|
||||
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
|
||||
|
||||
/**
|
||||
* Content-Security-Policy backend module view, loading the CSP lit-element and providing the current context.
|
||||
* @internal This is a specific Backend Controller implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
#[AsController]
|
||||
readonly class CspModuleController
|
||||
{
|
||||
public function __construct(
|
||||
protected Features $features,
|
||||
protected UriBuilder $uriBuilder,
|
||||
protected PageRenderer $pageRenderer,
|
||||
protected ScopeRepository $scopeRepository,
|
||||
protected ModuleTemplateFactory $moduleTemplateFactory,
|
||||
protected IconFactory $iconFactory,
|
||||
protected ComponentFactory $componentFactory,
|
||||
) {}
|
||||
|
||||
public function mainAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$view = $this->moduleTemplateFactory->create($request);
|
||||
$this->registerDocHeaderButtons($view, $request->getAttribute('module'));
|
||||
$view->assignMultiple([
|
||||
'configurationStatus' => $this->getConfigurationStatus(),
|
||||
'scopes' => array_map(strval(...), $this->scopeRepository->findAll()),
|
||||
'controlUri' => $this->uriBuilder->buildUriFromRoutePath('/ajax/security/csp/control'),
|
||||
'extLowlevelAvailable' => ExtensionManagementUtility::isLoaded('lowlevel'),
|
||||
]);
|
||||
return $view->renderResponse('Security/CspModule');
|
||||
}
|
||||
|
||||
protected function registerDocHeaderButtons(ModuleTemplate $view, ModuleInterface $currentModule): void
|
||||
{
|
||||
$view->getDocHeaderComponent()->setShortcutContext(
|
||||
$currentModule->getIdentifier(),
|
||||
$this->getLanguageService()->translate('title', 'backend.modules.content_security_policy')
|
||||
);
|
||||
$view->getDocHeaderComponent()->disableAutomaticReloadButton();
|
||||
$reloadButton = $this->componentFactory
|
||||
->createReloadButton((string)$this->uriBuilder->buildUriFromRoute($currentModule->getIdentifier()))
|
||||
->setDataAttributes(['csp-reports-handler' => 'refresh']);
|
||||
$view->addButtonToButtonBar($reloadButton, ButtonBar::BUTTON_POSITION_RIGHT);
|
||||
}
|
||||
|
||||
protected function getConfigurationStatus(): array
|
||||
{
|
||||
return [
|
||||
'featureDisabled' => array_filter([
|
||||
'backend' => [],
|
||||
'frontend' => !$this->features->isFeatureEnabled('security.frontend.enforceContentSecurityPolicy')
|
||||
&& !$this->features->isFeatureEnabled('security.frontend.reportContentSecurityPolicy')
|
||||
? ['enforce', 'report']
|
||||
: [],
|
||||
]),
|
||||
'customReporting' => array_filter([
|
||||
'BE' => $GLOBALS['TYPO3_CONF_VARS']['BE']['contentSecurityPolicyReportingUrl'] ?? '',
|
||||
'FE' => $GLOBALS['TYPO3_CONF_VARS']['FE']['contentSecurityPolicyReportingUrl'] ?? '',
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getBackendUser(): BackendUserAuthentication
|
||||
{
|
||||
return $GLOBALS['BE_USER'];
|
||||
}
|
||||
|
||||
protected function getLanguageService(): LanguageService
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Backend\Security;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Mailer\Exception\TransportException;
|
||||
use Symfony\Component\Mime\Exception\RfcComplianceException;
|
||||
use TYPO3\CMS\Core\Attribute\AsEventListener;
|
||||
use TYPO3\CMS\Core\Authentication\AbstractUserAuthentication;
|
||||
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
||||
use TYPO3\CMS\Core\Authentication\Event\AfterUserLoggedInEvent;
|
||||
use TYPO3\CMS\Core\Core\SystemEnvironmentBuilder;
|
||||
use TYPO3\CMS\Core\Http\ServerRequestFactory;
|
||||
use TYPO3\CMS\Core\Mail\MailerInterface;
|
||||
use TYPO3\CMS\Core\Mail\TemplatedEmailFactory;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Sends out an email if a backend user has just been logged in.
|
||||
*
|
||||
* Relevant settings:
|
||||
* $GLOBALS['TYPO3_CONF_VARS']['BE']['warning_mode']
|
||||
* $GLOBALS['TYPO3_CONF_VARS']['BE']['warning_email_addr']
|
||||
* $BE_USER->getUserSettings()->isEmailMeAtLoginEnabled()
|
||||
*
|
||||
* @internal this is not part of TYPO3 API as this is an internal hook
|
||||
*/
|
||||
final class EmailLoginNotification
|
||||
{
|
||||
private int $warningMode = 0;
|
||||
private string $warningEmailRecipient = '';
|
||||
|
||||
/**
|
||||
* @var ServerRequestInterface
|
||||
*/
|
||||
private $request;
|
||||
|
||||
public function __construct(
|
||||
private readonly MailerInterface $mailer,
|
||||
private readonly TemplatedEmailFactory $emailFactory,
|
||||
private readonly LoggerInterface $logger,
|
||||
) {
|
||||
$this->warningMode = (int)($GLOBALS['TYPO3_CONF_VARS']['BE']['warning_mode'] ?? 0);
|
||||
$this->warningEmailRecipient = $GLOBALS['TYPO3_CONF_VARS']['BE']['warning_email_addr'] ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an email notification to warning_email_address and/or the logged-in user's email address.
|
||||
*/
|
||||
#[AsEventListener('typo3/cms-backend/login-notification')]
|
||||
public function emailAtLogin(AfterUserLoggedInEvent $event): void
|
||||
{
|
||||
if (!$event->getUser() instanceof BackendUserAuthentication) {
|
||||
return;
|
||||
}
|
||||
$currentUser = $event->getUser();
|
||||
$user = $currentUser->user;
|
||||
$genericLoginWarning = $this->warningMode > 0 && !empty($this->warningEmailRecipient);
|
||||
$userLoginNotification = $currentUser->getUserSettings()->isEmailMeAtLoginEnabled() && GeneralUtility::validEmail($user['email']);
|
||||
if (!$genericLoginWarning && !$userLoginNotification) {
|
||||
return;
|
||||
}
|
||||
$this->request = $event->getRequest() ?? $GLOBALS['TYPO3_REQUEST'] ?? ServerRequestFactory::fromGlobals()
|
||||
->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE);
|
||||
|
||||
if ($genericLoginWarning) {
|
||||
$prefix = $currentUser->isAdmin() ? '[AdminLoginWarning]' : '[LoginWarning]';
|
||||
if ($this->warningMode & 1) {
|
||||
// First bit: Send warning email on any login
|
||||
$this->sendEmail($this->warningEmailRecipient, $currentUser, $prefix);
|
||||
} elseif ($currentUser->isAdmin() && $this->warningMode & 2) {
|
||||
// Second bit: Only send warning email when an admin logs in
|
||||
$this->sendEmail($this->warningEmailRecipient, $currentUser, $prefix);
|
||||
}
|
||||
}
|
||||
// Trigger an email to the current BE user, if this has been enabled in the user configuration
|
||||
if ($userLoginNotification) {
|
||||
$this->sendEmail($user['email'], $currentUser);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an email.
|
||||
*/
|
||||
private function sendEmail(string $recipient, AbstractUserAuthentication $user, ?string $subjectPrefix = null): void
|
||||
{
|
||||
$headline = 'TYPO3 Backend Login notification';
|
||||
$recipients = explode(',', $recipient);
|
||||
$email = $this->emailFactory->create($this->request)
|
||||
->to(...$recipients)
|
||||
->setTemplate('Security/LoginNotification')
|
||||
->assignMultiple([
|
||||
'user' => $user->user,
|
||||
'prefix' => $subjectPrefix,
|
||||
'language' => ($user->user['lang'] ?? '') ?: 'en',
|
||||
'headline' => $headline,
|
||||
]);
|
||||
try {
|
||||
$this->mailer->send($email);
|
||||
} catch (TransportException $e) {
|
||||
$this->logger->warning('Could not send notification email to "{recipient}" due to mailer settings error', [
|
||||
'recipient' => $recipient,
|
||||
'userId' => $user->user['uid'] ?? 0,
|
||||
'recipientList' => $recipients,
|
||||
'exception' => $e,
|
||||
]);
|
||||
} catch (RfcComplianceException $e) {
|
||||
$this->logger->warning('Could not send notification email to "{recipient}" due to invalid email address', [
|
||||
'recipient' => $recipient,
|
||||
'userId' => $user->user['uid'] ?? 0,
|
||||
'recipientList' => $recipients,
|
||||
'exception' => $e,
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
// Catch all other exceptions, otherwise a failed email login notification will keep
|
||||
// a user from logging in. See https://forge.typo3.org/issues/103546
|
||||
$this->logger->error('Could not send notification email to "{recipient}" due to a PHP exception', [
|
||||
'recipient' => $recipient,
|
||||
'userId' => $user->user['uid'] ?? 0,
|
||||
'recipientList' => $recipients,
|
||||
'exception' => $e,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Backend\Security\SudoMode\Access;
|
||||
|
||||
/**
|
||||
* Representation of a claim (request) to a specific subject, before being granted.
|
||||
* The user still has to verify, that this claim is correct, by entering their password.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class AccessClaim implements \JsonSerializable
|
||||
{
|
||||
public readonly string $id;
|
||||
|
||||
/**
|
||||
* @var AccessSubjectInterface[]
|
||||
*/
|
||||
public readonly array $subjects;
|
||||
|
||||
public function __construct(
|
||||
public readonly ServerRequestInstruction $instruction,
|
||||
public readonly int $expiration,
|
||||
public ?string $origin = null,
|
||||
?string $id = null,
|
||||
AccessSubjectInterface ...$subjects,
|
||||
) {
|
||||
$this->subjects = $subjects;
|
||||
$this->id = $id ?? bin2hex(random_bytes(20));
|
||||
}
|
||||
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return [
|
||||
'subjects' => $this->subjects,
|
||||
'instruction' => $this->instruction,
|
||||
'expiration' => $this->expiration,
|
||||
'origin' => $this->origin,
|
||||
'id' => $this->id,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Backend\Security\SudoMode\Access;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
use TYPO3\CMS\Backend\Routing\Route;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Factory to create `AccessClaim`, `AccessGrant` and `RouteAccessSubject` instances.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
#[Autoconfigure(public: true)]
|
||||
class AccessFactory
|
||||
{
|
||||
protected const DEFAULT_CLAIM_LIFETIME = 300;
|
||||
|
||||
protected readonly int $currentTimestamp;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->currentTimestamp = (int)($GLOBALS['EXEC_TIME'] ?? time());
|
||||
}
|
||||
|
||||
public function buildClaimFromArray(array $data): AccessClaim
|
||||
{
|
||||
return GeneralUtility::makeInstance(
|
||||
AccessClaim::class,
|
||||
ServerRequestInstruction::buildFromArray($data['instruction']),
|
||||
$data['expiration'] ?? 0,
|
||||
$data['origin'] ?? null,
|
||||
$data['id'] ?? null,
|
||||
...array_map(fn(array $subject) => $this->buildSubjectFromArray($subject), $data['subjects']),
|
||||
);
|
||||
}
|
||||
|
||||
public function buildGrantFromArray(array $data): AccessGrant
|
||||
{
|
||||
return GeneralUtility::makeInstance(
|
||||
AccessGrant::class,
|
||||
$this->buildSubjectFromArray($data['subject']),
|
||||
$data['expiration']
|
||||
);
|
||||
}
|
||||
|
||||
public function buildSubjectFromArray(array $data): AccessSubjectInterface
|
||||
{
|
||||
$className = $data['class'] ?? '[empty]';
|
||||
if (is_a($className, AccessSubjectInterface::class, true)) {
|
||||
return $className::fromArray($data);
|
||||
}
|
||||
throw new \LogicException(
|
||||
sprintf('Subject %s does not implement %s', $className, AccessSubjectInterface::class),
|
||||
1605861181
|
||||
);
|
||||
}
|
||||
|
||||
public function buildRouteAccessSubject(ServerRequestInterface $request): RouteAccessSubject
|
||||
{
|
||||
/** @var ?Route $route */
|
||||
$route = $request->getAttribute('route');
|
||||
if ($route === null) {
|
||||
throw new \LogicException(
|
||||
'Missing route request attribute',
|
||||
1605861905
|
||||
);
|
||||
}
|
||||
$settings = $route->getOption('sudoMode');
|
||||
return GeneralUtility::makeInstance(
|
||||
RouteAccessSubject::class,
|
||||
rtrim($route->getPath(), '/'),
|
||||
$settings['lifetime'] ?? null,
|
||||
$settings['group'] ?? null
|
||||
);
|
||||
}
|
||||
|
||||
public function buildTableAccessSubject(string $tableName, string $fieldName, string $id, array $settings): TableAccessSubject
|
||||
{
|
||||
$subjectParts = array_filter(
|
||||
[$tableName, $fieldName, $id],
|
||||
static fn(string $part): bool => $part !== ''
|
||||
);
|
||||
return GeneralUtility::makeInstance(
|
||||
TableAccessSubject::class,
|
||||
implode('.', $subjectParts),
|
||||
$settings['lifetime'] ?? null,
|
||||
$settings['group'] ?? null,
|
||||
$settings['once'] ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
public function buildClaimForSubjectRequest(ServerRequestInterface $request, ?string $origin, AccessSubjectInterface ...$subjects): AccessClaim
|
||||
{
|
||||
return GeneralUtility::makeInstance(
|
||||
AccessClaim::class,
|
||||
ServerRequestInstruction::createForServerRequest($request),
|
||||
$this->currentTimestamp + self::DEFAULT_CLAIM_LIFETIME,
|
||||
$origin,
|
||||
null,
|
||||
...$subjects
|
||||
);
|
||||
}
|
||||
|
||||
public function buildGrantForSubject(AccessSubjectInterface $subject): AccessGrant
|
||||
{
|
||||
return GeneralUtility::makeInstance(
|
||||
AccessGrant::class,
|
||||
$subject,
|
||||
$this->currentTimestamp + $subject->getLifetime()->inSeconds()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Backend\Security\SudoMode\Access;
|
||||
|
||||
/**
|
||||
* Representation of any granted access to a particular subject, having an expiration time.
|
||||
* The user successfully verified a previous `AccessClaim` by entering their password.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
readonly class AccessGrant implements \JsonSerializable
|
||||
{
|
||||
public function __construct(
|
||||
public AccessSubjectInterface $subject,
|
||||
public int $expiration,
|
||||
) {}
|
||||
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return [
|
||||
'subject' => $this->subject,
|
||||
'expiration' => $this->expiration,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Backend\Security\SudoMode\Access;
|
||||
|
||||
/**
|
||||
* Defines the lifetime of the sudo mode in a human-readable form.
|
||||
*/
|
||||
enum AccessLifetime: string
|
||||
{
|
||||
case veryShort = 'veryShort';
|
||||
case short = 'short';
|
||||
case medium = 'medium';
|
||||
case long = 'long';
|
||||
case veryLong = 'veryLong';
|
||||
|
||||
public function inSeconds(): int
|
||||
{
|
||||
return self::lifetimes()[$this] * 60;
|
||||
}
|
||||
|
||||
private static function lifetimes(): \WeakMap
|
||||
{
|
||||
$map = new \WeakMap();
|
||||
$map[self::veryShort] = 5;
|
||||
$map[self::short] = 10;
|
||||
$map[self::medium] = 15;
|
||||
$map[self::long] = 30;
|
||||
$map[self::veryLong] = 60;
|
||||
return $map;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Backend\Security\SudoMode\Access;
|
||||
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
||||
|
||||
/**
|
||||
* Wrapper for storing `AccessClaim` and `AccessGrant` items in the backend user session storage.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
#[Autoconfigure(public: true)]
|
||||
readonly class AccessStorage
|
||||
{
|
||||
protected const CLAIM_KEY = 'backend.sudo-mode.claim';
|
||||
protected const GRANT_KEY = 'backend.sudo-mode.grant';
|
||||
|
||||
protected int $currentTimestamp;
|
||||
|
||||
public function __construct(
|
||||
protected AccessFactory $factory,
|
||||
protected LoggerInterface $logger,
|
||||
) {
|
||||
$this->currentTimestamp = (int)($GLOBALS['EXEC_TIME'] ?? time());
|
||||
}
|
||||
|
||||
public function findGrantsBySubject(AccessSubjectInterface $subject): array
|
||||
{
|
||||
$relevantItems = array_filter(
|
||||
$this->fetchGrants(),
|
||||
// either group matches (if given), or subject matches
|
||||
fn(array $item) => $this->subjectMatchesItem($subject, $item)
|
||||
);
|
||||
return array_map($this->factory->buildGrantFromArray(...), $relevantItems);
|
||||
}
|
||||
|
||||
public function addGrant(AccessGrant $grant): void
|
||||
{
|
||||
$items = $this->fetchGrants();
|
||||
$identity = $grant->subject->getIdentity();
|
||||
if (isset($items[$identity])) {
|
||||
$this->logger->warning(
|
||||
sprintf('Grant %s does already exist', $identity),
|
||||
$grant->jsonSerialize()
|
||||
);
|
||||
}
|
||||
$items[$identity] = $grant;
|
||||
$this->commitItems(self::GRANT_KEY, $items);
|
||||
}
|
||||
|
||||
public function removeGrant(AccessGrant $grant): void
|
||||
{
|
||||
$items = $this->fetchGrants();
|
||||
$identity = $grant->subject->getIdentity();
|
||||
if (!isset($items[$identity])) {
|
||||
$this->logger->warning(
|
||||
sprintf('Grant %s does not exist', $identity),
|
||||
$grant->jsonSerialize()
|
||||
);
|
||||
}
|
||||
unset($items[$identity]);
|
||||
$this->commitItems(self::GRANT_KEY, $items);
|
||||
}
|
||||
|
||||
public function findClaimById(string $id): ?AccessClaim
|
||||
{
|
||||
$item = $this->fetchClaims()[$id] ?? null;
|
||||
return !empty($item) ? $this->factory->buildClaimFromArray($item) : null;
|
||||
}
|
||||
|
||||
public function findClaimBySubject(AccessSubjectInterface $subject): ?AccessClaim
|
||||
{
|
||||
foreach ($this->fetchClaims() as $item) {
|
||||
if ($this->subjectMatchesItem($subject, $item)) {
|
||||
return $this->factory->buildClaimFromArray($item);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public function addClaim(AccessClaim $claim): void
|
||||
{
|
||||
$items = $this->fetchClaims();
|
||||
$items[$claim->id] = $claim;
|
||||
$this->commitItems(self::CLAIM_KEY, $items);
|
||||
}
|
||||
|
||||
public function removeClaim(AccessClaim $claim): void
|
||||
{
|
||||
$items = $this->fetchClaims();
|
||||
unset($items[$claim->id]);
|
||||
$this->commitItems(self::CLAIM_KEY, $items);
|
||||
}
|
||||
|
||||
protected function fetchGrants(): array
|
||||
{
|
||||
return $this->fetchItems(self::GRANT_KEY);
|
||||
}
|
||||
|
||||
protected function fetchClaims(): array
|
||||
{
|
||||
return $this->fetchItems(self::CLAIM_KEY);
|
||||
}
|
||||
|
||||
protected function fetchItems(string $sessionKey): array
|
||||
{
|
||||
$sessionData = $this->getBackendUser()->getSessionData($sessionKey);
|
||||
$items = json_decode((string)$sessionData, true, 16) ?? [];
|
||||
$purgedItems = array_filter(
|
||||
$items,
|
||||
fn(array $item) => ($item['expiration'] ?? 0) >= $this->currentTimestamp
|
||||
);
|
||||
if (count($purgedItems) < count($items)) {
|
||||
$this->commitItems($sessionKey, $purgedItems);
|
||||
}
|
||||
return $purgedItems;
|
||||
}
|
||||
|
||||
protected function commitItems(string $sessionKey, array $items): void
|
||||
{
|
||||
// using `json_encode` here, since `UserSession` still uses PHP `serialize`
|
||||
$this->getBackendUser()->setAndSaveSessionData($sessionKey, json_encode($items, JSON_INVALID_UTF8_SUBSTITUTE));
|
||||
}
|
||||
|
||||
protected function subjectMatchesItem(AccessSubjectInterface $subject, array $item): bool
|
||||
{
|
||||
// either group matches (if given), or subject matches
|
||||
return ($item['subject']['identity'] ?? null) === $subject->getIdentity()
|
||||
|| ($subject->getGroup() !== null && ($item['subject']['group'] ?? null) === $subject->getGroup());
|
||||
}
|
||||
|
||||
protected function getBackendUser(): BackendUserAuthentication
|
||||
{
|
||||
return $GLOBALS['BE_USER'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Backend\Security\SudoMode\Access;
|
||||
|
||||
/**
|
||||
* Base interface for a subject that shall be handled during the sudo mode process.
|
||||
* A "subject" can be a resource, a route, a database record, anything.
|
||||
* Specific implementations of this interface provide the details and behavior.
|
||||
*/
|
||||
interface AccessSubjectInterface extends \JsonSerializable
|
||||
{
|
||||
/**
|
||||
* Reconstitutes a subject object from its serialized representation.
|
||||
*/
|
||||
public static function fromArray(array $data): static;
|
||||
|
||||
/**
|
||||
* Provides a unique string identifier of the subject.
|
||||
*/
|
||||
public function getIdentity(): string;
|
||||
|
||||
/**
|
||||
* Provides the actual subject name (e.g. a route, an aspect, a resource, ...)
|
||||
*/
|
||||
public function getSubject(): string;
|
||||
|
||||
/**
|
||||
* If given, grants access to same-group sudo mode subjects.
|
||||
*/
|
||||
public function getGroup(): ?string;
|
||||
|
||||
/**
|
||||
* Provides a distinct lifetime type, e.g. XS, S, M, L, XL.
|
||||
*/
|
||||
public function getLifetime(): AccessLifetime;
|
||||
|
||||
/**
|
||||
* If true, the subject may only be used once and requires a new grant for the same task.
|
||||
*/
|
||||
public function isOnce(): bool;
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Backend\Security\SudoMode\Access;
|
||||
|
||||
/**
|
||||
* Representation of a backend route (and implicitly a module) that
|
||||
* shall be handled during the sudo mode process.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class RouteAccessSubject implements AccessSubjectInterface
|
||||
{
|
||||
/**
|
||||
* The route subject, e.g. `/module/system/maintenance`
|
||||
*/
|
||||
protected string $subject;
|
||||
/**
|
||||
* The distinct lifetime type, e.g. XS, S, M, L, XL
|
||||
*/
|
||||
protected AccessLifetime $lifetime;
|
||||
/**
|
||||
* If given, grants access to same-group sudo mode subjects.
|
||||
* Example: If access to admin tool route "maintenance" (of group "systemMaintainer")
|
||||
* was granted, access to other groups, like "settings" or "upgrade" are granted as well.
|
||||
*/
|
||||
protected ?string $group;
|
||||
|
||||
public static function fromArray(array $data): static
|
||||
{
|
||||
$subject = $data['subject'] ?? null;
|
||||
$lifetime = AccessLifetime::tryFrom($data['lifetime']);
|
||||
$group = $data['group'] ?? null;
|
||||
if (!is_string($subject)) {
|
||||
throw new \LogicException('Property subject must be of type string', 1681111813);
|
||||
}
|
||||
if ($lifetime === null) {
|
||||
throw new \LogicException('Property lifetime cannot be resolved', 1681111814);
|
||||
}
|
||||
if ($group !== null && !is_string($group)) {
|
||||
throw new \LogicException('Property group must be of type string, or omitted', 1681111815);
|
||||
}
|
||||
return new static($subject, $lifetime, $group);
|
||||
}
|
||||
|
||||
final public function __construct(string $subject, ?AccessLifetime $lifetime = null, ?string $group = null)
|
||||
{
|
||||
$this->subject = $subject;
|
||||
$this->lifetime = $lifetime ?? AccessLifetime::veryShort;
|
||||
$this->group = $group;
|
||||
}
|
||||
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return [
|
||||
'class' => self::class,
|
||||
'identity' => $this->getIdentity(),
|
||||
'subject' => $this->subject,
|
||||
'lifetime' => $this->lifetime->value,
|
||||
'group' => $this->group,
|
||||
];
|
||||
}
|
||||
|
||||
public function getIdentity(): string
|
||||
{
|
||||
return sprintf('route:%s', $this->subject);
|
||||
}
|
||||
|
||||
public function getSubject(): string
|
||||
{
|
||||
return $this->subject;
|
||||
}
|
||||
|
||||
public function getGroup(): ?string
|
||||
{
|
||||
return $this->group;
|
||||
}
|
||||
|
||||
public function getLifetime(): AccessLifetime
|
||||
{
|
||||
return $this->lifetime;
|
||||
}
|
||||
|
||||
public function isOnce(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Backend\Security\SudoMode\Access;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
use Psr\Http\Message\UriInterface;
|
||||
use TYPO3\CMS\Core\Http\Stream;
|
||||
use TYPO3\CMS\Core\Http\Uri;
|
||||
|
||||
/**
|
||||
* Reduced representation of `ServerRequest` information, which is used
|
||||
* to replay the intercepted request later, once access was granted.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class ServerRequestInstruction implements \JsonSerializable
|
||||
{
|
||||
/**
|
||||
* Attribute names that shall be taken from the original request
|
||||
*/
|
||||
protected const KEEP_ATTRIBUTE_NAMES = [
|
||||
'applicationType',
|
||||
];
|
||||
|
||||
protected string $requestTarget;
|
||||
protected string $method;
|
||||
protected UriInterface $uri;
|
||||
protected StreamInterface $body;
|
||||
protected ?array $parsedBody;
|
||||
protected array $queryParams;
|
||||
protected array $attributes;
|
||||
protected array $serverParams;
|
||||
protected array $headers;
|
||||
|
||||
public static function createForServerRequest(ServerRequestInterface $request): self
|
||||
{
|
||||
$target = new self();
|
||||
$target->requestTarget = $request->getRequestTarget();
|
||||
$target->method = $request->getMethod();
|
||||
$target->uri = self::clone($request->getUri());
|
||||
$target->body = self::clone($request->getBody());
|
||||
$target->parsedBody = self::clone($request->getParsedBody());
|
||||
$target->queryParams = $request->getQueryParams();
|
||||
$target->serverParams = $request->getServerParams();
|
||||
$target->headers = $request->getHeaders();
|
||||
$target->attributes = array_filter(
|
||||
$request->getAttributes(),
|
||||
static fn(string $name) => in_array($name, self::KEEP_ATTRIBUTE_NAMES, true),
|
||||
ARRAY_FILTER_USE_KEY
|
||||
);
|
||||
return $target;
|
||||
}
|
||||
|
||||
public static function buildFromArray(array $data): self
|
||||
{
|
||||
$target = new self();
|
||||
$target->requestTarget = $data['requestTarget'];
|
||||
$target->method = $data['method'];
|
||||
$target->uri = new Uri($data['uri']);
|
||||
$target->body = new Stream('php://temp', 'w+b');
|
||||
$target->body->write($data['body']['contents']);
|
||||
$target->parsedBody = $data['parsedBody'];
|
||||
$target->queryParams = $data['queryParams'];
|
||||
$target->serverParams = $data['serverParams'];
|
||||
$target->headers = $data['headers'];
|
||||
$target->attributes = $data['attributes'] ?? [];
|
||||
return $target;
|
||||
}
|
||||
|
||||
protected static function clone($value)
|
||||
{
|
||||
if (is_object($value)) {
|
||||
return clone $value;
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
protected function __construct()
|
||||
{
|
||||
// avoid creating class instances directly from external
|
||||
}
|
||||
|
||||
protected function __clone()
|
||||
{
|
||||
// avoid cloning class instances directly from external
|
||||
}
|
||||
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return [
|
||||
'class' => self::class,
|
||||
'requestTarget' => $this->requestTarget,
|
||||
'method' => $this->method,
|
||||
'uri' => (string)$this->uri,
|
||||
'body' => [
|
||||
'contents' => (string)$this->body,
|
||||
],
|
||||
'parsedBody' => $this->parsedBody,
|
||||
'queryParams' => $this->queryParams,
|
||||
'serverParams' => $this->serverParams,
|
||||
'headers' => $this->headers,
|
||||
'attributes' => $this->attributes,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies instructions to given ServerRequest ("replaying the request").
|
||||
*/
|
||||
public function applyTo(ServerRequestInterface $request): ServerRequestInterface
|
||||
{
|
||||
$request = $request
|
||||
->withRequestTarget($this->requestTarget)
|
||||
->withMethod($this->method)
|
||||
->withUri($this->uri)
|
||||
->withBody($this->body)
|
||||
->withParsedBody($this->parsedBody)
|
||||
->withQueryParams($this->queryParams);
|
||||
foreach ($this->attributes as $name => $value) {
|
||||
$request = $request->withAttribute($name, $value);
|
||||
}
|
||||
return $request;
|
||||
}
|
||||
|
||||
public function getRequestTarget(): string
|
||||
{
|
||||
return $this->requestTarget;
|
||||
}
|
||||
|
||||
public function getMethod(): string
|
||||
{
|
||||
return $this->method;
|
||||
}
|
||||
|
||||
public function getUri(): UriInterface
|
||||
{
|
||||
return $this->uri;
|
||||
}
|
||||
|
||||
public function getBody(): StreamInterface
|
||||
{
|
||||
return $this->body;
|
||||
}
|
||||
|
||||
public function getParsedBody(): ?array
|
||||
{
|
||||
return $this->parsedBody;
|
||||
}
|
||||
|
||||
public function getQueryParams(): array
|
||||
{
|
||||
return $this->queryParams;
|
||||
}
|
||||
|
||||
public function getServerParams(): array
|
||||
{
|
||||
return $this->serverParams;
|
||||
}
|
||||
|
||||
public function getHeaders(): array
|
||||
{
|
||||
return $this->headers;
|
||||
}
|
||||
|
||||
public function getAttributes(): array
|
||||
{
|
||||
return $this->attributes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Backend\Security\SudoMode\Access;
|
||||
|
||||
/**
|
||||
* Representation of a table or table column that
|
||||
* shall be handled during the sudo mode process.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class TableAccessSubject implements AccessSubjectInterface
|
||||
{
|
||||
/**
|
||||
* The table column subject, e.g. `tx_foo`, `tx_foo.bar` or `tx_foo.bar.123`
|
||||
*/
|
||||
protected string $subject;
|
||||
|
||||
/**
|
||||
* The distinct lifetime type, e.g. XS, S, M, L, XL
|
||||
*/
|
||||
protected AccessLifetime $lifetime;
|
||||
|
||||
/**
|
||||
* If given, grants access to same-group sudo mode subjects.
|
||||
*/
|
||||
protected ?string $group;
|
||||
|
||||
/**
|
||||
* If true, the subject may only be used once and requires a new grant for the same task.
|
||||
*/
|
||||
protected bool $once;
|
||||
|
||||
public static function fromArray(array $data): static
|
||||
{
|
||||
$subject = $data['subject'] ?? null;
|
||||
$lifetime = AccessLifetime::tryFrom($data['lifetime']);
|
||||
$group = $data['group'] ?? null;
|
||||
$once = $data['once'] ?? null;
|
||||
if (!is_string($subject)) {
|
||||
throw new \LogicException('Property subject must be of type string', 1743646793);
|
||||
}
|
||||
if ($lifetime === null) {
|
||||
throw new \LogicException('Property lifetime cannot be resolved', 1743646794);
|
||||
}
|
||||
if ($group !== null && !is_string($group)) {
|
||||
throw new \LogicException('Property group must be of type string, or omitted', 1743646795);
|
||||
}
|
||||
if ($once !== null && !is_bool($once)) {
|
||||
throw new \LogicException('Property once must be of type bool, or omitted', 1743646796);
|
||||
}
|
||||
return new static($subject, $lifetime, $group, $once);
|
||||
}
|
||||
|
||||
final public function __construct(
|
||||
string $subject,
|
||||
?AccessLifetime $lifetime = null,
|
||||
?string $group = null,
|
||||
?bool $once = null,
|
||||
) {
|
||||
$this->subject = $subject;
|
||||
$this->lifetime = $lifetime ?? AccessLifetime::veryShort;
|
||||
$this->group = $group;
|
||||
$this->once = $once;
|
||||
}
|
||||
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return [
|
||||
'class' => self::class,
|
||||
'identity' => $this->getIdentity(),
|
||||
'subject' => $this->subject,
|
||||
'lifetime' => $this->lifetime->value,
|
||||
'group' => $this->group,
|
||||
'once' => $this->once,
|
||||
];
|
||||
}
|
||||
|
||||
public function getIdentity(): string
|
||||
{
|
||||
return sprintf('table:%s', $this->subject);
|
||||
}
|
||||
|
||||
public function getSubject(): string
|
||||
{
|
||||
return $this->subject;
|
||||
}
|
||||
|
||||
public function getGroup(): ?string
|
||||
{
|
||||
return $this->group;
|
||||
}
|
||||
|
||||
public function getLifetime(): AccessLifetime
|
||||
{
|
||||
return $this->lifetime;
|
||||
}
|
||||
|
||||
public function isOnce(): bool
|
||||
{
|
||||
return $this->once;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Backend\Security\SudoMode\Event;
|
||||
|
||||
use TYPO3\CMS\Backend\Security\SudoMode\Access\AccessClaim;
|
||||
|
||||
final class SudoModeRequiredEvent
|
||||
{
|
||||
private bool $verificationRequired = true;
|
||||
|
||||
public function __construct(private readonly AccessClaim $claim) {}
|
||||
|
||||
public function getClaim(): AccessClaim
|
||||
{
|
||||
return $this->claim;
|
||||
}
|
||||
|
||||
public function isVerificationRequired(): bool
|
||||
{
|
||||
return $this->verificationRequired;
|
||||
}
|
||||
|
||||
public function setVerificationRequired(bool $verificationRequired): void
|
||||
{
|
||||
$this->verificationRequired = $verificationRequired;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Backend\Security\SudoMode\Event;
|
||||
|
||||
use TYPO3\CMS\Backend\Security\SudoMode\Access\AccessClaim;
|
||||
|
||||
final class SudoModeVerifyEvent
|
||||
{
|
||||
private bool $verified = false;
|
||||
|
||||
public function __construct(
|
||||
private readonly AccessClaim $claim,
|
||||
#[\SensitiveParameter]
|
||||
private readonly string $password,
|
||||
private readonly bool $useInstallToolPassword,
|
||||
) {}
|
||||
|
||||
public function getClaim(): AccessClaim
|
||||
{
|
||||
return $this->claim;
|
||||
}
|
||||
|
||||
public function getPassword(): string
|
||||
{
|
||||
return $this->password;
|
||||
}
|
||||
|
||||
public function isUseInstallToolPassword(): bool
|
||||
{
|
||||
return $this->useInstallToolPassword;
|
||||
}
|
||||
|
||||
public function isVerified(): bool
|
||||
{
|
||||
return $this->verified;
|
||||
}
|
||||
|
||||
public function setVerified(bool $verified): void
|
||||
{
|
||||
$this->verified = $verified;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Backend\Security\SudoMode\Exception;
|
||||
|
||||
use TYPO3\CMS\Backend\Security\SudoMode\Access\ServerRequestInstruction;
|
||||
|
||||
/**
|
||||
* Exception that signals that the current user must verify the access for a
|
||||
* particular resource, route, module, etc. by entering their password again.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class RequestGrantedException extends \RuntimeException
|
||||
{
|
||||
private ServerRequestInstruction $instruction;
|
||||
|
||||
public function withInstruction(ServerRequestInstruction $instruction): self
|
||||
{
|
||||
$this->instruction = $instruction;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getInstruction(): ServerRequestInstruction
|
||||
{
|
||||
return $this->instruction;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Backend\Security\SudoMode\Exception;
|
||||
|
||||
use TYPO3\CMS\Backend\Security\SudoMode\Access\AccessClaim;
|
||||
|
||||
/**
|
||||
* Exception that signals that the verification process was successful, and that
|
||||
* the user shall be redirected to the URI, that has been requested originally.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class VerificationRequiredException extends \RuntimeException
|
||||
{
|
||||
private AccessClaim $claim;
|
||||
|
||||
public function withClaim(AccessClaim $bundle): self
|
||||
{
|
||||
$this->claim = $bundle;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getClaim(): AccessClaim
|
||||
{
|
||||
return $this->claim;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Backend\Security\SudoMode;
|
||||
|
||||
use TYPO3\CMS\Core\Authentication\AbstractAuthenticationService;
|
||||
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
||||
use TYPO3\CMS\Core\Crypto\PasswordHashing\InvalidPasswordHashException;
|
||||
use TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory;
|
||||
use TYPO3\CMS\Core\Http\ServerRequest;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Install\Controller\BackendModuleController;
|
||||
|
||||
/**
|
||||
* Service for either verifying the password of the current backend user
|
||||
* session, or the admin tool password (without actually going through
|
||||
* the complete authentication process).
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
readonly class PasswordVerification
|
||||
{
|
||||
public function __construct(
|
||||
protected PasswordHashFactory $passwordHashFactory
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Verifies that provided password matches Install Tool password.
|
||||
*/
|
||||
public function verifyInstallToolPassword(string $password): bool
|
||||
{
|
||||
$installToolPassword = $GLOBALS['TYPO3_CONF_VARS']['BE']['installToolPassword'] ?? null;
|
||||
if ($password === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->passwordHashFactory
|
||||
->get($installToolPassword, 'BE')
|
||||
->checkPassword($password, $installToolPassword);
|
||||
} catch (InvalidPasswordHashException) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that the provided password is actually correct for current backend user
|
||||
* by stepping through the authentication chain in `$GLOBALS['BE_USER]`.
|
||||
*/
|
||||
public function verifyBackendUserPassword(string $password, BackendUserAuthentication $backendUser): bool
|
||||
{
|
||||
if ($password === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// clone the current backend user object to avoid
|
||||
// possible side effects for the real instance
|
||||
$backendUser = clone $backendUser;
|
||||
$loginData = [
|
||||
'status' => 'sudo-mode',
|
||||
'origin' => BackendModuleController::class,
|
||||
'uname' => $backendUser->user['username'],
|
||||
'uident' => $password,
|
||||
];
|
||||
// currently there is no dedicated API to perform authentication
|
||||
// that's why this process partially has to be simulated here
|
||||
$fakeRequest = new ServerRequest();
|
||||
$loginData = $backendUser->processLoginData($loginData, $fakeRequest);
|
||||
$authInfo = $backendUser->getAuthInfoArray($fakeRequest);
|
||||
|
||||
$authenticated = false;
|
||||
/** @var AbstractAuthenticationService $service or any other service (sic!) */
|
||||
foreach ($this->getAuthServices($backendUser, $loginData, $authInfo) as $service) {
|
||||
if (!method_exists($service, 'authUser')) {
|
||||
// The abstract does not cover this method, but the actual implementations do.
|
||||
// Happy PHPStan, happy life (or so).
|
||||
continue;
|
||||
}
|
||||
|
||||
$ret = $service->authUser($backendUser->user);
|
||||
if ($ret <= 0) {
|
||||
return false;
|
||||
}
|
||||
if ($ret >= 200) {
|
||||
return true;
|
||||
}
|
||||
if ($ret < 100) {
|
||||
$authenticated = true;
|
||||
}
|
||||
}
|
||||
return $authenticated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes authentication services to be used in a foreach loop
|
||||
*
|
||||
* @return \Generator<int, object>
|
||||
*/
|
||||
protected function getAuthServices(BackendUserAuthentication $backendUser, array $loginData, array $authInfo): \Generator
|
||||
{
|
||||
$serviceChain = [];
|
||||
$subType = 'authUserBE';
|
||||
while ($service = GeneralUtility::makeInstanceService('auth', $subType, $serviceChain)) {
|
||||
if (!$service instanceof AbstractAuthenticationService) {
|
||||
continue;
|
||||
}
|
||||
$serviceChain[] = $service->getServiceKey();
|
||||
$service->initAuth($subType, $loginData, $authInfo, $backendUser);
|
||||
yield $service;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user