diff options
author | Carl Schwan <carl@carlschwan.eu> | 2021-07-22 11:41:29 +0200 |
---|---|---|
committer | Carl Schwan <carl@carlschwan.eu> | 2021-09-29 21:43:31 +0200 |
commit | 6958d8005ae3b86759f49746564bf7238456be52 (patch) | |
tree | aab851e09351c631129e4729aa49c03533ce6180 /lib/private | |
parent | ee987d74303cb38b864f96660cd2ee6d6552ebfd (diff) | |
download | nextcloud-server-6958d8005ae3b86759f49746564bf7238456be52.tar.gz nextcloud-server-6958d8005ae3b86759f49746564bf7238456be52.zip |
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>
Diffstat (limited to 'lib/private')
-rw-r--r-- | lib/private/App/InfoParser.php | 2 | ||||
-rw-r--r-- | lib/private/AppFramework/DependencyInjection/DIContainer.php | 5 | ||||
-rw-r--r-- | lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php | 47 | ||||
-rw-r--r-- | lib/private/Settings/AuthorizedGroup.php | 50 | ||||
-rw-r--r-- | lib/private/Settings/AuthorizedGroupMapper.php | 125 | ||||
-rw-r--r-- | lib/private/Settings/Manager.php | 98 | ||||
-rw-r--r-- | lib/private/legacy/OC_App.php | 9 |
7 files changed, 308 insertions, 28 deletions
diff --git a/lib/private/App/InfoParser.php b/lib/private/App/InfoParser.php index b9ca2d22c1c..9d57ef95688 100644 --- a/lib/private/App/InfoParser.php +++ b/lib/private/App/InfoParser.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)); diff --git a/lib/private/AppFramework/DependencyInjection/DIContainer.php b/lib/private/AppFramework/DependencyInjection/DIContainer.php index 89d59a471a8..293b9e47b25 100644 --- a/lib/private/AppFramework/DependencyInjection/DIContainer.php +++ b/lib/private/AppFramework/DependencyInjection/DIContainer.php @@ -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( diff --git a/lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php b/lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php index bd751183604..d162bb54108 100644 --- a/lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php +++ b/lib/private/AppFramework/Middleware/Security/SecurityMiddleware.php @@ -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() diff --git a/lib/private/Settings/AuthorizedGroup.php b/lib/private/Settings/AuthorizedGroup.php new file mode 100644 index 00000000000..d549e3d48f3 --- /dev/null +++ b/lib/private/Settings/AuthorizedGroup.php @@ -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 + ]; + } +} diff --git a/lib/private/Settings/AuthorizedGroupMapper.php b/lib/private/Settings/AuthorizedGroupMapper.php new file mode 100644 index 00000000000..4313ce60580 --- /dev/null +++ b/lib/private/Settings/AuthorizedGroupMapper.php @@ -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(); + } +} diff --git a/lib/private/Settings/Manager.php b/lib/private/Settings/Manager.php index d6b4ce7c080..6c567204253 100644 --- a/lib/private/Settings/Manager.php +++ b/lib/private/Settings/Manager.php @@ -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; + } } diff --git a/lib/private/legacy/OC_App.php b/lib/private/legacy/OC_App.php index bca0a3dd08e..811703570a2 100644 --- a/lib/private/legacy/OC_App.php +++ b/lib/private/legacy/OC_App.php @@ -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); } } |