diff options
author | Carl Schwan <carl@carlschwan.eu> | 2022-11-02 18:05:41 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-11-02 18:05:41 +0300 |
commit | 8e4053b98c5b3032054dc7d1f98aafb8145039a6 (patch) | |
tree | 514f72dbd3df180b4493cf2d246cb7a1a7183d9d | |
parent | dfe4d0564816999dcc35ceebff27ebb145f89c6e (diff) | |
parent | a2a4ebc61efc3708fb47593bcd9d74f6be7e4983 (diff) |
Merge pull request #2169 from nextcloud/backport/stable25/2072
[stable25] Improve admin delegation and expose api to other apps
-rw-r--r-- | appinfo/routes.php | 7 | ||||
-rw-r--r-- | lib/AppInfo/Application.php | 7 | ||||
-rw-r--r-- | lib/AuthorizedAdminSettingMiddleware.php | 82 | ||||
-rw-r--r-- | lib/Controller/DelegationController.php | 46 | ||||
-rw-r--r-- | lib/Controller/FolderController.php | 57 | ||||
-rw-r--r-- | lib/Listeners/LoadAdditionalScriptsListener.php | 1 | ||||
-rw-r--r-- | lib/Service/ApplicationService.php | 43 | ||||
-rw-r--r-- | lib/Service/DelegationService.php | 105 | ||||
-rw-r--r-- | lib/Service/FoldersFilter.php | 57 | ||||
-rw-r--r-- | lib/Settings/Admin.php | 40 | ||||
-rw-r--r-- | psalm.xml | 10 | ||||
-rw-r--r-- | src/Constants.js | 23 | ||||
-rw-r--r-- | src/settings/AdminGroupSelect.tsx | 114 | ||||
-rw-r--r-- | src/settings/Api.ts | 35 | ||||
-rw-r--r-- | src/settings/App.scss | 16 | ||||
-rw-r--r-- | src/settings/App.tsx | 43 | ||||
-rw-r--r-- | src/settings/FolderGroups.tsx | 24 | ||||
-rw-r--r-- | src/settings/Nextcloud.d.ts | 17 | ||||
-rw-r--r-- | src/settings/SubAdminGroupSelect.tsx | 113 | ||||
-rw-r--r-- | tests/ACL/RuleManagerTest.php | 6 |
20 files changed, 788 insertions, 58 deletions
diff --git a/appinfo/routes.php b/appinfo/routes.php index 27cb637c..da42c75a 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -73,5 +73,10 @@ return ['routes' => [ 'name' => 'Delegation#getAllGroups', 'url' => 'delegation/groups', 'verb' => 'GET' - ] + ], + [ + 'name' => 'Delegation#getAuthorizedGroups', + 'url' => '/delegation/authorized-groups', + 'verb' => 'GET', + ], ]]; diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index f6b448fa..6ba818af 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -28,6 +28,7 @@ use OCA\GroupFolders\ACL\ACLManagerFactory; use OCA\GroupFolders\ACL\RuleManager; use OCA\GroupFolders\ACL\UserMapping\IUserMappingManager; use OCA\GroupFolders\ACL\UserMapping\UserMappingManager; +use OCA\GroupFolders\AuthorizedAdminSettingMiddleware; use OCA\GroupFolders\BackgroundJob\ExpireGroupPlaceholder; use OCA\GroupFolders\BackgroundJob\ExpireGroupTrash as ExpireGroupTrashJob; use OCA\GroupFolders\BackgroundJob\ExpireGroupVersions as ExpireGroupVersionsJob; @@ -66,6 +67,10 @@ class Application extends App implements IBootstrap { parent::__construct('groupfolders', $urlParams); } + public const APPS_USE_GROUPFOLDERS = [ + 'workspace' + ]; + public function register(IRegistrationContext $context): void { $context->registerEventListener(LoadAdditionalScriptsEvent::class, LoadAdditionalScriptsListener::class); $context->registerEventListener(BeforeTemplateRenderedEvent::class, LoadAdditionalScriptsListener::class); @@ -187,6 +192,8 @@ class Application extends App implements IBootstrap { }); $context->registerServiceAlias(IUserMappingManager::class, UserMappingManager::class); + + $context->registerMiddleware(AuthorizedAdminSettingMiddleware::class); } public function boot(IBootContext $context): void { diff --git a/lib/AuthorizedAdminSettingMiddleware.php b/lib/AuthorizedAdminSettingMiddleware.php new file mode 100644 index 00000000..db83504e --- /dev/null +++ b/lib/AuthorizedAdminSettingMiddleware.php @@ -0,0 +1,82 @@ +<?php + +/** + * @author Cyrille Bollu <cyr.debian@bollu.be> for Arawa (https://www.arawa.fr/) + * + * GroupFolders + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OCA\GroupFolders; + +use Exception; +use OCA\GroupFolders\Service\DelegationService; +use OCP\AppFramework\Http\JSONResponse; +use OCP\AppFramework\Http\Response; +use OCP\AppFramework\Http\TemplateResponse; +use OCP\AppFramework\Middleware; +use OCP\AppFramework\Utility\IControllerMethodReflector; +use OCP\IRequest; + +class AuthorizedAdminSettingMiddleware extends Middleware { + private DelegationService $delegatedService; + private IControllerMethodReflector $reflector; + private IRequest $request; + + public function __construct( + DelegationService $delegatedService, + IControllerMethodReflector $reflector, + IRequest $request + ) { + $this->delegatedService = $delegatedService; + $this->reflector = $reflector; + $this->request = $request; + } + + /** + * + * {@inheritDoc} + * @see \OCP\AppFramework\Middleware::beforeController() + * + * Throws an error when the user is not allowed to use the app's APIs + * + */ + public function beforeController($controller, $methodName) { + if ($this->reflector->hasAnnotation('RequireGroupFolderAdmin')) { + if (!$this->delegatedService->hasApiAccess()) { + throw new Exception('Logged in user must be an admin, a sub admin or gotten special right to access this setting'); + } + } + } + + /** + * + * {@inheritDoc} + * @see \OCP\AppFramework\Middleware::afterException() + * + */ + public function afterException($controller, $methodName, \Exception $exception): Response { + if (stripos($this->request->getHeader('Accept'), 'html') === false) { + $response = new JSONResponse( + ['message' => $exception->getMessage()], + (int)$exception->getCode() + ); + } else { + $response = new TemplateResponse('core', '403', ['message' => $exception->getMessage()], 'guest'); + $response->setStatus((int)$exception->getCode()); + } + return $response; + } +} diff --git a/lib/Controller/DelegationController.php b/lib/Controller/DelegationController.php index 1a4c3677..264bea95 100644 --- a/lib/Controller/DelegationController.php +++ b/lib/Controller/DelegationController.php @@ -22,27 +22,40 @@ namespace OCA\GroupFolders\Controller; +use OCA\GroupFolders\Service\DelegationService; +use OCP\IConfig; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\OCSController; use OCP\IGroupManager; use OCP\IRequest; +use OCA\Settings\Service\AuthorizedGroupService; class DelegationController extends OCSController { private IGroupManager $groupManager; + private IConfig $config; + private DelegationService $delegation; + private AuthorizedGroupService $authorizedGroupService; public function __construct( string $AppName, + IConfig $config, IGroupManager $groupManager, - IRequest $request + IRequest $request, + DelegationService $delegation, + AuthorizedGroupService $authorizedGroupService ) { parent::__construct($AppName, $request); + $this->config = $config; $this->groupManager = $groupManager; + $this->delegation = $delegation; + $this->authorizedGroupService = $authorizedGroupService; } /** * Returns the list of all groups * - * @AuthorizedAdminSetting(settings=OCA\GroupFolders\Settings\Admin) + * @NoAdminRequired + * @RequireGroupFolderAdmin */ public function getAllGroups(): DataResponse { // Get all groups @@ -52,12 +65,35 @@ class DelegationController extends OCSController { $data = []; foreach ($groups as $group) { $data[] = [ - 'id' => $group->getGID(), - 'displayname' => $group->getDisplayName(), + 'gid' => $group->getGID(), + 'displayName' => $group->getDisplayName(), + ]; + } + + return new DataResponse($data); + } + + /** + * Get the list Groups related to classname. + * If the classname is + * - OCA\GroupFolders\Settings\Admin : It's reference to fields in Admin Priveleges. + * - OCA\GroupFolders\Controller\DelegationController : It's just to specific the subadmins. + * They can only manage groupfolders in which they are added in the Advanced Permissions (groups only) + * @NoAdminRequired + * @RequireGroupFolderAdmin + */ + public function getAuthorizedGroups(string $classname = ""): DataResponse { + $data = []; + $authorizedGroups = $this->authorizedGroupService->findExistingGroupsForClass($classname); + + foreach ($authorizedGroups as $authorizedGroup) { + $group = $this->groupManager->get($authorizedGroup->getGroupId()); + $data[] = [ + 'gid' => $group->getGID(), + 'displayName' => $group->getDisplayName(), ]; } - // return info return new DataResponse($data); } } diff --git a/lib/Controller/FolderController.php b/lib/Controller/FolderController.php index 0a401dfb..d7dd6598 100644 --- a/lib/Controller/FolderController.php +++ b/lib/Controller/FolderController.php @@ -24,6 +24,8 @@ namespace OCA\GroupFolders\Controller; use OC\AppFramework\OCS\V1Response; use OCA\GroupFolders\Folder\FolderManager; use OCA\GroupFolders\Mount\MountProvider; +use OCA\GroupFolders\Service\DelegationService; +use OCA\GroupFolders\Service\FoldersFilter; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\OCSController; use OCP\Files\IRootFolder; @@ -36,6 +38,8 @@ class FolderController extends OCSController { private MountProvider $mountProvider; private IRootFolder $rootFolder; private ?IUser $user = null; + private FoldersFilter $foldersFilter; + private DelegationService $delegationService; public function __construct( string $AppName, @@ -43,9 +47,12 @@ class FolderController extends OCSController { FolderManager $manager, MountProvider $mountProvider, IRootFolder $rootFolder, - IUserSession $userSession + IUserSession $userSession, + FoldersFilter $foldersFilter, + DelegationService $delegationService ) { parent::__construct($AppName, $request); + $this->foldersFilter = $foldersFilter; $this->manager = $manager; $this->mountProvider = $mountProvider; $this->rootFolder = $rootFolder; @@ -54,19 +61,27 @@ class FolderController extends OCSController { $this->registerResponder('xml', function ($data): V1Response { return $this->buildOCSResponseXML('xml', $data); }); + $this->delegationService = $delegationService; } /** - * @AuthorizedAdminSetting(settings=OCA\GroupFolders\Settings\Admin) + * @NoAdminRequired + * @RequireGroupFolderAdmin */ public function getFolders(): DataResponse { - return new DataResponse($this->manager->getAllFoldersWithSize($this->getRootFolderStorageId())); + $folders = $this->manager->getAllFoldersWithSize($this->getRootFolderStorageId()); + if ($this->delegationService->isAdminNextcloud() || $this->delegationService->isDelegatedAdmin()) { + return new DataResponse($folders); + } + if ($this->delegationService->hasOnlyApiAccess()) { + $folders = $this->foldersFilter->getForApiUser($folders); + } + return new DataResponse($folders); } /** - * @AuthorizedAdminSetting(settings=OCA\GroupFolders\Settings\Admin) - * @param int $id - * @return DataResponse + * @NoAdminRequired + * @RequireGroupFolderAdmin */ public function getFolder(int $id): DataResponse { return new DataResponse($this->manager->getFolder($id, $this->getRootFolderStorageId())); @@ -77,7 +92,8 @@ class FolderController extends OCSController { } /** - * @AuthorizedAdminSetting(settings=OCA\GroupFolders\Settings\Admin) + * @RequireGroupFolderAdmin + * @NoAdminRequired */ public function addFolder(string $mountpoint): DataResponse { $id = $this->manager->createFolder($mountpoint); @@ -85,7 +101,8 @@ class FolderController extends OCSController { } /** - * @AuthorizedAdminSetting(settings=OCA\GroupFolders\Settings\Admin) + * @NoAdminRequired + * @RequireGroupFolderAdmin */ public function removeFolder(int $id): DataResponse { $folder = $this->mountProvider->getFolder($id); @@ -97,7 +114,8 @@ class FolderController extends OCSController { } /** - * @AuthorizedAdminSetting(settings=OCA\GroupFolders\Settings\Admin) + * @NoAdminRequired + * @RequireGroupFolderAdmin */ public function setMountPoint(int $id, string $mountPoint): DataResponse { $this->manager->setMountPoint($id, $mountPoint); @@ -105,7 +123,8 @@ class FolderController extends OCSController { } /** - * @AuthorizedAdminSetting(settings=OCA\GroupFolders\Settings\Admin) + * @NoAdminRequired + * @RequireGroupFolderAdmin */ public function addGroup(int $id, string $group): DataResponse { $this->manager->addApplicableGroup($id, $group); @@ -113,7 +132,8 @@ class FolderController extends OCSController { } /** - * @AuthorizedAdminSetting(settings=OCA\GroupFolders\Settings\Admin) + * @NoAdminRequired + * @RequireGroupFolderAdmin */ public function removeGroup(int $id, string $group): DataResponse { $this->manager->removeApplicableGroup($id, $group); @@ -121,7 +141,8 @@ class FolderController extends OCSController { } /** - * @AuthorizedAdminSetting(settings=OCA\GroupFolders\Settings\Admin) + * @NoAdminRequired + * @RequireGroupFolderAdmin */ public function setPermissions(int $id, string $group, int $permissions): DataResponse { $this->manager->setGroupPermissions($id, $group, $permissions); @@ -129,7 +150,8 @@ class FolderController extends OCSController { } /** - * @AuthorizedAdminSetting(settings=OCA\GroupFolders\Settings\Admin) + * @NoAdminRequired + * @RequireGroupFolderAdmin * @throws \OCP\DB\Exception */ public function setManageACL(int $id, string $mappingType, string $mappingId, bool $manageAcl): DataResponse { @@ -138,7 +160,8 @@ class FolderController extends OCSController { } /** - * @AuthorizedAdminSetting(settings=OCA\GroupFolders\Settings\Admin) + * @NoAdminRequired + * @RequireGroupFolderAdmin */ public function setQuota(int $id, int $quota): DataResponse { $this->manager->setFolderQuota($id, $quota); @@ -146,7 +169,8 @@ class FolderController extends OCSController { } /** - * @AuthorizedAdminSetting(settings=OCA\GroupFolders\Settings\Admin) + * @NoAdminRequired + * @RequireGroupFolderAdmin */ public function setACL(int $id, bool $acl): DataResponse { $this->manager->setFolderACL($id, $acl); @@ -154,7 +178,8 @@ class FolderController extends OCSController { } /** - * @AuthorizedAdminSetting(settings=OCA\GroupFolders\Settings\Admin) + * @NoAdminRequired + * @RequireGroupFolderAdmin */ public function renameFolder(int $id, string $mountpoint): DataResponse { $this->manager->renameFolder($id, $mountpoint); diff --git a/lib/Listeners/LoadAdditionalScriptsListener.php b/lib/Listeners/LoadAdditionalScriptsListener.php index 5aff9f0e..6aafa587 100644 --- a/lib/Listeners/LoadAdditionalScriptsListener.php +++ b/lib/Listeners/LoadAdditionalScriptsListener.php @@ -23,7 +23,6 @@ declare(strict_types=1); - namespace OCA\GroupFolders\Listeners; use OCP\EventDispatcher\Event; diff --git a/lib/Service/ApplicationService.php b/lib/Service/ApplicationService.php new file mode 100644 index 00000000..04a4ae00 --- /dev/null +++ b/lib/Service/ApplicationService.php @@ -0,0 +1,43 @@ +<?php + +/** + * @author Baptiste Fotia <baptiste.fotia@arawa.fr> for Arawa (https://arawa.fr) + * + * GroupFolders + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OCA\GroupFolders\Service; + +use OCA\GroupFolders\AppInfo\Application; +use OCP\App\IAppManager; + +class ApplicationService { + private IAppManager $appManager; + + public function __construct(IAppManager $appManager) { + $this->appManager = $appManager; + } + + /** + * Check that all apps that depend on Groupfolders are installed + * @return boolean true if all apps are installed, false otherwise. + */ + public function checkAppsInstalled(): bool { + $diffApps = array_diff(Application::APPS_USE_GROUPFOLDERS, $this->appManager->getInstalledApps()); + + return empty($diffApps); + } +} diff --git a/lib/Service/DelegationService.php b/lib/Service/DelegationService.php new file mode 100644 index 00000000..363a4810 --- /dev/null +++ b/lib/Service/DelegationService.php @@ -0,0 +1,105 @@ +<?php + +/** + * @author Cyrille Bollu <cyr.debian@bollu.be> for Arawa (https://arawa.fr) + * + * GroupFolders + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OCA\GroupFolders\Service; + +use OC\Settings\AuthorizedGroupMapper; +use OCA\GroupFolders\Controller\DelegationController; +use OCA\GroupFolders\Settings\Admin; +use OCP\IGroupManager; +use OCP\IUserSession; + +class DelegationService { + /** + * Has access to the entire groupfolders + */ + private const CLASS_NAME_ADMIN_DELEGATION = Admin::class; + + /** + * Has access only to the groupfolders in which the user has advanced + * permissions. + */ + private const CLASS_API_ACCESS = DelegationController::class; + + private AuthorizedGroupMapper $groupAuthorizationMapper; + private IGroupManager $groupManager; + private IUserSession $userSession; + + public function __construct( + AuthorizedGroupMapper $groupAuthorizationMapper, + IGroupManager $groupManager, + IUserSession $userSession + ) { + $this->groupAuthorizationMapper = $groupAuthorizationMapper; + $this->groupManager = $groupManager; + $this->userSession = $userSession; + } + + /** + * @return bool true is admin of nextcloud otherwise false. + */ + public function isAdminNextcloud(): bool { + return $this->groupManager->isAdmin($this->userSession->getUser()->getUID()); + } + + /** + * @return bool true if the user is a delegated admin + */ + public function isDelegatedAdmin(): bool { + return $this->getAccessLevel([ + self::CLASS_NAME_ADMIN_DELEGATION, + ]); + } + + /** + * @return bool true if the user has api access + */ + public function hasApiAccess(): bool { + if ($this->isAdminNextcloud()) { + return true; + } + return $this->getAccessLevel([ + self::CLASS_API_ACCESS, + self::CLASS_NAME_ADMIN_DELEGATION, + ]); + } + + /** + * @return bool true if the user has api access + */ + public function hasOnlyApiAccess(): bool { + return $this->getAccessLevel([ + self::CLASS_API_ACCESS, + ]); + } + + private function getAccessLevel(array $settingClasses): bool { + $authorized = false; + $authorizedClasses = $this->groupAuthorizationMapper->findAllClassesForUser($this->userSession->getUser()); + foreach ($settingClasses as $settingClass) { + $authorized = in_array($settingClass, $authorizedClasses, true); + if ($authorized) { + break; + } + } + return $authorized; + } +} diff --git a/lib/Service/FoldersFilter.php b/lib/Service/FoldersFilter.php new file mode 100644 index 00000000..13cd88c8 --- /dev/null +++ b/lib/Service/FoldersFilter.php @@ -0,0 +1,57 @@ +<?php + +/** + * @author Baptiste Fotia <baptiste.fotia@hotmail.com> for Arawa (https://arawa.fr) + * + * GroupFolders + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OCA\GroupFolders\Service; + +use OCP\IUserSession; +use OCP\IGroupManager; + +class FoldersFilter { + private IUserSession $userSession; + private IGroupManager $groupManager; + + public function __construct(IUserSession $userSession, IGroupManager $groupManager) { + $this->userSession = $userSession; + $this->groupManager = $groupManager; + } + + /** + * @param array $folders List of all folders + * @return array $folders List of folders that the api user can access + */ + public function getForApiUser(array $folders): array { + $user = $this->userSession->getUser(); + $folders = array_filter($folders, function (array $folder) use ($user) { + foreach ($folder['manage'] as $manager) { + if ($manager['type'] === 'group') { + if ($this->groupManager->isInGroup($user->getUid(), $manager['id'])) { + return true; + } + } elseif ($manager['id'] === $user->getUid()) { + return true; + } + } + return false; + }); + + return $folders; + } +} diff --git a/lib/Settings/Admin.php b/lib/Settings/Admin.php index 7f098993..3f49b923 100644 --- a/lib/Settings/Admin.php +++ b/lib/Settings/Admin.php @@ -21,14 +21,39 @@ namespace OCA\GroupFolders\Settings; +use OCA\GroupFolders\Service\ApplicationService; +use OCA\GroupFolders\Service\DelegationService; use OCP\AppFramework\Http\TemplateResponse; use OCP\Settings\IDelegatedSettings; +use OCP\AppFramework\Services\IInitialState; class Admin implements IDelegatedSettings { - /** - * @return TemplateResponse - */ - public function getForm() { + private IInitialState $initialState; + private ApplicationService $applicationService; + private DelegationService $delegationService; + + public function __construct( + IInitialState $initialState, + ApplicationService $applicationService, + DelegationService $delegationService + ) { + $this->initialState = $initialState; + $this->applicationService = $applicationService; + $this->delegationService = $delegationService; + } + + public function getForm(): TemplateResponse { + $this->initialState->provideInitialState( + 'checkAppsInstalled', + $this->applicationService->checkAppsInstalled() + ); + + $this->initialState->provideInitialState( + 'isAdminNextcloud', + $this->delegationService->isAdminNextcloud() + ); + + return new TemplateResponse( 'groupfolders', 'index', @@ -37,10 +62,7 @@ class Admin implements IDelegatedSettings { ); } - /** - * @return string the section ID, e.g. 'sharing' - */ - public function getSection() { + public function getSection(): string { return 'groupfolders'; } @@ -51,7 +73,7 @@ class Admin implements IDelegatedSettings { * * E.g.: 70 */ - public function getPriority() { + public function getPriority(): int { return 90; } @@ -38,19 +38,23 @@ </UndefinedInterfaceMethod> <UndefinedClass> <errorLevel type="suppress"> - <referencedClass name="OCA\Files\Event\LoadAdditionalScriptsEvent"/> <referencedClass name="OC\AppFramework\OCS\V1Response"/> + <referencedClass name="OC\AppFramework\Utility\ControllerMethodReflector"/> <referencedClass name="OC\Security\CSP\ContentSecurityPolicyNonceManager" /> + <referencedClass name="OC\Settings\AuthorizedGroupMapper"/> + <referencedClass name="OCA\Files\Event\LoadAdditionalScriptsEvent"/> + <referencedClass name="OCA\Settings\Service\AuthorizedGroupService"/> </errorLevel> </UndefinedClass> <UndefinedDocblockClass> <errorLevel type="suppress"> - <referencedClass name="OC\AppFramework\OCS\BaseResponse"/> + <referencedClass name="Doctrine\DBAL\Driver\Statement" /> <referencedClass name="Doctrine\DBAL\Schema\Schema" /> <referencedClass name="Doctrine\DBAL\Schema\SchemaException" /> - <referencedClass name="Doctrine\DBAL\Driver\Statement" /> <referencedClass name="Doctrine\DBAL\Schema\Table" /> + <referencedClass name="OC\AppFramework\OCS\BaseResponse"/> <referencedClass name="OC\Security\CSP\ContentSecurityPolicyNonceManager" /> + <referencedClass name="OCA\Settings\Service\AuthorizedGroupService"/> </errorLevel> </UndefinedDocblockClass> </issueHandlers> diff --git a/src/Constants.js b/src/Constants.js new file mode 100644 index 00000000..467261ee --- /dev/null +++ b/src/Constants.js @@ -0,0 +1,23 @@ +/* + * @copyright Copyright (c) 2018 Baptiste Fotia <baptiste.fotia@arawa.fr> + * + * @author Baptiste Fotia <baptiste.fotia@arawa.fr> + * + * @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/>. + * + */ +export const CLASS_NAME_ADMIN_DELEGATION = 'OCA\\GroupFolders\\Settings\\Admin' +export const CLASS_NAME_SUBADMIN_DELEGATION = 'OCA\\GroupFolders\\Controller\\DelegationController' diff --git a/src/settings/AdminGroupSelect.tsx b/src/settings/AdminGroupSelect.tsx new file mode 100644 index 00000000..a8ee182c --- /dev/null +++ b/src/settings/AdminGroupSelect.tsx @@ -0,0 +1,114 @@ +/** + * @copyright Copyright (c) 2018 Julius Härtl <jus@bitgrid.net> + * + * @author Cyrille Bollu <cyr.debian@bollu.be> for Arawa (https://www.arawa.fr/) + * @author Baptiste Fotia <baptiste.fotia@hotmail.com> for Arawa (https://arawa.fr) + * + * @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/>. + * + */ + +import * as React from 'react'; +import Select from 'react-select'; +import {getCurrentUser} from '@nextcloud/auth'; +import {Component} from 'react'; +import {Group, Api} from './Api'; +import {CLASS_NAME_ADMIN_DELEGATION} from '../Constants.js' + +interface AdminGroupSelectProps { + groups: Group[], + allGroups: Group[], + delegatedAdminGroups: Group[], +} + +class AdminGroupSelect extends Component<AdminGroupSelectProps> { + + state: AdminGroupSelectProps = { + groups: [], + allGroups: [], + delegatedAdminGroups: [], + } + + constructor (props) { + super(props) + this.state.groups = props.groups + this.state.allGroups = props.allGroups + this.state.delegatedAdminGroups = props.delegatedAdminGroups + } + + api = new Api() + + componentDidMount() { + this.api.listGroups().then((groups) => { + this.setState({groups}); + }); + this.api.listDelegatedGroups(CLASS_NAME_ADMIN_DELEGATION).then((groups) => { + this.setState({delegatedAdminGroups: groups}); + }); + } + + updateDelegatedAdminGroups(options: {value: string, label: string}[]): void { + if (this.state.groups !== undefined) { + const groups = options.map(option => { + return this.state.groups.filter(g => g.gid === option.value)[0]; + }); + this.setState({delegatedAdminGroups: groups}, () => { + this.api.updateDelegatedGroups(this.state.delegatedAdminGroups, CLASS_NAME_ADMIN_DELEGATION); + }); + } + } + + render () { + const options = this.state.groups.map(group => { + return { + value: group.gid, + label: group.displayName + }; + }); + + return <Select + onChange={ this.updateDelegatedAdminGroups.bind(this) } + isDisabled={getCurrentUser() ? !getCurrentUser()!.isAdmin : true} + isMulti + value={this.state.delegatedAdminGroups.map(group => { + return { + value: group.gid, + label: group.displayName + }; + })} + className="delegated-admins-select" + options={options} + placeholder={t('groupfolders', 'Add group')} + styles={{ + input: (provided) => ({ + ...provided, + height: '30' + }), + control: (provided) => ({ + ...provided, + backgroundColor: 'var(--color-main-background)' + }), + menu: (provided) => ({ + ...provided, + backgroundColor: 'var(--color-main-background)', + borderColor: 'var(--color-border, #888)' + }) + }} + /> + } +} + +export default AdminGroupSelect
\ No newline at end of file diff --git a/src/settings/Api.ts b/src/settings/Api.ts index 3850cb17..27778910 100644 --- a/src/settings/Api.ts +++ b/src/settings/Api.ts @@ -1,10 +1,12 @@ -import {OCSResult} from "NC"; +import {OCSResult, AxiosOCSResult} from "NC"; import Thenable = JQuery.Thenable; import {FolderGroupsProps} from "./FolderGroups"; +import axios from '@nextcloud/axios' +import { generateUrl } from "@nextcloud/router"; export interface Group { - id: string; - displayname: string; + gid: string; + displayName: string; } export interface OCSUser { @@ -43,10 +45,33 @@ export class Api { return $.getJSON(this.getUrl('folders')) .then((data: OCSResult<Folder[]>) => Object.keys(data.ocs.data).map(id => data.ocs.data[id])); } - + // Returns all NC groups listGroups(): Thenable<Group[]> { return $.getJSON(this.getUrl('delegation/groups')) - .then((data: OCSResult<Group[]>) => data.ocs.data); + .then((data: OCSResult<Group[]>) => { + // No need to present the admin group as it is automaticaly added + const groups = data.ocs.data.filter(g => g.gid !== 'admin') + return groups + }); + } + + // Returns all groups that have been granted delegated admin or subadmin rights on groupfolders + listDelegatedGroups(classname: string): Thenable<Group[]> { + return axios.get(this.getUrl('/delegation/authorized-groups'), { params: { classname } }) + .then((data: AxiosOCSResult<Group[]>) => { + // The admin group is always there. We don't want the user to remove it + const groups = data.data.ocs.data.filter(g => g.gid !== 'admin') + return groups + }) + } + + // Updates the list of groups that have been granted delegated admin or subadmin rights on groupfolders + updateDelegatedGroups(newGroups: Group[], classname: string): Thenable<void> { + return axios.post(generateUrl('/apps/settings/') + '/settings/authorizedgroups/saveSettings', { + newGroups, + class: classname + }) + .then((data) => data.data) } createFolder(mountPoint: string): Thenable<number> { diff --git a/src/settings/App.scss b/src/settings/App.scss index f2b2466f..705bc063 100644 --- a/src/settings/App.scss +++ b/src/settings/App.scss @@ -124,3 +124,19 @@ max-width: calc(100% - 40px); } } + +#groupfolders-admin-delegation { + + h3 { + margin-top: 22px; + margin-bottom: 22px; + } + + .end-description-delegation { + margin-bottom: 20px; + } + + .delegated-admins-select { + margin-bottom: 30px; + } +}
\ No newline at end of file diff --git a/src/settings/App.tsx b/src/settings/App.tsx index 96327f8f..ddde3698 100644 --- a/src/settings/App.tsx +++ b/src/settings/App.tsx @@ -10,6 +10,9 @@ import {SortArrow} from "./SortArrow"; import FlipMove from "react-flip-move"; import AsyncSelect from 'react-select/async' import Thenable = JQuery.Thenable; +import AdminGroupSelect from './AdminGroupSelect'; +import SubAdminGroupSelect from './SubAdminGroupSelect'; +import { loadState } from '@nextcloud/initial-state' const defaultQuotaOptions = { '1 GB': 1073741274, @@ -21,6 +24,8 @@ const defaultQuotaOptions = { export type SortKey = 'mount_point' | 'quota' | 'groups' | 'acl'; export interface AppState { + delegatedAdminGroups: Group[], + delegatedSubAdminGroups: Group[], folders: Folder[]; groups: Group[], newMountPoint: string; @@ -30,12 +35,16 @@ export interface AppState { filter: string; sort: SortKey; sortOrder: number; + isAdminNextcloud: boolean; + checkAppsInstalled: boolean; } export class App extends Component<{}, AppState> implements OC.Plugin<OC.Search.Core> { api = new Api(); state: AppState = { + delegatedAdminGroups: [], + delegatedSubAdminGroups: [], folders: [], groups: [], newMountPoint: '', @@ -44,7 +53,9 @@ export class App extends Component<{}, AppState> implements OC.Plugin<OC.Search. renameMountPoint: '', filter: '', sort: 'mount_point', - sortOrder: 1 + sortOrder: 1, + isAdminNextcloud: false, + checkAppsInstalled: false, }; componentDidMount() { @@ -54,6 +65,10 @@ export class App extends Component<{}, AppState> implements OC.Plugin<OC.Search. this.api.listGroups().then((groups) => { this.setState({groups}); }); + + this.setState({ isAdminNextcloud: loadState('groupfolders', 'isAdminNextcloud') }); + this.setState({ checkAppsInstalled: loadState('groupfolders', 'checkAppsInstalled') }); + OC.Plugins.register('OCA.Search.Core', this); } @@ -161,6 +176,27 @@ export class App extends Component<{}, AppState> implements OC.Plugin<OC.Search. return parseInt(OC.config.version,10) >= 16; } + showAdminDelegationForms() { + if (this.state.isAdminNextcloud && this.state.checkAppsInstalled) { + return <div id="groupfolders-admin-delegation"> + <h3>{ t('groupfolders', 'Group folder admin delegation') }</h3> + <p><em>{ t('groupfolders', 'Nextcloud allows you to delegate the administration of groupfolders to non-admin users.') }</em></p> + <p><em>{ t('groupfolders', 'Specify below the groups that will be allowed to manage groupfolders and use its API/REST.') }</em></p> + <p className="end-description-delegation"><em>{ t('groupfolders', 'They will have access to all Groupfolders.') }</em></p> + <AdminGroupSelect + groups={this.state.groups} + allGroups={this.state.groups} + delegatedAdminGroups={this.state.delegatedAdminGroups} /> + <p><em>{ t('groupfolders', 'Specify below the groups that will be allowed to manage groupfolders and use its API/REST only.') }</em></p> + <p className="end-description-delegation"><em>{ t('groupfolders', 'They will only have access to Groupfolders for which they have advanced permissions.') }</em></p> + <SubAdminGroupSelect + groups={this.state.groups} + allGroups={this.state.groups} + delegatedSubAdminGroups={this.state.delegatedSubAdminGroups} /> + </div> + } + } + render() { const rows = this.state.folders .filter(folder => { @@ -266,6 +302,7 @@ export class App extends Component<{}, AppState> implements OC.Plugin<OC.Search. onClick={() => { this.setState({editingGroup: 0, editingMountPoint: 0}) }}> + {this.showAdminDelegationForms()} <table> <thead> <tr> @@ -305,7 +342,7 @@ export class App extends Component<{}, AppState> implements OC.Plugin<OC.Search. this.setState({newMountPoint: event.target.value}) }}/> <input type="submit" - value={t('groupfolders', 'Create')}/> + value={t('groupfolders', 'Create')}/> </form> </td> <td colSpan={3}/> @@ -327,7 +364,7 @@ interface ManageAclSelectProps { function ManageAclSelect({onChange, onSearch, folder}: ManageAclSelectProps) { const handleSearch = (inputValue: string) => { - return new Promise(resolve => { + return new Promise<any>(resolve => { onSearch(inputValue).then((result) => { resolve([...result.groups, ...result.users]) }) diff --git a/src/settings/FolderGroups.tsx b/src/settings/FolderGroups.tsx index 23fc7779..5c81456a 100644 --- a/src/settings/FolderGroups.tsx +++ b/src/settings/FolderGroups.tsx @@ -31,11 +31,11 @@ export function FolderGroups({groups, allGroups = [], onAddGroup, removeGroup, e <td> {( allGroups - .find(group => group.id === groupId) || { + .find(group => group.gid === groupId) || { id: groupId, - displayname: groupId + displayName: groupId } - ).displayname + ).displayName } </td> <td className="permissions"> @@ -76,8 +76,8 @@ export function FolderGroups({groups, allGroups = [], onAddGroup, removeGroup, e {rows} <tr> <td colSpan={5}> - <GroupSelect - allGroups={allGroups.filter(i => !groups[i.id])} + <AdminGroupSelect + allGroups={allGroups.filter(i => !groups[i.gid])} onChange={onAddGroup}/> </td> </tr> @@ -92,23 +92,23 @@ export function FolderGroups({groups, allGroups = [], onAddGroup, removeGroup, e } return <a className="action-rename" onClick={showEdit}> {Object.keys(groups) - .map(groupId => allGroups.find(group => group.id === groupId) || { + .map(groupId => allGroups.find(group => group.gid === groupId) || { id: groupId, - displayname: groupId + displayName: groupId }) - .map(group => group.displayname) + .map(group => group.displayName) .join(', ') } </a> } } -interface GroupSelectProps { +interface AdminGroupSelectProps { allGroups: Group[]; onChange: (name: string) => void; } -function GroupSelect({allGroups, onChange}: GroupSelectProps) { +function AdminGroupSelect({allGroups, onChange}: AdminGroupSelectProps) { if (allGroups.length === 0) { return <div> <p>No other groups available</p> @@ -116,8 +116,8 @@ function GroupSelect({allGroups, onChange}: GroupSelectProps) { } const options = allGroups.map(group => { return { - value: group.id, - label: group.displayname + value: group.gid, + label: group.displayName }; }); diff --git a/src/settings/Nextcloud.d.ts b/src/settings/Nextcloud.d.ts index 9bc90a15..175fc193 100644 --- a/src/settings/Nextcloud.d.ts +++ b/src/settings/Nextcloud.d.ts @@ -87,3 +87,20 @@ declare module 'NC' { } } } + +declare module 'NC' { + export interface AxiosOCSResult<T> { + data: { + ocs: { + data: T; + meta: { + status: 'ok' | 'failure'; + message: string; + statuscode: number; + totalitems: number; + itemsperpage: number; + } + } + } + } +}
\ No newline at end of file diff --git a/src/settings/SubAdminGroupSelect.tsx b/src/settings/SubAdminGroupSelect.tsx new file mode 100644 index 00000000..c5bafb30 --- /dev/null +++ b/src/settings/SubAdminGroupSelect.tsx @@ -0,0 +1,113 @@ +/** + * @copyright Copyright (c) 2018 Julius Härtl <jus@bitgrid.net> + * + * @author Baptiste Fotia <baptiste.fotia@hotmail.com> for Arawa (https://arawa.fr) + * + * @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/>. + * + */ + +import * as React from 'react'; +import Select from 'react-select'; +import {getCurrentUser} from '@nextcloud/auth'; +import {Component} from 'react'; +import {Group, Api} from './Api'; +import {CLASS_NAME_SUBADMIN_DELEGATION} from '../Constants.js'; + +interface SubAdminGroupSelectProps { + groups: Group[], + allGroups: Group[], + delegatedSubAdminGroups: Group[], +} + +class SubAdminGroupSelect extends Component<SubAdminGroupSelectProps> { + + state: SubAdminGroupSelectProps = { + groups: [], + allGroups: [], + delegatedSubAdminGroups: [], + } + + constructor (props) { + super(props) + this.state.groups = props.groups + this.state.allGroups = props.allGroups + this.state.delegatedSubAdminGroups = props.delegatedSubAdminGroups + } + + api = new Api() + + componentDidMount() { + this.api.listGroups().then((groups) => { + this.setState({groups}); + }); + this.api.listDelegatedGroups(CLASS_NAME_SUBADMIN_DELEGATION).then((groups) => { + this.setState({delegatedSubAdminGroups: groups}); + }); + } + + updateDelegatedSubAdminGroups(options: {value: string, label: string}[]): void { + if (this.state.groups !== undefined) { + const groups = options.map(option => { + return this.state.groups.filter(g => g.gid === option.value)[0]; + }); + this.setState({delegatedSubAdminGroups: groups}, () => { + this.api.updateDelegatedGroups(this.state.delegatedSubAdminGroups, CLASS_NAME_SUBADMIN_DELEGATION); + }); + } + } + + render () { + const options = this.state.groups.map(group => { + return { + value: group.gid, + label: group.displayName + }; + }); + + return <Select + onChange={ this.updateDelegatedSubAdminGroups.bind(this) } + isDisabled={getCurrentUser() ? !getCurrentUser()!.isAdmin : true} + isMulti + value={this.state.delegatedSubAdminGroups.map(group => { + return { + value: group.gid, + label: group.displayName + }; + })} + className="delegated-admins-select" + options={options} + placeholder={t('groupfolders', 'Add group')} + styles={{ + input: (provided) => ({ + ...provided, + height: '30' + }), + control: (provided) => ({ + ...provided, + backgroundColor: 'var(--color-main-background)' + }), + menu: (provided) => ({ + ...provided, + backgroundColor: 'var(--color-main-background)', + borderColor: 'var(--color-border, #888)' + }) + }} + /> + } +} + +export default SubAdminGroupSelect
\ No newline at end of file diff --git a/tests/ACL/RuleManagerTest.php b/tests/ACL/RuleManagerTest.php index e86dd0c9..c3c43181 100644 --- a/tests/ACL/RuleManagerTest.php +++ b/tests/ACL/RuleManagerTest.php @@ -79,7 +79,7 @@ class RuleManagerTest extends TestCase { $this->eventDispatcher->expects($this->any()) ->method('dispatchTyped') ->withConsecutive( - [$this->callback(function(CriticalActionPerformedEvent $event): bool { + [$this->callback(function (CriticalActionPerformedEvent $event): bool { return $event->getParameters() === [ 'permissions' => 0b00001001, 'mask' => 0b00001111, @@ -87,7 +87,7 @@ class RuleManagerTest extends TestCase { 'user' => 'The User (1)', ]; })], - [$this->callback(function(CriticalActionPerformedEvent $event): bool { + [$this->callback(function (CriticalActionPerformedEvent $event): bool { return $event->getParameters() === [ 'permissions' => 0b00001000, 'mask' => 0b00001111, @@ -95,7 +95,7 @@ class RuleManagerTest extends TestCase { 'user' => 'The User (1)', ]; })], - [$this->callback(function(CriticalActionPerformedEvent $event): bool { + [$this->callback(function (CriticalActionPerformedEvent $event): bool { return $event->getParameters() === [ 'fileId' => 10, 'user' => 'The User (1)', |