From c5536d987ba9daa73bdf5b442e99c81b4f728bb4 Mon Sep 17 00:00:00 2001 From: Arthur Schiwon Date: Thu, 21 Mar 2024 18:51:04 +0100 Subject: [PATCH] enh(Contexts): extend permission check - checkPermission(ById) takes inherited permissions via Context into account - adds query to fetch all contexts containing a certain node - adds permission constants to Application Signed-off-by: Arthur Schiwon --- lib/AppInfo/Application.php | 7 +++ lib/Db/ContextMapper.php | 34 +++++++++++ lib/Service/PermissionsService.php | 94 ++++++++++++++++++++++++------ lib/Service/TableService.php | 6 ++ lib/Service/ViewService.php | 23 ++++---- 5 files changed, 136 insertions(+), 28 deletions(-) diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 942fb2ad0..933fbf70c 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -34,6 +34,13 @@ class Application extends App implements IBootstrap { public const NAV_ENTRY_MODE_RECIPIENTS = 1; public const NAV_ENTRY_MODE_ALL = 2; + public const PERMISSION_READ = 1; + public const PERMISSION_CREATE = 2; + public const PERMISSION_UPDATE = 4; + public const PERMISSION_DELETE = 8; + public const PERMISSION_MANAGE = 16; + public const PERMISSION_ALL = 31; + public function __construct() { parent::__construct(self::APP_ID); } diff --git a/lib/Db/ContextMapper.php b/lib/Db/ContextMapper.php index 472c6d3d6..b22629873 100644 --- a/lib/Db/ContextMapper.php +++ b/lib/Db/ContextMapper.php @@ -216,6 +216,40 @@ public function findById(int $contextId, ?string $userId = null): Context { return $this->formatResultRows($r, $userId); } + /** + * @return Context[] + * @throws Exception + */ + public function findAllContainingNode(int $nodeType, int $nodeId, string $userId): array { + $qb = $this->getFindContextBaseQuery($userId); + + $qb->andWhere($qb->expr()->eq('r.node_id', $qb->createNamedParameter($nodeId))) + ->andWhere($qb->expr()->eq('r.node_type', $qb->createNamedParameter($nodeType))); + + $result = $qb->executeQuery(); + $r = $result->fetchAll(); + + $contextIds = []; + foreach ($r as $row) { + $contextIds[$row['id']] = 1; + } + $contextIds = array_keys($contextIds); + unset($row); + + $resultEntities = []; + foreach ($contextIds as $contextId) { + $workArray = []; + foreach ($r as $row) { + if ($row['id'] === $contextId) { + $workArray[] = $row; + } + } + $resultEntities[] = $this->formatResultRows($workArray, $userId); + } + + return $resultEntities; + } + protected function applyOwnedOrSharedQuery(IQueryBuilder $qb, string $userId): void { $sharedToConditions = $qb->expr()->orX(); diff --git a/lib/Service/PermissionsService.php b/lib/Service/PermissionsService.php index 922bd4685..8106381b8 100644 --- a/lib/Service/PermissionsService.php +++ b/lib/Service/PermissionsService.php @@ -153,20 +153,7 @@ public function canManageContextById(int $contextId, ?string $userId = null): bo } public function canAccessView(View $view, ?string $userId = null): bool { - if($this->basisCheck($view, 'view', $userId)) { - return true; - } - - if ($userId) { - try { - $this->getSharedPermissionsIfSharedWithMe($view->getId(), 'view', $userId); - return true; - } catch (NotFoundError $e) { - $this->logger->error($e->getMessage(), ['exception' => $e]); - } - } - - return false; + return $this->canAccessNodeById(Application::NODE_TYPE_VIEW, $view->getId(), $userId); } /** @@ -458,6 +445,64 @@ public function getSharedPermissionsIfSharedWithMe(int $elementId, string $eleme // private methods ========================================================================== + /** + * @throws NotFoundError + */ + public function getPermissionIfAvailableThroughContext(int $nodeId, string $nodeType, string $userId): int { + $permissions = 0; + $found = false; + $iNodeType = match ($nodeType) { + 'table' => Application::NODE_TYPE_TABLE, + 'view' => Application::NODE_TYPE_VIEW, + }; + $contexts = $this->contextMapper->findAllContainingNode($iNodeType, $nodeId, $userId); + foreach ($contexts as $context) { + $found = true; + if ($context->getOwnerType() === Application::OWNER_TYPE_USER + && $context->getOwnerId() === $userId) { + // Making someone owner of a context, makes this person also having manage permissions on the node. + // This is sort of an intended "privilege escalation". + return Application::PERMISSION_ALL; + } + foreach ($context->getNodes() as $nodeRelation) { + $permissions |= $nodeRelation['permissions']; + } + } + if (!$found) { + throw new NotFoundError('Node not found in any context'); + } + return $permissions; + } + + /** + * @throws NotFoundError + */ + public function getPermissionArrayForNodeFromContexts(int $nodeId, string $nodeType, string $userId) { + $permissions = $this->getPermissionIfAvailableThroughContext($nodeId, $nodeType, $userId); + return [ + 'read' => (bool)($permissions & Application::PERMISSION_READ), + 'create' => (bool)($permissions & Application::PERMISSION_CREATE), + 'update' => (bool)($permissions & Application::PERMISSION_UPDATE), + 'delete' => (bool)($permissions & Application::PERMISSION_DELETE), + 'manage' => (bool)($permissions & Application::PERMISSION_MANAGE), + ]; + } + + private function hasPermission(int $existingPermissions, string $permissionName): bool { + $constantName = 'PERMISSION_' . strtoupper($permissionName); + try { + $permissionBit = constant(Application::class . "::$constantName"); + } catch (\Throwable $t) { + $this->logger->error('Unexpected permission string {permission}', [ + 'app' => Application::APP_ID, + 'permission' => $permissionName, + 'exception' => $t, + ]); + return false; + } + return (bool)($existingPermissions & $permissionBit); + } + /** * @param mixed $element * @param 'table'|'view' $nodeType @@ -470,13 +515,22 @@ private function checkPermission($element, string $nodeType, string $permission, return true; } - if ($userId) { + if (!$userId) { + return false; + } + + try { + return $this->getSharedPermissionsIfSharedWithMe($element->getId(), $nodeType, $userId)[$permission]; + } catch (NotFoundError $e) { try { - return $this->getSharedPermissionsIfSharedWithMe($element->getId(), $nodeType, $userId)[$permission]; + if ($this->hasPermission($this->getPermissionIfAvailableThroughContext($element->getId(), $nodeType, $userId), $permission)) { + return true; + } } catch (NotFoundError $e) { - $this->logger->error($e->getMessage(), ['exception' => $e]); } + $this->logger->error($e->getMessage(), ['exception' => $e]); } + return false; } @@ -495,6 +549,12 @@ private function checkPermissionById(int $elementId, string $nodeType, string $p try { return $this->getSharedPermissionsIfSharedWithMe($elementId, $nodeType, $userId)[$permission]; } catch (NotFoundError $e) { + try { + if ($this->hasPermission($this->getPermissionIfAvailableThroughContext($elementId, $nodeType, $userId), $permission)) { + return true; + } + } catch (NotFoundError $e) { + } $this->logger->error($e->getMessage(), ['exception' => $e]); } } diff --git a/lib/Service/TableService.php b/lib/Service/TableService.php index e28fe5fcb..03659e594 100644 --- a/lib/Service/TableService.php +++ b/lib/Service/TableService.php @@ -196,6 +196,12 @@ private function enhanceTable(Table $table, string $userId): void { $table->setIsShared(true); $table->setOnSharePermissions($permissions); } catch (NotFoundError $e) { + try { + $table->setOnSharePermissions($this->permissionsService->getPermissionArrayForNodeFromContexts($table->getId(), 'table', $userId)); + $table->setIsShared(true); + } catch (NotFoundError $e) { + } + } } if (!$table->getIsShared() || $table->getOnSharePermissions()['manage']) { diff --git a/lib/Service/ViewService.php b/lib/Service/ViewService.php index 4593086a0..d509fa85c 100644 --- a/lib/Service/ViewService.php +++ b/lib/Service/ViewService.php @@ -322,24 +322,25 @@ private function enhanceView(View $view, string $userId): void { if ($userId !== '') { if ($userId !== $view->getOwnership()) { try { - $permissions = $this->shareService->getSharedPermissionsIfSharedWithMe($view->getId(), 'view', $userId); + try { + $permissions = $this->shareService->getSharedPermissionsIfSharedWithMe($view->getId(), 'view', $userId); + } catch (NotFoundError) { + $permissions = $this->permissionsService->getPermissionArrayForNodeFromContexts($view->getId(), 'view', $userId); + } $view->setIsShared(true); $canManageTable = false; try { - $manageTableShare = $this->shareService->getSharedPermissionsIfSharedWithMe($view->getTableId(), 'table', $userId); - $canManageTable = $manageTableShare['manage'] ?? false; + try { + $manageTableShare = $this->shareService->getSharedPermissionsIfSharedWithMe($view->getTableId(), 'table', $userId); + } catch (NotFoundError) { + $manageTableShare = $this->permissionsService->getPermissionArrayForNodeFromContexts($view->getTableId(), 'table', $userId); + } + $permissions['manageTable'] = $manageTableShare['manage'] ?? false; } catch (NotFoundError $e) { } catch (\Exception $e) { throw new InternalError($e->getMessage()); } - $view->setOnSharePermissions([ - 'read' => $permissions['read'] ?? false, - 'create' => $permissions['create'] ?? false, - 'update' => $permissions['update'] ?? false, - 'delete' => $permissions['delete'] ?? false, - 'manage' => $permissions['manage'] ?? false, - 'manageTable' => $canManageTable - ]); + $view->setOnSharePermissions($permissions); } catch (NotFoundError $e) { } catch (\Exception $e) { $this->logger->warning('Exception occurred while setting shared permissions: '.$e->getMessage().' No permissions granted.');