TYPO3 v15 dev-main snapshot ()

This commit is contained in:
2026-08-10 22:31:00 +02:00
commit f9941541b7
1178 changed files with 135377 additions and 0 deletions
@@ -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;
}
}
}