* * @author Julius Härtl * * @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\Deck\Service; use OCP\Cache\CappedMemoryCache; use OCA\Circles\Model\Member; use OCA\Deck\Db\Acl; use OCA\Deck\Db\AclMapper; use OCA\Deck\Db\Board; use OCA\Deck\Db\BoardMapper; use OCA\Deck\Db\IPermissionMapper; use OCA\Deck\Db\User; use OCA\Deck\NoPermissionException; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Db\MultipleObjectsReturnedException; use OCP\IConfig; use OCP\IGroupManager; use OCP\ILogger; use OCP\IUserManager; use OCP\Share\IManager; class PermissionService { /** @var CirclesService */ private $circlesService; /** @var BoardMapper */ private $boardMapper; /** @var AclMapper */ private $aclMapper; /** @var ILogger */ private $logger; /** @var IUserManager */ private $userManager; /** @var IGroupManager */ private $groupManager; /** @var IConfig */ private $config; /** @var IManager */ private $shareManager; /** @var string */ private $userId; /** @var array */ private $users = []; private CappedMemoryCache $boardCache; private CappedMemoryCache $permissionCache; public function __construct( ILogger $logger, CirclesService $circlesService, AclMapper $aclMapper, BoardMapper $boardMapper, IUserManager $userManager, IGroupManager $groupManager, IManager $shareManager, IConfig $config, $userId ) { $this->circlesService = $circlesService; $this->aclMapper = $aclMapper; $this->boardMapper = $boardMapper; $this->logger = $logger; $this->userManager = $userManager; $this->groupManager = $groupManager; $this->shareManager = $shareManager; $this->config = $config; $this->userId = $userId; $this->boardCache = new CappedMemoryCache(); $this->permissionCache = new CappedMemoryCache(); } /** * Get current user permissions for a board by id * * @param $boardId * @return bool|array */ public function getPermissions($boardId) { if ($cached = $this->permissionCache->get($boardId)) { return $cached; } $owner = $this->userIsBoardOwner($boardId); $acls = $this->aclMapper->findAll($boardId); $permissions = [ Acl::PERMISSION_READ => $owner || $this->userCan($acls, Acl::PERMISSION_READ), Acl::PERMISSION_EDIT => $owner || $this->userCan($acls, Acl::PERMISSION_EDIT), Acl::PERMISSION_MANAGE => $owner || $this->userCan($acls, Acl::PERMISSION_MANAGE), Acl::PERMISSION_SHARE => ($owner || $this->userCan($acls, Acl::PERMISSION_SHARE)) && (!$this->shareManager->sharingDisabledForUser($this->userId)) ]; $this->permissionCache->set($boardId, $permissions); return $permissions; } /** * Get current user permissions for a board * * @param Board $board * @return array|bool * @internal param $boardId */ public function matchPermissions(Board $board) { $owner = $this->userIsBoardOwner($board->getId()); $acls = $board->getAcl() ?? []; return [ Acl::PERMISSION_READ => $owner || $this->userCan($acls, Acl::PERMISSION_READ), Acl::PERMISSION_EDIT => $owner || $this->userCan($acls, Acl::PERMISSION_EDIT), Acl::PERMISSION_MANAGE => $owner || $this->userCan($acls, Acl::PERMISSION_MANAGE), Acl::PERMISSION_SHARE => ($owner || $this->userCan($acls, Acl::PERMISSION_SHARE)) && (!$this->shareManager->sharingDisabledForUser($this->userId)) ]; } /** * check permissions for replacing dark magic middleware * * @param $mapper IPermissionMapper|null null if $id is a boardId * @param $id int unique identifier of the Entity * @param $permission int * @return bool * @throws NoPermissionException */ public function checkPermission($mapper, $id, $permission, $userId = null): bool { $boardId = $id; if ($mapper instanceof IPermissionMapper && !($mapper instanceof BoardMapper)) { $boardId = $mapper->findBoardId($id); } if ($boardId === null) { // Throw NoPermission to not leak information about existing entries throw new NoPermissionException('Permission denied'); } $permissions = $this->getPermissions($boardId); if ($permissions[$permission] === true) { return true; } // Throw NoPermission to not leak information about existing entries throw new NoPermissionException('Permission denied'); } /** * @param $boardId * @return bool */ public function userIsBoardOwner($boardId, $userId = null) { if ($userId === null) { $userId = $this->userId; } try { $board = $this->getBoard($boardId); return $userId === $board->getOwner(); } catch (DoesNotExistException | MultipleObjectsReturnedException $e) { } return false; } /** * @throws MultipleObjectsReturnedException * @throws DoesNotExistException */ private function getBoard($boardId): Board { if (!isset($this->boardCache[$boardId])) { $this->boardCache[$boardId] = $this->boardMapper->find($boardId, false, true); } return $this->boardCache[$boardId]; } /** * Check if permission matches the acl rules for current user and groups * * @param Acl[] $acls * @param $permission * @return bool */ public function userCan(array $acls, $permission, $userId = null) { if ($userId === null) { $userId = $this->userId; } // check for users foreach ($acls as $acl) { if ($acl->getType() === Acl::PERMISSION_TYPE_USER && $acl->getParticipant() === $userId) { return $acl->getPermission($permission); } if ($this->circlesService->isCirclesEnabled() && $acl->getType() === Acl::PERMISSION_TYPE_CIRCLE) { try { if ($this->circlesService->isUserInCircle($acl->getParticipant(), $userId) && $acl->getPermission($permission)) { return true; } } catch (\Exception $e) { $this->logger->info('Member not found in circle that was accessed. This should not happen.'); } } } // check for groups $hasGroupPermission = false; foreach ($acls as $acl) { if (!$hasGroupPermission && $acl->getType() === Acl::PERMISSION_TYPE_GROUP && $this->groupManager->isInGroup($userId, $acl->getParticipant())) { $hasGroupPermission = $acl->getPermission($permission); } } return $hasGroupPermission; } /** * Find a list of all users (including the ones from groups) * Required to allow assigning them to cards * * @param $boardId * @return array */ public function findUsers($boardId, $refresh = false) { // cache users of a board so we don't query them for every cards if (array_key_exists((string) $boardId, $this->users) && !$refresh) { return $this->users[(string) $boardId]; } try { $board = $this->boardMapper->find($boardId); } catch (DoesNotExistException $e) { return []; } catch (MultipleObjectsReturnedException $e) { return []; } $users = []; if (!$this->userManager->userExists($board->getOwner())) { $this->logger->info('No owner found for board ' . $board->getId()); } else { $users[$board->getOwner()] = new User($board->getOwner(), $this->userManager); } $acls = $this->aclMapper->findAll($boardId); /** @var Acl $acl */ foreach ($acls as $acl) { if ($acl->getType() === Acl::PERMISSION_TYPE_USER) { if (!$this->userManager->userExists($acl->getParticipant())) { $this->logger->info('No user found for acl rule ' . $acl->getId()); continue; } $users[$acl->getParticipant()] = new User($acl->getParticipant(), $this->userManager); } if ($acl->getType() === Acl::PERMISSION_TYPE_GROUP) { $group = $this->groupManager->get($acl->getParticipant()); if ($group === null) { $this->logger->info('No group found for acl rule ' . $acl->getId()); continue; } foreach ($group->getUsers() as $user) { $users[$user->getUID()] = new User($user->getUID(), $this->userManager); } } if ($this->circlesService->isCirclesEnabled() && $acl->getType() === Acl::PERMISSION_TYPE_CIRCLE) { try { $circle = $this->circlesService->getCircle($acl->getParticipant()); if ($circle === null) { $this->logger->info('No circle found for acl rule ' . $acl->getId()); continue; } foreach ($circle->getInheritedMembers() as $member) { if ($member->getUserType() !== 1 || $member->getLevel() < Member::LEVEL_MEMBER) { // deck currently only supports user members in circles continue; } $user = $this->userManager->get($member->getUserId()); if ($user === null) { $this->logger->info('No user found for circle member ' . $member->getUserId()); } else { $users[$member->getUserId()] = new User($member->getUserId(), $this->userManager); } } } catch (\Exception $e) { $this->logger->info('Member not found in circle that was accessed. This should not happen.'); } } } $this->users[(string) $boardId] = $users; return $this->users[(string) $boardId]; } public function canCreate() { if ($this->userId === null) { return false; } $groups = $this->getGroupLimitList(); if (count($groups) === 0) { return true; } foreach ($groups as $group) { if ($this->groupManager->isInGroup($this->userId, $group)) { return true; } } return false; } private function getGroupLimitList() { $value = $this->config->getAppValue('deck', 'groupLimit', ''); $groups = explode(',', $value); if ($value === '') { return []; } return $groups; } /** * Set a different user than the current one, e.g. when no user is available in occ * * @param string $userId */ public function setUserId(string $userId): void { $this->userId = $userId; $this->permissionCache->clear(); } }