Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nextcloud/groupfolders.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCarl Schwan <carl@carlschwan.eu>2022-11-02 18:05:41 +0300
committerGitHub <noreply@github.com>2022-11-02 18:05:41 +0300
commit8e4053b98c5b3032054dc7d1f98aafb8145039a6 (patch)
tree514f72dbd3df180b4493cf2d246cb7a1a7183d9d
parentdfe4d0564816999dcc35ceebff27ebb145f89c6e (diff)
parenta2a4ebc61efc3708fb47593bcd9d74f6be7e4983 (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.php7
-rw-r--r--lib/AppInfo/Application.php7
-rw-r--r--lib/AuthorizedAdminSettingMiddleware.php82
-rw-r--r--lib/Controller/DelegationController.php46
-rw-r--r--lib/Controller/FolderController.php57
-rw-r--r--lib/Listeners/LoadAdditionalScriptsListener.php1
-rw-r--r--lib/Service/ApplicationService.php43
-rw-r--r--lib/Service/DelegationService.php105
-rw-r--r--lib/Service/FoldersFilter.php57
-rw-r--r--lib/Settings/Admin.php40
-rw-r--r--psalm.xml10
-rw-r--r--src/Constants.js23
-rw-r--r--src/settings/AdminGroupSelect.tsx114
-rw-r--r--src/settings/Api.ts35
-rw-r--r--src/settings/App.scss16
-rw-r--r--src/settings/App.tsx43
-rw-r--r--src/settings/FolderGroups.tsx24
-rw-r--r--src/settings/Nextcloud.d.ts17
-rw-r--r--src/settings/SubAdminGroupSelect.tsx113
-rw-r--r--tests/ACL/RuleManagerTest.php6
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;
}
diff --git a/psalm.xml b/psalm.xml
index a7210621..0f14f1e6 100644
--- a/psalm.xml
+++ b/psalm.xml
@@ -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)',