From 69c5f8f4211fa14a945399e0cfaefc4307d8256e Mon Sep 17 00:00:00 2001 From: Maximilian Ruta Date: Mon, 5 Aug 2019 14:30:09 +0200 Subject: [PATCH 01/10] feat(groups) Add group backend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ...to separate SAML groups from system/other groups Includes - use of DB migrations and repair steps - specific exceptions - owned database group table - firing of group related events - migration logic of already existing groups on local backend where applicable - group id collission detection and handling - map original gid by saml and avoid collissions - append SAML prefix to gid - create new SAML group on collision - cleanup logic: delete SAML groups without members - Ensure admin cannot unassign SAML groups/members - Append SAML_ prefix to groups - Settings - show default group prefix - order group related settings next to each other - allow local group modifications in some cases - must not be about admin group - must be about local group backend - must be in allow migration list - must not have users from mixed backends Signed-off-by: Arthur Schiwon Signed-off-by: Giuliano Mele Signed-off-by: Jonathan Treffler Signed-off-by: Julius Härtl Signed-off-by: Maximilian Ruta Co-authored-by: Carl Schwan Co-authored-by: Giuliano Mele Co-authored-by: Jonathan Treffler --- appinfo/app.php | 34 +- appinfo/info.xml | 5 + lib/Exceptions/GroupNotFoundException.php | 30 ++ .../NonMigratableGroupException.php | 30 ++ lib/GroupBackend.php | 300 +++++++++++++++++ lib/GroupManager.php | 301 ++++++++++++++++++ lib/Jobs/MigrateGroups.php | 172 ++++++++++ ...emberLocalGroupsForPotentialMigrations.php | 100 ++++++ .../Version6000Date20220912152700.php | 90 ++++++ lib/SAMLSettings.php | 14 +- lib/Settings/Admin.php | 14 +- lib/UserBackend.php | 26 +- tests/integration/features/Shibboleth.feature | 8 +- .../features/bootstrap/FeatureContext.php | 61 +++- tests/psalm-baseline.xml | 5 + tests/stub.phpstub | 22 ++ tests/unit/GroupBackendTest.php | 152 +++++++++ tests/unit/GroupManagerTest.php | 282 ++++++++++++++++ tests/unit/Settings/AdminTest.php | 5 + tests/unit/UserBackendTest.php | 53 +-- 20 files changed, 1617 insertions(+), 87 deletions(-) create mode 100644 lib/Exceptions/GroupNotFoundException.php create mode 100644 lib/Exceptions/NonMigratableGroupException.php create mode 100644 lib/GroupBackend.php create mode 100644 lib/GroupManager.php create mode 100644 lib/Jobs/MigrateGroups.php create mode 100644 lib/Migration/RememberLocalGroupsForPotentialMigrations.php create mode 100644 lib/Migration/Version6000Date20220912152700.php create mode 100644 tests/unit/GroupBackendTest.php create mode 100644 tests/unit/GroupManagerTest.php diff --git a/appinfo/app.php b/appinfo/app.php index 9001efa13..e86e8afb6 100644 --- a/appinfo/app.php +++ b/appinfo/app.php @@ -19,9 +19,19 @@ * */ +use OCA\User_SAML\GroupBackend; +use OCA\User_SAML\GroupManager; +use OCA\User_SAML\SAMLSettings; +use OCP\BackgroundJob\IJobList; +use OCP\IConfig; +use OCP\IDBConnection; +use OCP\IGroupManager; +use Psr\Container\ContainerInterface; +use Psr\Log\LoggerInterface; + require_once __DIR__ . '/../3rdparty/vendor/autoload.php'; -// If we run in CLI mode do not setup the app as it can fail the OCC execution +// If we run in CLI mode do not set up the app as it can fail the OCC execution // since the URLGenerator isn't accessible. $cli = false; if (OC::$CLI) { @@ -35,12 +45,26 @@ $userSession = \OC::$server->getUserSession(); $session = \OC::$server->getSession(); } catch (Throwable $e) { - $logger = \OCP\Server::get(\Psr\Log\LoggerInterface::class); + $logger = \OCP\Server::get(LoggerInterface::class); $logger->critical($e->getMessage(), ['exception' => $e, 'app' => 'user_saml']); return; } -$samlSettings = \OC::$server->query(\OCA\User_SAML\SAMLSettings::class); +$groupBackend = \OC::$server->get(GroupBackend::class); +\OC::$server->get(IGroupManager::class)->addBackend($groupBackend); + +$samlSettings = \OC::$server->get(SAMLSettings::class); + +\OC::$server->registerService(GroupManager::class, function (ContainerInterface $c) use ($groupBackend, $samlSettings) { + return new GroupManager( + $c->get(IDBConnection::class), + $c->get(IGroupManager::class), + $groupBackend, + $c->get(IConfig::class), + $c->get(IJobList::class), + $samlSettings, + ); +}); $userData = new \OCA\User_SAML\UserData( new \OCA\User_SAML\UserResolver(\OC::$server->getUserManager()), @@ -53,9 +77,9 @@ \OC::$server->getSession(), \OC::$server->getDatabaseConnection(), \OC::$server->getUserManager(), - \OC::$server->getGroupManager(), + \OC::$server->get(GroupManager::class), $samlSettings, - \OCP\Server::get(\Psr\Log\LoggerInterface::class), + \OCP\Server::get(LoggerInterface::class), $userData, \OC::$server->query(\OCP\EventDispatcher\IEventDispatcher::class), ); diff --git a/appinfo/info.xml b/appinfo/info.xml index 205a6a615..16944ee15 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -35,6 +35,11 @@ While theoretically any other authentication provider implementing either one of + + + OCA\User_SAML\Migration\RememberLocalGroupsForPotentialMigrations + + OCA\User_SAML\Command\ConfigCreate OCA\User_SAML\Command\ConfigDelete diff --git a/lib/Exceptions/GroupNotFoundException.php b/lib/Exceptions/GroupNotFoundException.php new file mode 100644 index 000000000..ad8c52a49 --- /dev/null +++ b/lib/Exceptions/GroupNotFoundException.php @@ -0,0 +1,30 @@ + + * + * @author Arthur Schiwon + * + * @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 . + * + */ + +namespace OCA\User_SAML\Exceptions; + +class GroupNotFoundException extends \RuntimeException { +} diff --git a/lib/Exceptions/NonMigratableGroupException.php b/lib/Exceptions/NonMigratableGroupException.php new file mode 100644 index 000000000..5b9cd9348 --- /dev/null +++ b/lib/Exceptions/NonMigratableGroupException.php @@ -0,0 +1,30 @@ + + * + * @author Arthur Schiwon + * + * @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 . + * + */ + +namespace OCA\User_SAML\Exceptions; + +class NonMigratableGroupException extends \RuntimeException { +} diff --git a/lib/GroupBackend.php b/lib/GroupBackend.php new file mode 100644 index 000000000..c3a16acbd --- /dev/null +++ b/lib/GroupBackend.php @@ -0,0 +1,300 @@ + + * + * @author Dominik Ach + * @author Arthur Schiwon + * @author Carl Schwan + * @author Maximilian Ruta + * @author Jonathan Treffler + * @author Giuliano Mele + * + * @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 . + * + */ + +namespace OCA\User_SAML; + +use OCP\DB\Exception; +use OCP\Group\Backend\ABackend; +use OCP\Group\Backend\IAddToGroupBackend; +use OCP\Group\Backend\ICountUsersBackend; +use OCP\Group\Backend\ICreateGroupBackend; +use OCP\Group\Backend\IDeleteGroupBackend; +use OCP\Group\Backend\INamedBackend; +use OCP\Group\Backend\IRemoveFromGroupBackend; +use OCP\IDBConnection; + +class GroupBackend extends ABackend implements IAddToGroupBackend, ICountUsersBackend, ICreateGroupBackend, IDeleteGroupBackend, IRemoveFromGroupBackend, INamedBackend { + /** @var IDBConnection */ + private $dbc; + + /** @var array */ + private $groupCache = []; + + public const TABLE_GROUPS = 'user_saml_groups'; + public const TABLE_MEMBERS = 'user_saml_group_members'; + + public function __construct(IDBConnection $dbc) { + $this->dbc = $dbc; + } + + public function inGroup($uid, $gid): bool { + $qb = $this->dbc->getQueryBuilder(); + $stmt = $qb->select('gid') + ->from(self::TABLE_MEMBERS) + ->where($qb->expr()->eq('uid', $qb->createNamedParameter($uid))) + ->andWhere($qb->expr()->eq('gid', $qb->createNamedParameter($gid))) + ->setMaxResults(1) + ->executeQuery(); + + $result = count($stmt->fetchAll()) > 0; + $stmt->closeCursor(); + return $result; + } + + /** + * @return string[] Group names + */ + public function getUserGroups($uid): array { + $qb = $this->dbc->getQueryBuilder(); + $cursor = $qb->select('gid') + ->from(self::TABLE_MEMBERS) + ->where($qb->expr()->eq('uid', $qb->createNamedParameter($uid))) + ->executeQuery(); + + $groups = []; + while ($row = $cursor->fetch()) { + $groups[] = $row['gid']; + $this->groupCache[$row['gid']] = $row['gid']; + } + $cursor->closeCursor(); + + return $groups; + } + + /** + * @return string[] Group names + */ + public function getGroups($search = '', $limit = null, $offset = null): array { + $query = $this->dbc->getQueryBuilder(); + $query->select('gid') + ->from(self::TABLE_GROUPS) + ->orderBy('gid', 'ASC'); + + if ($search !== '') { + $query->where($query->expr()->iLike('gid', $query->createNamedParameter( + '%' . $this->dbc->escapeLikeParameter($search) . '%' + ))); + } + + if ((int)$limit > 0) { + $query->setMaxResults((int)$limit); + } + if ((int)$offset > 0) { + $query->setFirstResult((int)$offset); + } + $result = $query->executeQuery(); + + $groups = []; + while ($row = $result->fetch()) { + $groups[] = $row['gid']; + } + $result->closeCursor(); + + return $groups; + } + + /** + * @param string $gid + * @return bool + */ + public function groupExists($gid): bool { + if (isset($this->groupCache[$gid])) { + return true; + } + + $qb = $this->dbc->getQueryBuilder(); + $cursor = $qb->select('gid') + ->from(self::TABLE_GROUPS) + ->where($qb->expr()->eq('gid', $qb->createNamedParameter($gid))) + ->executeQuery(); + $result = $cursor->fetch(); + $cursor->closeCursor(); + + if ($result !== false) { + $this->groupCache[$gid] = $gid; + return true; + } + return false; + } + + public function groupExistsWithDifferentGid(string $samlGid): ?string { + $qb = $this->dbc->getQueryBuilder(); + $cursor = $qb->select('gid') + ->from(self::TABLE_GROUPS) + ->where($qb->expr()->eq('saml_gid', $qb->createNamedParameter($samlGid))) + ->executeQuery(); + $result = $cursor->fetch(\PDO::FETCH_NUM); + $cursor->closeCursor(); + + if ($result !== false) { + return $result[0]; + } + return null; + } + + /** + * @param string $gid + * @param string $search + * @param int $limit + * @param int $offset + * @return string[] User ids + */ + public function usersInGroup($gid, $search = '', $limit = -1, $offset = 0): array { + $query = $this->dbc->getQueryBuilder(); + $query->select('uid') + ->from(self::TABLE_MEMBERS) + ->where($query->expr()->eq('gid', $query->createNamedParameter($gid))) + ->orderBy('uid', 'ASC'); + + if ($search !== '') { + $query->andWhere($query->expr()->like('uid', $query->createNamedParameter( + '%' . $this->dbc->escapeLikeParameter($search) . '%' + ))); + } + + if ($limit !== -1) { + $query->setMaxResults($limit); + } + if ($offset !== 0) { + $query->setFirstResult($offset); + } + + $result = $query->executeQuery(); + + $users = []; + while ($row = $result->fetch()) { + $users[] = $row['uid']; + } + $result->closeCursor(); + + return $users; + } + + public function createGroup(string $gid, string $samlGid = null): bool { + try { + // Add group + $builder = $this->dbc->getQueryBuilder(); + $samlGid = $samlGid ?? $gid; + $result = $builder->insert(self::TABLE_GROUPS) + ->setValue('gid', $builder->createNamedParameter($gid)) + ->setValue('displayname', $builder->createNamedParameter($samlGid)) + ->setValue('saml_gid', $builder->createNamedParameter($samlGid)) + ->executeStatement(); + } catch (Exception $e) { + if ($e->getReason() === \OCP\DB\Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) { + $result = 0; + } + } + + // Add to cache + $this->groupCache[$gid] = $gid; + + return $result === 1; + } + + /** + * @throws Exception + */ + public function addToGroup(string $uid, string $gid): bool { + if ($this->inGroup($uid, $gid)) { + return true; + } + + $qb = $this->dbc->getQueryBuilder(); + $qb->insert(self::TABLE_MEMBERS) + ->setValue('uid', $qb->createNamedParameter($uid)) + ->setValue('gid', $qb->createNamedParameter($gid)) + ->executeStatement(); + return true; + } + + public function removeFromGroup(string $uid, string $gid): bool { + $qb = $this->dbc->getQueryBuilder(); + $rows = $qb->delete(self::TABLE_MEMBERS) + ->where($qb->expr()->eq('uid', $qb->createNamedParameter($uid))) + ->andWhere($qb->expr()->eq('gid', $qb->createNamedParameter($gid))) + ->executeStatement(); + + return $rows > 0; + } + + public function countUsersInGroup(string $gid, string $search = ''): int { + $query = $this->dbc->getQueryBuilder(); + $query->select($query->func()->count('*', 'num_users')) + ->from(self::TABLE_MEMBERS) + ->where($query->expr()->eq('gid', $query->createNamedParameter($gid))); + + if ($search !== '') { + $query->andWhere($query->expr()->like('uid', $query->createNamedParameter( + '%' . $this->dbc->escapeLikeParameter($search) . '%' + ))); + } + + $result = $query->executeQuery(); + $count = $result->fetchOne(); + $result->closeCursor(); + + if ($count !== false) { + $count = (int)$count; + } else { + $count = 0; + } + + return $count; + } + + public function deleteGroup(string $gid): bool { + $query = $this->dbc->getQueryBuilder(); + + try { + $this->dbc->beginTransaction(); + + // delete the group + $query->delete(self::TABLE_GROUPS) + ->where($query->expr()->eq('gid', $query->createNamedParameter($gid))) + ->executeStatement(); + + // delete group user relation + $query->delete(self::TABLE_MEMBERS) + ->where($query->expr()->eq('gid', $query->createNamedParameter($gid))) + ->executeStatement(); + + $this->dbc->commit(); + unset($this->groupCache[$gid]); + } catch (\Throwable $t) { + $this->dbc->rollBack(); + throw $t; + } + + return true; + } + + public function getBackendName(): string { + return 'user_saml'; + } +} diff --git a/lib/GroupManager.php b/lib/GroupManager.php new file mode 100644 index 000000000..dc17bb3da --- /dev/null +++ b/lib/GroupManager.php @@ -0,0 +1,301 @@ + + * + * @author Dominik Ach + * @author Arthur Schiwon + * @author Carl Schwan + * @author Maximilian Ruta + * @author Jonathan Treffler + * @author Giuliano Mele + * + * @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 . + * + */ + +namespace OCA\User_SAML; + +use OC\Hooks\PublicEmitter; +use OCA\User_SAML\Exceptions\GroupNotFoundException; +use OCA\User_SAML\Exceptions\NonMigratableGroupException; +use OCA\User_SAML\Jobs\MigrateGroups; +use OCP\BackgroundJob\IJobList; +use OCP\IConfig; +use OCP\IDBConnection; +use OCP\IGroup; +use OCP\IGroupManager; +use OCP\IUser; + +class GroupManager { + public const LOCAL_GROUPS_CHECK_FOR_MIGRATION = 'localGroupsCheckForMigration'; + + /** + * @var IDBConnection $db + */ + protected $db; + + /** @var IGroupManager */ + private $groupManager; + /** @var GroupBackend */ + private $ownGroupBackend; + /** @var IConfig */ + private $config; + /** @var IJobList */ + private $jobList; + /** @var SAMLSettings */ + private $settings; + + + public function __construct( + IDBConnection $db, + IGroupManager $groupManager, + GroupBackend $ownGroupBackend, + IConfig $config, + IJobList $jobList, + SAMLSettings $settings + ) { + $this->db = $db; + $this->groupManager = $groupManager; + $this->ownGroupBackend = $ownGroupBackend; + $this->config = $config; + $this->jobList = $jobList; + $this->settings = $settings; + } + + /** + * @param string[] $samlGroupNames + * @param IGroup[] $assignedGroups + * @return string[] + */ + private function getGroupsToRemove(array $samlGroupNames, array $assignedGroups): array { + $groupsToRemove = []; + // FIXME: Seems unused + $this->config->getAppValue('user_saml', self::LOCAL_GROUPS_CHECK_FOR_MIGRATION, ''); + foreach ($assignedGroups as $group) { + // if group is not supplied by SAML and group has SAML backend + if (!in_array($group->getGID(), $samlGroupNames) && $this->hasSamlBackend($group)) { + $groupsToRemove[] = $group->getGID(); + } elseif ($this->mayModifyGroup($group)) { + $groupsToRemove[] = $group->getGID(); + } + } + return $groupsToRemove; + } + + /** + * @param string[] $samlGroupNames + * @param string[] $assignedGroupIds + * @return string[] + */ + private function getGroupsToAdd(array $samlGroupNames, array $assignedGroupIds): array { + $groupsToAdd = []; + foreach ($samlGroupNames as $groupName) { + $group = $this->groupManager->get($groupName); + // if user is not assigned to the group or the provided group has a non SAML backend + if (!in_array($groupName, $assignedGroupIds) || !$this->hasSamlBackend($group)) { + $groupsToAdd[] = $groupName; + } elseif ($this->mayModifyGroup($group)) { + $groupsToAdd[] = $group->getGID(); + } + } + return $groupsToAdd; + } + + public function handleIncomingGroups(IUser $user, array $samlGroupNames): void { + $this->updateUserGroups($user, $samlGroupNames); + // TODO: drop following line with dropping NC 28 support + $this->evaluateGroupMigrations($samlGroupNames); + } + + public function updateUserGroups(IUser $user, array $samlGroupNames): void { + $this->translateGroupToIds($samlGroupNames); + $assignedGroups = $this->groupManager->getUserGroups($user); + $assignedGroupIds = array_map(function (IGroup $group) { + return $group->getGID(); + }, $assignedGroups); + $groupsToRemove = $this->getGroupsToRemove($samlGroupNames, $assignedGroups); + $groupsToAdd = $this->getGroupsToAdd($samlGroupNames, $assignedGroupIds); + $this->handleUserUnassignedFromGroups($user, $groupsToRemove); + $this->handleUserAssignedToGroups($user, $groupsToAdd); + } + + protected function translateGroupToIds(array &$samlGroups): void { + array_walk($samlGroups, function (&$gid) { + $altGid = $this->ownGroupBackend->groupExistsWithDifferentGid($gid); + if ($altGid !== null) { + $gid = $altGid; + } + }); + } + + protected function handleUserUnassignedFromGroups(IUser $user, array $groupIds): void { + foreach ($groupIds as $gid) { + $this->unassignUserFromGroup($user, $gid); + } + } + + protected function unassignUserFromGroup(IUser $user, string $gid): void { + $group = $this->groupManager->get($gid); + if ($group === null) { + return; + } + + if ($this->hasSamlBackend($group)) { + $this->ownGroupBackend->removeFromGroup($user->getUID(), $group->getGID()); + if ($this->ownGroupBackend->countUsersInGroup($gid) === 0) { + /** @psalm-suppress UndefinedInterfaceMethod */ + $this->groupManager->emit('\OC\Group', 'preDelete', [$group]); + $this->ownGroupBackend->deleteGroup($group->getGID()); + /** @psalm-suppress UndefinedInterfaceMethod */ + $this->groupManager->emit('\OC\Group', 'postDelete', [$group]); + } + } else { + $group->removeUser($user); + } + } + + protected function handleUserAssignedToGroups(IUser $user, $groupIds): void { + foreach ($groupIds as $gid) { + $this->assignUserToGroup($user, $gid); + } + } + + protected function assignUserToGroup(IUser $user, string $gid): void { + try { + $group = $this->findGroup($gid); + } catch (GroupNotFoundException $e) { + $group = $this->createGroupInBackend($gid); + } catch (NonMigratableGroupException $e) { + $providerId = $this->settings->getProviderId(); + $settings = $this->settings->get($providerId); + $groupPrefix = $settings['saml-attribute-mapping-group_mapping_prefix'] ?? SAMLSettings::DEFAULT_GROUP_PREFIX; + $group = $this->createGroupInBackend($groupPrefix . $gid, $gid); + } + + $group->addUser($user); + } + + protected function createGroupInBackend(string $gid, ?string $originalGid = null): ?IGroup { + if ($this->groupManager instanceof PublicEmitter) { + $this->groupManager->emit('\OC\Group', 'preCreate', array($gid)); + } + if (!$this->ownGroupBackend->createGroup($gid, $originalGid ?? $gid)) { + return null; + } + + $group = $this->groupManager->get($gid); + if ($this->groupManager instanceof PublicEmitter) { + $this->groupManager->emit('\OC\Group', 'postCreate', array($group)); + } + + return $group; + } + + /** + * @throws GroupNotFoundException|NonMigratableGroupException + */ + protected function findGroup(string $gid): IGroup { + $migrationAllowList = $this->config->getAppValue( + 'user_saml', + GroupManager::LOCAL_GROUPS_CHECK_FOR_MIGRATION, + '' + ); + $strictBackendCheck = '' === $migrationAllowList; + if ($migrationAllowList !== '') { + $migrationAllowList = \json_decode($migrationAllowList, true); + } + if (!$strictBackendCheck && in_array($gid, $migrationAllowList['groups'] ?? [], true)) { + $group = $this->groupManager->get($gid); + if ($group === null) { + throw new GroupNotFoundException(); + } + return $group; + } + $group = $this->groupManager->get($gid); + if ($group === null) { + throw new GroupNotFoundException(); + } + if ($this->hasSamlBackend($group)) { + return $group; + } + + $altGid = $this->ownGroupBackend->groupExistsWithDifferentGid($gid); + if ($altGid) { + $group = $this->groupManager->get($altGid); + if ($group) { + return $group; + } + throw new GroupNotFoundException(); + } + + throw new NonMigratableGroupException(); + } + + protected function hasSamlBackend(IGroup $group): bool { + return in_array('user_saml', $group->getBackendNames()); + } + + public function evaluateGroupMigrations(array $groups): void { + $candidateInfo = $this->config->getAppValue('user_saml', self::LOCAL_GROUPS_CHECK_FOR_MIGRATION, ''); + if ($candidateInfo === '') { + return; + } + $candidateInfo = \json_decode($candidateInfo, true); + if (!isset($candidateInfo['dropAfter']) || $candidateInfo['dropAfter'] < time()) { + $this->config->deleteAppValue('user_saml', self::LOCAL_GROUPS_CHECK_FOR_MIGRATION); + return; + } + + $this->jobList->add(MigrateGroups::class, ['gids' => $groups]); + } + + protected function isGroupInTransitionList(string $groupId): bool { + $candidateInfo = $this->config->getAppValue('user_saml', self::LOCAL_GROUPS_CHECK_FOR_MIGRATION, ''); + if ($candidateInfo === '') { + return false; + } + $candidateInfo = \json_decode($candidateInfo, true); + if (!isset($candidateInfo['dropAfter']) || $candidateInfo['dropAfter'] < time()) { + $this->config->deleteAppValue('user_saml', self::LOCAL_GROUPS_CHECK_FOR_MIGRATION); + return false; + } + + return in_array($groupId, $candidateInfo['groups']); + } + + protected function hasGroupForeignMembers(IGroup $group): bool { + foreach ($group->getUsers() as $user) { + if ($user->getBackendClassName() !== 'user_saml') { + return true; + } + } + return false; + } + + /** + * In the transition phase, update group memberships of local groups only + * under very specific conditions. Otherwise, membership modifications are + * allowed only for groups owned by the SAML backend. + */ + protected function mayModifyGroup(?IGroup $group): bool { + return + $group !== null + && $group->getGID() !== 'admin' + && in_array('Database', $group->getBackendNames()) + && $this->isGroupInTransitionList($group->getGID()) + && !$this->hasGroupForeignMembers($group); + } +} diff --git a/lib/Jobs/MigrateGroups.php b/lib/Jobs/MigrateGroups.php new file mode 100644 index 000000000..576c5deb1 --- /dev/null +++ b/lib/Jobs/MigrateGroups.php @@ -0,0 +1,172 @@ + + * + * @author Arthur Schiwon + * + * @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 . + * + */ + +namespace OCA\User_SAML\Jobs; + +use OCA\User_SAML\GroupBackend; +use OCA\User_SAML\GroupManager; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\QueuedJob; +use OCP\IConfig; +use OCP\IDBConnection; +use OCP\IGroupManager; +use Psr\Log\LoggerInterface; + +/** + * Class MigrateGroups + * + * @package OCA\User_SAML\Jobs + * @todo: remove this, when dropping Nextcloud 29 support + */ +class MigrateGroups extends QueuedJob { + + /** @var IConfig */ + private $config; + /** @var IGroupManager */ + private $groupManager; + /** @var IDBConnection */ + private $dbc; + /** @var GroupBackend */ + private $ownGroupBackend; + /** @var LoggerInterface */ + private $logger; + + public function __construct( + IConfig $config, + IGroupManager $groupManager, + IDBConnection $dbc, + GroupBackend $ownGroupBackend, + LoggerInterface $logger, + ITimeFactory $timeFactory + ) { + parent::__construct($timeFactory); + $this->config = $config; + $this->groupManager = $groupManager; + $this->dbc = $dbc; + $this->ownGroupBackend = $ownGroupBackend; + $this->logger = $logger; + } + + protected function run($argument) { + try { + $candidates = $this->getMigratableGroups(); + $toMigrate = $this->getGroupsToMigrate($argument['gids'], $candidates); + $migrated = $this->migrateGroups($toMigrate); + $this->updateCandidatePool($migrated); + } catch (\RuntimeException $e) { + return; + } + } + + protected function updateCandidatePool($migrateGroups) { + $candidateInfo = $this->config->getAppValue('user_saml', GroupManager::LOCAL_GROUPS_CHECK_FOR_MIGRATION, ''); + if ($candidateInfo === null || $candidateInfo === '') { + return; + } + $candidateInfo = \json_decode($candidateInfo, true); + if (!isset($candidateInfo['dropAfter']) || !isset($candidateInfo['groups'])) { + return; + } + $candidateInfo['groups'] = array_diff($candidateInfo['groups'], $migrateGroups); + $this->config->setAppValue( + 'user_saml', + GroupManager::LOCAL_GROUPS_CHECK_FOR_MIGRATION, + json_encode($candidateInfo) + ); + } + + protected function migrateGroups(array $toMigrate): array { + return array_filter($toMigrate, function ($gid) { + return $this->migrateGroup($gid); + }); + } + + protected function migrateGroup(string $gid): bool { + try { + $this->dbc->beginTransaction(); + + $qb = $this->dbc->getQueryBuilder(); + $affected = $qb->delete('groups') + ->where($qb->expr()->eq('gid', $qb->createNamedParameter($gid))) + ->executeStatement(); + if ($affected === 0) { + throw new \RuntimeException('Could not delete group from local backend'); + } + + if (!$this->ownGroupBackend->createGroup($gid)) { + throw new \RuntimeException('Could not create group in SAML backend'); + } + + $this->dbc->commit(); + + return true; + } catch (\Exception $e) { + $this->dbc->rollBack(); + $this->logger->warning($e->getMessage(), ['app' => 'user_saml', 'exception' => $e]); + } + + return false; + } + + protected function getGroupsToMigrate(array $samlGroups, array $pool): array { + return array_filter($samlGroups, function (string $gid) use ($pool) { + if (!in_array($gid, $pool)) { + return false; + } + + $group = $this->groupManager->get($gid); + if ($group === null) { + return false; + } + + $backendNames = $group->getBackendNames(); + if (!in_array('Database', $backendNames, true)) { + return false; + } + + foreach ($group->getUsers() as $user) { + if ($user->getBackendClassName() !== 'user_saml') { + return false; + } + } + + return true; + }); + } + + protected function getMigratableGroups(): array { + $candidateInfo = $this->config->getAppValue('user_saml', GroupManager::LOCAL_GROUPS_CHECK_FOR_MIGRATION, ''); + if ($candidateInfo === null || $candidateInfo === '') { + throw new \RuntimeException('No migration of groups to SAML backend anymore'); + } + $candidateInfo = \json_decode($candidateInfo, true); + if (!isset($candidateInfo['dropAfter']) || !isset($candidateInfo['groups']) || $candidateInfo['dropAfter'] < time()) { + $this->config->deleteAppValue('user_saml', GroupManager::LOCAL_GROUPS_CHECK_FOR_MIGRATION); + throw new \RuntimeException('Period for migration groups is over'); + } + + return $candidateInfo['groups']; + } +} diff --git a/lib/Migration/RememberLocalGroupsForPotentialMigrations.php b/lib/Migration/RememberLocalGroupsForPotentialMigrations.php new file mode 100644 index 000000000..c05c1a544 --- /dev/null +++ b/lib/Migration/RememberLocalGroupsForPotentialMigrations.php @@ -0,0 +1,100 @@ + + * + * @author Arthur Schiwon + * + * @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 . + * + */ + +namespace OCA\User_SAML\Migration; + +use OC\Group\Database; +use OCA\User_SAML\GroupManager; +use OCP\IConfig; +use OCP\IGroupManager; +use OCP\Migration\IOutput; +use OCP\Migration\IRepairStep; +use UnexpectedValueException; +use function json_encode; + +class RememberLocalGroupsForPotentialMigrations implements IRepairStep { + + /** @var IGroupManager */ + private $groupManager; + /** @var IConfig */ + private $config; + + public function __construct( + IGroupManager $groupManager, + IConfig $config + ) { + $this->groupManager = $groupManager; + $this->config = $config; + } + + public function getName(): string { + return 'Remember local groups that might belong to SAML'; + } + + /** + * Run repair step. + * Must throw exception on error. + * + * @param IOutput $output + * @throws \Exception in case of failure + * @since 9.1.0 + */ + public function run(IOutput $output) { + try { + $backend = $this->findBackend(); + $groupIds = $this->findGroupIds($backend); + } catch (UnexpectedValueException $e) { + return; + } + + $this->config->setAppValue( + 'user_saml', + GroupManager::LOCAL_GROUPS_CHECK_FOR_MIGRATION, + json_encode([ + 'dropAfter' => time() + 86400 * 60, // 60 days + 'groups' => $groupIds + ]) + ); + } + + protected function findGroupIds(Database $backend): array { + $groupIds = $backend->getGroups(); + $adminGroupIndex = array_search('admin', $groupIds, true); + if ($adminGroupIndex !== false) { + unset($groupIds[$adminGroupIndex]); + } + return $groupIds; + } + + protected function findBackend(): Database { + $groupBackends = $this->groupManager->getBackends(); + foreach ($groupBackends as $backend) { + if ($backend instanceof Database) { + return $backend; + } + } + throw new UnexpectedValueException(); + } +} diff --git a/lib/Migration/Version6000Date20220912152700.php b/lib/Migration/Version6000Date20220912152700.php new file mode 100644 index 000000000..923e5a15e --- /dev/null +++ b/lib/Migration/Version6000Date20220912152700.php @@ -0,0 +1,90 @@ + + * + * @author Dominik Ach + * @author Arthur Schiwon + * @author Carl Schwan + * @author Maximilian Ruta + * @author Jonathan Treffler + * @author Giuliano Mele + * + * @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 . + * + */ + +namespace OCA\User_SAML\Migration; + +use Closure; +use OCP\DB\ISchemaWrapper; +use OCP\DB\Types; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +/** + * Auto-generated migration step: Please modify to your needs! + */ +class Version6000Date20220912152700 extends SimpleMigrationStep { + + /** + * @return null|ISchemaWrapper + */ + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options) { + /** @var ISchemaWrapper $schema */ + $schema = $schemaClosure(); + + if (!$schema->hasTable('user_saml_groups')) { + $table = $schema->createTable('user_saml_groups'); + $table->addColumn('gid', Types::STRING, [ + 'notnull' => true, + 'length' => 64, + 'default' => '', + ]); + $table->addColumn('displayname', Types::STRING, [ + 'notnull' => true, + 'length' => 255, + 'default' => '', + ]); + $table->addColumn('saml_gid', Types::STRING, [ + 'notnull' => true, + 'length' => 64, + 'default' => '', + ]); + $table->setPrimaryKey(['gid']); + $table->addUniqueIndex(['saml_gid']); + } + + if (!$schema->hasTable('user_saml_group_members')) { + $table = $schema->createTable('user_saml_group_members'); + $table->addColumn('uid', Types::STRING, [ + 'notnull' => true, + 'length' => 64, + 'default' => '', + ]); + $table->addColumn('gid', Types::STRING, [ + 'notnull' => true, + 'length' => 64, + 'default' => '', + ]); + $table->setPrimaryKey(['gid', 'uid'], 'idx_group_members'); + $table->addIndex(['gid']); + $table->addIndex(['uid']); + } + return $schema; + } +} diff --git a/lib/SAMLSettings.php b/lib/SAMLSettings.php index 80aaf0b00..b822dd627 100644 --- a/lib/SAMLSettings.php +++ b/lib/SAMLSettings.php @@ -25,7 +25,6 @@ use OCA\User_SAML\Db\ConfigurationsMapper; use OCP\DB\Exception; use OCP\IConfig; -use OCP\IRequest; use OCP\ISession; use OCP\IURLGenerator; use OneLogin\Saml2\Constants; @@ -35,6 +34,10 @@ class SAMLSettings { private const LOADED_CHOSEN = 1; private const LOADED_ALL = 2; + // list of global settings which are valid for every idp: + // 'general-require_provisioned_account', 'general-allow_multiple_user_back_ends', 'general-use_saml_auth_for_desktop' + + // IdP-specific keys public const IDP_CONFIG_KEYS = [ 'general-idp0_display_name', 'general-uid_mapping', @@ -66,6 +69,7 @@ class SAMLSettings { 'saml-attribute-mapping-home_mapping', 'saml-attribute-mapping-quota_mapping', 'saml-attribute-mapping-mfa_mapping', + 'saml-attribute-mapping-group_mapping_prefix', 'saml-user-filter-reject_groups', 'saml-user-filter-require_groups', 'sp-x509cert', @@ -73,6 +77,8 @@ class SAMLSettings { 'sp-privateKey', ]; + public const DEFAULT_GROUP_PREFIX = 'SAML_'; + /** @var IURLGenerator */ private $urlGenerator; /** @var IConfig */ @@ -86,12 +92,6 @@ class SAMLSettings { /** @var ConfigurationsMapper */ private $mapper; - /** - * @param IURLGenerator $urlGenerator - * @param IConfig $config - * @param IRequest $request - * @param ISession $session - */ public function __construct( IURLGenerator $urlGenerator, IConfig $config, diff --git a/lib/Settings/Admin.php b/lib/Settings/Admin.php index ad4d7d519..ed2d5ee8a 100644 --- a/lib/Settings/Admin.php +++ b/lib/Settings/Admin.php @@ -120,13 +120,13 @@ public function getForm() { 'type' => 'line', 'required' => false, ], - 'group_mapping' => [ - 'text' => $this->l10n->t('Attribute to map the users groups to.'), + 'home_mapping' => [ + 'text' => $this->l10n->t('Attribute to map the users home to.'), 'type' => 'line', 'required' => true, ], - 'home_mapping' => [ - 'text' => $this->l10n->t('Attribute to map the users home to.'), + 'group_mapping' => [ + 'text' => $this->l10n->t('Attribute to map the users groups to.'), 'type' => 'line', 'required' => true, ], @@ -135,7 +135,11 @@ public function getForm() { 'type' => 'line', 'required' => false, ], - + 'group_mapping_prefix' => [ + 'text' => $this->l10n->t('Group Mapping Prefix, default: %s', [SAMLSettings::DEFAULT_GROUP_PREFIX]), + 'type' => 'line', + 'required' => true, + ], ]; $userFilterSettings = [ diff --git a/lib/UserBackend.php b/lib/UserBackend.php index 55da44ccb..0872260ce 100644 --- a/lib/UserBackend.php +++ b/lib/UserBackend.php @@ -27,7 +27,6 @@ use OCP\Files\NotPermittedException; use OCP\IConfig; use OCP\IDBConnection; -use OCP\IGroupManager; use OCP\ISession; use OCP\IURLGenerator; use OCP\IUser; @@ -52,7 +51,7 @@ class UserBackend implements IApacheBackend, UserInterface, IUserBackend, IGetDi private $db; /** @var IUserManager */ private $userManager; - /** @var IGroupManager */ + /** @var GroupManager */ private $groupManager; /** @var \OCP\UserInterface[] */ private static $backends = []; @@ -71,7 +70,7 @@ public function __construct( ISession $session, IDBConnection $db, IUserManager $userManager, - IGroupManager $groupManager, + GroupManager $groupManager, SAMLSettings $settings, LoggerInterface $logger, UserData $userData, @@ -647,29 +646,10 @@ public function updateAttributes(string $uid, array $attributes): void { $user->setQuota($newQuota); } - if ($newGroups !== null) { - $groupManager = $this->groupManager; - $oldGroups = $groupManager->getUserGroupIds($user); - - $groupsToAdd = array_unique(array_diff($newGroups, $oldGroups)); - $groupsToRemove = array_diff($oldGroups, $newGroups); - - foreach ($groupsToAdd as $group) { - if (!($groupManager->groupExists($group))) { - $groupManager->createGroup($group); - } - $groupManager->get($group)->addUser($user); - } - - foreach ($groupsToRemove as $group) { - $groupManager->get($group)->removeUser($user); - } - } + $this->groupManager->handleIncomingGroups($user, $newGroups ?? []); } } - - public function countUsers() { $query = $this->db->getQueryBuilder(); $query->select($query->func()->count('uid')) diff --git a/tests/integration/features/Shibboleth.feature b/tests/integration/features/Shibboleth.feature index 11c8e8d84..d03614f42 100644 --- a/tests/integration/features/Shibboleth.feature +++ b/tests/integration/features/Shibboleth.feature @@ -63,7 +63,7 @@ Feature: Shibboleth And The response should be a SAML redirect page that gets submitted And I should be redirected to "http://localhost:8080/index.php/apps/dashboard/" Then The user value "id" should be "student1" - Then The user value "email" should be "" + And The user value "email" should be "" And The user value "display-name" should be "Default displayname of student1" And The last login timestamp of "student1" should not be empty @@ -80,6 +80,8 @@ Feature: Shibboleth And The setting "security-wantAssertionsSigned" is set to "1" And The setting "saml-attribute-mapping-email_mapping" is set to "urn:oid:0.9.2342.19200300.100.1.3" And The setting "saml-attribute-mapping-displayName_mapping" is set to "urn:oid:2.5.4.42 urn:oid:2.5.4.4" + And The setting "saml-attribute-mapping-quota_mapping" is set to "urn:oid:1.3.6.1.4.1.49213.1.1.2" + And The setting "saml-attribute-mapping-group_mapping" is set to "groups" When I send a GET request to "http://localhost:8080/index.php/login" Then I should be redirected to "https://localhost:4443/idp/profile/SAML2/Redirect/SSO" And I send a POST request to "https://localhost:4443/idp/profile/SAML2/Redirect/SSO?execution=e1s1" with the following data @@ -89,7 +91,11 @@ Feature: Shibboleth And I should be redirected to "http://localhost:8080/index.php/apps/dashboard/" And The user value "id" should be "student1" And The user value "email" should be "student1@idptestbed.edu" + And The user value "quota.total" should be "209715200" And The user value "display-name" should be "Stud Ent" + And The user value "groups" should be "Students, Astrophysics" + And the group "SAML_Astrophysics" should exists + And the group "SAML_Students" should exists And The last login timestamp of "student1" should not be empty Scenario: Authenticating using Shibboleth with SAML with custom redirect URL diff --git a/tests/integration/features/bootstrap/FeatureContext.php b/tests/integration/features/bootstrap/FeatureContext.php index 8cf332207..96dad4620 100644 --- a/tests/integration/features/bootstrap/FeatureContext.php +++ b/tests/integration/features/bootstrap/FeatureContext.php @@ -235,7 +235,7 @@ public function theResponseShouldBeASamlRedirectPageThatGetsSubmitted() { * @param string $value * @throws UnexpectedValueException */ - public function thUserValueShouldBe($key, $value) { + public function theUserValueShouldBe(string $key, string $value): void { $this->response = $this->client->request( 'GET', 'http://localhost:8080/ocs/v1.php/cloud/user', @@ -243,17 +243,47 @@ public function thUserValueShouldBe($key, $value) { 'headers' => [ 'OCS-APIRequest' => 'true', ], + 'query' => [ + 'format' => 'json', + ] ] ); - $xml = simplexml_load_string($this->response->getBody()); - /** @var array $responseArray */ - $responseArray = json_decode(json_encode((array)$xml), true); + $responseArray = (json_decode($this->response->getBody(), true))['ocs']; + + if (!isset($responseArray['data'][$key]) || count((array)$responseArray['data'][$key]) === 0) { + if (strpos($key, '.') !== false) { + // support nested arrays, specify the key seperated by "."s, e.g. quota.total + $keys = explode('.', $key); + if (isset($responseArray['data'][$keys[0]])) { + $source = $responseArray['data']; + foreach ($keys as $subKey) { + if (isset($source[$subKey])) { + $source = $source[$subKey]; + if (!is_array($source)) { + $actualValue = (string)$source; + } + } else { + break; + } + } + } + } - if (count((array)$responseArray['data'][$key]) === 0) { $responseArray['data'][$key] = ''; } - $actualValue = $responseArray['data'][$key]; + + $actualValue = $actualValue ?? $responseArray['data'][$key]; + if (is_array($actualValue)) { + // transform array to string, ensuring values are in the same order + $value = explode(',', $value); + $value = array_map('trim', $value); + sort($value); + $value = implode(',', $value); + + sort($actualValue); + $actualValue = implode(',', $actualValue); + } if ($actualValue !== $value) { throw new UnexpectedValueException( @@ -318,4 +348,23 @@ public function theEnvironmentVariableIsSetTo($key, $value) { EOF; file_put_contents(self::ENV_CONFIG_FILE, $envConfigPhp); } + + /** + * @Given /^the group "([^"]*)" should exists$/ + */ + public function theGroupShouldExists(string $gid): void { + $response = shell_exec( + sprintf( + '%s %s group:info --output=json %s', + PHP_BINARY, + __DIR__ . '/../../../../../../occ', + $gid + ) + ); + + $responseArray = json_decode($response, true); + if (!isset($responseArray['groupID']) || $responseArray['groupID'] !== $gid) { + throw new UnexpectedValueException('Group does not exist'); + } + } } diff --git a/tests/psalm-baseline.xml b/tests/psalm-baseline.xml index f940cfc3e..172ea3dd3 100644 --- a/tests/psalm-baseline.xml +++ b/tests/psalm-baseline.xml @@ -92,6 +92,11 @@ getConfigurationArray + + + string[] + + hasAnnotation diff --git a/tests/stub.phpstub b/tests/stub.phpstub index de9250af0..a6cc5c3c0 100644 --- a/tests/stub.phpstub +++ b/tests/stub.phpstub @@ -5,3 +5,25 @@ namespace OCA\Files\Event { class LoadAdditionalScriptsEvent extends Event { } } + +namespace OC\Group { + abstract class Database extends \OCP\Group\Backend\ABackend implements + \OCP\Group\Backend\IAddToGroupBackend, + \OCP\Group\Backend\ICountDisabledInGroup, + \OCP\Group\Backend\ICountUsersBackend, + \OCP\Group\Backend\ICreateGroupBackend, + \OCP\Group\Backend\IDeleteGroupBackend, + \OCP\Group\Backend\IGetDisplayNameBackend, + \OCP\Group\Backend\IGroupDetailsBackend, + \OCP\Group\Backend\IRemoveFromGroupBackend, + \OCP\Group\Backend\ISetDisplayNameBackend, + \OCP\Group\Backend\ISearchableGroupBackend, + \OCP\Group\Backend\INamedBackend { + } +} + +namespace OC\Hooks { + interface PublicEmitter { + public function emit($scope, $method, array $arguments = []); + } +} diff --git a/tests/unit/GroupBackendTest.php b/tests/unit/GroupBackendTest.php new file mode 100644 index 000000000..f22e3caab --- /dev/null +++ b/tests/unit/GroupBackendTest.php @@ -0,0 +1,152 @@ + + * + * @author Dominik Ach + * @author Arthur Schiwon + * @author Carl Schwan + * @author Maximilian Ruta + * @author Jonathan Treffler + * @author Giuliano Mele + * + * @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 . + * + */ + +use \OCA\User_SAML\GroupBackend; +use Test\TestCase; + +/** + * @group DB + */ +class GroupBackendTest extends TestCase { + + /** @var GroupBackend */ + private static $groupBackend; + private static $users = [ + [ + 'uid' => 'user_saml_integration_test_uid1', + 'groups' => [ + 'user_saml_integration_test_gid1', + 'SAML_user_saml_integration_test_gid2' + ] + ], + [ + 'uid' => 'user_saml_integration_test_uid2', + 'groups' => [ + 'user_saml_integration_test_gid1' + ] + ] + ]; + private static $groups = [ + [ + 'gid' => 'user_saml_integration_test_gid1', + 'saml_gid' => 'user_saml_integration_test_gid1', + 'members' => [ + 'user_saml_integration_test_uid1', + 'user_saml_integration_test_uid2' + ], + 'saml_gid_exists' => true + ], + [ + 'gid' => 'SAML_user_saml_integration_test_gid2', + 'saml_gid' => 'user_saml_integration_test_gid2', + 'members' => [ + 'user_saml_integration_test_uid1' + ], + 'saml_gid_exists' => false + ], + [ + 'gid' => 'user_saml_integration_test_gid3', + 'saml_gid' => 'user_saml_integration_test_gid3', + 'members' => [], + 'saml_gid_exists' => true + ], + ]; + + public static function setUpBeforeClass(): void { + parent::setUpBeforeClass(); + self::$groupBackend = new \OCA\User_SAML\GroupBackend(\OC::$server->getDatabaseConnection()); + foreach (self::$groups as $group) { + self::$groupBackend->createGroup($group['gid'], $group['saml_gid']); + } + foreach (self::$users as $user) { + foreach ($user['groups'] as $group) { + self::$groupBackend->addToGroup($user['uid'], $group); + } + } + } + + public static function tearDownAfterClass(): void { + parent::tearDownAfterClass(); + self::$groupBackend = new \OCA\User_SAML\GroupBackend(\OC::$server->getDatabaseConnection()); + foreach (self::$users as $user) { + foreach ($user['groups'] as $group) { + self::$groupBackend->removeFromGroup($user['uid'], $group); + } + } + foreach (self::$groups as $group) { + self::$groupBackend->deleteGroup($group['gid']); + } + } + + public function testInGroup() { + foreach (self::$groups as $group) { + foreach (self::$users as $user) { + $result = self::$groupBackend->inGroup($user['uid'], $group['gid']); + if (in_array($group['gid'], $user['groups'])) { + $this->assertTrue($result, sprintf("User %s should be member of group %s", $user['uid'], $group['gid'])); + } else { + $this->assertFalse($result, sprintf("User %s should not be member of group %s", $user['uid'], $group['gid'])); + } + } + } + } + + public function testGetGroups() { + $groups = self::$groupBackend->getGroups(); + foreach (self::$groups as $group) { + $this->assertContains($group['gid'], $groups, sprintf('Group %s should be retrieved', $group['gid'])); + } + } + + public function testGetUserGroups() { + foreach (self::$users as $user) { + $userGroups = self::$groupBackend->getUserGroups($user['uid']); + $this->assertCount(count($user['groups']), $userGroups, 'Should retrieve all user groups'); + foreach ($userGroups as $userGroup) { + $this->assertContains($userGroup, $user['groups'], sprintf('Users %s should be member of groups %s', $user['uid'], $userGroup)); + } + } + } + + public function testGroupExists() { + foreach (self::$groups as $group) { + $result = self::$groupBackend->groupExists($group['saml_gid']); + $this->assertSame($group['saml_gid_exists'], $result, sprintf('Group %s should exist', $group['saml_gid'])); + } + } + + public function testUsersInGroups() { + foreach (self::$groups as $group) { + $users = self::$groupBackend->usersInGroup($group['gid']); + $this->assertCount(count($group['members']), $users, 'Should retrieve all group members'); + foreach ($users as $user) { + $this->assertContains($user, $group['members'], sprintf('User %s should be member of group %s', $user, $group['gid'])); + } + } + } +} diff --git a/tests/unit/GroupManagerTest.php b/tests/unit/GroupManagerTest.php new file mode 100644 index 000000000..43e27db91 --- /dev/null +++ b/tests/unit/GroupManagerTest.php @@ -0,0 +1,282 @@ + + * + * @author Giuliano Mele + * + * @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 . + * + */ + +namespace OCA\User_SAML\Tests; + +use OC\BackgroundJob\JobList; +use OC\Group\Manager; +use OCA\User_SAML\GroupBackend; +use OCA\User_SAML\GroupManager; +use OCA\User_SAML\SAMLSettings; +use OCP\IConfig; +use OCP\IDBConnection; +use OCP\IGroup; +use OCP\IGroupManager; +use OCP\IUser; +use PHPUnit\Framework\MockObject\MockObject; +use Test\TestCase; + +class GroupManagerTest extends TestCase { + + /** @var IDBConnection|MockObject */ + private $db; + /** @var IGroupManager|MockObject */ + private $groupManager; + /** @var GroupBackend|MockObject */ + private $ownGroupBackend; + /** @var IConfig|MockObject */ + private $config; + /** @var JobList|MockObject */ + private $jobList; + /** @var SAMLSettings|MockObject */ + private $settings; + /** @var GroupManager|MockObject */ + private $ownGroupManager; + + protected function setUp(): void { + parent::setUp(); + $this->db = $this->createMock(IDBConnection::class); + $this->groupManager = $this->createMock(Manager::class); + $this->ownGroupBackend = $this->createMock(GroupBackend::class); + $this->config = $this->createMock(IConfig::class); + $this->jobList = $this->createMock(JobList::class); + $this->settings = $this->createMock(SAMLSettings::class); + $this->ownGroupManager = $this->createMock(GroupManager::class); + } + + public function getGroupManager(array $mockedFunctions = []) { + if (!empty($mockedFunctions)) { + $this->ownGroupManager = $this->getMockBuilder(GroupManager::class) + ->setConstructorArgs([ + $this->db, + $this->groupManager, + $this->ownGroupBackend, + $this->config, + $this->jobList, + $this->settings + ]) + ->onlyMethods($mockedFunctions) + ->getMock(); + } else { + $this->ownGroupManager = new GroupManager( + $this->db, + $this->groupManager, + $this->ownGroupBackend, + $this->config, + $this->jobList, + $this->settings + ); + } + } + + public function testUpdateUserGroups() { + // Case: The known memberships of the user are groupA and groupB. The new + // memberships are GroupB and GroupC. Hence, the user must be unassigned + // from GroupA and assigned to GroupC, while the GroupB association remains + // unchanged. + + $this->config->expects($this->any()) + ->method('getAppValue') + ->with('user_saml', GroupManager::LOCAL_GROUPS_CHECK_FOR_MIGRATION, '') + ->willReturn(\json_encode(['groups' => ['groupA'], 'dropAfter' => time() + 2000])); + + $this->getGroupManager(['handleUserUnassignedFromGroups', 'handleUserAssignedToGroups', 'translateGroupToIds', 'hasSamlBackend']); + $user = $this->createMock(IUser::class); + $groupA = $this->createMock(IGroup::class); + $groupA + ->method('getBackendNames') + ->willReturn(['Database']); + $groupA->method('getGID') + ->willReturn('groupA'); + $groupB = $this->createMock(IGroup::class); + $groupB + ->method('getBackendNames') + ->willReturn(['Database']); + $groupB->method('getGID') + ->willReturn('groupB'); + $this->ownGroupManager + ->expects($this->once()) + ->method('translateGroupToIds') + ->with(['groupB', 'groupC']); + // assert user is actually assigned to groupA and groupB + $this->groupManager + ->expects($this->once()) + ->method('getUserGroups') + ->with($user) + ->willReturn([$groupA, $groupB]); + $this->groupManager + ->method('get') + ->willReturnCallback(function ($groupId) use ($groupA, $groupB): ?IGroup { + switch ($groupId) { + case 'groupA': + return $groupA; + case 'groupB': + return $groupB; + default: + return null; + } + }); + // assert all groups are supplied by SAML backend + $this->ownGroupManager + ->method('hasSamlBackend') + ->willReturn(true); + // assert removing membership to groupA + $this->ownGroupManager + ->expects($this->once()) + ->method('handleUserUnassignedFromGroups') + ->with($user, ['groupA']); + // assert adding membership to groupC + $this->ownGroupManager + ->expects($this->once()) + ->method('handleUserAssignedToGroups') + ->with($user, ['groupC']); + + // assert SAML provides user groups groupB and groupC + $this->ownGroupManager->updateUserGroups($user, ['groupB', 'groupC']); + } + + public function testUnassignUserFromGroups() { + $this->getGroupManager(); + $user = $this->createMock(IUser::class); + $groupA = $this->createMock(IGroup::class); + $groupA->method('getBackendNames') + ->willReturn(['Database', 'user_saml']); + $this->groupManager + ->method('get') + ->with('groupA') + ->willReturn($groupA); + $user->expects($this->once()) + ->method('getUID') + ->willReturn('uid'); + $groupA->expects($this->exactly(2)) + ->method('getGID') + ->willReturn('gid'); + // assert membership gets removed + $this->ownGroupBackend + ->expects($this->once()) + ->method('removeFromGroup'); + // assert no remaining group memberships + $this->ownGroupBackend + ->expects($this->once()) + ->method('countUsersInGroup') + ->willReturn(0); + // assert group is deleted + $this->ownGroupBackend + ->expects($this->once()) + ->method('deleteGroup'); + + $this->invokePrivate($this->ownGroupManager, 'handleUserUnassignedFromGroups', [$user, ['groupA']]); + } + + public function testAssignUserToGroups() { + $this->getGroupManager(['hasSamlBackend', 'createGroupInBackend']); + $user = $this->createMock(IUser::class); + $groupA = $this->createMock(IGroup::class); + + // assert group already exists + $this->groupManager + ->expects($this->once()) + ->method('get') + ->with('groupA') + ->willReturn($groupA); + // assert SAML group backend + $this->ownGroupManager + ->expects($this->once()) + ->method('hasSamlBackend') + ->willReturn(true); + $groupA->expects($this->once()) + ->method('addUser') + ->with($user); + $this->ownGroupManager + ->expects($this->never()) + ->method('createGroupInBackend'); + + $this->invokePrivate($this->ownGroupManager, 'handleUserAssignedToGroups', [$user, ['groupA']]); + } + + public function testAssignUserToNonExistingGroups() { + $this->getGroupManager(); + $user = $this->createMock(IUser::class); + $groupB = $this->createMock(IGroup::class); + + // assert group does not exist + $this->groupManager + ->method('get') + ->willReturnOnConsecutiveCalls(null, $groupB); + // assert group is created + $this->ownGroupBackend + ->expects($this->once()) + ->method('createGroup') + ->with('groupB', 'groupB') + ->willReturn(true); + // assert user gets added to group + $groupB->expects($this->once()) + ->method('addUser') + ->with($user); + + $this->invokePrivate($this->ownGroupManager, 'handleUserAssignedToGroups', [$user, ['groupB']]); + } + + public function testAssignUserToGroupsWithCollision() { + $this->getGroupManager(['hasSamlBackend']); + $user = $this->createMock(IUser::class); + $groupC = $this->createMock(IGroup::class); + + // assert group exists + $this->groupManager + ->method('get') + ->willReturnCallback(function ($groupId) use ($groupC) { + switch ($groupId) { + case 'groupC': + return $groupC; + case 'SAML_groupC': + return $groupC; + } + return null; + }); + // assert differnt group backend + $this->ownGroupManager + ->expects($this->once()) + ->method('hasSamlBackend') + ->willReturn(false); + // assert there is only one idp config present + $this->settings + ->expects($this->once()) + ->method('getProviderId'); + // assert the default group prefix is configured + $this->settings + ->method('get'); + // assert group is created with prefix + gid + $this->ownGroupBackend + ->expects($this->once()) + ->method('createGroup') + ->with('SAML_groupC', 'groupC') + ->willReturn(true); + // assert user gets added to group + $groupC->expects($this->once()) + ->method('addUser') + ->with($user); + + $this->invokePrivate($this->ownGroupManager, 'handleUserAssignedToGroups', [$user, ['groupC']]); + } +} diff --git a/tests/unit/Settings/AdminTest.php b/tests/unit/Settings/AdminTest.php index dcbdc338d..08ea54cf1 100644 --- a/tests/unit/Settings/AdminTest.php +++ b/tests/unit/Settings/AdminTest.php @@ -152,6 +152,11 @@ public function formDataProvider() { 'type' => 'line', 'required' => false, ], + 'group_mapping_prefix' => [ + 'text' => $this->l10n->t('Group Mapping Prefix, default: SAML_'), + 'type' => 'line', + 'required' => true, + ], ]; $userFilterSettings = [ diff --git a/tests/unit/UserBackendTest.php b/tests/unit/UserBackendTest.php index e8aad2a88..0bc70884f 100644 --- a/tests/unit/UserBackendTest.php +++ b/tests/unit/UserBackendTest.php @@ -21,14 +21,13 @@ namespace OCA\User_SAML\Tests\Settings; +use OCA\User_SAML\GroupManager; use OCA\User_SAML\SAMLSettings; use OCA\User_SAML\UserBackend; use OCA\User_SAML\UserData; use OCP\EventDispatcher\IEventDispatcher; use OCP\IConfig; use OCP\IDBConnection; -use OCP\IGroup; -use OCP\IGroupManager; use OCP\ISession; use OCP\IURLGenerator; use OCP\IUser; @@ -51,7 +50,7 @@ class UserBackendTest extends TestCase { private $db; /** @var IUserManager|MockObject */ private $userManager; - /** @var IGroupManager|MockObject */ + /** @var GroupManager|MockObject */ private $groupManager; /** @var UserBackend|MockObject */ private $userBackend; @@ -70,7 +69,7 @@ protected function setUp(): void { $this->session = $this->createMock(ISession::class); $this->db = $this->createMock(IDBConnection::class); $this->userManager = $this->createMock(IUserManager::class); - $this->groupManager = $this->createMock(IGroupManager::class); + $this->groupManager = $this->createMock(GroupManager::class); $this->SAMLSettings = $this->getMockBuilder(SAMLSettings::class)->disableOriginalConstructor()->getMock(); $this->logger = $this->createMock(LoggerInterface::class); $this->userData = $this->createMock(UserData::class); @@ -149,6 +148,10 @@ public function testUpdateAttributesWithoutAttributes() { ->method('getDisplayName') ->with('ExistingUser') ->willReturn(''); + $this->groupManager + ->expects($this->once()) + ->method('handleIncomingGroups') + ->with($user, []); $this->userBackend->updateAttributes('ExistingUser', []); } @@ -172,8 +175,6 @@ public function testUpdateAttributes() { $this->getMockedBuilder(['getDisplayName', 'setDisplayName']); /** @var IUser|MockObject $user */ $user = $this->createMock(IUser::class); - $groupA = $this->createMock(IGroup::class); - $groupC = $this->createMock(IGroup::class); $this->config ->expects($this->at(0)) @@ -229,42 +230,10 @@ public function testUpdateAttributes() { ->expects($this->once()) ->method('setDisplayName') ->with('ExistingUser', 'New Displayname'); - - $this->groupManager - ->expects($this->once()) - ->method('getUserGroupIds') - ->with($user) - ->willReturn(['groupA', 'groupB']); - $this->groupManager - ->expects($this->once()) - ->method('groupExists') - ->with('groupC') - ->willReturn(false); - $this->groupManager - ->expects($this->once()) - ->method('createGroup') - ->with('groupC'); - - // updateAttributes first adds new groups, then removes old ones - // In this test groupA is removed from the user, groupB is unchanged - // and groupC is added $this->groupManager - ->expects($this->exactly(2)) - ->method('get') - ->withConsecutive(['groupC'], ['groupA']) - ->willReturnOnConsecutiveCalls($groupC, $groupA); - $groupA - ->expects($this->once()) - ->method('removeUser') - ->with($user); - $groupC ->expects($this->once()) - ->method('addUser') - ->with($user); - $this->eventDispatcher->expects($this->once()) - ->method('dispatchTyped') - ->with(new UserChangedEvent($user, 'displayName', 'New Displayname', '')); - + ->method('handleIncomingGroups') + ->with($user, ['groupB', 'groupC']); $this->userBackend->updateAttributes('ExistingUser', [ 'email' => 'new@example.com', 'displayname' => 'New Displayname', @@ -328,6 +297,10 @@ public function testUpdateAttributesQuotaDefaultFallback() { $this->eventDispatcher->expects($this->once()) ->method('dispatchTyped') ->with(new UserChangedEvent($user, 'displayName', 'New Displayname', '')); + $this->groupManager + ->expects($this->once()) + ->method('handleIncomingGroups') + ->with($user, []); $this->userBackend->updateAttributes('ExistingUser', ['email' => 'new@example.com', 'displayname' => 'New Displayname', 'quota' => '']); } } From d8d67e65e4c371c15ae510ca5a97049bbe5523f5 Mon Sep 17 00:00:00 2001 From: Arthur Schiwon Date: Fri, 10 Nov 2023 12:20:08 +0100 Subject: [PATCH 02/10] ci(actions): set debug log level for user_saml Signed-off-by: Arthur Schiwon --- .github/workflows/integration.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 78c69bd91..123cd15a2 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -100,6 +100,12 @@ jobs: mkdir data ./occ maintenance:install --verbose --database=${{ matrix.databases }} --database-name=nextcloud --database-host=127.0.0.1 --database-port=$DB_PORT --database-user=root --database-pass=rootpassword --admin-user admin --admin-pass admin ./occ app:enable --force ${{ env.APP_NAME }} + cat << EOF > config/debug.config.php + ['apps' => ['user_saml']], + ]; + EOF PHP_CLI_SERVER_WORKERS=4 php -S localhost:8080 & - name: Run behat From 5e192d12890b48eb7a8cc3ffb03c1705703aa11c Mon Sep 17 00:00:00 2001 From: Arthur Schiwon Date: Fri, 1 Dec 2023 11:15:46 +0100 Subject: [PATCH 03/10] ci(oci): run on GH runners it is failing much to often with container init Signed-off-by: Arthur Schiwon --- .github/workflows/phpunit-oci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/phpunit-oci.yml b/.github/workflows/phpunit-oci.yml index abe33cb6e..3d6e1f2fb 100644 --- a/.github/workflows/phpunit-oci.yml +++ b/.github/workflows/phpunit-oci.yml @@ -34,7 +34,7 @@ concurrency: jobs: phpunit-oci: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 strategy: matrix: From 5e7096f7a792207956aff19f39eaa246c238262a Mon Sep 17 00:00:00 2001 From: Arthur Schiwon Date: Fri, 1 Dec 2023 11:25:34 +0100 Subject: [PATCH 04/10] fix(GroupBackend): use prefix in new group IDs previous to this change groups would be creating in our own backend without the prefix. This is not a very predictable behaviour, and to be more consistent and with less surprises, we include the prefix now unconditionally. Signed-off-by: Arthur Schiwon --- lib/GroupManager.php | 16 +++++++++------- tests/integration/features/Shibboleth.feature | 2 +- tests/unit/GroupManagerTest.php | 17 ++++++++++++++++- 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/lib/GroupManager.php b/lib/GroupManager.php index dc17bb3da..620f4869b 100644 --- a/lib/GroupManager.php +++ b/lib/GroupManager.php @@ -176,9 +176,7 @@ protected function handleUserAssignedToGroups(IUser $user, $groupIds): void { protected function assignUserToGroup(IUser $user, string $gid): void { try { $group = $this->findGroup($gid); - } catch (GroupNotFoundException $e) { - $group = $this->createGroupInBackend($gid); - } catch (NonMigratableGroupException $e) { + } catch (GroupNotFoundException|NonMigratableGroupException $e) { $providerId = $this->settings->getProviderId(); $settings = $this->settings->get($providerId); $groupPrefix = $settings['saml-attribute-mapping-group_mapping_prefix'] ?? SAMLSettings::DEFAULT_GROUP_PREFIX; @@ -208,15 +206,19 @@ protected function createGroupInBackend(string $gid, ?string $originalGid = null * @throws GroupNotFoundException|NonMigratableGroupException */ protected function findGroup(string $gid): IGroup { - $migrationAllowList = $this->config->getAppValue( + $migrationAllowListRaw = $this->config->getAppValue( 'user_saml', GroupManager::LOCAL_GROUPS_CHECK_FOR_MIGRATION, '' ); - $strictBackendCheck = '' === $migrationAllowList; - if ($migrationAllowList !== '') { - $migrationAllowList = \json_decode($migrationAllowList, true); + $strictBackendCheck = '' === $migrationAllowListRaw; + + $migrationAllowList = null; + if ($migrationAllowListRaw !== '') { + /** @var array{dropAfter: int, groups: string[]} $migrationAllowList */ + $migrationAllowList = \json_decode($migrationAllowListRaw, true); } + if (!$strictBackendCheck && in_array($gid, $migrationAllowList['groups'] ?? [], true)) { $group = $this->groupManager->get($gid); if ($group === null) { diff --git a/tests/integration/features/Shibboleth.feature b/tests/integration/features/Shibboleth.feature index d03614f42..51babc8c4 100644 --- a/tests/integration/features/Shibboleth.feature +++ b/tests/integration/features/Shibboleth.feature @@ -93,7 +93,7 @@ Feature: Shibboleth And The user value "email" should be "student1@idptestbed.edu" And The user value "quota.total" should be "209715200" And The user value "display-name" should be "Stud Ent" - And The user value "groups" should be "Students, Astrophysics" + And The user value "groups" should be "SAML_Astrophysics,SAML_Students" And the group "SAML_Astrophysics" should exists And the group "SAML_Students" should exists And The last login timestamp of "student1" should not be empty diff --git a/tests/unit/GroupManagerTest.php b/tests/unit/GroupManagerTest.php index 43e27db91..f18fecb34 100644 --- a/tests/unit/GroupManagerTest.php +++ b/tests/unit/GroupManagerTest.php @@ -193,6 +193,11 @@ public function testAssignUserToGroups() { $user = $this->createMock(IUser::class); $groupA = $this->createMock(IGroup::class); + $this->config->expects($this->any()) + ->method('getAppValue') + ->with('user_saml', GroupManager::LOCAL_GROUPS_CHECK_FOR_MIGRATION, '') + ->willReturnArgument(2); + // assert group already exists $this->groupManager ->expects($this->once()) @@ -219,6 +224,11 @@ public function testAssignUserToNonExistingGroups() { $user = $this->createMock(IUser::class); $groupB = $this->createMock(IGroup::class); + $this->config->expects($this->any()) + ->method('getAppValue') + ->with('user_saml', GroupManager::LOCAL_GROUPS_CHECK_FOR_MIGRATION, '') + ->willReturnArgument(2); + // assert group does not exist $this->groupManager ->method('get') @@ -227,7 +237,7 @@ public function testAssignUserToNonExistingGroups() { $this->ownGroupBackend ->expects($this->once()) ->method('createGroup') - ->with('groupB', 'groupB') + ->with('SAML_groupB', 'groupB') ->willReturn(true); // assert user gets added to group $groupB->expects($this->once()) @@ -242,6 +252,11 @@ public function testAssignUserToGroupsWithCollision() { $user = $this->createMock(IUser::class); $groupC = $this->createMock(IGroup::class); + $this->config->expects($this->any()) + ->method('getAppValue') + ->with('user_saml', GroupManager::LOCAL_GROUPS_CHECK_FOR_MIGRATION, '') + ->willReturnArgument(2); + // assert group exists $this->groupManager ->method('get') From f72cb45696be7d30acfb5da87339df9521ef3665 Mon Sep 17 00:00:00 2001 From: Arthur Schiwon Date: Fri, 1 Dec 2023 11:39:42 +0100 Subject: [PATCH 05/10] ci(deps): use stable27 of nc/ocp Signed-off-by: Arthur Schiwon --- composer.json | 2 +- composer.lock | 22 ++++++++++++---------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/composer.json b/composer.json index 5e45e74b7..be2a898f8 100644 --- a/composer.json +++ b/composer.json @@ -18,6 +18,6 @@ "nextcloud/coding-standard": "^1.1", "phpunit/phpunit": "^9", "psalm/phar": "^5.13", - "nextcloud/ocp": "^27.0" + "nextcloud/ocp": "dev-stable27" } } diff --git a/composer.lock b/composer.lock index 7610ce641..ca81dd782 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "b736b5e5d69ab30551c9c38290da5c0a", + "content-hash": "41d35031172da5e869aad3ba0557b5c9", "packages": [], "packages-dev": [ { @@ -179,16 +179,16 @@ }, { "name": "nextcloud/ocp", - "version": "v27.0.1", + "version": "dev-stable27", "source": { "type": "git", "url": "https://github.com/nextcloud-deps/ocp.git", - "reference": "1aaa8b098961a3acadd51b413f7bcfd89f4c0638" + "reference": "4b22dae0cacc0e776d4cb83aa4c3b54fea99794a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nextcloud-deps/ocp/zipball/1aaa8b098961a3acadd51b413f7bcfd89f4c0638", - "reference": "1aaa8b098961a3acadd51b413f7bcfd89f4c0638", + "url": "https://api.github.com/repos/nextcloud-deps/ocp/zipball/4b22dae0cacc0e776d4cb83aa4c3b54fea99794a", + "reference": "4b22dae0cacc0e776d4cb83aa4c3b54fea99794a", "shasum": "" }, "require": { @@ -217,9 +217,9 @@ "description": "Composer package containing Nextcloud's public API (classes, interfaces)", "support": { "issues": "https://github.com/nextcloud-deps/ocp/issues", - "source": "https://github.com/nextcloud-deps/ocp/tree/v27.0.1" + "source": "https://github.com/nextcloud-deps/ocp/tree/stable27" }, - "time": "2023-07-17T09:26:48+00:00" + "time": "2023-11-17T00:33:22+00:00" }, { "name": "nikic/php-parser", @@ -2114,13 +2114,15 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": { + "nextcloud/ocp": 20 + }, "prefer-stable": false, "prefer-lowest": false, "platform": [], "platform-dev": [], "platform-overrides": { - "php": "7.4" + "php": "8.0" }, - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.6.0" } From 8be15ac66c144c49819580f16b99ccc158d88e6a Mon Sep 17 00:00:00 2001 From: Arthur Schiwon Date: Tue, 5 Dec 2023 23:00:52 +0100 Subject: [PATCH 06/10] test(integration): ensure group migration works Signed-off-by: Arthur Schiwon --- tests/integration/features/Shibboleth.feature | 295 ++++++++++++++++++ .../features/bootstrap/FeatureContext.php | 184 ++++++++++- 2 files changed, 472 insertions(+), 7 deletions(-) diff --git a/tests/integration/features/Shibboleth.feature b/tests/integration/features/Shibboleth.feature index 51babc8c4..6c5afafda 100644 --- a/tests/integration/features/Shibboleth.feature +++ b/tests/integration/features/Shibboleth.feature @@ -122,3 +122,298 @@ Feature: Shibboleth And The user value "email" should be "student1@idptestbed.edu" And The user value "display-name" should be "Stud Ent" And The last login timestamp of "student1" should not be empty + + Scenario: Migrating a local group to SAML backend while assigning a SAML user to it + Given The setting "type" is set to "saml" + And The setting "general-uid_mapping" is set to "urn:oid:0.9.2342.19200300.100.1.1" + And The setting "idp-entityId" is set to "https://shibboleth-integration-nextcloud.localdomain/idp/shibboleth" + And The setting "idp-singleSignOnService.url" is set to "https://localhost:4443/idp/profile/SAML2/Redirect/SSO" + And The setting "idp-singleLogoutService.url" is set to "https://localhost:4443/idp/profile/Logout" + And The setting "idp-x509cert" is set to "MIIDnTCCAoWgAwIBAgIUGPx9uPjCu7c172IUgV84Tm94pBcwDQYJKoZIhvcNAQEL BQAwNzE1MDMGA1UEAwwsc2hpYmJvbGV0aC1pbnRlZ3JhdGlvbi1uZXh0Y2xvdWQu bG9jYWxkb21haW4wHhcNMTcwMTA0MTAxMTI3WhcNMzcwMTA0MTAxMTI3WjA3MTUw MwYDVQQDDCxzaGliYm9sZXRoLWludGVncmF0aW9uLW5leHRjbG91ZC5sb2NhbGRv bWFpbjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKH+bPu45tk8/JRk XOxkyfbxocWZlY4mRumEUxscd3fn0oVzOrdWbHH7lCZV4bus4KxvJljc0Nm2K+Zr LoiRUUnf/LQ4LlehWVm5Kbc4kRgOXS0iGZN3SslAWPKyIg0tywg+TLOBPoS6EtST 1WuYg1JPMFxPfeFDWQ0dQYPlXIJWBFh6F2JMTb0FLECqA5l/ryYE13QisX5l+Mqo 6y3Dh7qIgaH0IJNobXoAcEWza7Kb2RnfhZRh9e0qjZIwBqTJUFM/6I86RYXn829s INUvYQQbez6VkGTdUQJ/GuXb/dD5sMQfOyK8hrRY5MozOmK32cz3JaAzSXpiSRS9 NxFwvicCAwEAAaOBoDCBnTAdBgNVHQ4EFgQUKn8+TV0WXSDeavvF0M8mWn1o8ukw fAYDVR0RBHUwc4Isc2hpYmJvbGV0aC1pbnRlZ3JhdGlvbi1uZXh0Y2xvdWQubG9j YWxkb21haW6GQ2h0dHBzOi8vc2hpYmJvbGV0aC1pbnRlZ3JhdGlvbi1uZXh0Y2xv dWQubG9jYWxkb21haW4vaWRwL3NoaWJib2xldGgwDQYJKoZIhvcNAQELBQADggEB ABI6uzoIeLZT9Az2KTlLxIc6jZ4MDmhaVja4ZuBxTXEb7BFLfeASEJmQm1wgIMOn pJId3Kh3njW+3tOBWKm7lj8JxVVpAu4yMFSoQGPaVUgYB1AVm+pmAyPLzfJ/XGhf esCU2F/b0eHWcaIb3x+BZFX089Cd/PBtP84MNXdo+TccibxC8N39sr45qJM/7SC7 TfDYU0L4q2WZHJr4S7+0F+F4KaxLx9NzCvN4h6XaoWofZWir2iHO4NzbrVQGC0ei QybS/neBfni4A2g1lyzCb6xFB58JBvNCn7AAnDJULOE7S5XWUKsDAQVQrxTNkUq7 pnhlCQqZDwUdgmIXd1KB1So=" + And The setting "security-authnRequestsSigned" is set to "1" + And The setting "security-wantAssertionsEncrypted" is set to "1" + And The setting "sp-x509cert" is set to "-----BEGIN CERTIFICATE-----MIIC+zCCAeOgAwIBAgIJAIgZuvWDBIrdMA0GCSqGSIb3DQEBBQUAMBQxEjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0xNzAxMDQxMTM5MjFaFw0yNzAxMDIxMTM5MjFaMBQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAN3ESWaDH1JiJTy9yRJQV7kahPOxgBkIH2xwcYDL1k9deKNhSKLx7aGfxE244+HBcC6WLHKVUnOm0ld2qxQ4bMYiJXzZuqL67r07L5wxGAssv12lO92qohGmlHy3+VzRYUBmovu6upqOv3R2F8HBbo7Jc7Hvt7hOEJn/jPuFuF/fHit3mqU8l6IkrIZjpaW8T9fIWOXRq98U4+hkgWpqEZWsqlfE8BxAs9DeIMZab0GxO9stHLp+GYKx10uE4ezFcaDS8W+g2C8enCTt1HXGvcnj4o5zkC1lITGvcFTsiFqfIWyXeSufcxdc0W7HoG6J3ks0WJyK38sfFn0t2Ao6kX0CAwEAAaNQME4wHQYDVR0OBBYEFAoJzX6TVYAwC1GSPe6nObBG54zaMB8GA1UdIwQYMBaAFAoJzX6TVYAwC1GSPe6nObBG54zaMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAJia9R70uXdUZtgujUPjLas4+sVajzlBFmqhBqpLAo934vljf9HISsHrPtdBcbS0d0rucqXwabDf0MlR18ksnT/NYpsTwMbMx76CrXi4zYEEW5lISKEO65aIkzVTcqKWSuhjtSnRdB6iOLsFiKmNMWXaIKMR5T0+AbR9wdQgn08W+3EEeHGvafVQfE3STVsSgNb1ft7DvcSUnfPXGU7KzvmTpZa0Hfmc7uY4vpdEEhLAdRhgLReS7USZskov7ooiPSoD+JRFi2gM4klBxTemHdNUa9oFnHMXuYKOkLbkgFvHxyy+QlLq2ELQTga5e7I83ZyOfGctyf8Ul6vGw10vbQ=-----END CERTIFICATE-----" + And The setting "sp-privateKey" is set to "-----BEGIN RSA PRIVATE KEY-----MIIEpAIBAAKCAQEA3cRJZoMfUmIlPL3JElBXuRqE87GAGQgfbHBxgMvWT114o2FIovHtoZ/ETbjj4cFwLpYscpVSc6bSV3arFDhsxiIlfNm6ovruvTsvnDEYCyy/XaU73aqiEaaUfLf5XNFhQGai+7q6mo6/dHYXwcFujslzse+3uE4Qmf+M+4W4X98eK3eapTyXoiSshmOlpbxP18hY5dGr3xTj6GSBamoRlayqV8TwHECz0N4gxlpvQbE72y0cun4ZgrHXS4Th7MVxoNLxb6DYLx6cJO3Udca9yePijnOQLWUhMa9wVOyIWp8hbJd5K59zF1zRbsegboneSzRYnIrfyx8WfS3YCjqRfQIDAQABAoIBAQC5CQAdcqZ9vLpJNilBCJxJLCFmm+HAAREHD8MErg9A5UK1P4S1wJp/0qieGPi68wXBOTgY2xKSwMycgb04/+NyZidVRu388takOW/+KNBg8pMxdZ6/05GqnI0kivSbR3CXpYuz8hekwhpo9+fWmKjApsHL47ItK6WaeKmPbAFsq1YJGzfp/DXg7LIvh9GA3C1LWWGV7SuCGOyX/2Moi8xRa7qBtH4hDo/0NRhTx7zjYjlBgNEr330pJUopc3+AtHE40R+xMr2zkGvq9RsCZxYxD2VWbLwQW0yNjWmQ2OTuMgJJvk2+N73QLHcB+tea82ZTszsNzRS9DLtc6qbsKEPZAoGBAO78U3vEuRyY56f/1hpo0xuCDwOkWGzgBQWkjJl6dlyVz/zKkhXBHpEYImyt8XRN0W3iGZYpZ2hCFJGTcDp32R6UiEyGLz0Uc8R/tva/TiRVW1FdNczzSHcB24b9OMK4vE9JLs8mA8Rp8YBgtLr5DDuMfYt/a/rZJbg/HIfIN98nAoGBAO2OInCX93t2I6zzRPIqKtI6q6FYNp64VIQjvw9Y8l0x3IdJZRP9H5C8ZhCeYPsgEqTXcXa4j5hL4rQzoUtxfxflBUUH60bcnd4LGaTCMYLS14G011E3GZlIP0sJi5OjEhy8fq3zt6jVzS9V/lPHB8i+w1D7CbPrMpW7B3k32vC7AoGAX/HvdkYhZyjAAEOG6m1hK68IZhbp5TP+8CgCxm9S65K9wKh3A8LXibrdvzIKOP4w8WOPkCipOkMlTNibeu24vj01hztr5aK7Y40+oEtnjNCz67N3MQQO+LBHOSeaTRqrh01DPKjvZECAU2D/zfzEe3fIw2Nxr3DUYub7hkvMmosCgYAzxbVVypjiLGYsDDyrdmsstCKxoDMPNmcdAVljc+QmUXaZeXJw/8qAVb78wjeqo1vM1zNgR2rsKyW2VkZB1fN39q7GU6qAIBa7zLmDAduegmr7VrlSduq6UFeS9/qWa4TIBICrUqFlR2tXdKtgANF+e6y/mmaL8qdsoH1JetXZfwKBgQC1vscRpdAXivjOOZAh+mzJWzS4BUl4CTJLYYIuOEXikmN5g0EdV2fhUEdkewmyKnXHsd0x83167bYgpTDNs71jUxDHy5NXlg2qIjLkf09X9wr19gBzDApfWzfh3vUqttyMZuQMLVNepGCWM2vjlY9KGl5OvZqY6d+7yO0mLV9GmQ==-----END RSA PRIVATE KEY-----" + And The setting "security-wantAssertionsSigned" is set to "1" + And The setting "localGroupsCheckForMigration" is set to '{\"dropAfter\":9223372036854775807,\"groups\":[\"Astrophysics\",\"Students\"]}' + And the local group "Astrophysics" is created + # Initial login so the user is known and belonging to SAML, directly followed by a logout + And I send a GET request to "http://localhost:8080/index.php/login" + And I should be redirected to "https://localhost:4443/idp/profile/SAML2/Redirect/SSO" + And I send a POST request to "https://localhost:4443/idp/profile/SAML2/Redirect/SSO?execution=e1s1" with the following data + |j_username|j_password|_eventId_proceed| + |student1 |password | | + And The response should be a SAML redirect page that gets submitted + And I send a GET request with requesttoken to "http://localhost:8080/index.php/apps/user_saml/saml/sls" + # Set the proper attributes + And The setting "saml-attribute-mapping-email_mapping" is set to "urn:oid:0.9.2342.19200300.100.1.3" + And The setting "saml-attribute-mapping-displayName_mapping" is set to "urn:oid:2.5.4.42 urn:oid:2.5.4.4" + And The setting "saml-attribute-mapping-group_mapping" is set to "groups" + And the cookies are cleared + # Now login in for good + When I send a GET request to "http://localhost:8080/index.php/login" + Then I should be redirected to "https://localhost:4443/idp/profile/SAML2/Redirect/SSO" + And I send a POST request to "https://localhost:4443/idp/profile/SAML2/Redirect/SSO?execution=e1s1" with the following data + |j_username|j_password|_eventId_proceed| + |student1 |password | | + And The response should be a SAML redirect page that gets submitted + #And I should be redirected to "http://localhost:8080/index.php/apps/dashboard/" + And The user value "groups" should be "Astrophysics,SAML_Students" + And the group backend of "Astrophysics" should be "Database" + And Then the group backend of "Astrophysics" must not be "user_saml" + When I execute the background job for class "OCA\\User_SAML\\Jobs\\MigrateGroups" + Then the group backend of "Astrophysics" should be "user_saml" + And Then the group backend of "Astrophysics" must not be "Database" + And The user value "groups" should be "Astrophysics,SAML_Students" + + Scenario: Keeping the local admin group assigned to the SAML user + Given The setting "type" is set to "saml" + And The setting "general-uid_mapping" is set to "urn:oid:0.9.2342.19200300.100.1.1" + And The setting "idp-entityId" is set to "https://shibboleth-integration-nextcloud.localdomain/idp/shibboleth" + And The setting "idp-singleSignOnService.url" is set to "https://localhost:4443/idp/profile/SAML2/Redirect/SSO" + And The setting "idp-singleLogoutService.url" is set to "https://localhost:4443/idp/profile/Logout" + And The setting "idp-x509cert" is set to "MIIDnTCCAoWgAwIBAgIUGPx9uPjCu7c172IUgV84Tm94pBcwDQYJKoZIhvcNAQEL BQAwNzE1MDMGA1UEAwwsc2hpYmJvbGV0aC1pbnRlZ3JhdGlvbi1uZXh0Y2xvdWQu bG9jYWxkb21haW4wHhcNMTcwMTA0MTAxMTI3WhcNMzcwMTA0MTAxMTI3WjA3MTUw MwYDVQQDDCxzaGliYm9sZXRoLWludGVncmF0aW9uLW5leHRjbG91ZC5sb2NhbGRv bWFpbjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKH+bPu45tk8/JRk XOxkyfbxocWZlY4mRumEUxscd3fn0oVzOrdWbHH7lCZV4bus4KxvJljc0Nm2K+Zr LoiRUUnf/LQ4LlehWVm5Kbc4kRgOXS0iGZN3SslAWPKyIg0tywg+TLOBPoS6EtST 1WuYg1JPMFxPfeFDWQ0dQYPlXIJWBFh6F2JMTb0FLECqA5l/ryYE13QisX5l+Mqo 6y3Dh7qIgaH0IJNobXoAcEWza7Kb2RnfhZRh9e0qjZIwBqTJUFM/6I86RYXn829s INUvYQQbez6VkGTdUQJ/GuXb/dD5sMQfOyK8hrRY5MozOmK32cz3JaAzSXpiSRS9 NxFwvicCAwEAAaOBoDCBnTAdBgNVHQ4EFgQUKn8+TV0WXSDeavvF0M8mWn1o8ukw fAYDVR0RBHUwc4Isc2hpYmJvbGV0aC1pbnRlZ3JhdGlvbi1uZXh0Y2xvdWQubG9j YWxkb21haW6GQ2h0dHBzOi8vc2hpYmJvbGV0aC1pbnRlZ3JhdGlvbi1uZXh0Y2xv dWQubG9jYWxkb21haW4vaWRwL3NoaWJib2xldGgwDQYJKoZIhvcNAQELBQADggEB ABI6uzoIeLZT9Az2KTlLxIc6jZ4MDmhaVja4ZuBxTXEb7BFLfeASEJmQm1wgIMOn pJId3Kh3njW+3tOBWKm7lj8JxVVpAu4yMFSoQGPaVUgYB1AVm+pmAyPLzfJ/XGhf esCU2F/b0eHWcaIb3x+BZFX089Cd/PBtP84MNXdo+TccibxC8N39sr45qJM/7SC7 TfDYU0L4q2WZHJr4S7+0F+F4KaxLx9NzCvN4h6XaoWofZWir2iHO4NzbrVQGC0ei QybS/neBfni4A2g1lyzCb6xFB58JBvNCn7AAnDJULOE7S5XWUKsDAQVQrxTNkUq7 pnhlCQqZDwUdgmIXd1KB1So=" + And The setting "security-authnRequestsSigned" is set to "1" + And The setting "security-wantAssertionsEncrypted" is set to "1" + And The setting "sp-x509cert" is set to "-----BEGIN CERTIFICATE-----MIIC+zCCAeOgAwIBAgIJAIgZuvWDBIrdMA0GCSqGSIb3DQEBBQUAMBQxEjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0xNzAxMDQxMTM5MjFaFw0yNzAxMDIxMTM5MjFaMBQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAN3ESWaDH1JiJTy9yRJQV7kahPOxgBkIH2xwcYDL1k9deKNhSKLx7aGfxE244+HBcC6WLHKVUnOm0ld2qxQ4bMYiJXzZuqL67r07L5wxGAssv12lO92qohGmlHy3+VzRYUBmovu6upqOv3R2F8HBbo7Jc7Hvt7hOEJn/jPuFuF/fHit3mqU8l6IkrIZjpaW8T9fIWOXRq98U4+hkgWpqEZWsqlfE8BxAs9DeIMZab0GxO9stHLp+GYKx10uE4ezFcaDS8W+g2C8enCTt1HXGvcnj4o5zkC1lITGvcFTsiFqfIWyXeSufcxdc0W7HoG6J3ks0WJyK38sfFn0t2Ao6kX0CAwEAAaNQME4wHQYDVR0OBBYEFAoJzX6TVYAwC1GSPe6nObBG54zaMB8GA1UdIwQYMBaAFAoJzX6TVYAwC1GSPe6nObBG54zaMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAJia9R70uXdUZtgujUPjLas4+sVajzlBFmqhBqpLAo934vljf9HISsHrPtdBcbS0d0rucqXwabDf0MlR18ksnT/NYpsTwMbMx76CrXi4zYEEW5lISKEO65aIkzVTcqKWSuhjtSnRdB6iOLsFiKmNMWXaIKMR5T0+AbR9wdQgn08W+3EEeHGvafVQfE3STVsSgNb1ft7DvcSUnfPXGU7KzvmTpZa0Hfmc7uY4vpdEEhLAdRhgLReS7USZskov7ooiPSoD+JRFi2gM4klBxTemHdNUa9oFnHMXuYKOkLbkgFvHxyy+QlLq2ELQTga5e7I83ZyOfGctyf8Ul6vGw10vbQ=-----END CERTIFICATE-----" + And The setting "sp-privateKey" is set to "-----BEGIN RSA PRIVATE KEY-----MIIEpAIBAAKCAQEA3cRJZoMfUmIlPL3JElBXuRqE87GAGQgfbHBxgMvWT114o2FIovHtoZ/ETbjj4cFwLpYscpVSc6bSV3arFDhsxiIlfNm6ovruvTsvnDEYCyy/XaU73aqiEaaUfLf5XNFhQGai+7q6mo6/dHYXwcFujslzse+3uE4Qmf+M+4W4X98eK3eapTyXoiSshmOlpbxP18hY5dGr3xTj6GSBamoRlayqV8TwHECz0N4gxlpvQbE72y0cun4ZgrHXS4Th7MVxoNLxb6DYLx6cJO3Udca9yePijnOQLWUhMa9wVOyIWp8hbJd5K59zF1zRbsegboneSzRYnIrfyx8WfS3YCjqRfQIDAQABAoIBAQC5CQAdcqZ9vLpJNilBCJxJLCFmm+HAAREHD8MErg9A5UK1P4S1wJp/0qieGPi68wXBOTgY2xKSwMycgb04/+NyZidVRu388takOW/+KNBg8pMxdZ6/05GqnI0kivSbR3CXpYuz8hekwhpo9+fWmKjApsHL47ItK6WaeKmPbAFsq1YJGzfp/DXg7LIvh9GA3C1LWWGV7SuCGOyX/2Moi8xRa7qBtH4hDo/0NRhTx7zjYjlBgNEr330pJUopc3+AtHE40R+xMr2zkGvq9RsCZxYxD2VWbLwQW0yNjWmQ2OTuMgJJvk2+N73QLHcB+tea82ZTszsNzRS9DLtc6qbsKEPZAoGBAO78U3vEuRyY56f/1hpo0xuCDwOkWGzgBQWkjJl6dlyVz/zKkhXBHpEYImyt8XRN0W3iGZYpZ2hCFJGTcDp32R6UiEyGLz0Uc8R/tva/TiRVW1FdNczzSHcB24b9OMK4vE9JLs8mA8Rp8YBgtLr5DDuMfYt/a/rZJbg/HIfIN98nAoGBAO2OInCX93t2I6zzRPIqKtI6q6FYNp64VIQjvw9Y8l0x3IdJZRP9H5C8ZhCeYPsgEqTXcXa4j5hL4rQzoUtxfxflBUUH60bcnd4LGaTCMYLS14G011E3GZlIP0sJi5OjEhy8fq3zt6jVzS9V/lPHB8i+w1D7CbPrMpW7B3k32vC7AoGAX/HvdkYhZyjAAEOG6m1hK68IZhbp5TP+8CgCxm9S65K9wKh3A8LXibrdvzIKOP4w8WOPkCipOkMlTNibeu24vj01hztr5aK7Y40+oEtnjNCz67N3MQQO+LBHOSeaTRqrh01DPKjvZECAU2D/zfzEe3fIw2Nxr3DUYub7hkvMmosCgYAzxbVVypjiLGYsDDyrdmsstCKxoDMPNmcdAVljc+QmUXaZeXJw/8qAVb78wjeqo1vM1zNgR2rsKyW2VkZB1fN39q7GU6qAIBa7zLmDAduegmr7VrlSduq6UFeS9/qWa4TIBICrUqFlR2tXdKtgANF+e6y/mmaL8qdsoH1JetXZfwKBgQC1vscRpdAXivjOOZAh+mzJWzS4BUl4CTJLYYIuOEXikmN5g0EdV2fhUEdkewmyKnXHsd0x83167bYgpTDNs71jUxDHy5NXlg2qIjLkf09X9wr19gBzDApfWzfh3vUqttyMZuQMLVNepGCWM2vjlY9KGl5OvZqY6d+7yO0mLV9GmQ==-----END RSA PRIVATE KEY-----" + And The setting "security-wantAssertionsSigned" is set to "1" + And The setting "localGroupsCheckForMigration" is set to '{\"dropAfter\":9223372036854775807,\"groups\":[\"Astrophysics\",\"Students\"]}' + # Initial login so the user is known and belonging to SAML, directly followed by a logout + And I send a GET request to "http://localhost:8080/index.php/login" + And I should be redirected to "https://localhost:4443/idp/profile/SAML2/Redirect/SSO" + And I send a POST request to "https://localhost:4443/idp/profile/SAML2/Redirect/SSO?execution=e1s1" with the following data + |j_username|j_password|_eventId_proceed| + |student1 |password | | + And The response should be a SAML redirect page that gets submitted + And I send a GET request with requesttoken to "http://localhost:8080/index.php/apps/user_saml/saml/sls" + And the user "student1" is added to the group "admin" + # Set the proper attributes + And The setting "saml-attribute-mapping-email_mapping" is set to "urn:oid:0.9.2342.19200300.100.1.3" + And The setting "saml-attribute-mapping-displayName_mapping" is set to "urn:oid:2.5.4.42 urn:oid:2.5.4.4" + And The setting "saml-attribute-mapping-group_mapping" is set to "groups" + And the cookies are cleared + # Now login in for good + When I send a GET request to "http://localhost:8080/index.php/login" + Then I should be redirected to "https://localhost:4443/idp/profile/SAML2/Redirect/SSO" + And I send a POST request to "https://localhost:4443/idp/profile/SAML2/Redirect/SSO?execution=e1s1" with the following data + |j_username|j_password|_eventId_proceed| + |student1 |password | | + And The response should be a SAML redirect page that gets submitted + And The user value "groups" should be "admin,SAML_Astrophysics,SAML_Students" + When I execute the background job for class "OCA\\User_SAML\\Jobs\\MigrateGroups" + Then the group backend of "admin" should be "Database" + And Then the group backend of "admin" must not be "user_saml" + And The user value "groups" should be "admin,SAML_Astrophysics,SAML_Students" + + Scenario: Not migrating a group with only SAML members when not in migration mode anymore + Given The setting "type" is set to "saml" + And The setting "general-uid_mapping" is set to "urn:oid:0.9.2342.19200300.100.1.1" + And The setting "idp-entityId" is set to "https://shibboleth-integration-nextcloud.localdomain/idp/shibboleth" + And The setting "idp-singleSignOnService.url" is set to "https://localhost:4443/idp/profile/SAML2/Redirect/SSO" + And The setting "idp-singleLogoutService.url" is set to "https://localhost:4443/idp/profile/Logout" + And The setting "idp-x509cert" is set to "MIIDnTCCAoWgAwIBAgIUGPx9uPjCu7c172IUgV84Tm94pBcwDQYJKoZIhvcNAQEL BQAwNzE1MDMGA1UEAwwsc2hpYmJvbGV0aC1pbnRlZ3JhdGlvbi1uZXh0Y2xvdWQu bG9jYWxkb21haW4wHhcNMTcwMTA0MTAxMTI3WhcNMzcwMTA0MTAxMTI3WjA3MTUw MwYDVQQDDCxzaGliYm9sZXRoLWludGVncmF0aW9uLW5leHRjbG91ZC5sb2NhbGRv bWFpbjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKH+bPu45tk8/JRk XOxkyfbxocWZlY4mRumEUxscd3fn0oVzOrdWbHH7lCZV4bus4KxvJljc0Nm2K+Zr LoiRUUnf/LQ4LlehWVm5Kbc4kRgOXS0iGZN3SslAWPKyIg0tywg+TLOBPoS6EtST 1WuYg1JPMFxPfeFDWQ0dQYPlXIJWBFh6F2JMTb0FLECqA5l/ryYE13QisX5l+Mqo 6y3Dh7qIgaH0IJNobXoAcEWza7Kb2RnfhZRh9e0qjZIwBqTJUFM/6I86RYXn829s INUvYQQbez6VkGTdUQJ/GuXb/dD5sMQfOyK8hrRY5MozOmK32cz3JaAzSXpiSRS9 NxFwvicCAwEAAaOBoDCBnTAdBgNVHQ4EFgQUKn8+TV0WXSDeavvF0M8mWn1o8ukw fAYDVR0RBHUwc4Isc2hpYmJvbGV0aC1pbnRlZ3JhdGlvbi1uZXh0Y2xvdWQubG9j YWxkb21haW6GQ2h0dHBzOi8vc2hpYmJvbGV0aC1pbnRlZ3JhdGlvbi1uZXh0Y2xv dWQubG9jYWxkb21haW4vaWRwL3NoaWJib2xldGgwDQYJKoZIhvcNAQELBQADggEB ABI6uzoIeLZT9Az2KTlLxIc6jZ4MDmhaVja4ZuBxTXEb7BFLfeASEJmQm1wgIMOn pJId3Kh3njW+3tOBWKm7lj8JxVVpAu4yMFSoQGPaVUgYB1AVm+pmAyPLzfJ/XGhf esCU2F/b0eHWcaIb3x+BZFX089Cd/PBtP84MNXdo+TccibxC8N39sr45qJM/7SC7 TfDYU0L4q2WZHJr4S7+0F+F4KaxLx9NzCvN4h6XaoWofZWir2iHO4NzbrVQGC0ei QybS/neBfni4A2g1lyzCb6xFB58JBvNCn7AAnDJULOE7S5XWUKsDAQVQrxTNkUq7 pnhlCQqZDwUdgmIXd1KB1So=" + And The setting "security-authnRequestsSigned" is set to "1" + And The setting "security-wantAssertionsEncrypted" is set to "1" + And The setting "sp-x509cert" is set to "-----BEGIN CERTIFICATE-----MIIC+zCCAeOgAwIBAgIJAIgZuvWDBIrdMA0GCSqGSIb3DQEBBQUAMBQxEjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0xNzAxMDQxMTM5MjFaFw0yNzAxMDIxMTM5MjFaMBQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAN3ESWaDH1JiJTy9yRJQV7kahPOxgBkIH2xwcYDL1k9deKNhSKLx7aGfxE244+HBcC6WLHKVUnOm0ld2qxQ4bMYiJXzZuqL67r07L5wxGAssv12lO92qohGmlHy3+VzRYUBmovu6upqOv3R2F8HBbo7Jc7Hvt7hOEJn/jPuFuF/fHit3mqU8l6IkrIZjpaW8T9fIWOXRq98U4+hkgWpqEZWsqlfE8BxAs9DeIMZab0GxO9stHLp+GYKx10uE4ezFcaDS8W+g2C8enCTt1HXGvcnj4o5zkC1lITGvcFTsiFqfIWyXeSufcxdc0W7HoG6J3ks0WJyK38sfFn0t2Ao6kX0CAwEAAaNQME4wHQYDVR0OBBYEFAoJzX6TVYAwC1GSPe6nObBG54zaMB8GA1UdIwQYMBaAFAoJzX6TVYAwC1GSPe6nObBG54zaMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAJia9R70uXdUZtgujUPjLas4+sVajzlBFmqhBqpLAo934vljf9HISsHrPtdBcbS0d0rucqXwabDf0MlR18ksnT/NYpsTwMbMx76CrXi4zYEEW5lISKEO65aIkzVTcqKWSuhjtSnRdB6iOLsFiKmNMWXaIKMR5T0+AbR9wdQgn08W+3EEeHGvafVQfE3STVsSgNb1ft7DvcSUnfPXGU7KzvmTpZa0Hfmc7uY4vpdEEhLAdRhgLReS7USZskov7ooiPSoD+JRFi2gM4klBxTemHdNUa9oFnHMXuYKOkLbkgFvHxyy+QlLq2ELQTga5e7I83ZyOfGctyf8Ul6vGw10vbQ=-----END CERTIFICATE-----" + And The setting "sp-privateKey" is set to "-----BEGIN RSA PRIVATE KEY-----MIIEpAIBAAKCAQEA3cRJZoMfUmIlPL3JElBXuRqE87GAGQgfbHBxgMvWT114o2FIovHtoZ/ETbjj4cFwLpYscpVSc6bSV3arFDhsxiIlfNm6ovruvTsvnDEYCyy/XaU73aqiEaaUfLf5XNFhQGai+7q6mo6/dHYXwcFujslzse+3uE4Qmf+M+4W4X98eK3eapTyXoiSshmOlpbxP18hY5dGr3xTj6GSBamoRlayqV8TwHECz0N4gxlpvQbE72y0cun4ZgrHXS4Th7MVxoNLxb6DYLx6cJO3Udca9yePijnOQLWUhMa9wVOyIWp8hbJd5K59zF1zRbsegboneSzRYnIrfyx8WfS3YCjqRfQIDAQABAoIBAQC5CQAdcqZ9vLpJNilBCJxJLCFmm+HAAREHD8MErg9A5UK1P4S1wJp/0qieGPi68wXBOTgY2xKSwMycgb04/+NyZidVRu388takOW/+KNBg8pMxdZ6/05GqnI0kivSbR3CXpYuz8hekwhpo9+fWmKjApsHL47ItK6WaeKmPbAFsq1YJGzfp/DXg7LIvh9GA3C1LWWGV7SuCGOyX/2Moi8xRa7qBtH4hDo/0NRhTx7zjYjlBgNEr330pJUopc3+AtHE40R+xMr2zkGvq9RsCZxYxD2VWbLwQW0yNjWmQ2OTuMgJJvk2+N73QLHcB+tea82ZTszsNzRS9DLtc6qbsKEPZAoGBAO78U3vEuRyY56f/1hpo0xuCDwOkWGzgBQWkjJl6dlyVz/zKkhXBHpEYImyt8XRN0W3iGZYpZ2hCFJGTcDp32R6UiEyGLz0Uc8R/tva/TiRVW1FdNczzSHcB24b9OMK4vE9JLs8mA8Rp8YBgtLr5DDuMfYt/a/rZJbg/HIfIN98nAoGBAO2OInCX93t2I6zzRPIqKtI6q6FYNp64VIQjvw9Y8l0x3IdJZRP9H5C8ZhCeYPsgEqTXcXa4j5hL4rQzoUtxfxflBUUH60bcnd4LGaTCMYLS14G011E3GZlIP0sJi5OjEhy8fq3zt6jVzS9V/lPHB8i+w1D7CbPrMpW7B3k32vC7AoGAX/HvdkYhZyjAAEOG6m1hK68IZhbp5TP+8CgCxm9S65K9wKh3A8LXibrdvzIKOP4w8WOPkCipOkMlTNibeu24vj01hztr5aK7Y40+oEtnjNCz67N3MQQO+LBHOSeaTRqrh01DPKjvZECAU2D/zfzEe3fIw2Nxr3DUYub7hkvMmosCgYAzxbVVypjiLGYsDDyrdmsstCKxoDMPNmcdAVljc+QmUXaZeXJw/8qAVb78wjeqo1vM1zNgR2rsKyW2VkZB1fN39q7GU6qAIBa7zLmDAduegmr7VrlSduq6UFeS9/qWa4TIBICrUqFlR2tXdKtgANF+e6y/mmaL8qdsoH1JetXZfwKBgQC1vscRpdAXivjOOZAh+mzJWzS4BUl4CTJLYYIuOEXikmN5g0EdV2fhUEdkewmyKnXHsd0x83167bYgpTDNs71jUxDHy5NXlg2qIjLkf09X9wr19gBzDApfWzfh3vUqttyMZuQMLVNepGCWM2vjlY9KGl5OvZqY6d+7yO0mLV9GmQ==-----END RSA PRIVATE KEY-----" + And The setting "security-wantAssertionsSigned" is set to "1" + And the local group "Astrophysics" is created + # Initial login so the user is known and belonging to SAML, directly followed by a logout + And I send a GET request to "http://localhost:8080/index.php/login" + And I should be redirected to "https://localhost:4443/idp/profile/SAML2/Redirect/SSO" + And I send a POST request to "https://localhost:4443/idp/profile/SAML2/Redirect/SSO?execution=e1s1" with the following data + |j_username|j_password|_eventId_proceed| + |student1 |password | | + And The response should be a SAML redirect page that gets submitted + And I send a GET request with requesttoken to "http://localhost:8080/index.php/apps/user_saml/saml/sls" + And the user "student1" is added to the group "Astrophysics" + # Set the proper attributes + And The setting "saml-attribute-mapping-email_mapping" is set to "urn:oid:0.9.2342.19200300.100.1.3" + And The setting "saml-attribute-mapping-displayName_mapping" is set to "urn:oid:2.5.4.42 urn:oid:2.5.4.4" + And The setting "saml-attribute-mapping-group_mapping" is set to "groups" + And the cookies are cleared + # Now login in for good + When I send a GET request to "http://localhost:8080/index.php/login" + Then I should be redirected to "https://localhost:4443/idp/profile/SAML2/Redirect/SSO" + And I send a POST request to "https://localhost:4443/idp/profile/SAML2/Redirect/SSO?execution=e1s1" with the following data + |j_username|j_password|_eventId_proceed| + |student1 |password | | + And The response should be a SAML redirect page that gets submitted + And The user value "groups" should be "Astrophysics,SAML_Astrophysics,SAML_Students" + And I expect no background job for class "OCA\\User_SAML\\Jobs\\MigrateGroups" + Then the group backend of "Astrophysics" should be "Database" + And Then the group backend of "Astrophysics" must not be "user_saml" + And The user value "groups" should be "Astrophysics,SAML_Astrophysics,SAML_Students" + + Scenario: Keeping a group with members of mixed backends local + Given The setting "type" is set to "saml" + And The setting "general-uid_mapping" is set to "urn:oid:0.9.2342.19200300.100.1.1" + And The setting "idp-entityId" is set to "https://shibboleth-integration-nextcloud.localdomain/idp/shibboleth" + And The setting "idp-singleSignOnService.url" is set to "https://localhost:4443/idp/profile/SAML2/Redirect/SSO" + And The setting "idp-singleLogoutService.url" is set to "https://localhost:4443/idp/profile/Logout" + And The setting "idp-x509cert" is set to "MIIDnTCCAoWgAwIBAgIUGPx9uPjCu7c172IUgV84Tm94pBcwDQYJKoZIhvcNAQEL BQAwNzE1MDMGA1UEAwwsc2hpYmJvbGV0aC1pbnRlZ3JhdGlvbi1uZXh0Y2xvdWQu bG9jYWxkb21haW4wHhcNMTcwMTA0MTAxMTI3WhcNMzcwMTA0MTAxMTI3WjA3MTUw MwYDVQQDDCxzaGliYm9sZXRoLWludGVncmF0aW9uLW5leHRjbG91ZC5sb2NhbGRv bWFpbjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKH+bPu45tk8/JRk XOxkyfbxocWZlY4mRumEUxscd3fn0oVzOrdWbHH7lCZV4bus4KxvJljc0Nm2K+Zr LoiRUUnf/LQ4LlehWVm5Kbc4kRgOXS0iGZN3SslAWPKyIg0tywg+TLOBPoS6EtST 1WuYg1JPMFxPfeFDWQ0dQYPlXIJWBFh6F2JMTb0FLECqA5l/ryYE13QisX5l+Mqo 6y3Dh7qIgaH0IJNobXoAcEWza7Kb2RnfhZRh9e0qjZIwBqTJUFM/6I86RYXn829s INUvYQQbez6VkGTdUQJ/GuXb/dD5sMQfOyK8hrRY5MozOmK32cz3JaAzSXpiSRS9 NxFwvicCAwEAAaOBoDCBnTAdBgNVHQ4EFgQUKn8+TV0WXSDeavvF0M8mWn1o8ukw fAYDVR0RBHUwc4Isc2hpYmJvbGV0aC1pbnRlZ3JhdGlvbi1uZXh0Y2xvdWQubG9j YWxkb21haW6GQ2h0dHBzOi8vc2hpYmJvbGV0aC1pbnRlZ3JhdGlvbi1uZXh0Y2xv dWQubG9jYWxkb21haW4vaWRwL3NoaWJib2xldGgwDQYJKoZIhvcNAQELBQADggEB ABI6uzoIeLZT9Az2KTlLxIc6jZ4MDmhaVja4ZuBxTXEb7BFLfeASEJmQm1wgIMOn pJId3Kh3njW+3tOBWKm7lj8JxVVpAu4yMFSoQGPaVUgYB1AVm+pmAyPLzfJ/XGhf esCU2F/b0eHWcaIb3x+BZFX089Cd/PBtP84MNXdo+TccibxC8N39sr45qJM/7SC7 TfDYU0L4q2WZHJr4S7+0F+F4KaxLx9NzCvN4h6XaoWofZWir2iHO4NzbrVQGC0ei QybS/neBfni4A2g1lyzCb6xFB58JBvNCn7AAnDJULOE7S5XWUKsDAQVQrxTNkUq7 pnhlCQqZDwUdgmIXd1KB1So=" + And The setting "security-authnRequestsSigned" is set to "1" + And The setting "security-wantAssertionsEncrypted" is set to "1" + And The setting "sp-x509cert" is set to "-----BEGIN CERTIFICATE-----MIIC+zCCAeOgAwIBAgIJAIgZuvWDBIrdMA0GCSqGSIb3DQEBBQUAMBQxEjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0xNzAxMDQxMTM5MjFaFw0yNzAxMDIxMTM5MjFaMBQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAN3ESWaDH1JiJTy9yRJQV7kahPOxgBkIH2xwcYDL1k9deKNhSKLx7aGfxE244+HBcC6WLHKVUnOm0ld2qxQ4bMYiJXzZuqL67r07L5wxGAssv12lO92qohGmlHy3+VzRYUBmovu6upqOv3R2F8HBbo7Jc7Hvt7hOEJn/jPuFuF/fHit3mqU8l6IkrIZjpaW8T9fIWOXRq98U4+hkgWpqEZWsqlfE8BxAs9DeIMZab0GxO9stHLp+GYKx10uE4ezFcaDS8W+g2C8enCTt1HXGvcnj4o5zkC1lITGvcFTsiFqfIWyXeSufcxdc0W7HoG6J3ks0WJyK38sfFn0t2Ao6kX0CAwEAAaNQME4wHQYDVR0OBBYEFAoJzX6TVYAwC1GSPe6nObBG54zaMB8GA1UdIwQYMBaAFAoJzX6TVYAwC1GSPe6nObBG54zaMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAJia9R70uXdUZtgujUPjLas4+sVajzlBFmqhBqpLAo934vljf9HISsHrPtdBcbS0d0rucqXwabDf0MlR18ksnT/NYpsTwMbMx76CrXi4zYEEW5lISKEO65aIkzVTcqKWSuhjtSnRdB6iOLsFiKmNMWXaIKMR5T0+AbR9wdQgn08W+3EEeHGvafVQfE3STVsSgNb1ft7DvcSUnfPXGU7KzvmTpZa0Hfmc7uY4vpdEEhLAdRhgLReS7USZskov7ooiPSoD+JRFi2gM4klBxTemHdNUa9oFnHMXuYKOkLbkgFvHxyy+QlLq2ELQTga5e7I83ZyOfGctyf8Ul6vGw10vbQ=-----END CERTIFICATE-----" + And The setting "sp-privateKey" is set to "-----BEGIN RSA PRIVATE KEY-----MIIEpAIBAAKCAQEA3cRJZoMfUmIlPL3JElBXuRqE87GAGQgfbHBxgMvWT114o2FIovHtoZ/ETbjj4cFwLpYscpVSc6bSV3arFDhsxiIlfNm6ovruvTsvnDEYCyy/XaU73aqiEaaUfLf5XNFhQGai+7q6mo6/dHYXwcFujslzse+3uE4Qmf+M+4W4X98eK3eapTyXoiSshmOlpbxP18hY5dGr3xTj6GSBamoRlayqV8TwHECz0N4gxlpvQbE72y0cun4ZgrHXS4Th7MVxoNLxb6DYLx6cJO3Udca9yePijnOQLWUhMa9wVOyIWp8hbJd5K59zF1zRbsegboneSzRYnIrfyx8WfS3YCjqRfQIDAQABAoIBAQC5CQAdcqZ9vLpJNilBCJxJLCFmm+HAAREHD8MErg9A5UK1P4S1wJp/0qieGPi68wXBOTgY2xKSwMycgb04/+NyZidVRu388takOW/+KNBg8pMxdZ6/05GqnI0kivSbR3CXpYuz8hekwhpo9+fWmKjApsHL47ItK6WaeKmPbAFsq1YJGzfp/DXg7LIvh9GA3C1LWWGV7SuCGOyX/2Moi8xRa7qBtH4hDo/0NRhTx7zjYjlBgNEr330pJUopc3+AtHE40R+xMr2zkGvq9RsCZxYxD2VWbLwQW0yNjWmQ2OTuMgJJvk2+N73QLHcB+tea82ZTszsNzRS9DLtc6qbsKEPZAoGBAO78U3vEuRyY56f/1hpo0xuCDwOkWGzgBQWkjJl6dlyVz/zKkhXBHpEYImyt8XRN0W3iGZYpZ2hCFJGTcDp32R6UiEyGLz0Uc8R/tva/TiRVW1FdNczzSHcB24b9OMK4vE9JLs8mA8Rp8YBgtLr5DDuMfYt/a/rZJbg/HIfIN98nAoGBAO2OInCX93t2I6zzRPIqKtI6q6FYNp64VIQjvw9Y8l0x3IdJZRP9H5C8ZhCeYPsgEqTXcXa4j5hL4rQzoUtxfxflBUUH60bcnd4LGaTCMYLS14G011E3GZlIP0sJi5OjEhy8fq3zt6jVzS9V/lPHB8i+w1D7CbPrMpW7B3k32vC7AoGAX/HvdkYhZyjAAEOG6m1hK68IZhbp5TP+8CgCxm9S65K9wKh3A8LXibrdvzIKOP4w8WOPkCipOkMlTNibeu24vj01hztr5aK7Y40+oEtnjNCz67N3MQQO+LBHOSeaTRqrh01DPKjvZECAU2D/zfzEe3fIw2Nxr3DUYub7hkvMmosCgYAzxbVVypjiLGYsDDyrdmsstCKxoDMPNmcdAVljc+QmUXaZeXJw/8qAVb78wjeqo1vM1zNgR2rsKyW2VkZB1fN39q7GU6qAIBa7zLmDAduegmr7VrlSduq6UFeS9/qWa4TIBICrUqFlR2tXdKtgANF+e6y/mmaL8qdsoH1JetXZfwKBgQC1vscRpdAXivjOOZAh+mzJWzS4BUl4CTJLYYIuOEXikmN5g0EdV2fhUEdkewmyKnXHsd0x83167bYgpTDNs71jUxDHy5NXlg2qIjLkf09X9wr19gBzDApfWzfh3vUqttyMZuQMLVNepGCWM2vjlY9KGl5OvZqY6d+7yO0mLV9GmQ==-----END RSA PRIVATE KEY-----" + And The setting "security-wantAssertionsSigned" is set to "1" + And The setting "localGroupsCheckForMigration" is set to '{\"dropAfter\":9223372036854775807,\"groups\":[\"Astrophysics\",\"Students\", \"Mixed Bag Of Sweets\"]}' + # Initial login so the user is known and belonging to SAML, directly followed by a logout + And I send a GET request to "http://localhost:8080/index.php/login" + And I should be redirected to "https://localhost:4443/idp/profile/SAML2/Redirect/SSO" + And I send a POST request to "https://localhost:4443/idp/profile/SAML2/Redirect/SSO?execution=e1s1" with the following data + |j_username|j_password|_eventId_proceed| + |student1 |password | | + And The response should be a SAML redirect page that gets submitted + And I send a GET request with requesttoken to "http://localhost:8080/index.php/apps/user_saml/saml/sls" + And A local user with uid "lochlan" exists + And the local group "Mixed Bag Of Sweets" is created + And the user "lochlan" is added to the group "Mixed Bag Of Sweets" + And the user "student1" is added to the group "Mixed Bag Of Sweets" + # Set the proper attributes + And The setting "saml-attribute-mapping-email_mapping" is set to "urn:oid:0.9.2342.19200300.100.1.3" + And The setting "saml-attribute-mapping-displayName_mapping" is set to "urn:oid:2.5.4.42 urn:oid:2.5.4.4" + And The setting "saml-attribute-mapping-group_mapping" is set to "groups" + And the cookies are cleared + # Now login in for good + When I send a GET request to "http://localhost:8080/index.php/login" + Then I should be redirected to "https://localhost:4443/idp/profile/SAML2/Redirect/SSO" + And I send a POST request to "https://localhost:4443/idp/profile/SAML2/Redirect/SSO?execution=e1s1" with the following data + |j_username|j_password|_eventId_proceed| + |student1 |password | | + And The response should be a SAML redirect page that gets submitted + And The user value "groups" should be "Mixed Bag Of Sweets,SAML_Astrophysics,SAML_Students" + When I execute the background job for class "OCA\\User_SAML\\Jobs\\MigrateGroups" + Then the group backend of "Mixed Bag Of Sweets" should be "Database" + And Then the group backend of "Mixed Bag Of Sweets" must not be "user_saml" + And The user value "groups" should be "Mixed Bag Of Sweets,SAML_Astrophysics,SAML_Students" + + Scenario: Not migrating a group with only SAML members that is not in the migration list + Given The setting "type" is set to "saml" + And The setting "general-uid_mapping" is set to "urn:oid:0.9.2342.19200300.100.1.1" + And The setting "idp-entityId" is set to "https://shibboleth-integration-nextcloud.localdomain/idp/shibboleth" + And The setting "idp-singleSignOnService.url" is set to "https://localhost:4443/idp/profile/SAML2/Redirect/SSO" + And The setting "idp-singleLogoutService.url" is set to "https://localhost:4443/idp/profile/Logout" + And The setting "idp-x509cert" is set to "MIIDnTCCAoWgAwIBAgIUGPx9uPjCu7c172IUgV84Tm94pBcwDQYJKoZIhvcNAQEL BQAwNzE1MDMGA1UEAwwsc2hpYmJvbGV0aC1pbnRlZ3JhdGlvbi1uZXh0Y2xvdWQu bG9jYWxkb21haW4wHhcNMTcwMTA0MTAxMTI3WhcNMzcwMTA0MTAxMTI3WjA3MTUw MwYDVQQDDCxzaGliYm9sZXRoLWludGVncmF0aW9uLW5leHRjbG91ZC5sb2NhbGRv bWFpbjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKH+bPu45tk8/JRk XOxkyfbxocWZlY4mRumEUxscd3fn0oVzOrdWbHH7lCZV4bus4KxvJljc0Nm2K+Zr LoiRUUnf/LQ4LlehWVm5Kbc4kRgOXS0iGZN3SslAWPKyIg0tywg+TLOBPoS6EtST 1WuYg1JPMFxPfeFDWQ0dQYPlXIJWBFh6F2JMTb0FLECqA5l/ryYE13QisX5l+Mqo 6y3Dh7qIgaH0IJNobXoAcEWza7Kb2RnfhZRh9e0qjZIwBqTJUFM/6I86RYXn829s INUvYQQbez6VkGTdUQJ/GuXb/dD5sMQfOyK8hrRY5MozOmK32cz3JaAzSXpiSRS9 NxFwvicCAwEAAaOBoDCBnTAdBgNVHQ4EFgQUKn8+TV0WXSDeavvF0M8mWn1o8ukw fAYDVR0RBHUwc4Isc2hpYmJvbGV0aC1pbnRlZ3JhdGlvbi1uZXh0Y2xvdWQubG9j YWxkb21haW6GQ2h0dHBzOi8vc2hpYmJvbGV0aC1pbnRlZ3JhdGlvbi1uZXh0Y2xv dWQubG9jYWxkb21haW4vaWRwL3NoaWJib2xldGgwDQYJKoZIhvcNAQELBQADggEB ABI6uzoIeLZT9Az2KTlLxIc6jZ4MDmhaVja4ZuBxTXEb7BFLfeASEJmQm1wgIMOn pJId3Kh3njW+3tOBWKm7lj8JxVVpAu4yMFSoQGPaVUgYB1AVm+pmAyPLzfJ/XGhf esCU2F/b0eHWcaIb3x+BZFX089Cd/PBtP84MNXdo+TccibxC8N39sr45qJM/7SC7 TfDYU0L4q2WZHJr4S7+0F+F4KaxLx9NzCvN4h6XaoWofZWir2iHO4NzbrVQGC0ei QybS/neBfni4A2g1lyzCb6xFB58JBvNCn7AAnDJULOE7S5XWUKsDAQVQrxTNkUq7 pnhlCQqZDwUdgmIXd1KB1So=" + And The setting "security-authnRequestsSigned" is set to "1" + And The setting "security-wantAssertionsEncrypted" is set to "1" + And The setting "sp-x509cert" is set to "-----BEGIN CERTIFICATE-----MIIC+zCCAeOgAwIBAgIJAIgZuvWDBIrdMA0GCSqGSIb3DQEBBQUAMBQxEjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0xNzAxMDQxMTM5MjFaFw0yNzAxMDIxMTM5MjFaMBQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAN3ESWaDH1JiJTy9yRJQV7kahPOxgBkIH2xwcYDL1k9deKNhSKLx7aGfxE244+HBcC6WLHKVUnOm0ld2qxQ4bMYiJXzZuqL67r07L5wxGAssv12lO92qohGmlHy3+VzRYUBmovu6upqOv3R2F8HBbo7Jc7Hvt7hOEJn/jPuFuF/fHit3mqU8l6IkrIZjpaW8T9fIWOXRq98U4+hkgWpqEZWsqlfE8BxAs9DeIMZab0GxO9stHLp+GYKx10uE4ezFcaDS8W+g2C8enCTt1HXGvcnj4o5zkC1lITGvcFTsiFqfIWyXeSufcxdc0W7HoG6J3ks0WJyK38sfFn0t2Ao6kX0CAwEAAaNQME4wHQYDVR0OBBYEFAoJzX6TVYAwC1GSPe6nObBG54zaMB8GA1UdIwQYMBaAFAoJzX6TVYAwC1GSPe6nObBG54zaMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAJia9R70uXdUZtgujUPjLas4+sVajzlBFmqhBqpLAo934vljf9HISsHrPtdBcbS0d0rucqXwabDf0MlR18ksnT/NYpsTwMbMx76CrXi4zYEEW5lISKEO65aIkzVTcqKWSuhjtSnRdB6iOLsFiKmNMWXaIKMR5T0+AbR9wdQgn08W+3EEeHGvafVQfE3STVsSgNb1ft7DvcSUnfPXGU7KzvmTpZa0Hfmc7uY4vpdEEhLAdRhgLReS7USZskov7ooiPSoD+JRFi2gM4klBxTemHdNUa9oFnHMXuYKOkLbkgFvHxyy+QlLq2ELQTga5e7I83ZyOfGctyf8Ul6vGw10vbQ=-----END CERTIFICATE-----" + And The setting "sp-privateKey" is set to "-----BEGIN RSA PRIVATE KEY-----MIIEpAIBAAKCAQEA3cRJZoMfUmIlPL3JElBXuRqE87GAGQgfbHBxgMvWT114o2FIovHtoZ/ETbjj4cFwLpYscpVSc6bSV3arFDhsxiIlfNm6ovruvTsvnDEYCyy/XaU73aqiEaaUfLf5XNFhQGai+7q6mo6/dHYXwcFujslzse+3uE4Qmf+M+4W4X98eK3eapTyXoiSshmOlpbxP18hY5dGr3xTj6GSBamoRlayqV8TwHECz0N4gxlpvQbE72y0cun4ZgrHXS4Th7MVxoNLxb6DYLx6cJO3Udca9yePijnOQLWUhMa9wVOyIWp8hbJd5K59zF1zRbsegboneSzRYnIrfyx8WfS3YCjqRfQIDAQABAoIBAQC5CQAdcqZ9vLpJNilBCJxJLCFmm+HAAREHD8MErg9A5UK1P4S1wJp/0qieGPi68wXBOTgY2xKSwMycgb04/+NyZidVRu388takOW/+KNBg8pMxdZ6/05GqnI0kivSbR3CXpYuz8hekwhpo9+fWmKjApsHL47ItK6WaeKmPbAFsq1YJGzfp/DXg7LIvh9GA3C1LWWGV7SuCGOyX/2Moi8xRa7qBtH4hDo/0NRhTx7zjYjlBgNEr330pJUopc3+AtHE40R+xMr2zkGvq9RsCZxYxD2VWbLwQW0yNjWmQ2OTuMgJJvk2+N73QLHcB+tea82ZTszsNzRS9DLtc6qbsKEPZAoGBAO78U3vEuRyY56f/1hpo0xuCDwOkWGzgBQWkjJl6dlyVz/zKkhXBHpEYImyt8XRN0W3iGZYpZ2hCFJGTcDp32R6UiEyGLz0Uc8R/tva/TiRVW1FdNczzSHcB24b9OMK4vE9JLs8mA8Rp8YBgtLr5DDuMfYt/a/rZJbg/HIfIN98nAoGBAO2OInCX93t2I6zzRPIqKtI6q6FYNp64VIQjvw9Y8l0x3IdJZRP9H5C8ZhCeYPsgEqTXcXa4j5hL4rQzoUtxfxflBUUH60bcnd4LGaTCMYLS14G011E3GZlIP0sJi5OjEhy8fq3zt6jVzS9V/lPHB8i+w1D7CbPrMpW7B3k32vC7AoGAX/HvdkYhZyjAAEOG6m1hK68IZhbp5TP+8CgCxm9S65K9wKh3A8LXibrdvzIKOP4w8WOPkCipOkMlTNibeu24vj01hztr5aK7Y40+oEtnjNCz67N3MQQO+LBHOSeaTRqrh01DPKjvZECAU2D/zfzEe3fIw2Nxr3DUYub7hkvMmosCgYAzxbVVypjiLGYsDDyrdmsstCKxoDMPNmcdAVljc+QmUXaZeXJw/8qAVb78wjeqo1vM1zNgR2rsKyW2VkZB1fN39q7GU6qAIBa7zLmDAduegmr7VrlSduq6UFeS9/qWa4TIBICrUqFlR2tXdKtgANF+e6y/mmaL8qdsoH1JetXZfwKBgQC1vscRpdAXivjOOZAh+mzJWzS4BUl4CTJLYYIuOEXikmN5g0EdV2fhUEdkewmyKnXHsd0x83167bYgpTDNs71jUxDHy5NXlg2qIjLkf09X9wr19gBzDApfWzfh3vUqttyMZuQMLVNepGCWM2vjlY9KGl5OvZqY6d+7yO0mLV9GmQ==-----END RSA PRIVATE KEY-----" + And The setting "security-wantAssertionsSigned" is set to "1" + And The setting "localGroupsCheckForMigration" is set to '{\"dropAfter\":9223372036854775807,\"groups\":[\"Students\"]}' + And the local group "Astrophysics" is created + # Initial login so the user is known and belonging to SAML, directly followed by a logout + And I send a GET request to "http://localhost:8080/index.php/login" + And I should be redirected to "https://localhost:4443/idp/profile/SAML2/Redirect/SSO" + And I send a POST request to "https://localhost:4443/idp/profile/SAML2/Redirect/SSO?execution=e1s1" with the following data + |j_username|j_password|_eventId_proceed| + |student1 |password | | + And The response should be a SAML redirect page that gets submitted + And I send a GET request with requesttoken to "http://localhost:8080/index.php/apps/user_saml/saml/sls" + And the user "student1" is added to the group "Astrophysics" + # Set the proper attributes + And The setting "saml-attribute-mapping-email_mapping" is set to "urn:oid:0.9.2342.19200300.100.1.3" + And The setting "saml-attribute-mapping-displayName_mapping" is set to "urn:oid:2.5.4.42 urn:oid:2.5.4.4" + And The setting "saml-attribute-mapping-group_mapping" is set to "groups" + And the cookies are cleared + # Now login in for good + When I send a GET request to "http://localhost:8080/index.php/login" + Then I should be redirected to "https://localhost:4443/idp/profile/SAML2/Redirect/SSO" + And I send a POST request to "https://localhost:4443/idp/profile/SAML2/Redirect/SSO?execution=e1s1" with the following data + |j_username|j_password|_eventId_proceed| + |student1 |password | | + And The response should be a SAML redirect page that gets submitted + And The user value "groups" should be "Astrophysics,SAML_Astrophysics,SAML_Students" + When I execute the background job for class "OCA\\User_SAML\\Jobs\\MigrateGroups" + Then the group backend of "Astrophysics" should be "Database" + And Then the group backend of "Astrophysics" must not be "user_saml" + And The user value "groups" should be "Astrophysics,SAML_Astrophysics,SAML_Students" + + Scenario: Not migrating a local group to SAML backend when removing the last SAML user of it + Given The setting "type" is set to "saml" + And The setting "general-uid_mapping" is set to "urn:oid:0.9.2342.19200300.100.1.1" + And The setting "idp-entityId" is set to "https://shibboleth-integration-nextcloud.localdomain/idp/shibboleth" + And The setting "idp-singleSignOnService.url" is set to "https://localhost:4443/idp/profile/SAML2/Redirect/SSO" + And The setting "idp-singleLogoutService.url" is set to "https://localhost:4443/idp/profile/Logout" + And The setting "idp-x509cert" is set to "MIIDnTCCAoWgAwIBAgIUGPx9uPjCu7c172IUgV84Tm94pBcwDQYJKoZIhvcNAQEL BQAwNzE1MDMGA1UEAwwsc2hpYmJvbGV0aC1pbnRlZ3JhdGlvbi1uZXh0Y2xvdWQu bG9jYWxkb21haW4wHhcNMTcwMTA0MTAxMTI3WhcNMzcwMTA0MTAxMTI3WjA3MTUw MwYDVQQDDCxzaGliYm9sZXRoLWludGVncmF0aW9uLW5leHRjbG91ZC5sb2NhbGRv bWFpbjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKH+bPu45tk8/JRk XOxkyfbxocWZlY4mRumEUxscd3fn0oVzOrdWbHH7lCZV4bus4KxvJljc0Nm2K+Zr LoiRUUnf/LQ4LlehWVm5Kbc4kRgOXS0iGZN3SslAWPKyIg0tywg+TLOBPoS6EtST 1WuYg1JPMFxPfeFDWQ0dQYPlXIJWBFh6F2JMTb0FLECqA5l/ryYE13QisX5l+Mqo 6y3Dh7qIgaH0IJNobXoAcEWza7Kb2RnfhZRh9e0qjZIwBqTJUFM/6I86RYXn829s INUvYQQbez6VkGTdUQJ/GuXb/dD5sMQfOyK8hrRY5MozOmK32cz3JaAzSXpiSRS9 NxFwvicCAwEAAaOBoDCBnTAdBgNVHQ4EFgQUKn8+TV0WXSDeavvF0M8mWn1o8ukw fAYDVR0RBHUwc4Isc2hpYmJvbGV0aC1pbnRlZ3JhdGlvbi1uZXh0Y2xvdWQubG9j YWxkb21haW6GQ2h0dHBzOi8vc2hpYmJvbGV0aC1pbnRlZ3JhdGlvbi1uZXh0Y2xv dWQubG9jYWxkb21haW4vaWRwL3NoaWJib2xldGgwDQYJKoZIhvcNAQELBQADggEB ABI6uzoIeLZT9Az2KTlLxIc6jZ4MDmhaVja4ZuBxTXEb7BFLfeASEJmQm1wgIMOn pJId3Kh3njW+3tOBWKm7lj8JxVVpAu4yMFSoQGPaVUgYB1AVm+pmAyPLzfJ/XGhf esCU2F/b0eHWcaIb3x+BZFX089Cd/PBtP84MNXdo+TccibxC8N39sr45qJM/7SC7 TfDYU0L4q2WZHJr4S7+0F+F4KaxLx9NzCvN4h6XaoWofZWir2iHO4NzbrVQGC0ei QybS/neBfni4A2g1lyzCb6xFB58JBvNCn7AAnDJULOE7S5XWUKsDAQVQrxTNkUq7 pnhlCQqZDwUdgmIXd1KB1So=" + And The setting "security-authnRequestsSigned" is set to "1" + And The setting "security-wantAssertionsEncrypted" is set to "1" + And The setting "sp-x509cert" is set to "-----BEGIN CERTIFICATE-----MIIC+zCCAeOgAwIBAgIJAIgZuvWDBIrdMA0GCSqGSIb3DQEBBQUAMBQxEjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0xNzAxMDQxMTM5MjFaFw0yNzAxMDIxMTM5MjFaMBQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAN3ESWaDH1JiJTy9yRJQV7kahPOxgBkIH2xwcYDL1k9deKNhSKLx7aGfxE244+HBcC6WLHKVUnOm0ld2qxQ4bMYiJXzZuqL67r07L5wxGAssv12lO92qohGmlHy3+VzRYUBmovu6upqOv3R2F8HBbo7Jc7Hvt7hOEJn/jPuFuF/fHit3mqU8l6IkrIZjpaW8T9fIWOXRq98U4+hkgWpqEZWsqlfE8BxAs9DeIMZab0GxO9stHLp+GYKx10uE4ezFcaDS8W+g2C8enCTt1HXGvcnj4o5zkC1lITGvcFTsiFqfIWyXeSufcxdc0W7HoG6J3ks0WJyK38sfFn0t2Ao6kX0CAwEAAaNQME4wHQYDVR0OBBYEFAoJzX6TVYAwC1GSPe6nObBG54zaMB8GA1UdIwQYMBaAFAoJzX6TVYAwC1GSPe6nObBG54zaMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAJia9R70uXdUZtgujUPjLas4+sVajzlBFmqhBqpLAo934vljf9HISsHrPtdBcbS0d0rucqXwabDf0MlR18ksnT/NYpsTwMbMx76CrXi4zYEEW5lISKEO65aIkzVTcqKWSuhjtSnRdB6iOLsFiKmNMWXaIKMR5T0+AbR9wdQgn08W+3EEeHGvafVQfE3STVsSgNb1ft7DvcSUnfPXGU7KzvmTpZa0Hfmc7uY4vpdEEhLAdRhgLReS7USZskov7ooiPSoD+JRFi2gM4klBxTemHdNUa9oFnHMXuYKOkLbkgFvHxyy+QlLq2ELQTga5e7I83ZyOfGctyf8Ul6vGw10vbQ=-----END CERTIFICATE-----" + And The setting "sp-privateKey" is set to "-----BEGIN RSA PRIVATE KEY-----MIIEpAIBAAKCAQEA3cRJZoMfUmIlPL3JElBXuRqE87GAGQgfbHBxgMvWT114o2FIovHtoZ/ETbjj4cFwLpYscpVSc6bSV3arFDhsxiIlfNm6ovruvTsvnDEYCyy/XaU73aqiEaaUfLf5XNFhQGai+7q6mo6/dHYXwcFujslzse+3uE4Qmf+M+4W4X98eK3eapTyXoiSshmOlpbxP18hY5dGr3xTj6GSBamoRlayqV8TwHECz0N4gxlpvQbE72y0cun4ZgrHXS4Th7MVxoNLxb6DYLx6cJO3Udca9yePijnOQLWUhMa9wVOyIWp8hbJd5K59zF1zRbsegboneSzRYnIrfyx8WfS3YCjqRfQIDAQABAoIBAQC5CQAdcqZ9vLpJNilBCJxJLCFmm+HAAREHD8MErg9A5UK1P4S1wJp/0qieGPi68wXBOTgY2xKSwMycgb04/+NyZidVRu388takOW/+KNBg8pMxdZ6/05GqnI0kivSbR3CXpYuz8hekwhpo9+fWmKjApsHL47ItK6WaeKmPbAFsq1YJGzfp/DXg7LIvh9GA3C1LWWGV7SuCGOyX/2Moi8xRa7qBtH4hDo/0NRhTx7zjYjlBgNEr330pJUopc3+AtHE40R+xMr2zkGvq9RsCZxYxD2VWbLwQW0yNjWmQ2OTuMgJJvk2+N73QLHcB+tea82ZTszsNzRS9DLtc6qbsKEPZAoGBAO78U3vEuRyY56f/1hpo0xuCDwOkWGzgBQWkjJl6dlyVz/zKkhXBHpEYImyt8XRN0W3iGZYpZ2hCFJGTcDp32R6UiEyGLz0Uc8R/tva/TiRVW1FdNczzSHcB24b9OMK4vE9JLs8mA8Rp8YBgtLr5DDuMfYt/a/rZJbg/HIfIN98nAoGBAO2OInCX93t2I6zzRPIqKtI6q6FYNp64VIQjvw9Y8l0x3IdJZRP9H5C8ZhCeYPsgEqTXcXa4j5hL4rQzoUtxfxflBUUH60bcnd4LGaTCMYLS14G011E3GZlIP0sJi5OjEhy8fq3zt6jVzS9V/lPHB8i+w1D7CbPrMpW7B3k32vC7AoGAX/HvdkYhZyjAAEOG6m1hK68IZhbp5TP+8CgCxm9S65K9wKh3A8LXibrdvzIKOP4w8WOPkCipOkMlTNibeu24vj01hztr5aK7Y40+oEtnjNCz67N3MQQO+LBHOSeaTRqrh01DPKjvZECAU2D/zfzEe3fIw2Nxr3DUYub7hkvMmosCgYAzxbVVypjiLGYsDDyrdmsstCKxoDMPNmcdAVljc+QmUXaZeXJw/8qAVb78wjeqo1vM1zNgR2rsKyW2VkZB1fN39q7GU6qAIBa7zLmDAduegmr7VrlSduq6UFeS9/qWa4TIBICrUqFlR2tXdKtgANF+e6y/mmaL8qdsoH1JetXZfwKBgQC1vscRpdAXivjOOZAh+mzJWzS4BUl4CTJLYYIuOEXikmN5g0EdV2fhUEdkewmyKnXHsd0x83167bYgpTDNs71jUxDHy5NXlg2qIjLkf09X9wr19gBzDApfWzfh3vUqttyMZuQMLVNepGCWM2vjlY9KGl5OvZqY6d+7yO0mLV9GmQ==-----END RSA PRIVATE KEY-----" + And The setting "security-wantAssertionsSigned" is set to "1" + And The setting "localGroupsCheckForMigration" is set to '{\"dropAfter\":9223372036854775807,\"groups\":[\"Archaeologists\", \"Astrophysics\",\"Students\"]}' + And the local group "Archaeologists" is created + # Initial login so the user is known and belonging to SAML, directly followed by a logout + And I send a GET request to "http://localhost:8080/index.php/login" + And I should be redirected to "https://localhost:4443/idp/profile/SAML2/Redirect/SSO" + And I send a POST request to "https://localhost:4443/idp/profile/SAML2/Redirect/SSO?execution=e1s1" with the following data + |j_username|j_password|_eventId_proceed| + |student1 |password | | + And The response should be a SAML redirect page that gets submitted + And I send a GET request with requesttoken to "http://localhost:8080/index.php/apps/user_saml/saml/sls" + # Set the proper attributes + And The setting "saml-attribute-mapping-email_mapping" is set to "urn:oid:0.9.2342.19200300.100.1.3" + And The setting "saml-attribute-mapping-displayName_mapping" is set to "urn:oid:2.5.4.42 urn:oid:2.5.4.4" + And The setting "saml-attribute-mapping-group_mapping" is set to "groups" + And the cookies are cleared + And the user "student1" is added to the group "Archaeologists" + # Now login in for good + When I send a GET request to "http://localhost:8080/index.php/login" + Then I should be redirected to "https://localhost:4443/idp/profile/SAML2/Redirect/SSO" + And I send a POST request to "https://localhost:4443/idp/profile/SAML2/Redirect/SSO?execution=e1s1" with the following data + |j_username|j_password|_eventId_proceed| + |student1 |password | | + And The response should be a SAML redirect page that gets submitted + #And I should be redirected to "http://localhost:8080/index.php/apps/dashboard/" + And The user value "groups" should be "SAML_Astrophysics,SAML_Students" + And the group backend of "Archaeologists" should be "Database" + And Then the group backend of "Archaeologists" must not be "user_saml" + When I execute the background job for class "OCA\\User_SAML\\Jobs\\MigrateGroups" + Then the group backend of "Archaeologists" should be "Database" + And Then the group backend of "Archaeologists" must not be "user_saml" + And The user value "groups" should be "SAML_Astrophysics,SAML_Students" + + Scenario: Removing a previously assigned SAML group + Given The setting "type" is set to "saml" + And The setting "general-uid_mapping" is set to "urn:oid:0.9.2342.19200300.100.1.1" + And The setting "idp-entityId" is set to "https://shibboleth-integration-nextcloud.localdomain/idp/shibboleth" + And The setting "idp-singleSignOnService.url" is set to "https://localhost:4443/idp/profile/SAML2/Redirect/SSO" + And The setting "idp-singleLogoutService.url" is set to "https://localhost:4443/idp/profile/Logout" + And The setting "idp-x509cert" is set to "MIIDnTCCAoWgAwIBAgIUGPx9uPjCu7c172IUgV84Tm94pBcwDQYJKoZIhvcNAQEL BQAwNzE1MDMGA1UEAwwsc2hpYmJvbGV0aC1pbnRlZ3JhdGlvbi1uZXh0Y2xvdWQu bG9jYWxkb21haW4wHhcNMTcwMTA0MTAxMTI3WhcNMzcwMTA0MTAxMTI3WjA3MTUw MwYDVQQDDCxzaGliYm9sZXRoLWludGVncmF0aW9uLW5leHRjbG91ZC5sb2NhbGRv bWFpbjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKH+bPu45tk8/JRk XOxkyfbxocWZlY4mRumEUxscd3fn0oVzOrdWbHH7lCZV4bus4KxvJljc0Nm2K+Zr LoiRUUnf/LQ4LlehWVm5Kbc4kRgOXS0iGZN3SslAWPKyIg0tywg+TLOBPoS6EtST 1WuYg1JPMFxPfeFDWQ0dQYPlXIJWBFh6F2JMTb0FLECqA5l/ryYE13QisX5l+Mqo 6y3Dh7qIgaH0IJNobXoAcEWza7Kb2RnfhZRh9e0qjZIwBqTJUFM/6I86RYXn829s INUvYQQbez6VkGTdUQJ/GuXb/dD5sMQfOyK8hrRY5MozOmK32cz3JaAzSXpiSRS9 NxFwvicCAwEAAaOBoDCBnTAdBgNVHQ4EFgQUKn8+TV0WXSDeavvF0M8mWn1o8ukw fAYDVR0RBHUwc4Isc2hpYmJvbGV0aC1pbnRlZ3JhdGlvbi1uZXh0Y2xvdWQubG9j YWxkb21haW6GQ2h0dHBzOi8vc2hpYmJvbGV0aC1pbnRlZ3JhdGlvbi1uZXh0Y2xv dWQubG9jYWxkb21haW4vaWRwL3NoaWJib2xldGgwDQYJKoZIhvcNAQELBQADggEB ABI6uzoIeLZT9Az2KTlLxIc6jZ4MDmhaVja4ZuBxTXEb7BFLfeASEJmQm1wgIMOn pJId3Kh3njW+3tOBWKm7lj8JxVVpAu4yMFSoQGPaVUgYB1AVm+pmAyPLzfJ/XGhf esCU2F/b0eHWcaIb3x+BZFX089Cd/PBtP84MNXdo+TccibxC8N39sr45qJM/7SC7 TfDYU0L4q2WZHJr4S7+0F+F4KaxLx9NzCvN4h6XaoWofZWir2iHO4NzbrVQGC0ei QybS/neBfni4A2g1lyzCb6xFB58JBvNCn7AAnDJULOE7S5XWUKsDAQVQrxTNkUq7 pnhlCQqZDwUdgmIXd1KB1So=" + And The setting "security-authnRequestsSigned" is set to "1" + And The setting "security-wantAssertionsEncrypted" is set to "1" + And The setting "sp-x509cert" is set to "-----BEGIN CERTIFICATE-----MIIC+zCCAeOgAwIBAgIJAIgZuvWDBIrdMA0GCSqGSIb3DQEBBQUAMBQxEjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0xNzAxMDQxMTM5MjFaFw0yNzAxMDIxMTM5MjFaMBQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAN3ESWaDH1JiJTy9yRJQV7kahPOxgBkIH2xwcYDL1k9deKNhSKLx7aGfxE244+HBcC6WLHKVUnOm0ld2qxQ4bMYiJXzZuqL67r07L5wxGAssv12lO92qohGmlHy3+VzRYUBmovu6upqOv3R2F8HBbo7Jc7Hvt7hOEJn/jPuFuF/fHit3mqU8l6IkrIZjpaW8T9fIWOXRq98U4+hkgWpqEZWsqlfE8BxAs9DeIMZab0GxO9stHLp+GYKx10uE4ezFcaDS8W+g2C8enCTt1HXGvcnj4o5zkC1lITGvcFTsiFqfIWyXeSufcxdc0W7HoG6J3ks0WJyK38sfFn0t2Ao6kX0CAwEAAaNQME4wHQYDVR0OBBYEFAoJzX6TVYAwC1GSPe6nObBG54zaMB8GA1UdIwQYMBaAFAoJzX6TVYAwC1GSPe6nObBG54zaMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAJia9R70uXdUZtgujUPjLas4+sVajzlBFmqhBqpLAo934vljf9HISsHrPtdBcbS0d0rucqXwabDf0MlR18ksnT/NYpsTwMbMx76CrXi4zYEEW5lISKEO65aIkzVTcqKWSuhjtSnRdB6iOLsFiKmNMWXaIKMR5T0+AbR9wdQgn08W+3EEeHGvafVQfE3STVsSgNb1ft7DvcSUnfPXGU7KzvmTpZa0Hfmc7uY4vpdEEhLAdRhgLReS7USZskov7ooiPSoD+JRFi2gM4klBxTemHdNUa9oFnHMXuYKOkLbkgFvHxyy+QlLq2ELQTga5e7I83ZyOfGctyf8Ul6vGw10vbQ=-----END CERTIFICATE-----" + And The setting "sp-privateKey" is set to "-----BEGIN RSA PRIVATE KEY-----MIIEpAIBAAKCAQEA3cRJZoMfUmIlPL3JElBXuRqE87GAGQgfbHBxgMvWT114o2FIovHtoZ/ETbjj4cFwLpYscpVSc6bSV3arFDhsxiIlfNm6ovruvTsvnDEYCyy/XaU73aqiEaaUfLf5XNFhQGai+7q6mo6/dHYXwcFujslzse+3uE4Qmf+M+4W4X98eK3eapTyXoiSshmOlpbxP18hY5dGr3xTj6GSBamoRlayqV8TwHECz0N4gxlpvQbE72y0cun4ZgrHXS4Th7MVxoNLxb6DYLx6cJO3Udca9yePijnOQLWUhMa9wVOyIWp8hbJd5K59zF1zRbsegboneSzRYnIrfyx8WfS3YCjqRfQIDAQABAoIBAQC5CQAdcqZ9vLpJNilBCJxJLCFmm+HAAREHD8MErg9A5UK1P4S1wJp/0qieGPi68wXBOTgY2xKSwMycgb04/+NyZidVRu388takOW/+KNBg8pMxdZ6/05GqnI0kivSbR3CXpYuz8hekwhpo9+fWmKjApsHL47ItK6WaeKmPbAFsq1YJGzfp/DXg7LIvh9GA3C1LWWGV7SuCGOyX/2Moi8xRa7qBtH4hDo/0NRhTx7zjYjlBgNEr330pJUopc3+AtHE40R+xMr2zkGvq9RsCZxYxD2VWbLwQW0yNjWmQ2OTuMgJJvk2+N73QLHcB+tea82ZTszsNzRS9DLtc6qbsKEPZAoGBAO78U3vEuRyY56f/1hpo0xuCDwOkWGzgBQWkjJl6dlyVz/zKkhXBHpEYImyt8XRN0W3iGZYpZ2hCFJGTcDp32R6UiEyGLz0Uc8R/tva/TiRVW1FdNczzSHcB24b9OMK4vE9JLs8mA8Rp8YBgtLr5DDuMfYt/a/rZJbg/HIfIN98nAoGBAO2OInCX93t2I6zzRPIqKtI6q6FYNp64VIQjvw9Y8l0x3IdJZRP9H5C8ZhCeYPsgEqTXcXa4j5hL4rQzoUtxfxflBUUH60bcnd4LGaTCMYLS14G011E3GZlIP0sJi5OjEhy8fq3zt6jVzS9V/lPHB8i+w1D7CbPrMpW7B3k32vC7AoGAX/HvdkYhZyjAAEOG6m1hK68IZhbp5TP+8CgCxm9S65K9wKh3A8LXibrdvzIKOP4w8WOPkCipOkMlTNibeu24vj01hztr5aK7Y40+oEtnjNCz67N3MQQO+LBHOSeaTRqrh01DPKjvZECAU2D/zfzEe3fIw2Nxr3DUYub7hkvMmosCgYAzxbVVypjiLGYsDDyrdmsstCKxoDMPNmcdAVljc+QmUXaZeXJw/8qAVb78wjeqo1vM1zNgR2rsKyW2VkZB1fN39q7GU6qAIBa7zLmDAduegmr7VrlSduq6UFeS9/qWa4TIBICrUqFlR2tXdKtgANF+e6y/mmaL8qdsoH1JetXZfwKBgQC1vscRpdAXivjOOZAh+mzJWzS4BUl4CTJLYYIuOEXikmN5g0EdV2fhUEdkewmyKnXHsd0x83167bYgpTDNs71jUxDHy5NXlg2qIjLkf09X9wr19gBzDApfWzfh3vUqttyMZuQMLVNepGCWM2vjlY9KGl5OvZqY6d+7yO0mLV9GmQ==-----END RSA PRIVATE KEY-----" + And The setting "security-wantAssertionsSigned" is set to "1" + # Initial login so the user is known and belonging to SAML, directly followed by a logout + And I send a GET request to "http://localhost:8080/index.php/login" + And I should be redirected to "https://localhost:4443/idp/profile/SAML2/Redirect/SSO" + And I send a POST request to "https://localhost:4443/idp/profile/SAML2/Redirect/SSO?execution=e1s1" with the following data + |j_username|j_password|_eventId_proceed| + |student1 |password | | + And The response should be a SAML redirect page that gets submitted + And I send a GET request with requesttoken to "http://localhost:8080/index.php/apps/user_saml/saml/sls" + And the cookies are cleared + # Set the proper attributes + And The setting "saml-attribute-mapping-email_mapping" is set to "urn:oid:0.9.2342.19200300.100.1.3" + And The setting "saml-attribute-mapping-displayName_mapping" is set to "urn:oid:2.5.4.42 urn:oid:2.5.4.4" + And The setting "saml-attribute-mapping-group_mapping" is set to "groups" + # Pull in an extra SAML group via another user + And I send a GET request to "http://localhost:8080/index.php/login" + And I should be redirected to "https://localhost:4443/idp/profile/SAML2/Redirect/SSO" + And I send a POST request to "https://localhost:4443/idp/profile/SAML2/Redirect/SSO?execution=e1s1" with the following data + |j_username|j_password|_eventId_proceed| + |student1 |password | | + And The response should be a SAML redirect page that gets submitted + And I send a GET request with requesttoken to "http://localhost:8080/index.php/apps/user_saml/saml/sls" + And the cookies are cleared + And the user "student2" is added to the group "SAML_Archaeologists" + # Now login in for good + When I send a GET request to "http://localhost:8080/index.php/login" + Then I should be redirected to "https://localhost:4443/idp/profile/SAML2/Redirect/SSO" + And I send a POST request to "https://localhost:4443/idp/profile/SAML2/Redirect/SSO?execution=e1s1" with the following data + |j_username|j_password|_eventId_proceed| + |student2 |password | | + And The response should be a SAML redirect page that gets submitted + And The user value "groups" should be "SAML_Students" diff --git a/tests/integration/features/bootstrap/FeatureContext.php b/tests/integration/features/bootstrap/FeatureContext.php index 96dad4620..93884fb15 100644 --- a/tests/integration/features/bootstrap/FeatureContext.php +++ b/tests/integration/features/bootstrap/FeatureContext.php @@ -21,6 +21,7 @@ use Behat\Behat\Context\Context; use Behat\Gherkin\Node\TableNode; +use GuzzleHttp\Cookie\CookieJar; class FeatureContext implements Context { /** @var \GuzzleHttp\Message\Response */ @@ -32,6 +33,7 @@ class FeatureContext implements Context { private const ENV_CONFIG_FILE = __DIR__ . '/../../../../../../config/env.config.php'; private const MAIN_CONFIG_FILE = __DIR__ . '/../../../../../../config/config.php'; + private CookieJar $cookieJar; public function __construct() { date_default_timezone_set('Europe/Berlin'); @@ -39,10 +41,10 @@ public function __construct() { /** @BeforeScenario */ public function before() { - $jar = new \GuzzleHttp\Cookie\FileCookieJar('/tmp/cookies_' . md5(openssl_random_pseudo_bytes(12))); + $this->cookieJar = new CookieJar(); $this->client = new \GuzzleHttp\Client([ 'version' => 2.0, - 'cookies' => $jar, + 'cookies' => $this->cookieJar, 'verify' => false, 'allow_redirects' => [ 'referer' => true, @@ -66,9 +68,28 @@ public function after() { $user ) ); - if (file_exists(self::ENV_CONFIG_FILE)) { - unlink(self::ENV_CONFIG_FILE); - } + } + + $groups = [ + 'Astrophysics', + 'SAML_Astrophysics', + 'Students', + 'SAML_Students' + ]; + + foreach ($groups as $group) { + shell_exec( + sprintf( + '%s %s group:delete "%s"', + PHP_BINARY, + __DIR__ . '/../../../../../../occ', + $group + ) + ); + } + + if (file_exists(self::ENV_CONFIG_FILE)) { + unlink(self::ENV_CONFIG_FILE); } foreach ($this->changedSettings as $setting) { @@ -105,7 +126,8 @@ public function theSettingIsSetTo($settingName, 'type', 'general-require_provisioned_account', 'general-allow_multiple_user_back_ends', - 'general-use_saml_auth_for_desktop' + 'general-use_saml_auth_for_desktop', + 'localGroupsCheckForMigration', ])) { $this->changedSettings[] = $settingName; shell_exec( @@ -355,7 +377,7 @@ public function theEnvironmentVariableIsSetTo($key, $value) { public function theGroupShouldExists(string $gid): void { $response = shell_exec( sprintf( - '%s %s group:info --output=json %s', + '%s %s group:info --output=json "%s"', PHP_BINARY, __DIR__ . '/../../../../../../occ', $gid @@ -367,4 +389,152 @@ public function theGroupShouldExists(string $gid): void { throw new UnexpectedValueException('Group does not exist'); } } + + /** + * @When /^I execute the background job for class "([^"]*)"$/ + */ + public function iExecuteTheBackgroundJobForClass(string $className) { + $response = shell_exec( + sprintf( + '%s %s background-job:list --output=json --class %s', + PHP_BINARY, + __DIR__ . '/../../../../../../occ', + $className + ) + ); + + $responseArray = json_decode($response, true); + if (count($responseArray) === 0) { + throw new UnexpectedValueException('Background job was not enqueued'); + } + + foreach ($responseArray as $jobDetails) { + $jobID = (int)$jobDetails['id']; + $response = shell_exec( + sprintf( + '%s %s background-job:execute --force-execute %d', + PHP_BINARY, + __DIR__ . '/../../../../../../occ', + $jobID + ) + ); + } + } + + /** + * @Then /^the group backend of "([^"]*)" should be "([^"]*)"$/ + */ + public function theGroupBackendOfShouldBe(string $groupId, string $backendName) { + $response = shell_exec( + sprintf( + '%s %s group:info --output=json "%s"', + PHP_BINARY, + __DIR__ . '/../../../../../../occ', + $groupId + ) + ); + + $responseArray = json_decode($response, true); + if (!isset($responseArray['groupID']) || $responseArray['groupID'] !== $groupId) { + throw new UnexpectedValueException('Group does not exist'); + } + if (!in_array($backendName, $responseArray['backends'], true)) { + throw new UnexpectedValueException('Group does not belong to this backend'); + } + } + + /** + * @Given /^Then the group backend of "([^"]*)" must not be "([^"]*)"$/ + */ + public function thenTheGroupBackendOfMustNotBe(string $groupId, string $backendName) { + try { + $this->theGroupBackendOfShouldBe($groupId, $backendName); + throw new UnexpectedValueException('Group does belong to this backend'); + } catch (UnexpectedValueException $e) { + if ($e->getMessage() !== 'Group does not belong to this backend') { + throw $e; + } + } + } + + /** + * @Given /^the local group "([^"]*)" is created$/ + */ + public function theLocalGroupIsCreated(string $groupName) { + shell_exec( + sprintf( + '%s %s group:add "%s"', + PHP_BINARY, + __DIR__ . '/../../../../../../occ', + $groupName + ) + ); + } + + + + /** + * @Given /^I send a GET request with requesttoken to "([^"]*)"$/ + */ + public function iSendAGETRequestWithRequesttokenTo($url) { + $requestToken = substr( + preg_replace( + '/(.*)data-requesttoken="(.*)">(.*)/sm', + '\2', + $this->response->getBody()->getContents() + ), + 0, + 89 + ); + $this->response = $this->client->request( + 'GET', + $url, + [ + 'query' => [ + 'requesttoken' => $requestToken + ], + ] + ); + } + + /** + * @Given /^the cookies are cleared$/ + */ + public function theCookiesAreCleared(): void { + $this->cookieJar->clear(); + } + + /** + * @Given /^the user "([^"]*)" is added to the group "([^"]*)"$/ + */ + public function theUserIsAddedToTheGroup(string $userId, string $groupId) { + shell_exec( + sprintf( + '%s %s group:adduser "%s" "%s"', + PHP_BINARY, + __DIR__ . '/../../../../../../occ', + $groupId, + $userId + ) + ); + } + + /** + * @Given /^I expect no background job for class "([^"]*)"$/ + */ + public function iExpectNoBackgroundJobForClassOCAUser_SAMLJobsMigrateGroups(string $className) { + $response = shell_exec( + sprintf( + '%s %s background-job:list --output=json --class %s', + PHP_BINARY, + __DIR__ . '/../../../../../../occ', + $className + ) + ); + + $responseArray = json_decode($response, true); + if (count($responseArray) > 0) { + throw new UnexpectedValueException('Background job axctuaslly was enqueued!'); + } + } } From 48d9506be1f87dccb224988bce45e5bd7ced62e1 Mon Sep 17 00:00:00 2001 From: Arthur Schiwon Date: Fri, 8 Dec 2023 18:17:58 +0100 Subject: [PATCH 07/10] meta(appinfo): bump version and add hint to changelog Signed-off-by: Arthur Schiwon --- CHANGELOG.md | 6 ++++++ appinfo/info.xml | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ee10625c5..2d9db75d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ # Changelog All notable changes to this project will be documented in this file. +## 6.1.0 + +### Added + +- [Group backend and migration of original SAML groups created as local database groups (user_saml#622)](https://github.com/nextcloud/user_saml/pull/622) + ## 6.0.1 ### Added diff --git a/appinfo/info.xml b/appinfo/info.xml index 16944ee15..d01098c81 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -16,7 +16,7 @@ The following providers are supported and tested at the moment: * Any other provider that authenticates using the environment variable While theoretically any other authentication provider implementing either one of those standards is compatible, we like to note that they are not part of any internal test matrix.]]> - 6.0.1 + 6.1.0 agpl Lukas Reschke User_SAML From ab39d2e4021a7009b043d0f37c7d7ca3b1d4b063 Mon Sep 17 00:00:00 2001 From: Arthur Schiwon Date: Wed, 20 Dec 2023 12:53:02 +0100 Subject: [PATCH 08/10] refactor(app): remove unnecessary instantiations and registrations Signed-off-by: Arthur Schiwon --- appinfo/app.php | 35 ++--------------------------------- 1 file changed, 2 insertions(+), 33 deletions(-) diff --git a/appinfo/app.php b/appinfo/app.php index e86e8afb6..aec6d9c5a 100644 --- a/appinfo/app.php +++ b/appinfo/app.php @@ -20,13 +20,9 @@ */ use OCA\User_SAML\GroupBackend; -use OCA\User_SAML\GroupManager; use OCA\User_SAML\SAMLSettings; -use OCP\BackgroundJob\IJobList; -use OCP\IConfig; -use OCP\IDBConnection; +use OCA\User_SAML\UserBackend; use OCP\IGroupManager; -use Psr\Container\ContainerInterface; use Psr\Log\LoggerInterface; require_once __DIR__ . '/../3rdparty/vendor/autoload.php'; @@ -55,34 +51,7 @@ $samlSettings = \OC::$server->get(SAMLSettings::class); -\OC::$server->registerService(GroupManager::class, function (ContainerInterface $c) use ($groupBackend, $samlSettings) { - return new GroupManager( - $c->get(IDBConnection::class), - $c->get(IGroupManager::class), - $groupBackend, - $c->get(IConfig::class), - $c->get(IJobList::class), - $samlSettings, - ); -}); - -$userData = new \OCA\User_SAML\UserData( - new \OCA\User_SAML\UserResolver(\OC::$server->getUserManager()), - $samlSettings, -); - -$userBackend = new \OCA\User_SAML\UserBackend( - $config, - $urlGenerator, - \OC::$server->getSession(), - \OC::$server->getDatabaseConnection(), - \OC::$server->getUserManager(), - \OC::$server->get(GroupManager::class), - $samlSettings, - \OCP\Server::get(LoggerInterface::class), - $userData, - \OC::$server->query(\OCP\EventDispatcher\IEventDispatcher::class), -); +$userBackend = \OCP\Server::get(UserBackend::class); $userBackend->registerBackends(\OC::$server->getUserManager()->getBackends()); OC_User::useBackend($userBackend); From a5bbde3366dcac48477177a01522216cff70fc56 Mon Sep 17 00:00:00 2001 From: Arthur Schiwon Date: Wed, 20 Dec 2023 12:53:59 +0100 Subject: [PATCH 09/10] feat(groups): enable group support display names Signed-off-by: Arthur Schiwon --- lib/GroupBackend.php | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/lib/GroupBackend.php b/lib/GroupBackend.php index c3a16acbd..e4ae30e06 100644 --- a/lib/GroupBackend.php +++ b/lib/GroupBackend.php @@ -34,11 +34,12 @@ use OCP\Group\Backend\ICountUsersBackend; use OCP\Group\Backend\ICreateGroupBackend; use OCP\Group\Backend\IDeleteGroupBackend; +use OCP\Group\Backend\IGetDisplayNameBackend; use OCP\Group\Backend\INamedBackend; use OCP\Group\Backend\IRemoveFromGroupBackend; use OCP\IDBConnection; -class GroupBackend extends ABackend implements IAddToGroupBackend, ICountUsersBackend, ICreateGroupBackend, IDeleteGroupBackend, IRemoveFromGroupBackend, INamedBackend { +class GroupBackend extends ABackend implements IAddToGroupBackend, ICountUsersBackend, ICreateGroupBackend, IDeleteGroupBackend, IGetDisplayNameBackend, IRemoveFromGroupBackend, INamedBackend { /** @var IDBConnection */ private $dbc; @@ -79,7 +80,6 @@ public function getUserGroups($uid): array { $groups = []; while ($row = $cursor->fetch()) { $groups[] = $row['gid']; - $this->groupCache[$row['gid']] = $row['gid']; } $cursor->closeCursor(); @@ -91,7 +91,7 @@ public function getUserGroups($uid): array { */ public function getGroups($search = '', $limit = null, $offset = null): array { $query = $this->dbc->getQueryBuilder(); - $query->select('gid') + $query->select('gid', 'displayname') ->from(self::TABLE_GROUPS) ->orderBy('gid', 'ASC'); @@ -112,6 +112,7 @@ public function getGroups($search = '', $limit = null, $offset = null): array { $groups = []; while ($row = $result->fetch()) { $groups[] = $row['gid']; + $this->groupCache[$row['gid']] = $row['displayname']; } $result->closeCursor(); @@ -128,15 +129,16 @@ public function groupExists($gid): bool { } $qb = $this->dbc->getQueryBuilder(); - $cursor = $qb->select('gid') + $cursor = $qb->select('gid', 'displayname') ->from(self::TABLE_GROUPS) ->where($qb->expr()->eq('gid', $qb->createNamedParameter($gid))) + ->setMaxResults(1) ->executeQuery(); $result = $cursor->fetch(); $cursor->closeCursor(); if ($result !== false) { - $this->groupCache[$gid] = $gid; + $this->groupCache[$gid] = $result['displayname']; return true; } return false; @@ -212,7 +214,7 @@ public function createGroup(string $gid, string $samlGid = null): bool { } // Add to cache - $this->groupCache[$gid] = $gid; + $this->groupCache[$gid] = $samlGid; return $result === 1; } @@ -297,4 +299,12 @@ public function deleteGroup(string $gid): bool { public function getBackendName(): string { return 'user_saml'; } + + public function getDisplayName(string $gid): string { + if (!isset($this->groupCache[$gid])) { + $this->getGroups($gid); + } + + return $this->groupCache[$gid] ?? $gid; + } } From 84ec0aa79ae4495e517d4bb62fe1cb23c7e40cd9 Mon Sep 17 00:00:00 2001 From: Arthur Schiwon Date: Wed, 20 Dec 2023 13:00:34 +0100 Subject: [PATCH 10/10] refactor(db): set and improve key/index name Signed-off-by: Arthur Schiwon --- lib/Migration/Version6000Date20220912152700.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/Migration/Version6000Date20220912152700.php b/lib/Migration/Version6000Date20220912152700.php index 923e5a15e..67548584d 100644 --- a/lib/Migration/Version6000Date20220912152700.php +++ b/lib/Migration/Version6000Date20220912152700.php @@ -81,9 +81,9 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt 'length' => 64, 'default' => '', ]); - $table->setPrimaryKey(['gid', 'uid'], 'idx_group_members'); - $table->addIndex(['gid']); - $table->addIndex(['uid']); + $table->setPrimaryKey(['gid', 'uid'], 'pk_saml_group_members'); + $table->addIndex(['gid'], 'saml_group_members_gid'); // prefix is added in callee + $table->addIndex(['uid'], 'saml_group_members_uid'); // prefix is added in callee } return $schema; }