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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user