From f652b13dd304af0f4dccee7baab2bfb4ec141c93 Mon Sep 17 00:00:00 2001 From: Jonathan Treffler Date: Mon, 4 Nov 2024 19:24:12 +0100 Subject: [PATCH] first draft of ACL rule management --- appinfo/info.xml | 1 + .../FixACLsOfOrganizationFolder.php | 34 +++++++++ lib/Db/ResourceMapper.php | 18 +++-- lib/Manager/PathManager.php | 41 +++++++++++ lib/Service/OrganizationFolderService.php | 73 +++++++++++++++++-- lib/Service/ResourceService.php | 73 +++++++++++++++++-- 6 files changed, 223 insertions(+), 17 deletions(-) create mode 100644 lib/Command/OrganizationFolder/FixACLsOfOrganizationFolder.php create mode 100644 lib/Manager/PathManager.php diff --git a/appinfo/info.xml b/appinfo/info.xml index 463e736..a379e3a 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -24,6 +24,7 @@ OCA\OrganizationFolders\Command\OrganizationFolder\CreateOrganizationFolder OCA\OrganizationFolders\Command\OrganizationFolder\UpdateOrganizationFolder OCA\OrganizationFolders\Command\OrganizationFolder\RemoveOrganizationFolder + OCA\OrganizationFolders\Command\OrganizationFolder\FixACLsOfOrganizationFolder OCA\OrganizationFolders\Command\Resource\CreateResource OCA\OrganizationFolders\Command\Resource\ListResources OCA\OrganizationFolders\Command\ResourceMember\CreateResourceMember diff --git a/lib/Command/OrganizationFolder/FixACLsOfOrganizationFolder.php b/lib/Command/OrganizationFolder/FixACLsOfOrganizationFolder.php new file mode 100644 index 0000000..8668fb6 --- /dev/null +++ b/lib/Command/OrganizationFolder/FixACLsOfOrganizationFolder.php @@ -0,0 +1,34 @@ +setName('organization-folders:recreate-acls') + ->setDescription('Ensures all ACLs of organization folder are correctly set. Should not be neccessary to run unless ACL rules have been modified accidentally by an admin as ACLs will be created/modified automatically as resources are changed') + ->addArgument('id', InputArgument::REQUIRED, 'Id of the organization folder'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $id = (int)$input->getArgument('id'); + + try { + $output->writeln(var_dump($this->organizationFolderService->applyPermissions($id))); + + $output->writeln("done"); + + return 0; + } catch (Exception $e) { + $output->writeln("Exception \"{$e->getMessage()}\" at {$e->getFile()} line {$e->getLine()}"); + return 1; + } + } +} diff --git a/lib/Db/ResourceMapper.php b/lib/Db/ResourceMapper.php index 66d61e0..3dc8ded 100644 --- a/lib/Db/ResourceMapper.php +++ b/lib/Db/ResourceMapper.php @@ -42,7 +42,7 @@ class ResourceMapper extends QBMapper { /* @var $qb IQueryBuilder */ $qb = $this->db->getQueryBuilder(); - $qb->select('resource.*', 'folder.members_acl_permission', 'folder.managers_acl_permission', 'folder.inherited_acl_permission') + $qb->select('resource.*', 'folder.members_acl_permission', 'folder.managers_acl_permission', 'folder.inherited_acl_permission', 'folder.file_id') ->from(self::RESOURCES_TABLE, "resource") ->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT))); @@ -56,21 +56,27 @@ class ResourceMapper extends QBMapper { * @param int $parentResourceId * @return array */ - public function findAll(int $organizationFolderId, ?int $parentResourceId = null): array { + public function findAll(int $organizationFolderId, ?int $parentResourceId = null, array $filters = []): array { /* @var $qb IQueryBuilder */ $qb = $this->db->getQueryBuilder(); - $qb->select('resource.*', 'folder.members_acl_permission', 'folder.managers_acl_permission', 'folder.inherited_acl_permission') + $qb->select('resource.*', 'folder.members_acl_permission', 'folder.managers_acl_permission', 'folder.inherited_acl_permission', 'folder.file_id') ->from(self::RESOURCES_TABLE, "resource") ->where($qb->expr()->eq('organization_folder_id', $qb->createNamedParameter($organizationFolderId, IQueryBuilder::PARAM_INT))); if(is_null($parentResourceId)) { - $qb->andWhere($qb->expr()->isNull('parent_resource')); + $qb->andWhere($qb->expr()->isNull('resource.parent_resource')); } else { - $qb->andWhere($qb->expr()->eq('parent_resource', $qb->createNamedParameter($parentResourceId, IQueryBuilder::PARAM_INT))); + $qb->andWhere($qb->expr()->eq('resource.parent_resource', $qb->createNamedParameter($parentResourceId, IQueryBuilder::PARAM_INT))); } - $qb->leftJoin('resource', self::FOLDER_RESOURCES_TABLE, 'folder', $qb->expr()->eq('resource.id', 'folder.resource_id'),); + $folderJoinCondition = $qb->expr()->eq('resource.id', 'folder.resource_id'); + if(isset($filters["type"]) && $filters["type"] === "folder") { + $qb->andWhere($qb->expr()->eq('resource.type', $qb->createNamedParameter("folder"))); + $qb->innerJoin('resource', self::FOLDER_RESOURCES_TABLE, 'folder', $folderJoinCondition); + } else { + $qb->leftJoin('resource', self::FOLDER_RESOURCES_TABLE, 'folder', $folderJoinCondition); + } return $this->findEntities($qb); } diff --git a/lib/Manager/PathManager.php b/lib/Manager/PathManager.php new file mode 100644 index 0000000..b0ebec3 --- /dev/null +++ b/lib/Manager/PathManager.php @@ -0,0 +1,41 @@ +rootFolder->getMountPoint()->getNumericStorageId(); + } + + public function getOrganizationFolderNode(OrganizationFolder $organizationFolder): ?Folder { + return $this->getOrganizationFolderNodeById($organizationFolder->getId()); + } + + public function getOrganizationFolderNodeById(int $id): ?Folder { + return $this->mountProvider->getFolder($id, False); + } + + public function getFolderResourceNode(FolderResource $resource): ?Folder { + $organizationFolderNode = $this->getOrganizationFolderNodeById($resource->getOrganizationFolderId()); + + return $organizationFolderNode->getFirstNodeById($resource->getFileId()); + } +} \ No newline at end of file diff --git a/lib/Service/OrganizationFolderService.php b/lib/Service/OrganizationFolderService.php index e691fde..7c6a3d2 100644 --- a/lib/Service/OrganizationFolderService.php +++ b/lib/Service/OrganizationFolderService.php @@ -9,19 +9,29 @@ use OCP\IDBConnection; use OCA\GroupFolders\Folder\FolderManager; use OCA\GroupfolderTags\Service\TagService; +use OCA\GroupFolders\ACL\UserMapping\UserMappingManager; +use OCA\GroupFolders\ACL\Rule; use OCA\OrganizationFolders\Errors\OrganizationFolderNotFound; use OCA\OrganizationFolders\Model\OrganizationFolder; use OCA\OrganizationFolders\OrganizationProvider\OrganizationProviderManager; +use OCA\OrganizationFolders\Manager\PathManager; +use OCA\OrganizationFolders\Manager\GroupfolderManager; +use OCA\OrganizationFolders\Manager\ACLManager; class OrganizationFolderService { use TTransactional; public function __construct( - private IDBConnection $db, - private FolderManager $folderManager, - private TagService $tagService, - private OrganizationProviderManager $organizationProviderManager, + protected IDBConnection $db, + protected FolderManager $folderManager, + protected UserMappingManager $userMappingManager, + protected TagService $tagService, + protected OrganizationProviderManager $organizationProviderManager, + protected PathManager $pathManager, + protected GroupfolderManager $groupfolderManager, + protected ACLManager $aclManager, + protected ResourceService $resourceService, ) { } @@ -132,7 +142,60 @@ class OrganizationFolderService { } public function applyPermissions(int $id) { - + $organizationFolder = $this->find($id); + + $memberGroups = $this->getMemberGroups($organizationFolder); + + $this->setGroupsAsGroupfolderMembers($organizationFolder->getId(), $memberGroups); + $this->setRootFolderACLs($organizationFolder, $memberGroups); + + return $this->resourceService->setAllFolderResourceAclsInOrganizationFolder($organizationFolder, $memberGroups); + } + + protected function getMemberGroups(OrganizationFolder $organizationFolder) { + // TODO: fetch member groups, for now only use organization members + $memberGroups = []; + + if(!is_null($organizationFolder->getOrganizationProvider()) && !is_null($organizationFolder->getOrganizationId())) { + $organizationProvider = $this->organizationProviderManager->getOrganizationProvider($organizationFolder->getOrganizationProvider()); + $organization = $organizationProvider->getOrganization($organizationFolder->getOrganizationId()); + + $memberGroups[] = $organization->getMembersGroup(); + } + + return $memberGroups; + } + + protected function setGroupsAsGroupfolderMembers($groupfolderId, array $groups) { + $groupfolderMembers = []; + + foreach($groups as $group) { + $groupfolderMembers[] = [ + "group_id" => $group, + "permissions" => \OCP\Constants::PERMISSION_ALL, + ]; + } + + return $this->groupfolderManager->overwriteMemberGroups($groupfolderId, $groupfolderMembers); + } + /** + * In the root folder of an organization folder only resource folders can exist + * To prevent adding files there all member groups of the groupfolder need to have a read-only ACL rule on the root folder + */ + protected function setRootFolderACLs(OrganizationFolder $organizationFolder, $groups) { + $folderNode = $this->pathManager->getOrganizationFolderNode($organizationFolder); + $fileId = $folderNode->getId(); + + $acls = []; + foreach($groups as $group) { + $acls[] = new Rule(userMapping: $this->userMappingManager->mappingFromId("group", $group), + fileId: $fileId, + mask: 31, + permissions: 1, + ); + } + + $this->aclManager->overwriteACLsForFileId($fileId, $acls); } public function remove($id): void { diff --git a/lib/Service/ResourceService.php b/lib/Service/ResourceService.php index 2cf2a97..bc553bc 100644 --- a/lib/Service/ResourceService.php +++ b/lib/Service/ResourceService.php @@ -7,21 +7,30 @@ use Exception; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Db\MultipleObjectsReturnedException; +use OCA\GroupFolders\ACL\UserMapping\UserMappingManager; +use OCA\GroupFolders\ACL\Rule; + use OCA\OrganizationFolders\Db\Resource; use OCA\OrganizationFolders\Db\FolderResource; use OCA\OrganizationFolders\Db\ResourceMapper; +use OCA\OrganizationFolders\Model\OrganizationFolder; use OCA\OrganizationFolders\Errors\InvalidResourceType; use OCA\OrganizationFolders\Errors\ResourceNotFound; use OCA\OrganizationFolders\Errors\ResourceNameNotUnique; +use OCA\OrganizationFolders\Manager\PathManager; +use OCA\OrganizationFolders\Manager\ACLManager; class ResourceService { public function __construct( - private ResourceMapper $mapper + private ResourceMapper $mapper, + private PathManager $pathManager, + protected ACLManager $aclManager, + private UserMappingManager $userMappingManager, ) { } - public function findAll(int $organizationFolderId, int $parentResourceId = null) { - return $this->mapper->findAll($organizationFolderId, $parentResourceId); + public function findAll(int $organizationFolderId, int $parentResourceId = null, array $filters = []) { + return $this->mapper->findAll($organizationFolderId, $parentResourceId, $filters); } private function handleException(Exception $e, int $id): void { @@ -46,7 +55,7 @@ class ResourceService { string $type, int $organizationFolderId, string $name, - ?int $parentResource = null, + ?int $parentResourceId = null, bool $active = true, ?int $membersAclPermission = null, @@ -59,18 +68,35 @@ class ResourceService { throw new InvalidResourceType($type); } - if(!$this->mapper->existsWithName($organizationFolderId, $parentResource, $name)) { + if(!$this->mapper->existsWithName($organizationFolderId, $parentResourceId, $name)) { $resource->setOrganizationFolderId($organizationFolderId); $resource->setName($name); - $resource->setParentResource($parentResource); $resource->setActive($active); $resource->setLastUpdatedTimestamp(time()); + if(isset($parentResourceId)) { + $parentResource = $this->find($parentResourceId); + + $resource->setParentResource($parentResource->getId()); + + $parentNode = $this->pathManager->getFolderResourceNode($parentResource); + } else { + $parentNode = $this->pathManager->getOrganizationFolderNodeById($organizationFolderId); + } + if($type === "folder") { + $resourceNode = $parentNode->newFolder($name); + $fileId = $resourceNode->getId(); + + if($fileId === -1) { + throw new Exception("Unknown error occured while creating resource folder"); + } + if(isset($membersAclPermission, $managersAclPermission, $inheritedAclPermission)) { $resource->setMembersAclPermission($membersAclPermission); $resource->setManagersAclPermission($managersAclPermission); $resource->setInheritedAclPermission($inheritedAclPermission); + $resource->setFileId($fileId); } else { throw new \InvalidArgumentException("Folder specific parameters must be included, when creating a resource of type folder"); } @@ -137,6 +163,41 @@ class ResourceService { return $this->mapper->update($resource); } + public function setAllFolderResourceAclsInOrganizationFolder(OrganizationFolder $organizationFolder, array $inheritingGroups) { + $topLevelFolderResources = $this->findAll($organizationFolder->getId(), null, ["type" => "folder"]); + + return $this->recursivelySetFolderResourceALCs($topLevelFolderResources, "", $inheritingGroups); + } + + /** + * Recursively overwrite ACL rules for an array of folder resources + * + * @param array $folderResources + * @psalm-param FolderResource[] $folderResources + * @param string $path + * @psalm-param string $path + * @param array $inheritingGroups + * @psalm-param string[] $inheritingGroups + */ + public function recursivelySetFolderResourceALCs(array $folderResources, string $path, array $inheritingGroups) { + foreach($folderResources as $folderResource) { + $resourceFileId = $folderResource->getFileId(); + $acls = []; + + foreach($inheritingGroups as $inheritingGroup) { + $acls[] = new Rule(userMapping: $this->userMappingManager->mappingFromId("group", $inheritingGroup), + fileId: $resourceFileId, + mask: 31, + permissions: $folderResource->getInheritedAclPermission(), + ); + } + + $this->aclManager->overwriteACLsForFileId($resourceFileId, $acls); + + // TODO: recurse sub-resources + } + } + public function delete(int $id): Resource { try { $resource = $this->mapper->find($id);