<?php declare(strict_types=1);
namespace Shopware\Core\Framework\Api\Acl;
use Shopware\Core\Framework\Api\Acl\Event\CommandAclValidationEvent;
use Shopware\Core\Framework\Api\Acl\Role\AclRoleDefinition;
use Shopware\Core\Framework\Api\Context\AdminApiSource;
use Shopware\Core\Framework\Api\Context\AdminSalesChannelApiSource;
use Shopware\Core\Framework\Api\Exception\MissingPrivilegeException;
use Shopware\Core\Framework\Context;
use Shopware\Core\Framework\DataAbstractionLayer\EntityTranslationDefinition;
use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\WriteCommand;
use Shopware\Core\Framework\DataAbstractionLayer\Write\Validation\PreWriteValidationEvent;
use Shopware\Core\Framework\Uuid\Uuid;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
class AclWriteValidator implements EventSubscriberInterface
{
/**
* @var EventDispatcherInterface
*/
private $eventDispatcher;
/**
* @internal
*/
public function __construct(EventDispatcherInterface $eventDispatcher)
{
$this->eventDispatcher = $eventDispatcher;
}
/**
* @return array<string, string|array{0: string, 1: int}|list<array{0: string, 1?: int}>>
*/
public static function getSubscribedEvents()
{
return [PreWriteValidationEvent::class => 'preValidate'];
}
public function preValidate(PreWriteValidationEvent $event): void
{
$context = $event->getContext();
$source = $event->getContext()->getSource();
if ($source instanceof AdminSalesChannelApiSource) {
$context = $source->getOriginalContext();
$source = $context->getSource();
}
if ($context->getScope() === Context::SYSTEM_SCOPE || !$source instanceof AdminApiSource || $source->isAdmin()) {
return;
}
$commands = $event->getCommands();
$missingPrivileges = [];
foreach ($commands as $command) {
$resource = $command->getDefinition()->getEntityName();
$privilege = $command->getPrivilege();
if ($privilege === null) {
continue;
}
if (is_subclass_of($command->getDefinition(), EntityTranslationDefinition::class)) {
$resource = $command->getDefinition()->getParentDefinition()->getEntityName();
if ($privilege !== AclRoleDefinition::PRIVILEGE_DELETE) {
$privilege = $this->getPrivilegeForParentWriteOperation($command, $commands);
}
}
if (!$source->isAllowed($resource . ':' . $privilege)) {
$missingPrivileges[] = $resource . ':' . $privilege;
}
$event = new CommandAclValidationEvent($missingPrivileges, $source, $command);
$this->eventDispatcher->dispatch($event);
$missingPrivileges = $event->getMissingPrivileges();
}
$this->tryToThrow($missingPrivileges);
}
private function tryToThrow(array $missingPrivileges): void
{
if (!empty($missingPrivileges)) {
throw new MissingPrivilegeException($missingPrivileges);
}
}
/**
* @param WriteCommand[] $commands
*/
private function getPrivilegeForParentWriteOperation(WriteCommand $command, array $commands): string
{
$pathSuffix = '/translations/' . Uuid::fromBytesToHex($command->getPrimaryKey()['language_id']);
$parentCommandPath = str_replace($pathSuffix, '', $command->getPath());
$parentCommand = $this->findCommandByPath($parentCommandPath, $commands);
// writes to translation need privilege from parent command
// if we update e.g. a product and add translations for a new language
// the writeCommand on the translation would be an insert
if ($parentCommand) {
return (string) $parentCommand->getPrivilege();
}
// if we don't have a parentCommand it must be a update,
// because the parentEntity must already exist
return AclRoleDefinition::PRIVILEGE_UPDATE;
}
/**
* @param WriteCommand[] $commands
*/
private function findCommandByPath(string $commandPath, array $commands): ?WriteCommand
{
foreach ($commands as $command) {
if ($command->getPath() === $commandPath) {
return $command;
}
}
return null;
}
}