From 4aba1f1cff194fd8d0af20f9d80c878152fc5e00 Mon Sep 17 00:00:00 2001 From: Arthur Schiwon Date: Mon, 19 Aug 2019 17:13:47 +0200 Subject: scope aware workflow controller and manager Signed-off-by: Arthur Schiwon --- .../composer/composer/autoload_classmap.php | 3 + .../composer/composer/autoload_static.php | 3 + apps/workflowengine/lib/Command/Index.php | 7 +- .../lib/Controller/AWorkflowController.php | 148 ++++++++++++ .../lib/Controller/GlobalWorkflowsController.php | 88 +------- .../lib/Controller/UserWorkflowsController.php | 117 ++++++++++ apps/workflowengine/lib/Helper/ScopeContext.php | 78 +++++++ apps/workflowengine/lib/Manager.php | 227 +++++++++++++------ apps/workflowengine/tests/ManagerTest.php | 249 ++++++++++++++++++++- 9 files changed, 761 insertions(+), 159 deletions(-) create mode 100644 apps/workflowengine/lib/Controller/AWorkflowController.php create mode 100644 apps/workflowengine/lib/Controller/UserWorkflowsController.php create mode 100644 apps/workflowengine/lib/Helper/ScopeContext.php (limited to 'apps/workflowengine') diff --git a/apps/workflowengine/composer/composer/autoload_classmap.php b/apps/workflowengine/composer/composer/autoload_classmap.php index 269a41a96b0..81047b10614 100644 --- a/apps/workflowengine/composer/composer/autoload_classmap.php +++ b/apps/workflowengine/composer/composer/autoload_classmap.php @@ -18,12 +18,15 @@ return array( 'OCA\\WorkflowEngine\\Check\\RequestUserAgent' => $baseDir . '/../lib/Check/RequestUserAgent.php', 'OCA\\WorkflowEngine\\Check\\UserGroupMembership' => $baseDir . '/../lib/Check/UserGroupMembership.php', 'OCA\\WorkflowEngine\\Command\\Index' => $baseDir . '/../lib/Command/Index.php', + 'OCA\\WorkflowEngine\\Controller\\AWorkflowController' => $baseDir . '/../lib/Controller/AWorkflowController.php', 'OCA\\WorkflowEngine\\Controller\\FlowOperations' => $baseDir . '/../lib/Controller/FlowOperations.php', 'OCA\\WorkflowEngine\\Controller\\GlobalWorkflowsController' => $baseDir . '/../lib/Controller/GlobalWorkflowsController.php', 'OCA\\WorkflowEngine\\Controller\\RequestTime' => $baseDir . '/../lib/Controller/RequestTime.php', + 'OCA\\WorkflowEngine\\Controller\\UserWorkflowsController' => $baseDir . '/../lib/Controller/UserWorkflowsController.php', 'OCA\\WorkflowEngine\\Entity\\File' => $baseDir . '/../lib/Entity/File.php', 'OCA\\WorkflowEngine\\Entity\\GenericEntityEmitterEvent' => $baseDir . '/../lib/Entity/GenericEntityEmitterEvent.php', 'OCA\\WorkflowEngine\\Entity\\IEntityEmitterEvent' => $baseDir . '/../lib/Entity/IEntityEmitterEvent.php', + 'OCA\\WorkflowEngine\\Helper\\ScopeContext' => $baseDir . '/../lib/Helper/ScopeContext.php', 'OCA\\WorkflowEngine\\Manager' => $baseDir . '/../lib/Manager.php', 'OCA\\WorkflowEngine\\Migration\\PopulateNewlyIntroducedDatabaseFields' => $baseDir . '/../lib/Migration/PopulateNewlyIntroducedDatabaseFields.php', 'OCA\\WorkflowEngine\\Migration\\Version2019Date20190808074233' => $baseDir . '/../lib/Migration/Version2019Date20190808074233.php', diff --git a/apps/workflowengine/composer/composer/autoload_static.php b/apps/workflowengine/composer/composer/autoload_static.php index eed3f208f86..d016377ea54 100644 --- a/apps/workflowengine/composer/composer/autoload_static.php +++ b/apps/workflowengine/composer/composer/autoload_static.php @@ -33,12 +33,15 @@ class ComposerStaticInitWorkflowEngine 'OCA\\WorkflowEngine\\Check\\RequestUserAgent' => __DIR__ . '/..' . '/../lib/Check/RequestUserAgent.php', 'OCA\\WorkflowEngine\\Check\\UserGroupMembership' => __DIR__ . '/..' . '/../lib/Check/UserGroupMembership.php', 'OCA\\WorkflowEngine\\Command\\Index' => __DIR__ . '/..' . '/../lib/Command/Index.php', + 'OCA\\WorkflowEngine\\Controller\\AWorkflowController' => __DIR__ . '/..' . '/../lib/Controller/AWorkflowController.php', 'OCA\\WorkflowEngine\\Controller\\FlowOperations' => __DIR__ . '/..' . '/../lib/Controller/FlowOperations.php', 'OCA\\WorkflowEngine\\Controller\\GlobalWorkflowsController' => __DIR__ . '/..' . '/../lib/Controller/GlobalWorkflowsController.php', 'OCA\\WorkflowEngine\\Controller\\RequestTime' => __DIR__ . '/..' . '/../lib/Controller/RequestTime.php', + 'OCA\\WorkflowEngine\\Controller\\UserWorkflowsController' => __DIR__ . '/..' . '/../lib/Controller/UserWorkflowsController.php', 'OCA\\WorkflowEngine\\Entity\\File' => __DIR__ . '/..' . '/../lib/Entity/File.php', 'OCA\\WorkflowEngine\\Entity\\GenericEntityEmitterEvent' => __DIR__ . '/..' . '/../lib/Entity/GenericEntityEmitterEvent.php', 'OCA\\WorkflowEngine\\Entity\\IEntityEmitterEvent' => __DIR__ . '/..' . '/../lib/Entity/IEntityEmitterEvent.php', + 'OCA\\WorkflowEngine\\Helper\\ScopeContext' => __DIR__ . '/..' . '/../lib/Helper/ScopeContext.php', 'OCA\\WorkflowEngine\\Manager' => __DIR__ . '/..' . '/../lib/Manager.php', 'OCA\\WorkflowEngine\\Migration\\PopulateNewlyIntroducedDatabaseFields' => __DIR__ . '/..' . '/../lib/Migration/PopulateNewlyIntroducedDatabaseFields.php', 'OCA\\WorkflowEngine\\Migration\\Version2019Date20190808074233' => __DIR__ . '/..' . '/../lib/Migration/Version2019Date20190808074233.php', diff --git a/apps/workflowengine/lib/Command/Index.php b/apps/workflowengine/lib/Command/Index.php index 8098bc5d5ea..7f3af3c0811 100644 --- a/apps/workflowengine/lib/Command/Index.php +++ b/apps/workflowengine/lib/Command/Index.php @@ -24,6 +24,7 @@ declare(strict_types=1); namespace OCA\WorkflowEngine\Command; +use OCA\WorkflowEngine\Helper\ScopeContext; use OCA\WorkflowEngine\Manager; use OCP\WorkflowEngine\IManager; use Symfony\Component\Console\Command\Command; @@ -69,8 +70,10 @@ class Index extends Command { protected function execute(InputInterface $input, OutputInterface $output) { $ops = $this->manager->getAllOperations( - $this->mappedScope($input->getArgument('scope')), - $input->getArgument('scopeId') + new ScopeContext( + $this->mappedScope($input->getArgument('scope')), + $input->getArgument('scopeId') + ) ); $output->writeln(\json_encode($ops)); } diff --git a/apps/workflowengine/lib/Controller/AWorkflowController.php b/apps/workflowengine/lib/Controller/AWorkflowController.php new file mode 100644 index 00000000000..2e54e417a34 --- /dev/null +++ b/apps/workflowengine/lib/Controller/AWorkflowController.php @@ -0,0 +1,148 @@ + + * + * @author Arthur Schiwon + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\WorkflowEngine\Controller; + +use Doctrine\DBAL\DBALException; +use OCA\WorkflowEngine\Helper\ScopeContext; +use OCA\WorkflowEngine\Manager; +use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\OCS\OCSBadRequestException; +use OCP\AppFramework\OCS\OCSException; +use OCP\AppFramework\OCS\OCSForbiddenException; +use OCP\AppFramework\OCSController; +use OCP\IRequest; + +abstract class AWorkflowController extends OCSController { + + /** @var Manager */ + protected $manager; + + public function __construct( + $appName, + IRequest $request, + Manager $manager + ) { + parent::__construct($appName, $request); + + $this->manager = $manager; + } + + /** + * @throws OCSForbiddenException + */ + abstract protected function getScopeContext(): ScopeContext; + + /** + * Example: curl -u joann -H "OCS-APIREQUEST: true" "http://my.nc.srvr/ocs/v2.php/apps/workflowengine/api/v1/workflows/global?format=json" + * + * @throws OCSForbiddenException + */ + public function index(): DataResponse { + $operationsByClass = $this->manager->getAllOperations($this->getScopeContext()); + + foreach ($operationsByClass as &$operations) { + foreach ($operations as &$operation) { + $operation = $this->manager->formatOperation($operation); + } + } + + return new DataResponse($operationsByClass); + } + + /** + * Example: curl -u joann -H "OCS-APIREQUEST: true" "http://my.nc.srvr/ocs/v2.php/apps/workflowengine/api/v1/workflows/global/OCA\\Workflow_DocToPdf\\Operation?format=json" + * + * @throws OCSForbiddenException + */ + public function show(string $id): DataResponse { + $context = $this->getScopeContext(); + + // The ID corresponds to a class name + $operations = $this->manager->getOperations($id, $context); + + foreach ($operations as &$operation) { + $operation = $this->manager->formatOperation($operation); + } + + return new DataResponse($operations); + } + + /** + * @throws OCSBadRequestException + * @throws OCSForbiddenException + * @throws OCSException + */ + public function create(string $class, string $name, array $checks, string $operation): DataResponse { + $context = $this->getScopeContext(); + try { + $operation = $this->manager->addOperation($class, $name, $checks, $operation, $context); + $operation = $this->manager->formatOperation($operation); + return new DataResponse($operation); + } catch (\UnexpectedValueException $e) { + throw new OCSBadRequestException($e->getMessage(), $e); + } catch (\DomainException $e) { + throw new OCSForbiddenException($e->getMessage(), $e); + } catch(DBALException $e) { + throw new OCSException('An internal error occurred', $e); + } + } + + /** + * @throws OCSBadRequestException + * @throws OCSForbiddenException + * @throws OCSException + */ + public function update(int $id, string $name, array $checks, string $operation): DataResponse { + try { + $operation = $this->manager->updateOperation($id, $name, $checks, $operation, $this->getScopeContext()); + $operation = $this->manager->formatOperation($operation); + return new DataResponse($operation); + } catch (\UnexpectedValueException $e) { + throw new OCSBadRequestException($e->getMessage(), $e); + } catch (\DomainException $e) { + throw new OCSForbiddenException($e->getMessage(), $e); + } catch(DBALException $e) { + throw new OCSException('An internal error occurred', $e); + } + } + + /** + * @throws OCSBadRequestException + * @throws OCSForbiddenException + * @throws OCSException + */ + public function destroy(int $id): DataResponse { + try { + $deleted = $this->manager->deleteOperation($id, $this->getScopeContext()); + return new DataResponse($deleted); + } catch (\UnexpectedValueException $e) { + throw new OCSBadRequestException($e->getMessage(), $e); + } catch (\DomainException $e) { + throw new OCSForbiddenException($e->getMessage(), $e); + } catch(DBALException $e) { + throw new OCSException('An internal error occurred', $e); + } + } +} diff --git a/apps/workflowengine/lib/Controller/GlobalWorkflowsController.php b/apps/workflowengine/lib/Controller/GlobalWorkflowsController.php index e4321b9c0b6..6d49c87b83e 100644 --- a/apps/workflowengine/lib/Controller/GlobalWorkflowsController.php +++ b/apps/workflowengine/lib/Controller/GlobalWorkflowsController.php @@ -24,88 +24,18 @@ declare(strict_types=1); namespace OCA\WorkflowEngine\Controller; -use OCA\WorkflowEngine\Manager; -use OCP\AppFramework\Http\DataResponse; -use OCP\AppFramework\OCS\OCSBadRequestException; -use OCP\AppFramework\OCSController; -use OCP\IRequest; +use OCA\WorkflowEngine\Helper\ScopeContext; +use OCP\WorkflowEngine\IManager; -class GlobalWorkflowsController extends OCSController { +class GlobalWorkflowsController extends AWorkflowController { - /** @var Manager */ - private $manager; + /** @var ScopeContext */ + private $scopeContext; - public function __construct( - $appName, - IRequest $request, - Manager $manager - ) { - parent::__construct($appName, $request); - - $this->manager = $manager; - } - - /** - * Example: curl -u joann -H "OCS-APIREQUEST: true" "http://my.nc.srvr/ocs/v2.php/apps/workflowengine/api/v1/workflows/global?format=json" - */ - public function index(): DataResponse { - $operationsByClass = $this->manager->getAllOperations(); - - foreach ($operationsByClass as &$operations) { - foreach ($operations as &$operation) { - $operation = $this->manager->formatOperation($operation); - } - } - - return new DataResponse($operationsByClass); - } - - /** - * @throws OCSBadRequestException - * - * Example: curl -u joann -H "OCS-APIREQUEST: true" "http://my.nc.srvr/ocs/v2.php/apps/workflowengine/api/v1/workflows/global/OCA\\Workflow_DocToPdf\\Operation?format=json" - */ - public function show(string $id): DataResponse { - // The ID corresponds to a class name - $operations = $this->manager->getOperations($id); - - foreach ($operations as &$operation) { - $operation = $this->manager->formatOperation($operation); + protected function getScopeContext(): ScopeContext { + if($this->scopeContext === null) { + $this->scopeContext = new ScopeContext(IManager::SCOPE_ADMIN); } - - return new DataResponse($operations); - } - - /** - * @throws OCSBadRequestException - */ - public function create(string $class, string $name, array $checks, string $operation): DataResponse { - try { - $operation = $this->manager->addOperation($class, $name, $checks, $operation); - $operation = $this->manager->formatOperation($operation); - return new DataResponse($operation); - } catch (\UnexpectedValueException $e) { - throw new OCSBadRequestException($e->getMessage(), $e); - } - } - - /** - * @throws OCSBadRequestException - */ - public function update(int $id, string $name, array $checks, string $operation): DataResponse { - try { - $operation = $this->manager->updateOperation($id, $name, $checks, $operation); - $operation = $this->manager->formatOperation($operation); - return new DataResponse($operation); - } catch (\UnexpectedValueException $e) { - throw new OCSBadRequestException($e->getMessage(), $e); - } - } - - /** - */ - public function destroy(int $id): DataResponse { - $deleted = $this->manager->deleteOperation((int) $id); - return new DataResponse($deleted); + return $this->scopeContext; } } diff --git a/apps/workflowengine/lib/Controller/UserWorkflowsController.php b/apps/workflowengine/lib/Controller/UserWorkflowsController.php new file mode 100644 index 00000000000..179e6b1ad11 --- /dev/null +++ b/apps/workflowengine/lib/Controller/UserWorkflowsController.php @@ -0,0 +1,117 @@ + + * + * @author Arthur Schiwon + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\WorkflowEngine\Controller; + +use OCA\WorkflowEngine\Helper\ScopeContext; +use OCA\WorkflowEngine\Manager; +use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\OCS\OCSBadRequestException; +use OCP\AppFramework\OCS\OCSForbiddenException; +use OCP\IRequest; +use OCP\IUserSession; +use OCP\WorkflowEngine\IManager; + +class UserWorkflowsController extends AWorkflowController { + + /** @var IUserSession */ + private $session; + + /** @var ScopeContext */ + private $scopeContext; + + public function __construct( + $appName, + IRequest $request, + Manager $manager, + IUserSession $session + ) { + parent::__construct($appName, $request, $manager); + + $this->session = $session; + } + + /** + * Retrieve all configured workflow rules + * + * Example: curl -u joann -H "OCS-APIREQUEST: true" "http://my.nc.srvr/ocs/v2.php/apps/workflowengine/api/v1/workflows/user?format=json" + * + * @NoAdminRequired + * @throws OCSForbiddenException + */ + public function index(): DataResponse { + return parent::index(); + } + + /** + * @NoAdminRequired + * + * Example: curl -u joann -H "OCS-APIREQUEST: true" "http://my.nc.srvr/ocs/v2.php/apps/workflowengine/api/v1/workflows/user/OCA\\Workflow_DocToPdf\\Operation?format=json" + * @throws OCSForbiddenException + */ + public function show(string $id): DataResponse { + return parent::show($id); + } + + /** + * @NoAdminRequired + * @throws OCSBadRequestException + * @throws OCSForbiddenException + */ + public function create(string $class, string $name, array $checks, string $operation): DataResponse { + return parent::create($class, $name, $checks, $operation); + } + + /** + * @NoAdminRequired + * @throws OCSBadRequestException + * @throws OCSForbiddenException + */ + public function update(int $id, string $name, array $checks, string $operation): DataResponse { + return parent::update($id, $name, $checks, $operation); + } + + /** + * @NoAdminRequired + * @throws OCSForbiddenException + */ + public function destroy(int $id): DataResponse { + return parent::destroy($id); + } + + /** + * @throws OCSForbiddenException + */ + protected function getScopeContext(): ScopeContext { + if($this->scopeContext === null) { + $user = $this->session->getUser(); + if(!$user) { + throw new OCSForbiddenException('User not logged in'); + } + $this->scopeContext = new ScopeContext(IManager::SCOPE_USER, $user->getUID()); + } + return $this->scopeContext; + } + +} diff --git a/apps/workflowengine/lib/Helper/ScopeContext.php b/apps/workflowengine/lib/Helper/ScopeContext.php new file mode 100644 index 00000000000..fecc4db0ed7 --- /dev/null +++ b/apps/workflowengine/lib/Helper/ScopeContext.php @@ -0,0 +1,78 @@ + + * + * @author Arthur Schiwon + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\WorkflowEngine\Helper; + +use OCP\WorkflowEngine\IManager; + +class ScopeContext { + /** @var int */ + private $scope; + /** @var string */ + private $scopeId; + /** @var string */ + private $hash; + + public function __construct(int $scope, string $scopeId = null) { + $this->scope = $this->evaluateScope($scope); + $this->scopeId = $this->evaluateScopeId($scopeId); + } + + private function evaluateScope(int $scope): int { + if(in_array($scope, [IManager::SCOPE_ADMIN, IManager::SCOPE_USER], true)) { + return $scope; + } + throw new \InvalidArgumentException('Invalid scope'); + } + + private function evaluateScopeId(string $scopeId = null): string { + if($this->scope === IManager::SCOPE_USER + && trim((string)$scopeId) === '') + { + throw new \InvalidArgumentException('user scope requires a user id'); + } + return trim((string)$scopeId); + } + + /** + * @return int + */ + public function getScope(): int { + return $this->scope; + } + + /** + * @return string + */ + public function getScopeId(): string { + return $this->scopeId; + } + + public function getHash(): string { + if($this->hash === null) { + $this->hash = \hash('sha256', $this->getScope() . '::' . $this->getScopeId()); + } + return $this->hash; + } +} diff --git a/apps/workflowengine/lib/Manager.php b/apps/workflowengine/lib/Manager.php index 3bfedfbfbf4..ecf3f24bda1 100644 --- a/apps/workflowengine/lib/Manager.php +++ b/apps/workflowengine/lib/Manager.php @@ -23,7 +23,10 @@ namespace OCA\WorkflowEngine; use OC\Files\Storage\Wrapper\Jail; +use Doctrine\DBAL\DBALException; +use OC\Cache\CappedMemoryCache; use OCA\WorkflowEngine\Entity\File; +use OCA\WorkflowEngine\Helper\ScopeContext; use OCP\AppFramework\QueryException; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\Files\Storage\IStorage; @@ -74,6 +77,10 @@ class Manager implements IManager, IEntityAware { /** @var ILogger */ protected $logger; + + /** @var CappedMemoryCache */ + protected $operationsByScope = []; + /** @var IUserSession */ protected $session; @@ -95,6 +102,7 @@ class Manager implements IManager, IEntityAware { $this->l = $l; $this->eventDispatcher = $eventDispatcher; $this->logger = $logger; + $this->operationsByScope = new CappedMemoryCache(64); $this->session = $session; } @@ -114,7 +122,16 @@ class Manager implements IManager, IEntityAware { * @inheritdoc */ public function getMatchingOperations($class, $returnFirstMatchingOperationOnly = true) { - $operations = $this->getOperations($class); + $scopes[] = new ScopeContext(IManager::SCOPE_ADMIN); + $user = $this->session->getUser(); + if($user !== null) { + $scopes[] = new ScopeContext(IManager::SCOPE_USER, $user->getUID()); + } + + $operations = []; + foreach ($scopes as $scope) { + $operations = array_merge($operations, $this->getOperations($class, $scope)); + } $matches = []; foreach ($operations as $operation) { @@ -160,19 +177,10 @@ class Manager implements IManager, IEntityAware { throw new \UnexpectedValueException($this->l->t('Check %s is invalid or does not exist', $check['class'])); } } - public function getAllOperations(int $scope = IManager::SCOPE_ADMIN, string $scopeId = null): array { - if(!in_array($scope, [IManager::SCOPE_ADMIN, IManager::SCOPE_USER])) { - throw new \InvalidArgumentException('Provided value for scope is not supported'); + public function getAllOperations(ScopeContext $scopeContext): array { + if(isset($this->operations[$scopeContext->getHash()])) { + return $this->operations[$scopeContext->getHash()]; } - if($scope === IManager::SCOPE_USER && $scopeId === null) { - $user = $this->session->getUser(); - if($user === null) { - throw new \InvalidArgumentException('No user ID was provided'); - } - $scopeId = $user->getUID(); - } - - $this->operations = []; $query = $this->connection->getQueryBuilder(); @@ -181,48 +189,29 @@ class Manager implements IManager, IEntityAware { ->leftJoin('o', 'flow_operations_scope', 's', $query->expr()->eq('o.id', 's.operation_id')) ->where($query->expr()->eq('s.type', $query->createParameter('scope'))); - if($scope === IManager::SCOPE_USER) { + if($scopeContext->getScope() === IManager::SCOPE_USER) { $query->andWhere($query->expr()->eq('s.value', $query->createParameter('scopeId'))); } - $query->setParameters(['scope' => $scope, 'scopeId' => $scopeId]); - + $query->setParameters(['scope' => $scopeContext->getScope(), 'scopeId' => $scopeContext->getScopeId()]); $result = $query->execute(); + $this->operations[$scopeContext->getHash()] = []; while ($row = $result->fetch()) { - if(!isset($this->operations[$row['class']])) { - $this->operations[$row['class']] = []; + if(!isset($this->operations[$scopeContext->getHash()][$row['class']])) { + $this->operations[$scopeContext->getHash()][$row['class']] = []; } - $this->operations[$row['class']][] = $row; + $this->operations[$scopeContext->getHash()][$row['class']][] = $row; } - return $this->operations; + return $this->operations[$scopeContext->getHash()]; } - - /** - * @param string $class - * @return array[] - */ - public function getOperations($class) { - if (isset($this->operations[$class])) { - return $this->operations[$class]; - } - - $query = $this->connection->getQueryBuilder(); - - $query->select('*') - ->from('flow_operations') - ->where($query->expr()->eq('class', $query->createNamedParameter($class))); - $result = $query->execute(); - - $this->operations[$class] = []; - while ($row = $result->fetch()) { - $this->operations[$class][] = $row; + public function getOperations(string $class, ScopeContext $scopeContext): array { + if (!isset($this->operations[$scopeContext->getHash()])) { + $this->getAllOperations($scopeContext); } - $result->closeCursor(); - - return $this->operations[$class]; + return $this->operations[$scopeContext->getHash()][$class] ?? []; } /** @@ -246,6 +235,20 @@ class Manager implements IManager, IEntityAware { throw new \UnexpectedValueException($this->l->t('Operation #%s does not exist', [$id])); } + protected function insertOperation(string $class, string $name, array $checkIds, string $operation): int { + $query = $this->connection->getQueryBuilder(); + $query->insert('flow_operations') + ->values([ + 'class' => $query->createNamedParameter($class), + 'name' => $query->createNamedParameter($name), + 'checks' => $query->createNamedParameter(json_encode(array_unique($checkIds))), + 'operation' => $query->createNamedParameter($operation), + ]); + $query->execute(); + + return $query->getLastInsertId(); + } + /** * @param string $class * @param string $name @@ -253,29 +256,58 @@ class Manager implements IManager, IEntityAware { * @param string $operation * @return array The added operation * @throws \UnexpectedValueException + * @throws DBALException */ - public function addOperation($class, $name, array $checks, $operation) { + public function addOperation($class, $name, array $checks, $operation, ScopeContext $scope) { $this->validateOperation($class, $name, $checks, $operation); - $checkIds = []; - foreach ($checks as $check) { - $checkIds[] = $this->addCheck($check['class'], $check['operator'], $check['value']); - } + $this->connection->beginTransaction(); - $query = $this->connection->getQueryBuilder(); - $query->insert('flow_operations') - ->values([ - 'class' => $query->createNamedParameter($class), - 'name' => $query->createNamedParameter($name), - 'checks' => $query->createNamedParameter(json_encode(array_unique($checkIds))), - 'operation' => $query->createNamedParameter($operation), - ]); - $query->execute(); + try { + $checkIds = []; + foreach ($checks as $check) { + $checkIds[] = $this->addCheck($check['class'], $check['operator'], $check['value']); + } + + $id = $this->insertOperation($class, $name, $checkIds, $operation); + $this->addScope($id, $scope); + + $this->connection->commit(); + } catch (DBALException $e) { + $this->connection->rollBack(); + throw $e; + } - $id = $query->getLastInsertId(); return $this->getOperation($id); } + protected function canModify(int $id, ScopeContext $scopeContext):bool { + if(isset($this->operationsByScope[$scopeContext->getHash()])) { + return in_array($id, $this->operationsByScope[$scopeContext->getHash()], true); + } + + $qb = $this->connection->getQueryBuilder(); + $qb = $qb->select('o.id') + ->from('flow_operations', 'o') + ->leftJoin('o', 'flow_operations_scope', 's', $qb->expr()->eq('o.id', 's.operation_id')) + ->where($qb->expr()->eq('s.type', $qb->createParameter('scope'))); + + if($scopeContext->getScope() !== IManager::SCOPE_ADMIN) { + $qb->where($qb->expr()->eq('s.value', $qb->createParameter('scopeId'))); + } + + $qb->setParameters(['scope' => $scopeContext->getScope(), 'scopeId' => $scopeContext->getScopeId()]); + $result = $qb->execute(); + + $this->operationsByScope[$scopeContext->getHash()] = []; + while($opId = $result->fetchColumn(0)) { + $this->operationsByScope[$scopeContext->getHash()][] = (int)$opId; + } + $result->closeCursor(); + + return in_array($id, $this->operationsByScope[$scopeContext->getHash()], true); + } + /** * @param int $id * @param string $name @@ -283,23 +315,36 @@ class Manager implements IManager, IEntityAware { * @param string $operation * @return array The updated operation * @throws \UnexpectedValueException + * @throws \DomainException + * @throws DBALException */ - public function updateOperation($id, $name, array $checks, $operation) { + public function updateOperation($id, $name, array $checks, $operation, ScopeContext $scopeContext): array { + if(!$this->canModify($id, $scopeContext)) { + throw new \DomainException('Target operation not within scope'); + }; $row = $this->getOperation($id); $this->validateOperation($row['class'], $name, $checks, $operation); $checkIds = []; - foreach ($checks as $check) { - $checkIds[] = $this->addCheck($check['class'], $check['operator'], $check['value']); - } + try { + $this->connection->beginTransaction(); + foreach ($checks as $check) { + $checkIds[] = $this->addCheck($check['class'], $check['operator'], $check['value']); + } - $query = $this->connection->getQueryBuilder(); - $query->update('flow_operations') - ->set('name', $query->createNamedParameter($name)) - ->set('checks', $query->createNamedParameter(json_encode(array_unique($checkIds)))) - ->set('operation', $query->createNamedParameter($operation)) - ->where($query->expr()->eq('id', $query->createNamedParameter($id))); - $query->execute(); + $query = $this->connection->getQueryBuilder(); + $query->update('flow_operations') + ->set('name', $query->createNamedParameter($name)) + ->set('checks', $query->createNamedParameter(json_encode(array_unique($checkIds)))) + ->set('operation', $query->createNamedParameter($operation)) + ->where($query->expr()->eq('id', $query->createNamedParameter($id))); + $query->execute(); + $this->connection->commit(); + } catch (DBALException $e) { + $this->connection->rollBack(); + throw $e; + } + unset($this->operations[$scopeContext->getHash()]); return $this->getOperation($id); } @@ -308,12 +353,36 @@ class Manager implements IManager, IEntityAware { * @param int $id * @return bool * @throws \UnexpectedValueException + * @throws DBALException + * @throws \DomainException */ - public function deleteOperation($id) { + public function deleteOperation($id, ScopeContext $scopeContext) { + if(!$this->canModify($id, $scopeContext)) { + throw new \DomainException('Target operation not within scope'); + }; $query = $this->connection->getQueryBuilder(); - $query->delete('flow_operations') - ->where($query->expr()->eq('id', $query->createNamedParameter($id))); - return (bool) $query->execute(); + try { + $this->connection->beginTransaction(); + $result = (bool)$query->delete('flow_operations') + ->where($query->expr()->eq('id', $query->createNamedParameter($id))) + ->execute(); + if($result) { + $qb = $this->connection->getQueryBuilder(); + $result &= (bool)$qb->delete('flow_operations_scope') + ->where($qb->expr()->eq('operation_id', $query->createNamedParameter($id))) + ->execute(); + } + $this->connection->commit(); + } catch (DBALException $e) { + $this->connection->rollBack(); + throw $e; + } + + if(isset($this->operations[$scopeContext->getHash()])) { + unset($this->operations[$scopeContext->getHash()]); + } + + return $result; } /** @@ -427,6 +496,18 @@ class Manager implements IManager, IEntityAware { return $query->getLastInsertId(); } + protected function addScope(int $operationId, ScopeContext $scope): void { + $query = $this->connection->getQueryBuilder(); + + $insertQuery = $query->insert('flow_operations_scope'); + $insertQuery->values([ + 'operation_id' => $query->createNamedParameter($operationId), + 'type' => $query->createNamedParameter($scope->getScope()), + 'value' => $query->createNamedParameter($scope->getScopeId()), + ]); + $insertQuery->execute(); + } + public function formatOperation(array $operation): array { $checkIds = json_decode($operation['checks'], true); $checks = $this->getChecks($checkIds); diff --git a/apps/workflowengine/tests/ManagerTest.php b/apps/workflowengine/tests/ManagerTest.php index ecfc92713a0..b25adb96f15 100644 --- a/apps/workflowengine/tests/ManagerTest.php +++ b/apps/workflowengine/tests/ManagerTest.php @@ -23,12 +23,16 @@ namespace OCA\WorkflowEngine\Tests; use OCA\WorkflowEngine\Entity\File; +use OCA\WorkflowEngine\Helper\ScopeContext; use OCA\WorkflowEngine\Manager; use OCP\IDBConnection; use OCP\IL10N; use OCP\ILogger; use OCP\IServerContainer; +use OCP\WorkflowEngine\ICheck; use OCP\WorkflowEngine\IEntity; +use OCP\WorkflowEngine\IManager; +use OCP\WorkflowEngine\IOperation; use PHPUnit\Framework\MockObject\MockObject; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Test\TestCase; @@ -74,18 +78,38 @@ class ManagerTest extends TestCase { $this->eventDispatcher, $this->logger ); - $this->clearChecks(); + $this->clearTables(); } protected function tearDown() { - $this->clearChecks(); + $this->clearTables(); parent::tearDown(); } - public function clearChecks() { + /** + * @return MockObject|ScopeContext + */ + protected function buildScope(string $scopeId = null): MockObject { + $scopeContext = $this->createMock(ScopeContext::class); + $scopeContext->expects($this->any()) + ->method('getScope') + ->willReturn($scopeId ? IManager::SCOPE_USER : IManager::SCOPE_ADMIN); + $scopeContext->expects($this->any()) + ->method('getScopeId') + ->willReturn($scopeId ?? ''); + $scopeContext->expects($this->any()) + ->method('getHash') + ->willReturn(md5($scopeId ?? '')); + + return $scopeContext; + } + + public function clearTables() { $query = $this->db->getQueryBuilder(); - $query->delete('flow_checks') - ->execute(); + foreach(['flow_checks', 'flow_operations', 'flow_operations_scope'] as $table) { + $query->delete($table) + ->execute(); + } } public function testChecks() { @@ -109,6 +133,221 @@ class ManagerTest extends TestCase { $this->assertArrayHasKey($check2, $data); } + public function testScope() { + $adminScope = $this->buildScope(); + $userScope = $this->buildScope('jackie'); + + $opId1 = $this->invokePrivate( + $this->manager, + 'insertOperation', + ['OCA\WFE\TestOp', 'Test01', [11, 22], 'foo'] + ); + $this->invokePrivate($this->manager, 'addScope', [$opId1, $adminScope]); + + $opId2 = $this->invokePrivate( + $this->manager, + 'insertOperation', + ['OCA\WFE\TestOp', 'Test02', [33, 22], 'bar'] + ); + $this->invokePrivate($this->manager, 'addScope', [$opId2, $userScope]); + $opId3 = $this->invokePrivate( + $this->manager, + 'insertOperation', + ['OCA\WFE\TestOp', 'Test03', [11, 44], 'foobar'] + ); + $this->invokePrivate($this->manager, 'addScope', [$opId3, $userScope]); + + $this->assertTrue($this->invokePrivate($this->manager, 'canModify', [$opId1, $adminScope])); + $this->assertFalse($this->invokePrivate($this->manager, 'canModify', [$opId2, $adminScope])); + $this->assertFalse($this->invokePrivate($this->manager, 'canModify', [$opId3, $adminScope])); + + $this->assertFalse($this->invokePrivate($this->manager, 'canModify', [$opId1, $userScope])); + $this->assertTrue($this->invokePrivate($this->manager, 'canModify', [$opId2, $userScope])); + $this->assertTrue($this->invokePrivate($this->manager, 'canModify', [$opId3, $userScope])); + } + + public function testGetAllOperations() { + $adminScope = $this->buildScope(); + $userScope = $this->buildScope('jackie'); + + $opId1 = $this->invokePrivate( + $this->manager, + 'insertOperation', + ['OCA\WFE\TestAdminOp', 'Test01', [11, 22], 'foo'] + ); + $this->invokePrivate($this->manager, 'addScope', [$opId1, $adminScope]); + + $opId2 = $this->invokePrivate( + $this->manager, + 'insertOperation', + ['OCA\WFE\TestUserOp', 'Test02', [33, 22], 'bar'] + ); + $this->invokePrivate($this->manager, 'addScope', [$opId2, $userScope]); + $opId3 = $this->invokePrivate( + $this->manager, + 'insertOperation', + ['OCA\WFE\TestUserOp', 'Test03', [11, 44], 'foobar'] + ); + $this->invokePrivate($this->manager, 'addScope', [$opId3, $userScope]); + + $adminOps = $this->manager->getAllOperations($adminScope); + $userOps = $this->manager->getAllOperations($userScope); + + $this->assertSame(1, count($adminOps)); + $this->assertTrue(array_key_exists('OCA\WFE\TestAdminOp', $adminOps)); + $this->assertFalse(array_key_exists('OCA\WFE\TestUserOp', $adminOps)); + + $this->assertSame(1, count($userOps)); + $this->assertFalse(array_key_exists('OCA\WFE\TestAdminOp', $userOps)); + $this->assertTrue(array_key_exists('OCA\WFE\TestUserOp', $userOps)); + $this->assertSame(2, count($userOps['OCA\WFE\TestUserOp'])); + } + + public function testGetOperations() { + $adminScope = $this->buildScope(); + $userScope = $this->buildScope('jackie'); + + $opId1 = $this->invokePrivate( + $this->manager, + 'insertOperation', + ['OCA\WFE\TestOp', 'Test01', [11, 22], 'foo'] + ); + $this->invokePrivate($this->manager, 'addScope', [$opId1, $adminScope]); + $opId4 = $this->invokePrivate( + $this->manager, + 'insertOperation', + ['OCA\WFE\OtherTestOp', 'Test04', [5], 'foo'] + ); + $this->invokePrivate($this->manager, 'addScope', [$opId4, $adminScope]); + + $opId2 = $this->invokePrivate( + $this->manager, + 'insertOperation', + ['OCA\WFE\TestOp', 'Test02', [33, 22], 'bar'] + ); + $this->invokePrivate($this->manager, 'addScope', [$opId2, $userScope]); + $opId3 = $this->invokePrivate( + $this->manager, + 'insertOperation', + ['OCA\WFE\TestOp', 'Test03', [11, 44], 'foobar'] + ); + $this->invokePrivate($this->manager, 'addScope', [$opId3, $userScope]); + $opId5 = $this->invokePrivate( + $this->manager, + 'insertOperation', + ['OCA\WFE\OtherTestOp', 'Test05', [5], 'foobar'] + ); + $this->invokePrivate($this->manager, 'addScope', [$opId5, $userScope]); + + $adminOps = $this->manager->getOperations('OCA\WFE\TestOp', $adminScope); + $userOps = $this->manager->getOperations('OCA\WFE\TestOp', $userScope); + + $this->assertSame(1, count($adminOps)); + array_walk($adminOps, function ($op) { + $this->assertTrue($op['class'] === 'OCA\WFE\TestOp'); + }); + + $this->assertSame(2, count($userOps)); + array_walk($userOps, function ($op) { + $this->assertTrue($op['class'] === 'OCA\WFE\TestOp'); + }); + + } + + public function testUpdateOperation() { + $adminScope = $this->buildScope(); + $userScope = $this->buildScope('jackie'); + + $this->container->expects($this->any()) + ->method('query') + ->willReturnCallback(function ($class) { + if(substr($class, -2) === 'Op') { + return $this->createMock(IOperation::class); + } + return $this->createMock(ICheck::class); + }); + + $opId1 = $this->invokePrivate( + $this->manager, + 'insertOperation', + ['OCA\WFE\TestAdminOp', 'Test01', [11, 22], 'foo'] + ); + $this->invokePrivate($this->manager, 'addScope', [$opId1, $adminScope]); + + $opId2 = $this->invokePrivate( + $this->manager, + 'insertOperation', + ['OCA\WFE\TestUserOp', 'Test02', [33, 22], 'bar'] + ); + $this->invokePrivate($this->manager, 'addScope', [$opId2, $userScope]); + + $check1 = ['class' => 'OCA\WFE\C22', 'operator' => 'eq', 'value' => 'asdf']; + $check2 = ['class' => 'OCA\WFE\C33', 'operator' => 'eq', 'value' => 23456]; + + /** @noinspection PhpUnhandledExceptionInspection */ + $op = $this->manager->updateOperation($opId1, 'Test01a', [$check1, $check2], 'foohur', $adminScope); + $this->assertSame('Test01a', $op['name']); + $this->assertSame('foohur', $op['operation']); + + /** @noinspection PhpUnhandledExceptionInspection */ + $op = $this->manager->updateOperation($opId2, 'Test02a', [$check1], 'barfoo', $userScope); + $this->assertSame('Test02a', $op['name']); + $this->assertSame('barfoo', $op['operation']); + + foreach([[$adminScope, $opId2], [$userScope, $opId1]] as $run) { + try { + /** @noinspection PhpUnhandledExceptionInspection */ + $this->manager->updateOperation($run[1], 'Evil', [$check2], 'hackx0r', $run[0]); + $this->assertTrue(false, 'DomainException not thrown'); + } catch (\DomainException $e) { + $this->assertTrue(true); + } + } + } + + public function testDeleteOperation() { + $adminScope = $this->buildScope(); + $userScope = $this->buildScope('jackie'); + + $opId1 = $this->invokePrivate( + $this->manager, + 'insertOperation', + ['OCA\WFE\TestAdminOp', 'Test01', [11, 22], 'foo'] + ); + $this->invokePrivate($this->manager, 'addScope', [$opId1, $adminScope]); + + $opId2 = $this->invokePrivate( + $this->manager, + 'insertOperation', + ['OCA\WFE\TestUserOp', 'Test02', [33, 22], 'bar'] + ); + $this->invokePrivate($this->manager, 'addScope', [$opId2, $userScope]); + + foreach([[$adminScope, $opId2], [$userScope, $opId1]] as $run) { + try { + /** @noinspection PhpUnhandledExceptionInspection */ + $this->manager->deleteOperation($run[1], $run[0]); + $this->assertTrue(false, 'DomainException not thrown'); + } catch (\Exception $e) { + $this->assertInstanceOf(\DomainException::class, $e); + } + } + + /** @noinspection PhpUnhandledExceptionInspection */ + $this->manager->deleteOperation($opId1, $adminScope); + /** @noinspection PhpUnhandledExceptionInspection */ + $this->manager->deleteOperation($opId2, $userScope); + + foreach([$opId1, $opId2] as $opId) { + try { + $this->invokePrivate($this->manager, 'getOperation', [$opId]); + $this->assertTrue(false, 'UnexpectedValueException not thrown'); + } catch(\Exception $e) { + $this->assertInstanceOf(\UnexpectedValueException::class, $e); + } + } + } + public function testGetEntitiesListBuildInOnly() { $fileEntityMock = $this->createMock(File::class); -- cgit v1.2.3