TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -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