diff --git a/application/Espo/Core/Console/Commands/AclCheck.php b/application/Espo/Core/Console/Commands/AclCheck.php new file mode 100644 index 0000000000..1512976f91 --- /dev/null +++ b/application/Espo/Core/Console/Commands/AclCheck.php @@ -0,0 +1,80 @@ +getContainer(); + + if ($portalId) { + $application = new \Espo\Core\Portal\Application($portalId); + $container = $application->getContainer(); + } + + $entityManager = $container->get('entityManager'); + + $user = $entityManager->getEntity('User', $userId); + if (!$user) return; + + if ($portalId) { + if ( + !$user->isPortal() + || + !in_array($portalId, $user->getLinkMultipleIdList('portals')) + ) { + return; + } + } + + $entity = $entityManager->getEntity($scope, $id); + if (!$entity) return; + + $aclManager = $container->get('aclManager'); + + if ($aclManager->check($user, $entity, $action)) { + return 'true'; + } + } +} diff --git a/application/Espo/Core/WebSocket/Pusher.php b/application/Espo/Core/WebSocket/Pusher.php index c24d94b817..3f170a9257 100644 --- a/application/Espo/Core/WebSocket/Pusher.php +++ b/application/Espo/Core/WebSocket/Pusher.php @@ -36,6 +36,8 @@ class Pusher implements WampServerInterface { private $categoryList; + protected $categoriesData; + protected $isDebugMode = false; protected $connectionIdUserIdMap = []; @@ -48,9 +50,11 @@ class Pusher implements WampServerInterface private $phpExecutablePath; - public function __construct(array $categoryList = [], $phpExecutablePath = null, $isDebugMode = false) + public function __construct(array $categoriesData = [], $phpExecutablePath = null, $isDebugMode = false) { - $this->categoryList = $categoryList; + $this->categoryList = array_keys($categoriesData); + $this->categoriesData = $categoriesData; + $this->phpExecutablePath = $phpExecutablePath ?: (new \Symfony\Component\Process\PhpExecutableFinder)->find(); $this->isDebugMode = $isDebugMode; } @@ -60,7 +64,7 @@ class Pusher implements WampServerInterface $topicId = $topic->getId(); if (!$topicId) return; - if (!$this->isCategoryAllowed($topicId)) return; + if (!$this->isTopicAllowed($topicId)) return; $connectionId = $connection->resourceId; @@ -69,6 +73,14 @@ class Pusher implements WampServerInterface if (!isset($this->connectionIdTopicIdListMap[$connectionId])) $this->connectionIdTopicIdListMap[$connectionId] = []; + $checkCommand = $this->getAccessCheckCommandForTopic($connection, $topic); + if ($checkCommand) { + $checkResult = shell_exec($checkCommand); + if ($checkResult !== 'true') { + return; + } + } + if (!in_array($topicId, $this->connectionIdTopicIdListMap[$connectionId])) { if ($this->isDebugMode) echo "add topic {$topicId} for user {$userId}\n"; $this->connectionIdTopicIdListMap[$connectionId][] = $topicId; @@ -80,7 +92,7 @@ class Pusher implements WampServerInterface $topicId = $topic->getId(); if (!$topicId) return; - if (!$this->isCategoryAllowed($topicId)) return; + if (!$this->isTopicAllowed($topicId)) return; $connectionId = $connection->resourceId; @@ -96,8 +108,55 @@ class Pusher implements WampServerInterface } } - protected function isCategoryAllowed($category) + protected function getParamsFromTopicId(string $topicId) : array { + $arr = explode('.', $topicId); + $category = $arr[0]; + + $params = []; + + if (array_key_exists('paramList', $this->categoriesData[$category])) { + foreach ($this->categoriesData[$category]['paramList'] as $i => $item) { + if (isset($arr[$i + 1])) { + $params[$item] = $arr[$i + 1]; + } + } + } + + return $params; + } + + protected function getAccessCheckCommandForTopic(ConnectionInterface $connection, $topic) : ?string + { + $topicId = $topic->getId(); + + $params = $this->getParamsFromTopicId($topicId); + $params['userId'] = $this->getUserIdByConnection($connection); + if (!$params['userId']) { + $connection->close(); + return null; + } + + $category = $this->getTopicCategory($topic); + if (!array_key_exists('accessCheckCommand', $this->categoriesData[$category])) return null; + + $command = $this->phpExecutablePath . " command.php " . $this->categoriesData[$category]['accessCheckCommand']; + foreach ($params as $key => $value) { + str_replace(':' . $key, $value, $command); + } + + return $command; + } + + protected function getTopicCategory($topic) + { + list($category) = explode('.', $topic->getId()); + return $category; + } + + protected function isTopicAllowed($topicId) + { + list($category) = explode('.', $topicId); return in_array($category, $this->categoryList); } diff --git a/application/Espo/Resources/metadata/app/webSocket.json b/application/Espo/Resources/metadata/app/webSocket.json index 7f81831aec..0abe89ee01 100644 --- a/application/Espo/Resources/metadata/app/webSocket.json +++ b/application/Espo/Resources/metadata/app/webSocket.json @@ -1,5 +1,9 @@ { "categories": { - "newNotification": {} + "newNotification": {}, + "streamUpdate": { + "paramList": ["scope", "id", "portalId"], + "accessCheckCommand": "AclCheck :userId :scope :id stream :portalId" + } } } diff --git a/websocket.php b/websocket.php index 60fa22e85f..8bf656eb8f 100644 --- a/websocket.php +++ b/websocket.php @@ -33,11 +33,12 @@ include "bootstrap.php"; $app = new \Espo\Core\Application(); -$categoryList = array_keys($app->getContainer()->get('metadata')->get(['app', 'webSocket', 'categories'], [])); +$categoriesData = $app->getContainer()->get('metadata')->get(['app', 'webSocket', 'categories'], []); + $phpExecutablePath = $app->getContainer()->get('config')->get('phpExecutablePath'); $loop = \React\EventLoop\Factory::create(); -$pusher = new \Espo\Core\WebSocket\Pusher($categoryList, $phpExecutablePath); +$pusher = new \Espo\Core\WebSocket\Pusher($categoriesData, $phpExecutablePath); $context = new \React\ZMQ\Context($loop); $pull = $context->getSocket(\ZMQ::SOCKET_PULL);