diff options
author | Morris Jobke <hey@morrisjobke.de> | 2016-07-26 11:16:34 +0200 |
---|---|---|
committer | Morris Jobke <hey@morrisjobke.de> | 2016-07-26 11:16:34 +0200 |
commit | 2f42a3fc319f4cffcae6db6b0874786f26fa4817 (patch) | |
tree | 080256d466720ae21b0ca1a0804ff15c9b6f3fea /apps/workflowengine/lib | |
parent | cc5ddcf537d03c3f2f4cdc6817e02e098f8e8edb (diff) | |
download | nextcloud-server-2f42a3fc319f4cffcae6db6b0874786f26fa4817.tar.gz nextcloud-server-2f42a3fc319f4cffcae6db6b0874786f26fa4817.zip |
Add workflowengine
Diffstat (limited to 'apps/workflowengine/lib')
-rw-r--r-- | apps/workflowengine/lib/AppInfo/Application.php | 62 | ||||
-rw-r--r-- | apps/workflowengine/lib/Check/UserGroupMembership.php | 108 | ||||
-rw-r--r-- | apps/workflowengine/lib/Controller/FlowOperations.php | 141 | ||||
-rw-r--r-- | apps/workflowengine/lib/Manager.php | 306 |
4 files changed, 617 insertions, 0 deletions
diff --git a/apps/workflowengine/lib/AppInfo/Application.php b/apps/workflowengine/lib/AppInfo/Application.php new file mode 100644 index 00000000000..c196ecd955c --- /dev/null +++ b/apps/workflowengine/lib/AppInfo/Application.php @@ -0,0 +1,62 @@ +<?php +/** + * @copyright Copyright (c) 2016 Morris Jobke <hey@morrisjobke.de> + * + * @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 <http://www.gnu.org/licenses/>. + * + */ + +namespace OCA\WorkflowEngine\AppInfo; + +use OCP\Util; +use OCP\WorkflowEngine\RegisterCheckEvent; + +class Application extends \OCP\AppFramework\App { + + public function __construct() { + parent::__construct('workflowengine'); + + $this->getContainer()->registerAlias('FlowOperationsController', 'OCA\WorkflowEngine\Controller\FlowOperations'); + } + + /** + * Register all hooks and listeners + */ + public function registerHooksAndListeners() { + $dispatcher = $this->getContainer()->getServer()->getEventDispatcher(); + $dispatcher->addListener( + 'OCP\WorkflowEngine\RegisterCheckEvent', + function(RegisterCheckEvent $event) { + $event->addCheck( + 'OCA\WorkflowEngine\Check\UserGroupMembership', + 'User group membership', + ['is', '!is'] + ); + }, + -100 + ); + + $dispatcher->addListener( + 'OCP\WorkflowEngine::loadAdditionalSettingScripts', + function() { + Util::addStyle('workflowengine', 'admin'); + Util::addScript('workflowengine', 'admin'); + Util::addScript('workflowengine', 'usergroupmembershipplugin'); + }, + -100 + ); + } +} diff --git a/apps/workflowengine/lib/Check/UserGroupMembership.php b/apps/workflowengine/lib/Check/UserGroupMembership.php new file mode 100644 index 00000000000..f437dbfc2d1 --- /dev/null +++ b/apps/workflowengine/lib/Check/UserGroupMembership.php @@ -0,0 +1,108 @@ +<?php +/** + * @copyright Copyright (c) 2016 Morris Jobke <hey@morrisjobke.de> + * + * @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 <http://www.gnu.org/licenses/>. + * + */ + +namespace OCA\WorkflowEngine\Check; + + +use OCP\Files\Storage\IStorage; +use OCP\IGroupManager; +use OCP\IUser; +use OCP\IUserSession; +use OCP\WorkflowEngine\ICheck; + +class UserGroupMembership implements ICheck { + + /** @var string */ + protected $cachedUser; + + /** @var string[] */ + protected $cachedGroupMemberships; + + /** @var IUserSession */ + protected $userSession; + + /** @var IGroupManager */ + protected $groupManager; + + /** + * @param IUserSession $userSession + * @param IGroupManager $groupManager + */ + public function __construct(IUserSession $userSession, IGroupManager $groupManager) { + $this->userSession = $userSession; + $this->groupManager = $groupManager; + } + + /** + * @param IStorage $storage + * @param string $path + */ + public function setFileInfo(IStorage $storage, $path) { + // A different path doesn't change group memberships, so nothing to do here. + } + + /** + * @param string $operator + * @param string $value + * @return bool + */ + public function executeCheck($operator, $value) { + $user = $this->userSession->getUser(); + + if ($user instanceof IUser) { + $groupIds = $this->getUserGroups($user); + return ($operator === 'is') === in_array($value, $groupIds); + } else { + return $operator !== 'is'; + } + } + + + /** + * @param string $operator + * @param string $value + * @throws \UnexpectedValueException + */ + public function validateCheck($operator, $value) { + if (!in_array($operator, ['is', '!is'])) { + throw new \UnexpectedValueException('Invalid operator', 1); + } + + if (!$this->groupManager->groupExists($value)) { + throw new \UnexpectedValueException('Group does not exist', 2); + } + } + + /** + * @param IUser $user + * @return string[] + */ + protected function getUserGroups(IUser $user) { + $uid = $user->getUID(); + + if ($this->cachedUser !== $uid) { + $this->cachedUser = $uid; + $this->cachedGroupMemberships = $this->groupManager->getUserGroupIds($user); + } + + return $this->cachedGroupMemberships; + } +} diff --git a/apps/workflowengine/lib/Controller/FlowOperations.php b/apps/workflowengine/lib/Controller/FlowOperations.php new file mode 100644 index 00000000000..e0836c727a2 --- /dev/null +++ b/apps/workflowengine/lib/Controller/FlowOperations.php @@ -0,0 +1,141 @@ +<?php +/** + * @copyright Copyright (c) 2016 Morris Jobke <hey@morrisjobke.de> + * + * @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 <http://www.gnu.org/licenses/>. + * + */ + +namespace OCA\WorkflowEngine\Controller; + +use OCA\WorkflowEngine\Manager; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\JSONResponse; +use OCP\IRequest; +use OCP\WorkflowEngine\RegisterCheckEvent; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; + +class FlowOperations extends Controller { + + /** @var Manager */ + protected $manager; + + /** @var EventDispatcherInterface */ + protected $dispatcher; + + /** + * @param IRequest $request + * @param Manager $manager + * @param EventDispatcherInterface $dispatcher + */ + public function __construct(IRequest $request, Manager $manager, EventDispatcherInterface $dispatcher) { + parent::__construct('workflowengine', $request); + $this->manager = $manager; + $this->dispatcher = $dispatcher; + } + + /** + * @NoCSRFRequired + * + * @return JSONResponse + */ + public function getChecks() { + $event = new RegisterCheckEvent(); + $this->dispatcher->dispatch('OCP\WorkflowEngine\RegisterCheckEvent', $event); + + return new JSONResponse($event->getChecks()); + } + + /** + * @NoCSRFRequired + * + * @param string $class + * @return JSONResponse + */ + public function getOperations($class) { + $operations = $this->manager->getOperations($class); + + foreach ($operations as &$operation) { + $operation = $this->prepareOperation($operation); + } + + return new JSONResponse($operations); + } + + /** + * @param string $class + * @param string $name + * @param array[] $checks + * @param string $operation + * @return JSONResponse The added element + */ + public function addOperation($class, $name, $checks, $operation) { + try { + $operation = $this->manager->addOperation($class, $name, $checks, $operation); + $operation = $this->prepareOperation($operation); + return new JSONResponse($operation); + } catch (\UnexpectedValueException $e) { + return new JSONResponse($e->getMessage(), Http::STATUS_BAD_REQUEST); + } + } + + /** + * @param int $id + * @param string $name + * @param array[] $checks + * @param string $operation + * @return JSONResponse The updated element + */ + public function updateOperation($id, $name, $checks, $operation) { + try { + $operation = $this->manager->updateOperation($id, $name, $checks, $operation); + $operation = $this->prepareOperation($operation); + return new JSONResponse($operation); + } catch (\UnexpectedValueException $e) { + return new JSONResponse($e->getMessage(), Http::STATUS_BAD_REQUEST); + } + } + + /** + * @param int $id + * @return JSONResponse + */ + public function deleteOperation($id) { + $deleted = $this->manager->deleteOperation((int) $id); + return new JSONResponse($deleted); + } + + /** + * @param array $operation + * @return array + */ + protected function prepareOperation(array $operation) { + $checkIds = json_decode($operation['checks']); + $checks = $this->manager->getChecks($checkIds); + + $operation['checks'] = []; + foreach ($checks as $check) { + // Remove internal values + unset($check['id']); + unset($check['hash']); + + $operation['checks'][] = $check; + } + + return $operation; + } +} diff --git a/apps/workflowengine/lib/Manager.php b/apps/workflowengine/lib/Manager.php new file mode 100644 index 00000000000..98c34e894cc --- /dev/null +++ b/apps/workflowengine/lib/Manager.php @@ -0,0 +1,306 @@ +<?php +/** + * @copyright Copyright (c) 2016 Morris Jobke <hey@morrisjobke.de> + * + * @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 <http://www.gnu.org/licenses/>. + * + */ + +namespace OCA\WorkflowEngine; + + +use OCP\AppFramework\QueryException; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\Files\Storage\IStorage; +use OCP\IDBConnection; +use OCP\IServerContainer; +use OCP\WorkflowEngine\ICheck; +use OCP\WorkflowEngine\IManager; + +class Manager implements IManager { + + /** @var IStorage */ + protected $storage; + + /** @var string */ + protected $path; + + /** @var array[] */ + protected $operations = []; + + /** @var array[] */ + protected $checks = []; + + /** @var IDBConnection */ + protected $connection; + + /** @var IServerContainer|\OC\Server */ + protected $container; + + /** + * @param IDBConnection $connection + * @param IServerContainer $container + */ + public function __construct(IDBConnection $connection, IServerContainer $container) { + $this->connection = $connection; + $this->container = $container; + } + + /** + * @inheritdoc + */ + public function setFileInfo(IStorage $storage, $path) { + $this->storage = $storage; + $this->path = $path; + } + + /** + * @inheritdoc + */ + public function getMatchingOperations($class, $returnFirstMatchingOperationOnly = true) { + $operations = $this->getOperations($class); + + $matches = []; + foreach ($operations as $operation) { + $checkIds = json_decode($operation['checks'], true); + $checks = $this->getChecks($checkIds); + + foreach ($checks as $check) { + if (!$this->check($check)) { + // Check did not match, continue with the next operation + continue 2; + } + } + + if ($returnFirstMatchingOperationOnly) { + return $operation; + } + $matches[] = $operation; + } + + return $matches; + } + + /** + * @param array $check + * @return bool + */ + protected function check(array $check) { + try { + $checkInstance = $this->container->query($check['class']); + } catch (QueryException $e) { + // Check does not exist, assume it matches. + return true; + } + + if ($checkInstance instanceof ICheck) { + $checkInstance->setFileInfo($this->storage, $this->path); + return $checkInstance->executeCheck($check['operator'], $check['value']); + } else { + // Check is invalid, assume it matches. + return true; + } + } + + /** + * @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; + } + $result->closeCursor(); + + return $this->operations[$class]; + } + + /** + * @param int $id + * @return array + * @throws \UnexpectedValueException + */ + protected function getOperation($id) { + $query = $this->connection->getQueryBuilder(); + $query->select('*') + ->from('flow_operations') + ->where($query->expr()->eq('id', $query->createNamedParameter($id))); + $result = $query->execute(); + $row = $result->fetch(); + $result->closeCursor(); + + if ($row) { + return $row; + } + + throw new \UnexpectedValueException('Operation does not exist'); + } + + /** + * @param string $class + * @param string $name + * @param array[] $checks + * @param string $operation + * @return array The added operation + * @throws \UnexpectedValueException + */ + public function addOperation($class, $name, array $checks, $operation) { + $checkIds = []; + foreach ($checks as $check) { + $checkIds[] = $this->addCheck($check['class'], $check['operator'], $check['value']); + } + + $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(); + + $id = $query->getLastInsertId(); + return $this->getOperation($id); + } + + /** + * @param int $id + * @param string $name + * @param array[] $checks + * @param string $operation + * @return array The updated operation + * @throws \UnexpectedValueException + */ + public function updateOperation($id, $name, array $checks, $operation) { + $checkIds = []; + 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(); + + return $this->getOperation($id); + } + + /** + * @param int $id + * @return bool + * @throws \UnexpectedValueException + */ + public function deleteOperation($id) { + $query = $this->connection->getQueryBuilder(); + $query->delete('flow_operations') + ->where($query->expr()->eq('id', $query->createNamedParameter($id))); + return (bool) $query->execute(); + } + + /** + * @param int[] $checkIds + * @return array[] + */ + public function getChecks(array $checkIds) { + $checkIds = array_map('intval', $checkIds); + + $checks = []; + foreach ($checkIds as $i => $checkId) { + if (isset($this->checks[$checkId])) { + $checks[$checkId] = $this->checks[$checkId]; + unset($checkIds[$i]); + } + } + + if (empty($checkIds)) { + return $checks; + } + + $query = $this->connection->getQueryBuilder(); + $query->select('*') + ->from('flow_checks') + ->where($query->expr()->in('id', $query->createNamedParameter($checkIds, IQueryBuilder::PARAM_INT_ARRAY))); + $result = $query->execute(); + + $checks = []; + while ($row = $result->fetch()) { + $this->checks[(int) $row['id']] = $row; + $checks[(int) $row['id']] = $row; + } + $result->closeCursor(); + + // TODO What if a check is missing? Should we throw? + // As long as we only allow AND-concatenation of checks, a missing check + // is like a matching check, so it evaluates to true and therefor blocks + // access. So better save than sorry. + + return $checks; + } + + /** + * @param string $class + * @param string $operator + * @param string $value + * @return int Check unique ID + * @throws \UnexpectedValueException + */ + protected function addCheck($class, $operator, $value) { + /** @var ICheck $check */ + $check = $this->container->query($class); + $check->validateCheck($operator, $value); + + $hash = md5($class . '::' . $operator . '::' . $value); + + $query = $this->connection->getQueryBuilder(); + $query->select('id') + ->from('flow_checks') + ->where($query->expr()->eq('hash', $query->createNamedParameter($hash))); + $result = $query->execute(); + + if ($row = $result->fetch()) { + $result->closeCursor(); + return (int) $row['id']; + } + + $query = $this->connection->getQueryBuilder(); + $query->insert('flow_checks') + ->values([ + 'class' => $query->createNamedParameter($class), + 'operator' => $query->createNamedParameter($operator), + 'value' => $query->createNamedParameter($value), + 'hash' => $query->createNamedParameter($hash), + ]); + $query->execute(); + + return $query->getLastInsertId(); + } +} |