Add admin privilege delegation for admin settings

This makes it possible for selected groups to access some settings
pages.

Signed-off-by: Carl Schwan <carl@carlschwan.eu>
pull/28189/head
Carl Schwan 3 years ago
parent ee987d7430
commit 6958d8005a
No known key found for this signature in database
GPG Key ID: 06B35D38387B67BE
  1. 4
      apps/settings/appinfo/info.xml
  2. 4
      apps/settings/appinfo/routes.php
  3. 7
      apps/settings/composer/composer/autoload_classmap.php
  4. 7
      apps/settings/composer/composer/autoload_static.php
  5. 367
      apps/settings/js/vue-settings-admin-delegation.js
  6. 1
      apps/settings/js/vue-settings-admin-delegation.js.map
  7. 38
      apps/settings/js/vue-settings-admin-security.js
  8. 2
      apps/settings/js/vue-settings-admin-security.js.map
  9. 10
      apps/settings/js/vue-settings-apps-users-management.js
  10. 2
      apps/settings/js/vue-settings-apps-users-management.js.map
  11. 4
      apps/settings/js/vue-settings-apps.js
  12. 2
      apps/settings/js/vue-settings-apps.js.map
  13. 4
      apps/settings/js/vue-settings-nextcloud-pdf.js
  14. 2
      apps/settings/js/vue-settings-nextcloud-pdf.js.map
  15. 24
      apps/settings/js/vue-settings-personal-info.js
  16. 2
      apps/settings/js/vue-settings-personal-info.js.map
  17. 68
      apps/settings/js/vue-settings-personal-security.js
  18. 2
      apps/settings/js/vue-settings-personal-security.js.map
  19. 20
      apps/settings/js/vue-settings-personal-webauthn.js
  20. 2
      apps/settings/js/vue-settings-personal-webauthn.js.map
  21. 6
      apps/settings/js/vue-settings-users.js
  22. 2
      apps/settings/js/vue-settings-users.js.map
  23. 50
      apps/settings/js/vue-vendors-settings-apps-settings-users.js
  24. 2
      apps/settings/js/vue-vendors-settings-apps-settings-users.js.map
  25. 14
      apps/settings/js/vue-vendors-settings-apps.js
  26. 2
      apps/settings/js/vue-vendors-settings-apps.js.map
  27. 52
      apps/settings/js/vue-vendors-settings-users.js
  28. 2
      apps/settings/js/vue-vendors-settings-users.js.map
  29. 3
      apps/settings/lib/AppInfo/Application.php
  30. 19
      apps/settings/lib/Controller/AdminSettingsController.php
  31. 80
      apps/settings/lib/Controller/AuthorizedGroupController.php
  32. 5
      apps/settings/lib/Controller/CheckSetupController.php
  33. 28
      apps/settings/lib/Controller/CommonSettingsTrait.php
  34. 3
      apps/settings/lib/Controller/MailSettingsController.php
  35. 5
      apps/settings/lib/Controller/PersonalSettingsController.php
  36. 51
      apps/settings/lib/Listener/GroupRemovedListener.php
  37. 2
      apps/settings/lib/Middleware/SubadminMiddleware.php
  38. 76
      apps/settings/lib/Sections/Admin/Delegation.php
  39. 116
      apps/settings/lib/Service/AuthorizedGroupService.php
  40. 27
      apps/settings/lib/Service/NotFoundException.php
  41. 27
      apps/settings/lib/Service/ServiceException.php
  42. 148
      apps/settings/lib/Settings/Admin/Delegation.php
  43. 20
      apps/settings/lib/Settings/Admin/Mail.php
  44. 19
      apps/settings/lib/Settings/Admin/Overview.php
  45. 23
      apps/settings/lib/Settings/Admin/Server.php
  46. 14
      apps/settings/lib/Settings/Admin/Sharing.php
  47. 37
      apps/settings/src/components/AdminDelegating.vue
  48. 75
      apps/settings/src/components/AdminDelegation/GroupSelect.vue
  49. 32
      apps/settings/src/main-admin-delegation.js
  50. 29
      apps/settings/templates/settings/admin/delegation.php
  51. 7
      apps/settings/tests/Controller/AdminSettingsControllerTest.php
  52. 77
      apps/settings/tests/Controller/DelegationControllerTest.php
  53. 14
      apps/settings/tests/Middleware/SubadminMiddlewareTest.php
  54. 7
      apps/settings/tests/Settings/Admin/MailTest.php
  55. 5
      apps/settings/tests/Settings/Admin/ServerTest.php
  56. 1
      apps/settings/webpack.js
  57. 62
      core/Migrations/Version23000Date20210721100600.php
  58. 4
      lib/composer/composer/autoload_classmap.php
  59. 4
      lib/composer/composer/autoload_static.php
  60. 2
      lib/private/App/InfoParser.php
  61. 5
      lib/private/AppFramework/DependencyInjection/DIContainer.php
  62. 47
      lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php
  63. 50
      lib/private/Settings/AuthorizedGroup.php
  64. 125
      lib/private/Settings/AuthorizedGroupMapper.php
  65. 98
      lib/private/Settings/Manager.php
  66. 9
      lib/private/legacy/OC_App.php
  67. 54
      lib/public/Settings/IDelegatedSettings.php
  68. 29
      lib/public/Settings/IManager.php
  69. 3
      resources/app-info-shipped.xsd
  70. 5
      resources/app-info.xsd
  71. 12
      tests/lib/AppFramework/Middleware/Security/SecurityMiddlewareTest.php
  72. 31
      tests/lib/Settings/ManagerTest.php

@ -20,10 +20,12 @@
<settings>
<admin>OCA\Settings\Settings\Admin\Mail</admin>
<admin>OCA\Settings\Settings\Admin\Overview</admin>
<admin>OCA\Settings\Settings\Admin\Security</admin>
<admin>OCA\Settings\Settings\Admin\Server</admin>
<admin>OCA\Settings\Settings\Admin\Sharing</admin>
<admin>OCA\Settings\Settings\Admin\Security</admin>
<admin>OCA\Settings\Settings\Admin\Delegation</admin>
<admin-section>OCA\Settings\Sections\Admin\Additional</admin-section>
<admin-section>OCA\Settings\Sections\Admin\Delegation</admin-section>
<admin-section>OCA\Settings\Sections\Admin\Groupware</admin-section>
<admin-section>OCA\Settings\Sections\Admin\Overview</admin-section>
<admin-section>OCA\Settings\Sections\Admin\Security</admin-section>

@ -31,6 +31,10 @@ return [
'AuthSettings' => ['url' => '/settings/personal/authtokens' , 'root' => ''],
],
'routes' => [
['name' => 'AuthorizedGroup#create', 'url' => '/settings/authorizedgroups', 'verb' => 'POST'],
['name' => 'AuthorizedGroup#saveSettings', 'url' => '/settings/authorizedgroups/saveSettings', 'verb' => 'POST'],
['name' => 'AuthorizedGroup#destroy', 'url' => '/settings/authorizedgroups', 'verb' => 'DELETE'],
['name' => 'AuthSettings#wipe', 'url' => '/settings/personal/authtokens/wipe/{id}', 'verb' => 'POST' , 'root' => ''],
['name' => 'MailSettings#setMailSettings', 'url' => '/settings/admin/mailsettings', 'verb' => 'POST' , 'root' => ''],

@ -19,6 +19,7 @@ return array(
'OCA\\Settings\\Controller\\AdminSettingsController' => $baseDir . '/../lib/Controller/AdminSettingsController.php',
'OCA\\Settings\\Controller\\AppSettingsController' => $baseDir . '/../lib/Controller/AppSettingsController.php',
'OCA\\Settings\\Controller\\AuthSettingsController' => $baseDir . '/../lib/Controller/AuthSettingsController.php',
'OCA\\Settings\\Controller\\AuthorizedGroupController' => $baseDir . '/../lib/Controller/AuthorizedGroupController.php',
'OCA\\Settings\\Controller\\ChangePasswordController' => $baseDir . '/../lib/Controller/ChangePasswordController.php',
'OCA\\Settings\\Controller\\CheckSetupController' => $baseDir . '/../lib/Controller/CheckSetupController.php',
'OCA\\Settings\\Controller\\CommonSettingsTrait' => $baseDir . '/../lib/Controller/CommonSettingsTrait.php',
@ -33,6 +34,7 @@ return array(
'OCA\\Settings\\Events\\BeforeTemplateRenderedEvent' => $baseDir . '/../lib/Events/BeforeTemplateRenderedEvent.php',
'OCA\\Settings\\Hooks' => $baseDir . '/../lib/Hooks.php',
'OCA\\Settings\\Listener\\AppPasswordCreatedActivityListener' => $baseDir . '/../lib/Listener/AppPasswordCreatedActivityListener.php',
'OCA\\Settings\\Listener\\GroupRemovedListener' => $baseDir . '/../lib/Listener/GroupRemovedListener.php',
'OCA\\Settings\\Listener\\UserAddedToGroupActivityListener' => $baseDir . '/../lib/Listener/UserAddedToGroupActivityListener.php',
'OCA\\Settings\\Listener\\UserRemovedFromGroupActivityListener' => $baseDir . '/../lib/Listener/UserRemovedFromGroupActivityListener.php',
'OCA\\Settings\\Mailer\\NewUserMailHelper' => $baseDir . '/../lib/Mailer/NewUserMailHelper.php',
@ -40,6 +42,7 @@ return array(
'OCA\\Settings\\Search\\AppSearch' => $baseDir . '/../lib/Search/AppSearch.php',
'OCA\\Settings\\Search\\SectionSearch' => $baseDir . '/../lib/Search/SectionSearch.php',
'OCA\\Settings\\Sections\\Admin\\Additional' => $baseDir . '/../lib/Sections/Admin/Additional.php',
'OCA\\Settings\\Sections\\Admin\\Delegation' => $baseDir . '/../lib/Sections/Admin/Delegation.php',
'OCA\\Settings\\Sections\\Admin\\Groupware' => $baseDir . '/../lib/Sections/Admin/Groupware.php',
'OCA\\Settings\\Sections\\Admin\\Overview' => $baseDir . '/../lib/Sections/Admin/Overview.php',
'OCA\\Settings\\Sections\\Admin\\Security' => $baseDir . '/../lib/Sections/Admin/Security.php',
@ -48,6 +51,10 @@ return array(
'OCA\\Settings\\Sections\\Personal\\PersonalInfo' => $baseDir . '/../lib/Sections/Personal/PersonalInfo.php',
'OCA\\Settings\\Sections\\Personal\\Security' => $baseDir . '/../lib/Sections/Personal/Security.php',
'OCA\\Settings\\Sections\\Personal\\SyncClients' => $baseDir . '/../lib/Sections/Personal/SyncClients.php',
'OCA\\Settings\\Service\\AuthorizedGroupService' => $baseDir . '/../lib/Service/AuthorizedGroupService.php',
'OCA\\Settings\\Service\\NotFoundException' => $baseDir . '/../lib/Service/NotFoundException.php',
'OCA\\Settings\\Service\\ServiceException' => $baseDir . '/../lib/Service/ServiceException.php',
'OCA\\Settings\\Settings\\Admin\\Delegation' => $baseDir . '/../lib/Settings/Admin/Delegation.php',
'OCA\\Settings\\Settings\\Admin\\Mail' => $baseDir . '/../lib/Settings/Admin/Mail.php',
'OCA\\Settings\\Settings\\Admin\\Overview' => $baseDir . '/../lib/Settings/Admin/Overview.php',
'OCA\\Settings\\Settings\\Admin\\Security' => $baseDir . '/../lib/Settings/Admin/Security.php',

@ -34,6 +34,7 @@ class ComposerStaticInitSettings
'OCA\\Settings\\Controller\\AdminSettingsController' => __DIR__ . '/..' . '/../lib/Controller/AdminSettingsController.php',
'OCA\\Settings\\Controller\\AppSettingsController' => __DIR__ . '/..' . '/../lib/Controller/AppSettingsController.php',
'OCA\\Settings\\Controller\\AuthSettingsController' => __DIR__ . '/..' . '/../lib/Controller/AuthSettingsController.php',
'OCA\\Settings\\Controller\\AuthorizedGroupController' => __DIR__ . '/..' . '/../lib/Controller/AuthorizedGroupController.php',
'OCA\\Settings\\Controller\\ChangePasswordController' => __DIR__ . '/..' . '/../lib/Controller/ChangePasswordController.php',
'OCA\\Settings\\Controller\\CheckSetupController' => __DIR__ . '/..' . '/../lib/Controller/CheckSetupController.php',
'OCA\\Settings\\Controller\\CommonSettingsTrait' => __DIR__ . '/..' . '/../lib/Controller/CommonSettingsTrait.php',
@ -48,6 +49,7 @@ class ComposerStaticInitSettings
'OCA\\Settings\\Events\\BeforeTemplateRenderedEvent' => __DIR__ . '/..' . '/../lib/Events/BeforeTemplateRenderedEvent.php',
'OCA\\Settings\\Hooks' => __DIR__ . '/..' . '/../lib/Hooks.php',
'OCA\\Settings\\Listener\\AppPasswordCreatedActivityListener' => __DIR__ . '/..' . '/../lib/Listener/AppPasswordCreatedActivityListener.php',
'OCA\\Settings\\Listener\\GroupRemovedListener' => __DIR__ . '/..' . '/../lib/Listener/GroupRemovedListener.php',
'OCA\\Settings\\Listener\\UserAddedToGroupActivityListener' => __DIR__ . '/..' . '/../lib/Listener/UserAddedToGroupActivityListener.php',
'OCA\\Settings\\Listener\\UserRemovedFromGroupActivityListener' => __DIR__ . '/..' . '/../lib/Listener/UserRemovedFromGroupActivityListener.php',
'OCA\\Settings\\Mailer\\NewUserMailHelper' => __DIR__ . '/..' . '/../lib/Mailer/NewUserMailHelper.php',
@ -55,6 +57,7 @@ class ComposerStaticInitSettings
'OCA\\Settings\\Search\\AppSearch' => __DIR__ . '/..' . '/../lib/Search/AppSearch.php',
'OCA\\Settings\\Search\\SectionSearch' => __DIR__ . '/..' . '/../lib/Search/SectionSearch.php',
'OCA\\Settings\\Sections\\Admin\\Additional' => __DIR__ . '/..' . '/../lib/Sections/Admin/Additional.php',
'OCA\\Settings\\Sections\\Admin\\Delegation' => __DIR__ . '/..' . '/../lib/Sections/Admin/Delegation.php',
'OCA\\Settings\\Sections\\Admin\\Groupware' => __DIR__ . '/..' . '/../lib/Sections/Admin/Groupware.php',
'OCA\\Settings\\Sections\\Admin\\Overview' => __DIR__ . '/..' . '/../lib/Sections/Admin/Overview.php',
'OCA\\Settings\\Sections\\Admin\\Security' => __DIR__ . '/..' . '/../lib/Sections/Admin/Security.php',
@ -63,6 +66,10 @@ class ComposerStaticInitSettings
'OCA\\Settings\\Sections\\Personal\\PersonalInfo' => __DIR__ . '/..' . '/../lib/Sections/Personal/PersonalInfo.php',
'OCA\\Settings\\Sections\\Personal\\Security' => __DIR__ . '/..' . '/../lib/Sections/Personal/Security.php',
'OCA\\Settings\\Sections\\Personal\\SyncClients' => __DIR__ . '/..' . '/../lib/Sections/Personal/SyncClients.php',
'OCA\\Settings\\Service\\AuthorizedGroupService' => __DIR__ . '/..' . '/../lib/Service/AuthorizedGroupService.php',
'OCA\\Settings\\Service\\NotFoundException' => __DIR__ . '/..' . '/../lib/Service/NotFoundException.php',
'OCA\\Settings\\Service\\ServiceException' => __DIR__ . '/..' . '/../lib/Service/ServiceException.php',
'OCA\\Settings\\Settings\\Admin\\Delegation' => __DIR__ . '/..' . '/../lib/Settings/Admin/Delegation.php',
'OCA\\Settings\\Settings\\Admin\\Mail' => __DIR__ . '/..' . '/../lib/Settings/Admin/Mail.php',
'OCA\\Settings\\Settings\\Admin\\Overview' => __DIR__ . '/..' . '/../lib/Settings/Admin/Overview.php',
'OCA\\Settings\\Settings\\Admin\\Security' => __DIR__ . '/..' . '/../lib/Settings/Admin/Security.php',

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -39,6 +39,7 @@ use OC\Authentication\Token\IProvider;
use OC\Server;
use OCA\Settings\Hooks;
use OCA\Settings\Listener\AppPasswordCreatedActivityListener;
use OCA\Settings\Listener\GroupRemovedListener;
use OCA\Settings\Listener\UserAddedToGroupActivityListener;
use OCA\Settings\Listener\UserRemovedFromGroupActivityListener;
use OCA\Settings\Mailer\NewUserMailHelper;
@ -52,6 +53,7 @@ use OCP\AppFramework\Bootstrap\IBootstrap;
use OCP\AppFramework\Bootstrap\IRegistrationContext;
use OCP\AppFramework\IAppContainer;
use OCP\Defaults;
use OCP\Group\Events\GroupDeletedEvent;
use OCP\Group\Events\UserAddedEvent;
use OCP\Group\Events\UserRemovedEvent;
use OCP\IServerContainer;
@ -79,6 +81,7 @@ class Application extends App implements IBootstrap {
$context->registerEventListener(AppPasswordCreatedEvent::class, AppPasswordCreatedActivityListener::class);
$context->registerEventListener(UserAddedEvent::class, UserAddedToGroupActivityListener::class);
$context->registerEventListener(UserRemovedEvent::class, UserRemovedFromGroupActivityListener::class);
$context->registerEventListener(GroupDeletedEvent::class, GroupRemovedListener::class);
// Register well-known handlers
$context->registerWellKnownHandler(SecurityTxtHandler::class);

@ -25,6 +25,7 @@
*/
namespace OCA\Settings\Controller;
use OC\AppFramework\Middleware\Security\Exceptions\NotAdminException;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\Group\ISubAdmin;
@ -57,13 +58,13 @@ class AdminSettingsController extends Controller {
}
/**
* @param string $section
* @return TemplateResponse
*
* @NoCSRFRequired
* @SubAdminRequired
* @NoAdminRequired
* @NoSubAdminRequired
* We are checking the permissions in the getSettings method. If there is no allowed
* settings for the given section. The user will be gretted by an error message.
*/
public function index($section) {
public function index(string $section): TemplateResponse {
return $this->getIndexResponse('admin', $section);
}
@ -75,10 +76,10 @@ class AdminSettingsController extends Controller {
/** @var IUser $user */
$user = $this->userSession->getUser();
$isSubAdmin = !$this->groupManager->isAdmin($user->getUID()) && $this->subAdmin->isSubAdmin($user);
$settings = $this->settingsManager->getAdminSettings(
$section,
$isSubAdmin
);
$settings = $this->settingsManager->getAllowedAdminSettings($section, $user);
if (empty($settings)) {
throw new NotAdminException("Logged in user doesn't have permission to access these settings.");
}
$formatted = $this->formatSettings($settings);
// Do not show legacy forms for sub admins
if ($section === 'additional' && !$isSubAdmin) {

@ -0,0 +1,80 @@
<?php
/**
* @copyright Copyright (c) 2021 Carl Schwan <carl@carlschwan.eu>
*
* @author Carl Schwan <carl@carlschwan.eu>
*
* @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 <https://www.gnu.org/licenses/>.
*
*/
namespace OCA\Settings\Controller;
use OC\Settings\AuthorizedGroup;
use OCA\Settings\Service\AuthorizedGroupService;
use OCA\Settings\Service\NotFoundException;
use OCP\DB\Exception;
use OCP\IRequest;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\Controller;
class AuthorizedGroupController extends Controller {
/** @var AuthorizedGroupService $authorizedGroupService */
private $authorizedGroupService;
public function __construct(string $AppName, IRequest $request, AuthorizedGroupService $authorizedGroupService) {
parent::__construct($AppName, $request);
$this->authorizedGroupService = $authorizedGroupService;
}
/**
* @throws NotFoundException
* @throws Exception
*/
public function saveSettings(array $newGroups, string $class): DataResponse {
$currentGroups = $this->authorizedGroupService->findExistingGroupsForClass($class);
foreach ($currentGroups as $group) {
/** @var AuthorizedGroup $group */
$removed = true;
foreach ($newGroups as $groupData) {
if ($groupData['gid'] === $group->getGroupId()) {
$removed = false;
break;
}
}
if ($removed) {
$this->authorizedGroupService->delete($group->getId());
}
}
foreach ($newGroups as $groupData) {
$added = true;
foreach ($currentGroups as $group) {
/** @var AuthorizedGroup $group */
if ($groupData['gid'] === $group->getGroupId()) {
$added = false;
break;
}
}
if ($added) {
$this->authorizedGroupService->create($groupData['gid'], $class);
}
}
return new DataResponse(['valid' => true]);
}
}

@ -366,8 +366,9 @@ class CheckSetupController extends Controller {
/**
* @return RedirectResponse
* @AuthorizedAdminSetting(settings=OCA\Settings\Settings\Admin\Overview)
*/
public function rescanFailedIntegrityCheck() {
public function rescanFailedIntegrityCheck(): RedirectResponse {
$this->checker->runInstanceVerification();
return new RedirectResponse(
$this->urlGenerator->linkToRoute('settings.AdminSettings.index', ['section' => 'overview'])
@ -376,6 +377,7 @@ class CheckSetupController extends Controller {
/**
* @NoCSRFRequired
* @AuthorizedAdminSetting(settings=OCA\Settings\Settings\Admin\Overview)
*/
public function getFailedIntegrityCheckFiles(): DataDisplayResponse {
if (!$this->checker->isCodeCheckEnforced()) {
@ -740,6 +742,7 @@ Raw output
/**
* @return DataResponse
* @AuthorizedAdminSetting(settings=OCA\Settings\Settings\Admin\Overview)
*/
public function check() {
$phpDefaultCharset = new PhpDefaultCharset();

@ -32,7 +32,6 @@ use OCP\AppFramework\Http\TemplateResponse;
use OCP\Group\ISubAdmin;
use OCP\IGroupManager;
use OCP\INavigationManager;
use OCP\IUser;
use OCP\IUserSession;
use OCP\Settings\IManager as ISettingsManager;
use OCP\Settings\ISettings;
@ -64,30 +63,23 @@ trait CommonSettingsTrait {
'admin' => []
];
/** @var IUser $user */
$user = $this->userSession->getUser();
$isAdmin = $this->groupManager->isAdmin($user->getUID());
$isSubAdmin = $this->subAdmin->isSubAdmin($user);
if ($isAdmin || $isSubAdmin) {
$templateParameters['admin'] = $this->formatAdminSections(
$templateParameters['admin'] = $this->formatAdminSections(
$currentType,
$currentSection,
!$isAdmin && $isSubAdmin
$currentSection
);
}
return [
'forms' => $templateParameters
];
}
protected function formatSections($sections, $currentSection, $type, $currentType, bool $subAdminOnly = false) {
protected function formatSections(array $sections, string $currentSection, string $type, string $currentType): array {
$templateParameters = [];
/** @var \OCP\Settings\IIconSection[] $prioritizedSections */
foreach ($sections as $prioritizedSections) {
foreach ($prioritizedSections as $section) {
if ($type === 'admin') {
$settings = $this->settingsManager->getAdminSettings($section->getID(), $subAdminOnly);
$settings = $this->settingsManager->getAllowedAdminSettings($section->getID(), $this->userSession->getUser());
} elseif ($type === 'personal') {
$settings = $this->settingsManager->getPersonalSettings($section->getID());
}
@ -111,29 +103,29 @@ trait CommonSettingsTrait {
return $templateParameters;
}
protected function formatPersonalSections($currentType, $currentSections) {
protected function formatPersonalSections(string $currentType, string $currentSections): array {
$sections = $this->settingsManager->getPersonalSections();
$templateParameters = $this->formatSections($sections, $currentSections, 'personal', $currentType);
return $templateParameters;
}
protected function formatAdminSections($currentType, $currentSections, bool $subAdminOnly) {
protected function formatAdminSections(string $currentType, string $currentSections): array {
$sections = $this->settingsManager->getAdminSections();
$templateParameters = $this->formatSections($sections, $currentSections, 'admin', $currentType, $subAdminOnly);
$templateParameters = $this->formatSections($sections, $currentSections, 'admin', $currentType);
return $templateParameters;
}
/**
* @param ISettings[] $settings
* @param array<int, list<\OCP\Settings\ISettings>> $settings
* @return array
*/
private function formatSettings($settings) {
private function formatSettings(array $settings): array {
$html = '';
foreach ($settings as $prioritizedSettings) {
foreach ($prioritizedSettings as $setting) {
/** @var \OCP\Settings\ISettings $setting */
/** @var ISettings $setting */
$form = $setting->getForm();
$html .= $form->renderAs('')->render();
}

@ -72,6 +72,7 @@ class MailSettingsController extends Controller {
* Sets the email settings
*
* @PasswordConfirmationRequired
* @AuthorizedAdminSetting(settings=OCA\Settings\Settings\Admin\Overview)
*
* @param string $mail_domain
* @param string $mail_from_address
@ -113,6 +114,7 @@ class MailSettingsController extends Controller {
* Store the credentials used for SMTP in the config
*
* @PasswordConfirmationRequired
* @AuthorizedAdminSetting(settings=OCA\Settings\Settings\Admin\Overview)
*
* @param string $mail_smtpname
* @param string $mail_smtppassword
@ -133,6 +135,7 @@ class MailSettingsController extends Controller {
/**
* Send a mail to test the settings
* @AuthorizedAdminSetting(settings=OCA\Settings\Settings\Admin\Overview)
* @return DataResponse
*/
public function sendTestMail() {

@ -56,14 +56,11 @@ class PersonalSettingsController extends Controller {
}
/**
* @param string $section
* @return TemplateResponse
*
* @NoCSRFRequired
* @NoAdminRequired
* @NoSubAdminRequired
*/
public function index($section) {
public function index(string $section): TemplateResponse {
return $this->getIndexResponse('personal', $section);
}

@ -0,0 +1,51 @@
<?php
/**
* @copyright Copyright (c) 2021 Carl Schwan <carl@carlschwan.eu>
*
* @author Carl Schwan <carl@carlschwan.eu>
*
* @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 <https://www.gnu.org/licenses/>.
*
*/
namespace OCA\Settings\Listener;
use OCA\Settings\Service\AuthorizedGroupService;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\Group\Events\GroupDeletedEvent;
class GroupRemovedListener implements IEventListener {
/** @var AuthorizedGroupService $authorizedGroupService */
private $authorizedGroupService;
public function __construct(AuthorizedGroupService $authorizedGroupService) {
$this->authorizedGroupService = $authorizedGroupService;
}
/**
* @inheritDoc
*/
public function handle(Event $event): void {
if (!($event instanceof GroupDeletedEvent)) {
return;
}
/** @var GroupDeletedEvent $event */
$this->authorizedGroupService->removeAuthorizationAssociatedTo($event->getGroup());
}
}

@ -65,7 +65,7 @@ class SubadminMiddleware extends Middleware {
* @throws \Exception
*/
public function beforeController($controller, $methodName) {
if (!$this->reflector->hasAnnotation('NoSubAdminRequired')) {
if (!$this->reflector->hasAnnotation('NoSubAdminRequired') && !$this->reflector->hasAnnotation('AuthorizedAdminSetting')) {
if (!$this->isSubAdmin) {
throw new NotAdminException($this->l10n->t('Logged in user must be a subadmin'));
}

@ -0,0 +1,76 @@
<?php
/**
* @copyright Copyright (c) 2021 Nextcloud GmbH
*
* @author Carl Schwan <carl@carlschwan.eu>
*
* @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/>.
*
*/
namespace OCA\Settings\Sections\Admin;
use OCP\IL10N;
use OCP\IURLGenerator;
use OCP\Settings\IIconSection;
class Delegation implements IIconSection {
/** @var IL10N */
private $l;
/** @var IURLGenerator */
private $url;
/**
* @param IURLGenerator $url
* @param IL10N $l
*/
public function __construct(IURLGenerator $url, IL10N $l) {
$this->url = $url;
$this->l = $l;
}
/**
* {@inheritdoc}
* @return string
*/
public function getID() {
return 'admindelegation';
}
/**
* {@inheritdoc}
* @return string
*/
public function getName() {
return $this->l->t('Admin right privilege');
}
/**
* {@inheritdoc}
* @return int
*/
public function getPriority() {
return 54;
}
/**
* {@inheritdoc}
* @return string
*/
public function getIcon() {
return $this->url->imagePath('core', 'places/contacts.svg');
}
}

@ -0,0 +1,116 @@
<?php
/**
* @copyright Copyright (c) 2021 Nextcloud GmbH
*
* @author Carl Schwan <carl@carlschwan.eu>
*
* @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 <https://www.gnu.org/licenses/>.
*
*/
namespace OCA\Settings\Service;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
use OC\Settings\AuthorizedGroup;
use OC\Settings\AuthorizedGroupMapper;
use OCP\DB\Exception;
use OCP\IGroup;
class AuthorizedGroupService {
/** @var AuthorizedGroupMapper $mapper */
private $mapper;
public function __construct(AuthorizedGroupMapper $mapper) {
$this->mapper = $mapper;
}
/**
* @return AuthorizedGroup[]
*/
public function findAll(): array {
return $this->mapper->findAll();
}
/**
* Find AuthorizedGroup by id.
*
* @param int $id
*/
public function find(int $id): ?AuthorizedGroup {
return $this->mapper->find($id);
}
/**
* @param $e
* @throws NotFoundException
*/
private function handleException(\Exception $e): void {
if ($e instanceof DoesNotExistException ||
$e instanceof MultipleObjectsReturnedException) {
throw new NotFoundException("AuthorizedGroup not found");
} else {
throw $e;
}
}
/**
* Create a new AuthorizedGroup
*
* @param string $groupId
* @param string $class
* @return AuthorizedGroup
* @throws Exception
*/
public function create(string $groupId, string $class): AuthorizedGroup {
$authorizedGroup = new AuthorizedGroup();
$authorizedGroup->setGroupId($groupId);
$authorizedGroup->setClass($class);
return $this->mapper->insert($authorizedGroup);
}
/**
* @throws NotFoundException
*/
public function delete(int $id): void {
try {
$authorizedGroup = $this->mapper->find($id);
$this->mapper->delete($authorizedGroup);
} catch (\Exception $e) {
$this->handleException($e);
}
}
public function findExistingGroupsForClass(string $class): array {
try {
$authorizedGroup = $this->mapper->findExistingGroupsForClass($class);
return $authorizedGroup;
} catch (\Exception $e) {
return [];
}
}
public function removeAuthorizationAssociatedTo(IGroup $group): void {
try {
$this->mapper->removeGroup($group->getGID());
} catch (\Exception $e) {
$this->handleException($e);
}
}
}

@ -0,0 +1,27 @@
<?php
/**
* @copyright Copyright (c) 2021 Carl Schwan <carl@carlschwan.eu>
*
* @author Carl Schwan <carl@carlschwan.eu>
*
* @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 <https://www.gnu.org/licenses/>.
*
*/
namespace OCA\Settings\Service;
class NotFoundException extends ServiceException {
}

@ -0,0 +1,27 @@
<?php
/**
* @copyright Copyright (c) 2021 Nextcloud GmbH
*
* @author Carl Schwan <carl@carlschwan.eu>
*
* @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 <https://www.gnu.org/licenses/>.
*
*/
namespace OCA\Settings\Service;
class ServiceException extends \Exception {
}

@ -0,0 +1,148 @@
<?php
/**
* @copyright Copyright (c) 2021 Carl Schwan <carl@carlschwan.eu>
*
* @author Carl Schwan <carl@carlschwan.eu>
*
* @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 <https://www.gnu.org/licenses/>.
*
*/
namespace OCA\Settings\Settings\Admin;
use OCA\Settings\AppInfo\Application;
use OCA\Settings\Service\AuthorizedGroupService;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Services\IInitialState;
use OCP\IGroupManager;
use OCP\Settings\IDelegatedSettings;
use OCP\Settings\IManager;
use OCP\Settings\ISettings;
class Delegation implements ISettings {
/** @var IManager */
private $settingManager;
/** @var IInitialState $initialStateService */
private $initialStateService;
/** @var IGroupManager $groupManager */
private $groupManager;
/** @var AuthorizedGroupService $authorizedGroupService */
private $authorizedGroupService;
public function __construct(
IManager $settingManager,
IInitialState $initialStateService,
IGroupManager $groupManager,
AuthorizedGroupService $authorizedGroupService
) {
$this->settingManager = $settingManager;
$this->initialStateService = $initialStateService;
$this->groupManager = $groupManager;
$this->authorizedGroupService = $authorizedGroupService;
}
/**
* Filter out the ISettings that are not IDelegatedSettings from $innerSection
* and add them to $settings.
*
* @param IDelegatedSettings[] $settings
* @param ISettings[] $innerSection
* @return IDelegatedSettings[]
*/
private function getDelegatedSettings(array $settings, array $innerSection): array {
foreach ($innerSection as $setting) {
if ($setting instanceof IDelegatedSettings) {
$settings[] = $setting;
}
}
return $settings;
}
private function initSettingState(): void {
// Available settings page initialization
$sections = $this->settingManager->getAdminSections();
$settings = [];
foreach ($sections as $sectionPriority) {
foreach ($sectionPriority as $section) {
$sectionSettings = $this->settingManager->getAdminSettings($section->getId());
$sectionSettings = array_reduce($sectionSettings, [$this, 'getDelegatedSettings'], []);
$settings = array_merge(
$settings,
array_map(function (IDelegatedSettings $setting) use ($section) {
return [
'class' => get_class($setting),
'sectionName' => $section->getName() . ($setting->getName() !== null ? ' - ' . $setting->getName() : ''),
'priority' => $section->getPriority(),
];
}, $sectionSettings)
);
}
}
usort($settings, function (array $a, array $b) {
if ($a['priority'] == $b['priority']) {
return 0;
}
return ($a['priority'] < $b['priority']) ? -1 : 1;
});
$this->initialStateService->provideInitialState('available-settings', $settings);
}
public function initAvailableGroupState(): void {
// Available groups initialization
$groups = [];
$groupsClass = $this->groupManager->search('');
foreach ($groupsClass as $group) {
if ($group->getGID() === 'admin') {
continue; // Admin already have access to everything
}
$groups[] = [
'displayName' => $group->getDisplayName(),
'gid' => $group->getGID(),
];
}
$this->initialStateService->provideInitialState('available-groups', $groups);
}
public function initAuthorizedGroupState(): void {
// Already set authorized groups
$this->initialStateService->provideInitialState('authorized-groups', $this->authorizedGroupService->findAll());
}
public function getForm(): TemplateResponse {
$this->initSettingState();
$this->initAvailableGroupState();
$this->initAuthorizedGroupState();
return new TemplateResponse(Application::APP_ID, 'settings/admin/delegation', [], '');
}
/**
* @return string the section ID, e.g. 'sharing'
*/
public function getSection() {
return 'admindelegation';
}
/*
* @inheritdoc
*/
public function getPriority() {
return 75;
}
}

@ -29,17 +29,23 @@ namespace OCA\Settings\Settings\Admin;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\IConfig;
use OCP\Settings\ISettings;
use OCP\IL10N;
use OCP\Settings\IDelegatedSettings;
class Mail implements ISettings {
class Mail implements IDelegatedSettings {
/** @var IConfig */
private $config;
/** @var IL10N $l */
private $l;
/**
* @param IConfig $config
* @param IL10N $l
*/
public function __construct(IConfig $config) {
public function __construct(IConfig $config, IL10N $l) {
$this->config = $config;
$this->l = $l;
}
/**
@ -90,4 +96,12 @@ class Mail implements ISettings {
public function getPriority() {
return 10;
}
public function getName(): ?string {
return $this->l->t('Email server');
}
public function getAuthorizedAppConfig(): array {
return [];
}
}

@ -27,14 +27,19 @@ namespace OCA\Settings\Settings\Admin;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\IConfig;
use OCP\Settings\ISettings;
use OCP\IL10N;
use OCP\Settings\IDelegatedSettings;
class Overview implements ISettings {
class Overview implements IDelegatedSettings {
/** @var IConfig */
private $config;
public function __construct(IConfig $config) {
/** @var IL10N $l*/
private $l;
public function __construct(IConfig $config, IL10N $l) {
$this->config = $config;
$this->l = $l;
}
/**
@ -65,4 +70,12 @@ class Overview implements ISettings {
public function getPriority() {
return 10;
}
public function getName(): ?string {
return $this->l->t('Security & setup warnings');
}
public function getAuthorizedAppConfig(): array {
return [];
}
}

@ -29,9 +29,10 @@ use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\IConfig;
use OCP\IDBConnection;
use OCP\Settings\ISettings;
use OCP\IL10N;
use OCP\Settings\IDelegatedSettings;
class Server implements ISettings {
class Server implements IDelegatedSettings {
/** @var IDBConnection */
private $connection;
@ -39,13 +40,17 @@ class Server implements ISettings {
private $timeFactory;
/** @var IConfig */
private $config;
/** @var IL10N $l */
private $l;
public function __construct(IDBConnection $connection,
ITimeFactory $timeFactory,
IConfig $config) {
IConfig $config,
IL10N $l) {
$this->connection = $connection;
$this->timeFactory = $timeFactory;
$this->config = $config;
$this->l = $l;
}
/**
@ -100,4 +105,16 @@ class Server implements ISettings {
public function getPriority(): int {
return 0;
}
public function getName(): ?string {
return $this->l->t('Background jobs');
}
public function getAuthorizedAppConfig(): array {
return [
'core' => [
'/mail_general_settings/',
],
];
}
}

@ -37,11 +37,11 @@ use OCP\AppFramework\Http\TemplateResponse;
use OCP\Constants;
use OCP\IConfig;
use OCP\IL10N;
use OCP\Settings\ISettings;
use OCP\Settings\IDelegatedSettings;
use OCP\Share\IManager;
use OCP\Util;
class Sharing implements ISettings {
class Sharing implements IDelegatedSettings {
/** @var IConfig */
private $config;
@ -154,4 +154,14 @@ class Sharing implements ISettings {
public function getPriority() {
return 0;
}
public function getAuthorizedAppConfig(): array {
return [
'core' => ['/shareapi_.*/'],
];
}
public function getName(): ?string {
return null;
}
}

@ -0,0 +1,37 @@
<template>
<div id="admin-right-sub-granting" class="section">
<h2>{{ t('settings', 'Admin right privilege') }}</h2>
<p class="settings-hint">
{{ t('settings', 'Here you can decide which group can access some of the admin settings.') }}
</p>
<div class="setting-list">
<div v-for="setting in availableSettings" :key="setting.class">
<h3>{{ setting.sectionName }}</h3>
<GroupSelect :available-groups="availableGroups" :authorized-groups="authorizedGroups" :setting="setting" />
</div>
</div>
</div>
</template>
<script>
import GroupSelect from './AdminDelegation/GroupSelect'
import { loadState } from '@nextcloud/initial-state'
export default {
name: 'AdminDelegating',
components: {
GroupSelect,
},
data() {
const availableSettings = loadState('settings', 'available-settings')
const availableGroups = loadState('settings', 'available-groups')
const authorizedGroups = loadState('settings', 'authorized-groups')
return {
availableSettings,
availableGroups,
authorizedGroups,
}
},
}
</script>

@ -0,0 +1,75 @@
<template>
<Multiselect
v-model="selected"
class="group-multiselect"
:placeholder="t('settings', 'None')"
track-by="gid"
label="displayName"
:options="availableGroups"
open-direction="bottom"
:multiple="true"
:allow-empty="true" />
</template>
<script>
import Multiselect from '@nextcloud/vue/dist/Components/Multiselect'
import { generateUrl } from '@nextcloud/router'
import axios from '@nextcloud/axios'
import { showError } from '@nextcloud/dialogs'
import logger from '../../logger'
export default {
name: 'GroupSelect',
components: {
Multiselect,
},
props: {
availableGroups: {
type: Array,
default: () => [],
},
setting: {
type: Object,
required: true,
},
authorizedGroups: {
type: Array,
required: true,
},
},
data() {
return {
selected: this.authorizedGroups
.filter((group) => group.class === this.setting.class)
.map((groupToMap) => this.availableGroups.find((group) => group.gid === groupToMap.group_id))
.filter((group) => group !== undefined)
}
},
watch: {
selected() {
this.saveGroups()
},
},
methods: {
async saveGroups() {
const data = {
newGroups: this.selected,
class: this.setting.class,
}
try {
await axios.post(generateUrl('/apps/settings/') + '/settings/authorizedgroups/saveSettings', data)
} catch (e) {
showError(t('settings', 'Unable to modify setting'))
logger.error('Unable to modify setting', e)
}
},
}
}
</script>
<style lang="scss">
.group-multiselect {
width: 100%;
margin-right: 0;
}
</style>

@ -0,0 +1,32 @@
/**
* @copyright Copyright (c) 2021 Carl Schwan <carl@carlschwan.eu>
*
* @author Carl Schwan <carl@carlschwan.eu>
*
* @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 Vue from 'vue'
import App from './components/AdminDelegating.vue'
// bind to window
Vue.prototype.OC = OC
Vue.prototype.t = t
const View = Vue.extend(App)
const accessibility = new View()
accessibility.$mount('#admin-right-sub-granting')

@ -0,0 +1,29 @@
<?php
/**
* @copyright Copyright (c) 2021 Nextcloud GmbH
*
* @author Carl Schwan <carl@carlschwan.eu>
*
* @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 <https://www.gnu.org/licenses/>.
*
*/
script('settings', 'vue-settings-admin-delegation');
?>
<div id="admin-right-sub-granting">
</div>

@ -120,13 +120,16 @@ class AdminSettingsControllerTest extends TestCase {
->willReturn([]);
$this->settingsManager
->expects($this->once())
->method('getAdminSettings')
->method('getAllowedAdminSettings')
->with('test')
->willReturn([5 => $this->createMock(ServerDevNotice::class)]);
$idx = $this->adminSettingsController->index('test');
$expected = new TemplateResponse('settings', 'settings/frame', ['forms' => ['personal' => [], 'admin' => []], 'content' => '']);
$expected = new TemplateResponse('settings', 'settings/frame', [
'forms' => ['personal' => [], 'admin' => []],
'content' => ''
]);
$this->assertEquals($expected, $idx);
}
}

@ -0,0 +1,77 @@
<?php
/**
* @copyright Copyright (c) 2021 Nextcloud GmbH
*
* @author Carl Schwan <carl@carlschwan.eu>
*
* @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 <https://www.gnu.org/licenses/>.
*
*/
namespace OCA\Settings\Tests\Controller\Admin;
use OC\Settings\AuthorizedGroup;
use OCA\Settings\Controller\AuthorizedGroupController;
use OCA\Settings\Service\AuthorizedGroupService;
use OCP\IRequest;
use Test\TestCase;
class DelegationControllerTest extends TestCase {
/** @var AuthorizedGroupService */
private $service;
/** @var IRequest */
private $request;
/** @var AuthorizedGroupController */
private $controller;
protected function setUp(): void {
parent::setUp();
$this->request = $this->getMockBuilder(IRequest::class)->getMock();
$this->service = $this->getMockBuilder(AuthorizedGroupService::class)->disableOriginalConstructor()->getMock();
$this->controller = new AuthorizedGroupController(
'settings', $this->request, $this->service
);
}
public function testSaveSettings() {
$setting = 'MySecretSetting';
$oldGroups = [];
$oldGroups[] = AuthorizedGroup::fromParams(['groupId' => 'hello', 'class' => $setting]);
$goodbye = AuthorizedGroup::fromParams(['groupId' => 'goodbye', 'class' => $setting, 'id' => 42]);
$oldGroups[] = $goodbye;
$this->service->expects($this->once())
->method('findExistingGroupsForClass')
->with('MySecretSetting')
->will($this->returnValue($oldGroups));
$this->service->expects($this->once())
->method('delete')
->with(42);
$this->service->expects($this->once())
->method('create')
->with('world', 'MySecretSetting')
->will($this->returnValue(AuthorizedGroup::fromParams(['groupId' => 'world', 'class' => $setting])));
$result = $this->controller->saveSettings([['gid' => 'hello'], ['gid' => 'world']], 'MySecretSetting');
$this->assertEquals(['valid' => true], $result->getData());
}
}

@ -68,10 +68,15 @@ class SubadminMiddlewareTest extends \Test\TestCase {
$this->expectException(\OC\AppFramework\Middleware\Security\Exceptions\NotAdminException::class);
$this->reflector
->expects($this->once())
->expects($this->at(0))
->method('hasAnnotation')
->with('NoSubAdminRequired')
->willReturn(false);
$this->reflector
->expects($this->at(1))
->method('hasAnnotation')
->with('AuthorizedAdminSetting')
->willReturn(false);
$this->subadminMiddleware->beforeController($this->controller, 'foo');
}
@ -87,10 +92,15 @@ class SubadminMiddlewareTest extends \Test\TestCase {
public function testBeforeControllerAsSubAdminWithoutExemption() {
$this->reflector
->expects($this->once())
->expects($this->at(0))
->method('hasAnnotation')
->with('NoSubAdminRequired')
->willReturn(false);
$this->reflector
->expects($this->at(1))
->method('hasAnnotation')
->with('AuthorizedAdminSetting')
->willReturn(false);
$this->subadminMiddlewareAsSubAdmin->beforeController($this->controller, 'foo');
}

@ -31,6 +31,7 @@ namespace OCA\Settings\Tests\Settings\Admin;
use OCA\Settings\Settings\Admin\Mail;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\IConfig;
use OCP\IL10N;
use Test\TestCase;
class MailTest extends TestCase {
@ -38,13 +39,17 @@ class MailTest extends TestCase {
private $admin;
/** @var IConfig */
private $config;
/** @var IL10N */
private $l10n;
protected function setUp(): void {
parent::setUp();
$this->config = $this->getMockBuilder(IConfig::class)->getMock();
$this->l10n = $this->getMockBuilder(IL10N::class)->getMock();
$this->admin = new Mail(
$this->config
$this->config,
$this->l10n
);
}

@ -36,6 +36,7 @@ use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\IConfig;
use OCP\IDBConnection;
use OCP\IL10N;
use PHPUnit\Framework\MockObject\MockObject;
use Test\TestCase;
@ -51,12 +52,15 @@ class ServerTest extends TestCase {
private $timeFactory;
/** @var IConfig|MockObject */
private $config;
/** @var IL10N|MockObject */
private $l10n;
protected function setUp(): void {
parent::setUp();
$this->connection = \OC::$server->getDatabaseConnection();
$this->timeFactory = $this->createMock(ITimeFactory::class);
$this->config = $this->createMock(IConfig::class);
$this->l10n = $this->createMock(IL10N::class);
$this->admin = $this->getMockBuilder(Server::class)
->onlyMethods(['cronMaxAge'])
@ -64,6 +68,7 @@ class ServerTest extends TestCase {
$this->connection,
$this->timeFactory,
$this->config,
$this->l10n,
])
->getMock();
}

@ -42,6 +42,7 @@ module.exports = {
entry: {
'settings-apps-users-management': path.join(__dirname, 'src', 'main-apps-users-management'),
'settings-admin-security': path.join(__dirname, 'src', 'main-admin-security'),
'settings-admin-delegation': path.join(__dirname, 'src', 'main-admin-delegation'),
'settings-personal-security': path.join(__dirname, 'src', 'main-personal-security'),
'settings-personal-webauthn': path.join(__dirname, 'src', 'main-personal-webauth'),
'settings-nextcloud-pdf': path.join(__dirname, 'src', 'main-nextcloud-pdf'),

@ -0,0 +1,62 @@
<?php
/**
* @copyright Copyright (c) 2021 Nextcloud GmbH
*
* @author Carl Schwan <carl@carlschwan.eu>
*
* @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 <https://www.gnu.org/licenses/>.
*
*/
namespace OC\Core\Migrations;
use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\Migration\SimpleMigrationStep;
use OCP\Migration\IOutput;
class Version23000Date20210721100600 extends SimpleMigrationStep {
/**
* @param IOutput $output
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param array $options
* @return null|ISchemaWrapper
*/
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options) {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
$table = $schema->createTable('authorized_groups');
$table->addColumn('id', 'integer', [
'autoincrement' => true,
'notnull' => true,
]);
$table->addColumn('group_id', 'string', [
'notnull' => true,
'length' => 200
]);
$table->addColumn('class', 'string', [
'notnull' => true,
'length' => 200,
]);
$table->setPrimaryKey(['id']);
$table->addIndex(['group_id'], 'admindel_groupid_idx');
return $schema;
}
}

@ -491,6 +491,7 @@ return array(
'OCP\\Security\\VerificationToken\\IVerificationToken' => $baseDir . '/lib/public/Security/VerificationToken/IVerificationToken.php',
'OCP\\Security\\VerificationToken\\InvalidTokenException' => $baseDir . '/lib/public/Security/VerificationToken/InvalidTokenException.php',
'OCP\\Session\\Exceptions\\SessionNotAvailableException' => $baseDir . '/lib/public/Session/Exceptions/SessionNotAvailableException.php',
'OCP\\Settings\\IDelegatedSettings' => $baseDir . '/lib/public/Settings/IDelegatedSettings.php',
'OCP\\Settings\\IIconSection' => $baseDir . '/lib/public/Settings/IIconSection.php',
'OCP\\Settings\\IManager' => $baseDir . '/lib/public/Settings/IManager.php',
'OCP\\Settings\\ISettings' => $baseDir . '/lib/public/Settings/ISettings.php',
@ -971,6 +972,7 @@ return array(
'OC\\Core\\Migrations\\Version21000Date20210309185126' => $baseDir . '/core/Migrations/Version21000Date20210309185126.php',
'OC\\Core\\Migrations\\Version21000Date20210309185127' => $baseDir . '/core/Migrations/Version21000Date20210309185127.php',
'OC\\Core\\Migrations\\Version22000Date20210216080825' => $baseDir . '/core/Migrations/Version22000Date20210216080825.php',
'OC\\Core\\Migrations\\Version23000Date20210721100600' => $baseDir . '/core/Migrations/Version23000Date20210721100600.php',
'OC\\Core\\Migrations\\Version23000Date20210906132259' => $baseDir . '/core/Migrations/Version23000Date20210906132259.php',
'OC\\Core\\Notification\\CoreNotifier' => $baseDir . '/core/Notification/CoreNotifier.php',
'OC\\Core\\Service\\LoginFlowV2Service' => $baseDir . '/core/Service/LoginFlowV2Service.php',
@ -1386,6 +1388,8 @@ return array(
'OC\\Session\\Internal' => $baseDir . '/lib/private/Session/Internal.php',
'OC\\Session\\Memory' => $baseDir . '/lib/private/Session/Memory.php',
'OC\\Session\\Session' => $baseDir . '/lib/private/Session/Session.php',
'OC\\Settings\\AuthorizedGroup' => $baseDir . '/lib/private/Settings/AuthorizedGroup.php',
'OC\\Settings\\AuthorizedGroupMapper' => $baseDir . '/lib/private/Settings/AuthorizedGroupMapper.php',
'OC\\Settings\\Manager' => $baseDir . '/lib/private/Settings/Manager.php',
'OC\\Settings\\Section' => $baseDir . '/lib/private/Settings/Section.php',
'OC\\Setup' => $baseDir . '/lib/private/Setup.php',

@ -520,6 +520,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c
'OCP\\Security\\VerificationToken\\IVerificationToken' => __DIR__ . '/../../..' . '/lib/public/Security/VerificationToken/IVerificationToken.php',
'OCP\\Security\\VerificationToken\\InvalidTokenException' => __DIR__ . '/../../..' . '/lib/public/Security/VerificationToken/InvalidTokenException.php',
'OCP\\Session\\Exceptions\\SessionNotAvailableException' => __DIR__ . '/../../..' . '/lib/public/Session/Exceptions/SessionNotAvailableException.php',
'OCP\\Settings\\IDelegatedSettings' => __DIR__ . '/../../..' . '/lib/public/Settings/IDelegatedSettings.php',
'OCP\\Settings\\IIconSection' => __DIR__ . '/../../..' . '/lib/public/Settings/IIconSection.php',
'OCP\\Settings\\IManager' => __DIR__ . '/../../..' . '/lib/public/Settings/IManager.php',
'OCP\\Settings\\ISettings' => __DIR__ . '/../../..' . '/lib/public/Settings/ISettings.php',
@ -1000,6 +1001,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c
'OC\\Core\\Migrations\\Version21000Date20210309185126' => __DIR__ . '/../../..' . '/core/Migrations/Version21000Date20210309185126.php',
'OC\\Core\\Migrations\\Version21000Date20210309185127' => __DIR__ . '/../../..' . '/core/Migrations/Version21000Date20210309185127.php',
'OC\\Core\\Migrations\\Version22000Date20210216080825' => __DIR__ . '/../../..' . '/core/Migrations/Version22000Date20210216080825.php',
'OC\\Core\\Migrations\\Version23000Date20210721100600' => __DIR__ . '/../../..' . '/core/Migrations/Version23000Date20210721100600.php',
'OC\\Core\\Migrations\\Version23000Date20210906132259' => __DIR__ . '/../../..' . '/core/Migrations/Version23000Date20210906132259.php',
'OC\\Core\\Notification\\CoreNotifier' => __DIR__ . '/../../..' . '/core/Notification/CoreNotifier.php',
'OC\\Core\\Service\\LoginFlowV2Service' => __DIR__ . '/../../..' . '/core/Service/LoginFlowV2Service.php',
@ -1415,6 +1417,8 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c
'OC\\Session\\Internal' => __DIR__ . '/../../..' . '/lib/private/Session/Internal.php',
'OC\\Session\\Memory' => __DIR__ . '/../../..' . '/lib/private/Session/Memory.php',
'OC\\Session\\Session' => __DIR__ . '/../../..' . '/lib/private/Session/Session.php',
'OC\\Settings\\AuthorizedGroup' => __DIR__ . '/../../..' . '/lib/private/Settings/AuthorizedGroup.php',
'OC\\Settings\\AuthorizedGroupMapper' => __DIR__ . '/../../..' . '/lib/private/Settings/AuthorizedGroupMapper.php',
'OC\\Settings\\Manager' => __DIR__ . '/../../..' . '/lib/private/Settings/Manager.php',
'OC\\Settings\\Section' => __DIR__ . '/../../..' . '/lib/private/Settings/Section.php',
'OC\\Setup' => __DIR__ . '/../../..' . '/lib/private/Setup.php',

@ -253,7 +253,7 @@ class InfoParser {
if (!count($node->children())) {
$value = (string)$node;
if (!empty($value)) {
$data['@value'] = (string)$node;
$data['@value'] = $value;
}
} else {
$data = array_merge($data, $this->xmlToArray($node));

@ -48,6 +48,7 @@ use OC\AppFramework\Utility\SimpleContainer;
use OC\Core\Middleware\TwoFactorMiddleware;
use OC\Log\PsrLoggerAdapter;
use OC\ServerContainer;
use OC\Settings\AuthorizedGroupMapper;
use OCA\WorkflowEngine\Manager;
use OCP\AppFramework\Http\IOutput;
use OCP\AppFramework\IAppContainer;
@ -246,7 +247,9 @@ class DIContainer extends SimpleContainer implements IAppContainer {
$this->getUserId() !== null && $server->getGroupManager()->isAdmin($this->getUserId()),
$server->getUserSession()->getUser() !== null && $server->query(ISubAdmin::class)->isSubAdmin($server->getUserSession()->getUser()),
$server->getAppManager(),
$server->getL10N('lib')
$server->getL10N('lib'),
$c->get(AuthorizedGroupMapper::class),
$server->get(IUserSession::class)
);
$dispatcher->registerMiddleware($securityMiddleware);
$dispatcher->registerMiddleware(

@ -34,6 +34,7 @@ declare(strict_types=1);
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OC\AppFramework\Middleware\Security;
use OC\AppFramework\Middleware\Security\Exceptions\AppNotEnabledException;
@ -43,6 +44,7 @@ use OC\AppFramework\Middleware\Security\Exceptions\NotLoggedInException;
use OC\AppFramework\Middleware\Security\Exceptions\SecurityException;
use OC\AppFramework\Middleware\Security\Exceptions\StrictCookieMissingException;
use OC\AppFramework\Utility\ControllerMethodReflector;
use OC\Settings\AuthorizedGroupMapper;
use OCP\App\AppPathNotFoundException;
use OCP\App\IAppManager;
use OCP\AppFramework\Controller;
@ -56,6 +58,7 @@ use OCP\IL10N;
use OCP\INavigationManager;
use OCP\IRequest;
use OCP\IURLGenerator;
use OCP\IUserSession;
use OCP\Util;
use Psr\Log\LoggerInterface;
@ -88,6 +91,10 @@ class SecurityMiddleware extends Middleware {
private $appManager;
/** @var IL10N */
private $l10n;
/** @var AuthorizedGroupMapper */
private $groupAuthorizationMapper;
/** @var IUserSession */
private $userSession;
public function __construct(IRequest $request,
ControllerMethodReflector $reflector,
@ -99,7 +106,9 @@ class SecurityMiddleware extends Middleware {
bool $isAdminUser,
bool $isSubAdmin,
IAppManager $appManager,
IL10N $l10n
IL10N $l10n,
AuthorizedGroupMapper $mapper,
IUserSession $userSession
) {
$this->navigationManager = $navigationManager;
$this->request = $request;
@ -112,12 +121,15 @@ class SecurityMiddleware extends Middleware {
$this->isSubAdmin = $isSubAdmin;
$this->appManager = $appManager;
$this->l10n = $l10n;
$this->groupAuthorizationMapper = $mapper;
$this->userSession = $userSession;
}
/**
* This runs all the security checks before a method call. The
* security checks are determined by inspecting the controller method
* annotations
*
* @param Controller $controller the controller
* @param string $methodName the name of the method
* @throws SecurityException when a security check fails
@ -140,15 +152,39 @@ class SecurityMiddleware extends Middleware {
if (!$this->isLoggedIn) {
throw new NotLoggedInException();
}
$authorized = false;
if ($this->reflector->hasAnnotation('AuthorizedAdminSetting')) {
$authorized = $this->isAdminUser;
if (!$authorized && $this->reflector->hasAnnotation('SubAdminRequired')) {
$authorized = $this->isSubAdmin;
}
if (!$authorized) {
$settingClasses = explode(';', $this->reflector->getAnnotationParameter('AuthorizedAdminSetting', 'settings'));
$authorizedClasses = $this->groupAuthorizationMapper->findAllClassesForUser($this->userSession->getUser());
foreach ($settingClasses as $settingClass) {
$authorized = in_array($settingClass, $authorizedClasses, true);
if ($authorized) {
break;
}
}
}
if (!$authorized) {
throw new NotAdminException($this->l10n->t('Logged in user must be an admin, a sub admin or gotten special right to access this setting'));
}
}
if ($this->reflector->hasAnnotation('SubAdminRequired')
&& !$this->isSubAdmin
&& !$this->isAdminUser) {
&& !$this->isAdminUser
&& !$authorized) {
throw new NotAdminException($this->l10n->t('Logged in user must be an admin or sub admin'));
}
if (!$this->reflector->hasAnnotation('SubAdminRequired')
&& !$this->reflector->hasAnnotation('NoAdminRequired')
&& !$this->isAdminUser) {
&& !$this->isAdminUser
&& !$authorized) {
throw new NotAdminException($this->l10n->t('Logged in user must be an admin'));
}
}
@ -200,19 +236,20 @@ class SecurityMiddleware extends Middleware {
/**
* If an SecurityException is being caught, ajax requests return a JSON error
* response and non ajax requests redirect to the index
*
* @param Controller $controller the controller that is being called
* @param string $methodName the name of the method that will be called on
* the controller
* @param \Exception $exception the thrown exception
* @throws \Exception the passed in exception if it can't handle it
* @return Response a Response object or null in case that the exception could not be handled
* @throws \Exception the passed in exception if it can't handle it
*/
public function afterException($controller, $methodName, \Exception $exception): Response {
if ($exception instanceof SecurityException) {
if ($exception instanceof StrictCookieMissingException) {
return new RedirectResponse(\OC::$WEBROOT . '/');
}
if (stripos($this->request->getHeader('Accept'),'html') === false) {
if (stripos($this->request->getHeader('Accept'), 'html') === false) {
$response = new JSONResponse(
['message' => $exception->getMessage()],
$exception->getCode()

@ -0,0 +1,50 @@
<?php
/**
* @copyright Copyright (c) 2021 Carl Schwan <carl@carlschwan.eu>
*
* @author Carl Schwan <carl@carlschwan.eu>
*
* @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 <https://www.gnu.org/licenses/>.
*
*/
namespace OC\Settings;
use OCP\AppFramework\Db\Entity;
/**
* @method setGroupId(string $groupId)
* @method setClass(string $class)
* @method getGroupId(): string
* @method getClass(): string
*/
class AuthorizedGroup extends Entity implements \JsonSerializable {
/** @var string $group_id */
protected $groupId;
/** @var string $class */
protected $class;
public function jsonSerialize(): array {
return [
'id' => $this->id,
'group_id' => $this->groupId,
'class' => $this->class
];
}
}

@ -0,0 +1,125 @@
<?php
/**
* @copyright Copyright (c) 2021 Carl Schwan <carl@carlschwan.eu>
*
* @author Carl Schwan <carl@carlschwan.eu>
*
* @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 <https://www.gnu.org/licenses/>.
*
*/
namespace OC\Settings;
use OCP\AppFramework\Db\Entity;
use OCP\AppFramework\Db\QBMapper;
use OCP\DB\Exception;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
use OCP\IGroup;
use OCP\IGroupManager;
use OCP\IUser;
class AuthorizedGroupMapper extends QBMapper {
public function __construct(IDBConnection $db) {
parent::__construct($db, 'authorized_groups', AuthorizedGroup::class);
}
/**
* @throws Exception
*/
public function findAllClassesForUser(IUser $user): array {
$qb = $this->db->getQueryBuilder();
/** @var IGroupManager $groupManager */
$groupManager = \OC::$server->get(IGroupManager::class);
$groups = $groupManager->getUserGroups($user);
if (count($groups) === 0) {
return [];
}
$result = $qb->select('class')
->from($this->getTableName(), 'auth')
->where($qb->expr()->in('group_id', array_map(function (IGroup $group) use ($qb) {
return $qb->createNamedParameter($group->getGID());
}, $groups), IQueryBuilder::PARAM_STR))
->executeQuery();
$classes = [];
while ($row = $result->fetch()) {
$classes[] = $row['class'];
}
$result->closeCursor();
return $classes;
}
/**
* @throws \OCP\AppFramework\Db\DoesNotExistException
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
* @throws \OCP\DB\Exception
*/
public function find(int $id): AuthorizedGroup {
$queryBuilder = $this->db->getQueryBuilder();
$queryBuilder->select('*')
->from($this->getTableName())
->where($queryBuilder->expr()->eq('id', $queryBuilder->createNamedParameter($id)));
/** @var AuthorizedGroup $authorizedGroup */
$authorizedGroup = $this->findEntity($queryBuilder);
return $authorizedGroup;
}
/**
* Get all the authorizations stored in the database.
*
* @return AuthorizedGroup[]
* @throws \OCP\DB\Exception
*/
public function findAll(): array {
$qb = $this->db->getQueryBuilder();
$qb->select('*')->from($this->getTableName());
return $this->findEntities($qb);
}
public function findByGroupIdAndClass(string $groupId, string $class) {
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from($this->getTableName())
->where($qb->expr()->eq('group_id', $qb->createNamedParameter($groupId)))
->andWhere($qb->expr()->eq('class', $qb->createNamedParameter($class)));
return $this->findEntity($qb);
}
/**
* @return Entity[]
* @throws \OCP\DB\Exception
*/
public function findExistingGroupsForClass(string $class): array {
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from($this->getTableName())
->where($qb->expr()->eq('class', $qb->createNamedParameter($class)));
return $this->findEntities($qb);
}
/**
* @throws Exception
*/
public function removeGroup(string $gid) {
$qb = $this->db->getQueryBuilder();
$qb->delete($this->getTableName())
->where($qb->expr()->eq('group_id', $qb->createNamedParameter($gid)))
->executeStatement();
}
}

@ -11,6 +11,7 @@
* @author Robin Appelman <robin@icewind.nl>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author sualko <klaus@jsxc.org>
* @author Carl Schwan <carl@carlschwan.eu>
*
* @license GNU AGPL version 3 or any later version
*
@ -28,23 +29,27 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OC\Settings;
use Closure;
use OCP\AppFramework\QueryException;
use OCP\Group\ISubAdmin;
use OCP\IGroupManager;
use OCP\IL10N;
use OCP\ILogger;
use OCP\IServerContainer;
use OCP\IURLGenerator;
use OCP\IUser;
use OCP\L10N\IFactory;
use OCP\Settings\IIconSection;
use OCP\Settings\IManager;
use OCP\Settings\ISettings;
use OCP\Settings\ISubAdminSettings;
use Psr\Log\LoggerInterface;
class Manager implements IManager {
/** @var ILogger */
/** @var LoggerInterface */
private $log;
/** @var IL10N */
@ -59,16 +64,31 @@ class Manager implements IManager {
/** @var IServerContainer */
private $container;
/** @var AuthorizedGroupMapper $mapper */
private $mapper;
/** @var IGroupManager $groupManager */
private $groupManager;
/** @var ISubAdmin $subAdmin */
private $subAdmin;
public function __construct(
ILogger $log,
LoggerInterface $log,
IFactory $l10nFactory,
IURLGenerator $url,
IServerContainer $container
IServerContainer $container,
AuthorizedGroupMapper $mapper,
IGroupManager $groupManager,
ISubAdmin $subAdmin
) {
$this->log = $log;
$this->l10nFactory = $l10nFactory;
$this->url = $url;
$this->container = $container;
$this->mapper = $mapper;
$this->groupManager = $groupManager;
$this->subAdmin = $subAdmin;
}
/** @var array */
@ -106,18 +126,13 @@ class Manager implements IManager {
}
foreach (array_unique($this->sectionClasses[$type]) as $index => $class) {
try {
/** @var IIconSection $section */
$section = \OC::$server->query($class);
} catch (QueryException $e) {
$this->log->logException($e, ['level' => ILogger::INFO]);
continue;
}
/** @var IIconSection $section */
$section = \OC::$server->get($class);
$sectionID = $section->getID();
if ($sectionID !== 'connected-accounts' && isset($this->sections[$type][$sectionID])) {
$this->log->logException(new \InvalidArgumentException('Section with the same ID already registered: ' . $sectionID . ', class: ' . $class), ['level' => ILogger::INFO]);
$this->log->info('', ['exception' => new \InvalidArgumentException('Section with the same ID already registered: ' . $sectionID . ', class: ' . $class)]);
continue;
}
@ -136,8 +151,9 @@ class Manager implements IManager {
protected $settings = [];
/**
* @param string $type 'admin' or 'personal'
* @param string $setting Class must implement OCP\Settings\ISetting
* @psam-param 'admin'|'personal' $type The type of the setting.
* @param string $setting Class must implement OCP\Settings\ISettings
* @param bool $allowedDelegation
*
* @return void
*/
@ -167,14 +183,14 @@ class Manager implements IManager {
try {
/** @var ISettings $setting */
$setting = $this->container->query($class);
$setting = $this->container->get($class);
} catch (QueryException $e) {
$this->log->logException($e, ['level' => ILogger::INFO]);
$this->log->info($e->getMessage(), ['exception' => $e]);
continue;
}
if (!$setting instanceof ISettings) {
$this->log->logException(new \InvalidArgumentException('Invalid settings setting registered (' . $class . ')'), ['level' => ILogger::INFO]);
$this->log->info('', ['exception' => new \InvalidArgumentException('Invalid settings setting registered (' . $class . ')')]);
continue;
}
@ -307,4 +323,52 @@ class Manager implements IManager {
ksort($settings);
return $settings;
}
public function getAllowedAdminSettings(string $section, IUser $user): array {
$isAdmin = $this->groupManager->isAdmin($user->getUID());
$isSubAdmin = $this->subAdmin->isSubAdmin($user);
$subAdminOnly = !$isAdmin && $isSubAdmin;
if ($subAdminOnly) {
// not an admin => look if the user is still authorized to access some
// settings
$subAdminSettingsFilter = function (ISettings $settings) {
return $settings instanceof ISubAdminSettings;
};
$appSettings = $this->getSettings('admin', $section, $subAdminSettingsFilter);
} elseif ($isAdmin) {
$appSettings = $this->getSettings('admin', $section);
} else {
$authorizedSettingsClasses = $this->mapper->findAllClassesForUser($user);
$authorizedGroupFilter = function (ISettings $settings) use ($authorizedSettingsClasses) {
return in_array(get_class($settings), $authorizedSettingsClasses) === true;
};
$appSettings = $this->getSettings('admin', $section, $authorizedGroupFilter);
}
$settings = [];
foreach ($appSettings as $setting) {
if (!isset($settings[$setting->getPriority()])) {
$settings[$setting->getPriority()] = [];
}
$settings[$setting->getPriority()][] = $setting;
}
ksort($settings);
return $settings;
}
public function getAllAllowedAdminSettings(IUser $user): array {
$this->getSettings('admin', ''); // Make sure all the settings are loaded
$settings = [];
$authorizedSettingsClasses = $this->mapper->findAllClassesForUser($user);
foreach ($this->settings['admin'] as $section) {
foreach ($section as $setting) {
if (in_array(get_class($setting), $authorizedSettingsClasses) === true) {
$settings[] = $setting;
}
}
}
return $settings;
}
}

@ -61,6 +61,7 @@ use OCP\App\ManagerEvent;
use OCP\AppFramework\QueryException;
use OCP\Authentication\IAlternativeLogin;
use OCP\ILogger;
use OCP\Settings\IManager as ISettingsManager;
use Psr\Log\LoggerInterface;
/**
@ -223,22 +224,22 @@ class OC_App {
if (!empty($info['settings']['admin'])) {
foreach ($info['settings']['admin'] as $setting) {
\OC::$server->getSettingsManager()->registerSetting('admin', $setting);
\OC::$server->get(ISettingsManager::class)->registerSetting('admin', $setting);
}
}
if (!empty($info['settings']['admin-section'])) {
foreach ($info['settings']['admin-section'] as $section) {
\OC::$server->getSettingsManager()->registerSection('admin', $section);
\OC::$server->get(ISettingsManager::class)->registerSection('admin', $section);
}
}
if (!empty($info['settings']['personal'])) {
foreach ($info['settings']['personal'] as $setting) {
\OC::$server->getSettingsManager()->registerSetting('personal', $setting);
\OC::$server->get(ISettingsManager::class)->registerSetting('personal', $setting);
}
}
if (!empty($info['settings']['personal-section'])) {
foreach ($info['settings']['personal-section'] as $section) {
\OC::$server->getSettingsManager()->registerSection('personal', $section);
\OC::$server->get(ISettingsManager::class)->registerSection('personal', $section);
}
}

@ -0,0 +1,54 @@
<?php
/**
* @copyright Copyright (c) Nextcloud GmbH
*
* @author Carl Schwan <carl@carlschwan.eu>
*
* @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/>.
*
*/
namespace OCP\Settings;
/**
* Special cases of settings that can be allowed to use by member of special
* groups.
* @since 23.0.0
*/
interface IDelegatedSettings extends ISettings {
/**
* Get the name of the settings to differentiate settings inside a section or
* null if only the section name should be displayed.
* @since 23.0.0
*/
public function getName(): ?string;
/**
* Get a list of authorized app config that this setting is allowed to modify.
* The format of the array is the following:
* ```php
* <?php
* [
* 'app_name' => [
* '/simple_key/', # value
* '/s[a-z]*ldap/', # regex
* ],
* 'another_app_name => [ ... ],
* ]
* ```
* @since 23.0.0
*/
public function getAuthorizedAppConfig(): array;
}

@ -23,8 +23,11 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCP\Settings;
use OCP\IUser;
/**
* @since 9.1
*/
@ -50,7 +53,7 @@ interface IManager {
public const KEY_PERSONAL_SECTION = 'personal-section';
/**
* @param string $type 'admin' or 'personal'
* @param string $type 'admin-section' or 'personal-section'
* @param string $section Class must implement OCP\Settings\ISection
* @since 14.0.0
*/
@ -58,7 +61,7 @@ interface IManager {
/**
* @param string $type 'admin' or 'personal'
* @param string $setting Class must implement OCP\Settings\ISetting
* @param string $setting Class must implement OCP\Settings\ISettings
* @since 14.0.0
*/
public function registerSetting(string $type, string $setting);
@ -66,7 +69,7 @@ interface IManager {
/**
* returns a list of the admin sections
*
* @return array array of ISection[] where key is the priority
* @return array<int, array<int, IIconSection>> array from IConSection[] where key is the priority
* @since 9.1.0
*/
public function getAdminSections(): array;
@ -84,16 +87,32 @@ interface IManager {
*
* @param string $section the section id for which to load the settings
* @param bool $subAdminOnly only return settings sub admins are supposed to see (since 17.0.0)
* @return array array of IAdmin[] where key is the priority
* @return array<int, array<int, ISettings>> array of ISettings[] where key is the priority
* @since 9.1.0
*/
public function getAdminSettings($section, bool $subAdminOnly = false): array;
/**
* Returns a list of admin settings that the given user can use for the give section
*
* @return array<int, list<ISettings>> The array of admin settings there admin delegation is allowed.
* @since 23.0.0
*/
public function getAllowedAdminSettings(string $section, IUser $user): array;
/**
* Returns a list of admin settings that the given user can use.
*
* @return array<int, list<ISettings>> The array of admin settings there admin delegation is allowed.
* @since 23.0.0
*/
public function getAllAllowedAdminSettings(IUser $user): array;
/**
* returns a list of the personal settings
*
* @param string $section the section id for which to load the settings
* @return array array of IPersonal[] where key is the priority
* @return array array of ISettings[] where key is the priority
* @since 13.0.0
*/
public function getPersonalSettings($section): array;

@ -393,8 +393,7 @@
<xs:complexType name="settings">
<xs:sequence>
<xs:element name="admin" type="php-class" minOccurs="0"
maxOccurs="unbounded"/>
<xs:element name="admin" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="admin-section" type="php-class" minOccurs="0"
maxOccurs="unbounded"/>
<xs:element name="personal" type="php-class" minOccurs="0"

@ -389,9 +389,8 @@
<xs:complexType name="settings">
<xs:sequence>
<xs:element name="admin" type="php-class" minOccurs="0"
maxOccurs="unbounded"/>
<xs:element name="admin-section" type="php-class" minOccurs="0"
<xs:element name="admin" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="admin-section" type="php-class" minOccurs="0"
maxOccurs="unbounded"/>
<xs:element name="personal" type="php-class" minOccurs="0"
maxOccurs="unbounded"/>

@ -32,6 +32,7 @@ use OC\AppFramework\Middleware\Security\Exceptions\SecurityException;
use OC\Appframework\Middleware\Security\Exceptions\StrictCookieMissingException;
use OC\AppFramework\Middleware\Security\SecurityMiddleware;
use OC\AppFramework\Utility\ControllerMethodReflector;
use OC\Settings\AuthorizedGroupMapper;
use OCP\App\IAppManager;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\JSONResponse;
@ -42,6 +43,7 @@ use OCP\IL10N;
use OCP\INavigationManager;
use OCP\IRequest;
use OCP\IURLGenerator;
use OCP\IUserSession;
use OCP\Security\ISecureRandom;
use Psr\Log\LoggerInterface;
@ -69,10 +71,16 @@ class SecurityMiddlewareTest extends \Test\TestCase {
private $appManager;
/** @var IL10N|\PHPUnit\Framework\MockObject\MockObject */
private $l10n;
/** @var IUserSession|\PHPUnit\Framework\MockObject\MockObject */
private $userSession;
/** @var AuthorizedGroupMapper|\PHPUnit\Framework\MockObject\MockObject */
private $authorizedGroupMapper;
protected function setUp(): void {
parent::setUp();
$this->authorizedGroupMapper = $this->createMock(AuthorizedGroupMapper::class);
$this->userSession = $this->createMock(IUserSession::class);
$this->controller = $this->createMock(Controller::class);
$this->reader = new ControllerMethodReflector();
$this->logger = $this->createMock(LoggerInterface::class);
@ -102,7 +110,9 @@ class SecurityMiddlewareTest extends \Test\TestCase {
$isAdminUser,
$isSubAdmin,
$this->appManager,
$this->l10n
$this->l10n,
$this->authorizedGroupMapper,
$this->userSession
);
}

@ -23,22 +23,25 @@
namespace OCA\Settings\Tests\AppInfo;
use OC\Settings\AuthorizedGroupMapper;
use OC\Settings\Manager;
use OCP\Group\ISubAdmin;
use OCP\IDBConnection;
use OCP\IGroupManager;
use OCP\IL10N;
use OCP\ILogger;
use OCP\IServerContainer;
use OCP\IURLGenerator;
use OCP\L10N\IFactory;
use OCP\Settings\ISettings;
use OCP\Settings\ISubAdminSettings;
use Psr\Log\LoggerInterface;
use Test\TestCase;
class ManagerTest extends TestCase {
/** @var Manager|\PHPUnit\Framework\MockObject\MockObject */
private $manager;
/** @var ILogger|\PHPUnit\Framework\MockObject\MockObject */
/** @var LoggerInterface|\PHPUnit\Framework\MockObject\MockObject */
private $logger;
/** @var IDBConnection|\PHPUnit\Framework\MockObject\MockObject */
private $l10n;
@ -48,21 +51,31 @@ class ManagerTest extends TestCase {
private $url;
/** @var IServerContainer|\PHPUnit\Framework\MockObject\MockObject */
private $container;
/** @var IGroupManager|\PHPUnit\Framework\MockObject\MockObject */
private $groupManager;
/** @var ISubAdmin|\PHPUnit\Framework\MockObject\MockObject */
private $subAdmin;
protected function setUp(): void {
parent::setUp();
$this->logger = $this->createMock(ILogger::class);
$this->logger = $this->createMock(LoggerInterface::class);
$this->l10n = $this->createMock(IL10N::class);
$this->l10nFactory = $this->createMock(IFactory::class);
$this->url = $this->createMock(IURLGenerator::class);
$this->container = $this->createMock(IServerContainer::class);
$this->mapper = $this->createMock(AuthorizedGroupMapper::class);
$this->groupManager = $this->createMock(IGroupManager::class);
$this->subAdmin = $this->createMock(ISubAdmin::class);
$this->manager = new Manager(
$this->logger,
$this->l10nFactory,
$this->url,
$this->container
$this->container,
$this->mapper,
$this->groupManager,
$this->subAdmin,
);
}
@ -106,7 +119,7 @@ class ManagerTest extends TestCase {
->willReturn(13);
$section->method('getSection')
->willReturn('sharing');
$this->container->method('query')
$this->container->method('get')
->with('myAdminClass')
->willReturn($section);
@ -124,7 +137,7 @@ class ManagerTest extends TestCase {
->willReturn(13);
$section->method('getSection')
->willReturn('sharing');
$this->container->method('query')
$this->container->method('get')
->with('myAdminClass')
->willReturn($section);
@ -141,7 +154,7 @@ class ManagerTest extends TestCase {
$section->method('getSection')
->willReturn('sharing');
$this->container->expects($this->once())
->method('query')
->method('get')
->with('mySubAdminClass')
->willReturn($section);
@ -169,11 +182,11 @@ class ManagerTest extends TestCase {
$this->manager->registerSetting('personal', 'section2');
$this->container->expects($this->at(0))
->method('query')
->method('get')
->with('section1')
->willReturn($section);
$this->container->expects($this->at(1))
->method('query')
->method('get')
->with('section2')
->willReturn($section2);

Loading…
Cancel
Save