diff --git a/src/Controller/Studio/Config/AddController.php b/src/Controller/Studio/Config/AddController.php new file mode 100644 index 00000000..9279663b --- /dev/null +++ b/src/Controller/Studio/Config/AddController.php @@ -0,0 +1,82 @@ +value] + )] + #[StringParameter('name', 'assets', 'The name of the configuration')] + #[StringParameter('type', 'graphql', 'Type of the adapter')] + #[StringParameter('path', '', 'Configuration path', false)] + #[CreatedResponse( + description: 'bundle_data_hub_config_add_success_response' + )] + #[DefaultResponses([ + HttpResponseCodes::UNAUTHORIZED, + HttpResponseCodes::NOT_FOUND, + ])] + public function addConfiguration( + #[MapQueryString] AddConfiguration $addConfiguration + ): Response { + $this->configurationService->addConfiguration( + $addConfiguration->getName(), + $addConfiguration->getType(), + $addConfiguration->getPath() ?? '' + ); + + return new Response(); + } +} + diff --git a/src/Controller/Studio/Config/CollectionController.php b/src/Controller/Studio/Config/CollectionController.php index 2dbe3bcc..fa4338ce 100644 --- a/src/Controller/Studio/Config/CollectionController.php +++ b/src/Controller/Studio/Config/CollectionController.php @@ -67,7 +67,6 @@ public function __construct( description: 'bundle_copilot_actions_success_response', content: new CollectionJson(new GenericCollection(Configuration::class)), )] - #[IsGranted(PermissionConstants::PLUGIN_DATA_HUB_CONFIG)] #[DefaultResponses([ HttpResponseCodes::UNAUTHORIZED, HttpResponseCodes::NOT_FOUND, diff --git a/src/Controller/Studio/Config/DeleteController.php b/src/Controller/Studio/Config/DeleteController.php new file mode 100644 index 00000000..3b6a370c --- /dev/null +++ b/src/Controller/Studio/Config/DeleteController.php @@ -0,0 +1,81 @@ +value] + )] + #[IdParameter( + type: 'configuration', + schema: new Schema(type: 'string'), + name: 'name', + )] + #[SuccessResponse( + description: 'bundle_data_hub_config_delete_success_response', + )] + #[IsGranted(PermissionConstants::PLUGIN_DATA_HUB_CONFIG)] + #[DefaultResponses([ + HttpResponseCodes::UNAUTHORIZED, + HttpResponseCodes::NOT_FOUND, + ])] + public function deleteConfiguration(string $name): Response + { + $this->configurationService->deleteConfiguration($name); + + return new Response(); + } +} + diff --git a/src/Controller/Studio/Config/GetController.php b/src/Controller/Studio/Config/GetController.php new file mode 100644 index 00000000..734e1794 --- /dev/null +++ b/src/Controller/Studio/Config/GetController.php @@ -0,0 +1,81 @@ +value] + )] + #[IdParameter( + type: 'configuration', + schema: new Schema(type: 'string'), + name: 'name', + )] + #[SuccessResponse( + description: 'bundle_data_hub_config_get_success_response', + )] + #[IsGranted(PermissionConstants::PLUGIN_DATA_HUB_CONFIG)] + #[DefaultResponses([ + HttpResponseCodes::UNAUTHORIZED, + HttpResponseCodes::NOT_FOUND, + ])] + public function getConfiguration(string $name): JsonResponse + { + return $this->jsonResponse( + $this->configurationService->getConfiguration($name) + ); + } +} + diff --git a/src/Hydrator/ConfigurationDetailHydrator.php b/src/Hydrator/ConfigurationDetailHydrator.php new file mode 100644 index 00000000..bee95469 --- /dev/null +++ b/src/Hydrator/ConfigurationDetailHydrator.php @@ -0,0 +1,40 @@ +getName(), + $configuration->getConfiguration(), + $configuration->getPermissionsConfig(), + $supportedQueryDataTypes, + $supportedMutationDataTypes, + $configuration->getModificationDate() + ); + } +} + diff --git a/src/Hydrator/ConfigurationDetailHydratorInterface.php b/src/Hydrator/ConfigurationDetailHydratorInterface.php new file mode 100644 index 00000000..a1cca7ef --- /dev/null +++ b/src/Hydrator/ConfigurationDetailHydratorInterface.php @@ -0,0 +1,30 @@ +name; + } + + public function getType(): string + { + return $this->type; + } + + public function getPath(): ?string + { + return $this->path; + } +} + diff --git a/src/Schema/ConfigurationDetail.php b/src/Schema/ConfigurationDetail.php new file mode 100644 index 00000000..b0600baf --- /dev/null +++ b/src/Schema/ConfigurationDetail.php @@ -0,0 +1,61 @@ +name; + } + + public function getConfiguration(): array + { + return $this->configuration; + } + + public function getUserPermissions(): array + { + return $this->userPermissions; + } + + public function getSupportedGraphQLQueryDataTypes(): array + { + return $this->supportedGraphQLQueryDataTypes; + } + + public function getSupportedGraphQLMutationDataTypes(): array + { + return $this->supportedGraphQLMutationDataTypes; + } + + public function getModificationDate(): int + { + return $this->modificationDate; + } +} + diff --git a/src/Service/Studio/ConfigurationService.php b/src/Service/Studio/ConfigurationService.php index 72f6a3b1..057be822 100644 --- a/src/Service/Studio/ConfigurationService.php +++ b/src/Service/Studio/ConfigurationService.php @@ -14,13 +14,26 @@ namespace Pimcore\Bundle\DataHubBundle\Service\Studio; +use Pimcore\Bundle\DataHubBundle\ConfigEvents; use Pimcore\Bundle\DataHubBundle\Configuration; use Pimcore\Bundle\DataHubBundle\Event\AdminEvents; +use Pimcore\Bundle\DataHubBundle\Event\Config\SpecialEntitiesEvent; use Pimcore\Bundle\DataHubBundle\Event\Studio\PreResponse\ConfigurationEvent; +use Pimcore\Bundle\DataHubBundle\GraphQL\Service; +use Pimcore\Bundle\DataHubBundle\Hydrator\ConfigurationDetailHydratorInterface; use Pimcore\Bundle\DataHubBundle\Hydrator\ConfigurationHydratorInterface; +use Pimcore\Bundle\DataHubBundle\Model\SpecialEntitySetting; use Pimcore\Bundle\DataHubBundle\Schema\Configuration as HydratedConfiguration; +use Pimcore\Bundle\DataHubBundle\Schema\ConfigurationDetail; +use Pimcore\Bundle\DataHubBundle\Utils\Constants\PermissionConstants; +use Pimcore\Bundle\DataHubBundle\WorkspaceHelper; +use Pimcore\Bundle\StudioBackendBundle\Exception\Api\ElementExistsException; +use Pimcore\Bundle\StudioBackendBundle\Exception\Api\ForbiddenException; +use Pimcore\Bundle\StudioBackendBundle\Exception\Api\NotWriteableException; +use Pimcore\Bundle\StudioBackendBundle\Security\Service\SecurityServiceInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\GenericEvent; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; /** @internal */ final readonly class ConfigurationService implements ConfigurationServiceInterface @@ -28,6 +41,9 @@ public function __construct( private EventDispatcherInterface $eventDispatcher, private ConfigurationHydratorInterface $configurationHydrator, + private ConfigurationDetailHydratorInterface $configurationDetailHydrator, + private Service $graphQlService, + private SecurityServiceInterface $securityService ) { } @@ -37,7 +53,10 @@ public function getConfigurations(): array $configs = $this->resolveConfigurationList(Configuration::getList()); foreach ($configs as $config) { - if (!$config instanceof Configuration) { + if ( + !$config instanceof Configuration || + !$config->isAllowed(PermissionConstants::PLUGIN_DATA_HUB_PERMISSION_READ) + ){ continue; } @@ -65,6 +84,176 @@ public function getConfigurations(): array return array_values($hydratedConfigs); } + /** + * @throws \Exception + */ + public function getConfiguration(string $name): ConfigurationDetail + { + $configuration = $this->fetchConfiguration($name); + $config = $this->normalizeConfigurationSchema($configuration->getConfiguration()); + $config = $this->processSpecialEntities($config); + + $configuration->setConfiguration($config); + + $supportedQueryDataTypes = $this->graphQlService->getSupportedDataObjectQueryDataTypes(); + $supportedMutationDataTypes = $this->graphQlService->getSupportedDataObjectMutationDataTypes(); + + return $this->configurationDetailHydrator->hydrate( + $configuration, + $supportedQueryDataTypes, + $supportedMutationDataTypes + ); + } + + /** + * @throws \Exception + */ + public function addConfiguration(string $name, string $type, string $path): string + { + if ((new Configuration(null, null))->isWriteable() === false) { + throw new NotWriteableException( + PermissionConstants::PLUGIN_DATA_HUB_PERMISSION_CREATE, + 'Cannot create configuration as configurations are not writeable.' + ); + } + + $this->checkUserPermission( + PermissionConstants::PLUGIN_DATA_HUB_CONFIG + ); + + if ($this->configExists($name)) { + throw new ElementExistsException('Configuration with name "' . $name . '" already exists.'); + } + + $config = new Configuration($type, $path, $name); + $config->save(); + + return $name; + } + + /** + * @throws \Exception + */ + public function deleteConfiguration(string $name): void + { + $config = $this->fetchConfiguration($name); + + if ($config->isWriteable() === false) { + throw new NotWriteableException( + PermissionConstants::PLUGIN_DATA_HUB_PERMISSION_DELETE, + 'Cant delete configuration "' . $name . '" as it is not writeable.' + ); + } + + $this->checkConfigPermission($config, PermissionConstants::PLUGIN_DATA_HUB_PERMISSION_DELETE); + + WorkspaceHelper::deleteConfiguration($config); + $config->delete(); + } + + private function checkConfigPermission( + Configuration $configuration, + string $permission + ): void { + if (!$configuration->isAllowed($permission)) { + throw new ForbiddenException('Permission denied: ' . $permission); + } + } + + private function fetchConfiguration(string $name): Configuration + { + $configuration = Configuration::getByName($name); + + if (!$configuration instanceof Configuration) { + throw new NotFoundHttpException('Datahub configuration ' . $name . ' does not exist.'); + } + + $this->checkConfigPermission($configuration, PermissionConstants::PLUGIN_DATA_HUB_PERMISSION_READ); + + return $configuration; + } + + private function configExists(string $name): bool + { + $configuration = Configuration::getByName($name); + return $configuration instanceof Configuration; + } + + private function normalizeConfigurationSchema(array $config): array + { + $config['schema']['queryEntities'] = array_values($config['schema']['queryEntities'] ?? []); + $config['schema']['mutationEntities'] = array_values($config['schema']['mutationEntities'] ?? []); + $config['schema']['specialEntities'] = $config['schema']['specialEntities'] ?? []; + + return $config; + } + + private function processSpecialEntities(array $config): array + { + $coreSettings = $this->buildCoreSpecialEntitySettings($config['schema']['specialEntities']); + $specialSettingsEvent = new SpecialEntitiesEvent($coreSettings, $config); + $this->eventDispatcher->dispatch($specialSettingsEvent, ConfigEvents::SPECIAL_ENTITIES); + + $config['schema']['specialEntities'] = $specialSettingsEvent->getSpecialSettings(); + + return $config; + } + + /** + * @return SpecialEntitySetting[] + */ + private function buildCoreSpecialEntitySettings(array $specialEntities): array + { + return [ + $this->createSpecialEntitySetting('document', true, true, true, true, $specialEntities), + $this->createSpecialEntitySetting('document_folder', true, false, false, true, $specialEntities), + $this->createSpecialEntitySetting('asset', true, true, true, true, $specialEntities), + $this->createSpecialEntitySetting('asset_folder', true, true, true, true, $specialEntities), + $this->createSpecialEntitySetting('asset_listing', true, true, true, true, $specialEntities), + $this->createSpecialEntitySetting('object_folder', true, true, true, true, $specialEntities), + $this->createTranslationSpecialEntitySetting('translation', $specialEntities), + $this->createTranslationSpecialEntitySetting('translation_listing', $specialEntities), + ]; + } + + private function createSpecialEntitySetting( + string $name, + bool $read, + bool $create, + bool $update, + bool $delete, + array $specialEntities + ): SpecialEntitySetting { + return new SpecialEntitySetting( + $name, + $read, + $create, + $update, + $delete, + $specialEntities[$name]['read'] ?? false, + $specialEntities[$name]['create'] ?? false, + $specialEntities[$name]['update'] ?? false, + $specialEntities[$name]['delete'] ?? false + ); + } + + private function createTranslationSpecialEntitySetting( + string $name, + array $specialEntities + ): SpecialEntitySetting { + return new SpecialEntitySetting( + $name, + true, + false, + false, + false, + $specialEntities['translation_listing']['read'] ?? false, + $specialEntities['translation_listing']['create'] ?? false, + $specialEntities['translation_listing']['update'] ?? false, + $specialEntities['translation_listing']['delete'] ?? false + ); + } + private function resolveConfigurationList(array $configs): iterable { $event = new GenericEvent($this, ['list' => $configs]); @@ -86,4 +275,13 @@ private function addHydratedConfiguration( $hydratedConfigs[] = $hydratedItem; } + + private function checkUserPermission(string $permission): void + { + if(!$this->securityService->getCurrentUser()->isAllowed( + $permission + )) { + throw new ForbiddenException('Permission denied: ' . $permission); + } + } } diff --git a/src/Service/Studio/ConfigurationServiceInterface.php b/src/Service/Studio/ConfigurationServiceInterface.php index 2e8b4c8d..20f2e44d 100644 --- a/src/Service/Studio/ConfigurationServiceInterface.php +++ b/src/Service/Studio/ConfigurationServiceInterface.php @@ -15,6 +15,7 @@ namespace Pimcore\Bundle\DataHubBundle\Service\Studio; use Pimcore\Bundle\DataHubBundle\Schema\Configuration; +use Pimcore\Bundle\DataHubBundle\Schema\ConfigurationDetail; /** @internal */ interface ConfigurationServiceInterface @@ -23,4 +24,19 @@ interface ConfigurationServiceInterface * @return Configuration[] */ public function getConfigurations(): array; + + /** + * @throws \Exception + */ + public function deleteConfiguration(string $name): void; + + /** + * @throws \Exception + */ + public function addConfiguration(string $name, string $type, string $path): string; + + /** + * @throws \Exception + */ + public function getConfiguration(string $name): ConfigurationDetail; } diff --git a/src/Utils/Constants/PermissionConstants.php b/src/Utils/Constants/PermissionConstants.php index fc2ab185..29212a23 100644 --- a/src/Utils/Constants/PermissionConstants.php +++ b/src/Utils/Constants/PermissionConstants.php @@ -18,5 +18,11 @@ */ class PermissionConstants { - public const PLUGIN_DATA_HUB_CONFIG = 'plugin_datahub_config'; + public const string PLUGIN_DATA_HUB_CONFIG = 'plugin_datahub_config'; + public const string PLUGIN_DATA_HUB_ADMIN = 'plugin_datahub_admin'; + public const string PLUGIN_DATA_HUB_ADAPTER_PREFIX = 'plugin_datahub_adapter_'; + public const string PLUGIN_DATA_HUB_PERMISSION_READ = 'read'; + public const string PLUGIN_DATA_HUB_PERMISSION_DELETE = 'delete'; + public const string PLUGIN_DATA_HUB_PERMISSION_UPDATE = 'update'; + public const string PLUGIN_DATA_HUB_PERMISSION_CREATE = 'create'; }