diff options
Diffstat (limited to 'lib/private/Group/Manager.php')
-rw-r--r-- | lib/private/Group/Manager.php | 475 |
1 files changed, 475 insertions, 0 deletions
diff --git a/lib/private/Group/Manager.php b/lib/private/Group/Manager.php new file mode 100644 index 00000000000..e58a1fe6585 --- /dev/null +++ b/lib/private/Group/Manager.php @@ -0,0 +1,475 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +namespace OC\Group; + +use OC\Hooks\PublicEmitter; +use OC\Settings\AuthorizedGroupMapper; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\Group\Backend\IBatchMethodsBackend; +use OCP\Group\Backend\ICreateNamedGroupBackend; +use OCP\Group\Backend\IGroupDetailsBackend; +use OCP\Group\Events\BeforeGroupCreatedEvent; +use OCP\Group\Events\GroupCreatedEvent; +use OCP\GroupInterface; +use OCP\ICacheFactory; +use OCP\IGroup; +use OCP\IGroupManager; +use OCP\IUser; +use OCP\Security\Ip\IRemoteAddress; +use Psr\Log\LoggerInterface; +use function is_string; + +/** + * Class Manager + * + * Hooks available in scope \OC\Group: + * - preAddUser(\OC\Group\Group $group, \OC\User\User $user) + * - postAddUser(\OC\Group\Group $group, \OC\User\User $user) + * - preRemoveUser(\OC\Group\Group $group, \OC\User\User $user) + * - postRemoveUser(\OC\Group\Group $group, \OC\User\User $user) + * - preDelete(\OC\Group\Group $group) + * - postDelete(\OC\Group\Group $group) + * - preCreate(string $groupId) + * - postCreate(\OC\Group\Group $group) + * + * @package OC\Group + */ +class Manager extends PublicEmitter implements IGroupManager { + /** @var GroupInterface[] */ + private $backends = []; + + /** @var array<string, IGroup> */ + private $cachedGroups = []; + + /** @var array<string, list<string>> */ + private $cachedUserGroups = []; + + /** @var \OC\SubAdmin */ + private $subAdmin = null; + + private DisplayNameCache $displayNameCache; + + private const MAX_GROUP_LENGTH = 255; + + public function __construct( + private \OC\User\Manager $userManager, + private IEventDispatcher $dispatcher, + private LoggerInterface $logger, + ICacheFactory $cacheFactory, + private IRemoteAddress $remoteAddress, + ) { + $this->displayNameCache = new DisplayNameCache($cacheFactory, $this); + + $this->listen('\OC\Group', 'postDelete', function (IGroup $group): void { + unset($this->cachedGroups[$group->getGID()]); + $this->cachedUserGroups = []; + }); + $this->listen('\OC\Group', 'postAddUser', function (IGroup $group): void { + $this->cachedUserGroups = []; + }); + $this->listen('\OC\Group', 'postRemoveUser', function (IGroup $group): void { + $this->cachedUserGroups = []; + }); + } + + /** + * Checks whether a given backend is used + * + * @param string $backendClass Full classname including complete namespace + * @return bool + */ + public function isBackendUsed($backendClass) { + $backendClass = strtolower(ltrim($backendClass, '\\')); + + foreach ($this->backends as $backend) { + if (strtolower(get_class($backend)) === $backendClass) { + return true; + } + } + + return false; + } + + /** + * @param \OCP\GroupInterface $backend + */ + public function addBackend($backend) { + $this->backends[] = $backend; + $this->clearCaches(); + } + + public function clearBackends() { + $this->backends = []; + $this->clearCaches(); + } + + /** + * Get the active backends + * + * @return \OCP\GroupInterface[] + */ + public function getBackends() { + return $this->backends; + } + + + protected function clearCaches() { + $this->cachedGroups = []; + $this->cachedUserGroups = []; + } + + /** + * @param string $gid + * @return IGroup|null + */ + public function get($gid) { + if (isset($this->cachedGroups[$gid])) { + return $this->cachedGroups[$gid]; + } + return $this->getGroupObject($gid); + } + + /** + * @param string $gid + * @param string $displayName + * @return \OCP\IGroup|null + */ + protected function getGroupObject($gid, $displayName = null) { + $backends = []; + foreach ($this->backends as $backend) { + if ($backend->implementsActions(Backend::GROUP_DETAILS)) { + $groupData = $backend->getGroupDetails($gid); + if (is_array($groupData) && !empty($groupData)) { + // take the display name from the last backend that has a non-null one + if (is_null($displayName) && isset($groupData['displayName'])) { + $displayName = $groupData['displayName']; + } + $backends[] = $backend; + } + } elseif ($backend->groupExists($gid)) { + $backends[] = $backend; + } + } + if (count($backends) === 0) { + return null; + } + /** @var GroupInterface[] $backends */ + $this->cachedGroups[$gid] = new Group($gid, $backends, $this->dispatcher, $this->userManager, $this, $displayName); + return $this->cachedGroups[$gid]; + } + + /** + * @brief Batch method to create group objects + * + * @param list<string> $gids List of groupIds for which we want to create a IGroup object + * @param array<string, string> $displayNames Array containing already know display name for a groupId + * @return array<string, IGroup> + */ + protected function getGroupsObjects(array $gids, array $displayNames = []): array { + $backends = []; + $groups = []; + foreach ($gids as $gid) { + $backends[$gid] = []; + if (!isset($displayNames[$gid])) { + $displayNames[$gid] = null; + } + } + foreach ($this->backends as $backend) { + if ($backend instanceof IGroupDetailsBackend || $backend->implementsActions(GroupInterface::GROUP_DETAILS)) { + /** @var IGroupDetailsBackend $backend */ + if ($backend instanceof IBatchMethodsBackend) { + $groupDatas = $backend->getGroupsDetails($gids); + } else { + $groupDatas = []; + foreach ($gids as $gid) { + $groupDatas[$gid] = $backend->getGroupDetails($gid); + } + } + foreach ($groupDatas as $gid => $groupData) { + if (!empty($groupData)) { + // take the display name from the last backend that has a non-null one + if (isset($groupData['displayName'])) { + $displayNames[$gid] = $groupData['displayName']; + } + $backends[$gid][] = $backend; + } + } + } else { + if ($backend instanceof IBatchMethodsBackend) { + $existingGroups = $backend->groupsExists($gids); + } else { + $existingGroups = array_filter($gids, fn (string $gid): bool => $backend->groupExists($gid)); + } + foreach ($existingGroups as $group) { + $backends[$group][] = $backend; + } + } + } + foreach ($gids as $gid) { + if (count($backends[$gid]) === 0) { + continue; + } + $this->cachedGroups[$gid] = new Group($gid, $backends[$gid], $this->dispatcher, $this->userManager, $this, $displayNames[$gid]); + $groups[$gid] = $this->cachedGroups[$gid]; + } + return $groups; + } + + /** + * @param string $gid + * @return bool + */ + public function groupExists($gid) { + return $this->get($gid) instanceof IGroup; + } + + /** + * @param string $gid + * @return IGroup|null + */ + public function createGroup($gid) { + if ($gid === '' || $gid === null) { + return null; + } elseif ($group = $this->get($gid)) { + return $group; + } elseif (mb_strlen($gid) > self::MAX_GROUP_LENGTH) { + throw new \Exception('Group name is limited to ' . self::MAX_GROUP_LENGTH . ' characters'); + } else { + $this->dispatcher->dispatchTyped(new BeforeGroupCreatedEvent($gid)); + $this->emit('\OC\Group', 'preCreate', [$gid]); + foreach ($this->backends as $backend) { + if ($backend->implementsActions(Backend::CREATE_GROUP)) { + if ($backend instanceof ICreateNamedGroupBackend) { + $groupName = $gid; + if (($gid = $backend->createGroup($groupName)) !== null) { + $group = $this->getGroupObject($gid); + $this->dispatcher->dispatchTyped(new GroupCreatedEvent($group)); + $this->emit('\OC\Group', 'postCreate', [$group]); + return $group; + } + } elseif ($backend->createGroup($gid)) { + $group = $this->getGroupObject($gid); + $this->dispatcher->dispatchTyped(new GroupCreatedEvent($group)); + $this->emit('\OC\Group', 'postCreate', [$group]); + return $group; + } + } + } + return null; + } + } + + /** + * @param string $search + * @param ?int $limit + * @param ?int $offset + * @return \OC\Group\Group[] + */ + public function search(string $search, ?int $limit = null, ?int $offset = 0) { + $groups = []; + foreach ($this->backends as $backend) { + $groupIds = $backend->getGroups($search, $limit ?? -1, $offset ?? 0); + $newGroups = $this->getGroupsObjects($groupIds); + foreach ($newGroups as $groupId => $group) { + $groups[$groupId] = $group; + } + if (!is_null($limit) and $limit <= 0) { + return array_values($groups); + } + } + return array_values($groups); + } + + /** + * @param IUser|null $user + * @return \OC\Group\Group[] + */ + public function getUserGroups(?IUser $user = null) { + if (!$user instanceof IUser) { + return []; + } + return $this->getUserIdGroups($user->getUID()); + } + + /** + * @param string $uid the user id + * @return \OC\Group\Group[] + */ + public function getUserIdGroups(string $uid): array { + $groups = []; + + foreach ($this->getUserIdGroupIds($uid) as $groupId) { + $aGroup = $this->get($groupId); + if ($aGroup instanceof IGroup) { + $groups[$groupId] = $aGroup; + } else { + $this->logger->debug('User "' . $uid . '" belongs to deleted group: "' . $groupId . '"', ['app' => 'core']); + } + } + + return $groups; + } + + /** + * Checks if a userId is in the admin group + * + * @param string $userId + * @return bool if admin + */ + public function isAdmin($userId) { + if (!$this->remoteAddress->allowsAdminActions()) { + return false; + } + + foreach ($this->backends as $backend) { + if (is_string($userId) && $backend->implementsActions(Backend::IS_ADMIN) && $backend->isAdmin($userId)) { + return true; + } + } + return $this->isInGroup($userId, 'admin'); + } + + public function isDelegatedAdmin(string $userId): bool { + if (!$this->remoteAddress->allowsAdminActions()) { + return false; + } + + // Check if the user as admin delegation for users listing + $authorizedGroupMapper = \OCP\Server::get(AuthorizedGroupMapper::class); + $user = $this->userManager->get($userId); + $authorizedClasses = $authorizedGroupMapper->findAllClassesForUser($user); + return in_array(\OCA\Settings\Settings\Admin\Users::class, $authorizedClasses, true); + } + + /** + * Checks if a userId is in a group + * + * @param string $userId + * @param string $group + * @return bool if in group + */ + public function isInGroup($userId, $group) { + return in_array($group, $this->getUserIdGroupIds($userId)); + } + + /** + * get a list of group ids for a user + * + * @param IUser $user + * @return list<string> with group ids + */ + public function getUserGroupIds(IUser $user): array { + return $this->getUserIdGroupIds($user->getUID()); + } + + /** + * @param string $uid the user id + * @return list<string> + */ + private function getUserIdGroupIds(string $uid): array { + if (!isset($this->cachedUserGroups[$uid])) { + $groups = []; + foreach ($this->backends as $backend) { + if ($groupIds = $backend->getUserGroups($uid)) { + $groups = array_merge($groups, $groupIds); + } + } + $this->cachedUserGroups[$uid] = $groups; + } + + return $this->cachedUserGroups[$uid]; + } + + /** + * @param string $groupId + * @return ?string + */ + public function getDisplayName(string $groupId): ?string { + return $this->displayNameCache->getDisplayName($groupId); + } + + /** + * get an array of groupid and displayName for a user + * + * @param IUser $user + * @return array ['displayName' => displayname] + */ + public function getUserGroupNames(IUser $user) { + return array_map(function ($group) { + return ['displayName' => $this->displayNameCache->getDisplayName($group->getGID())]; + }, $this->getUserGroups($user)); + } + + /** + * get a list of all display names in a group + * + * @param string $gid + * @param string $search + * @param int $limit + * @param int $offset + * @return array an array of display names (value) and user ids (key) + */ + public function displayNamesInGroup($gid, $search = '', $limit = -1, $offset = 0) { + $group = $this->get($gid); + if (is_null($group)) { + return []; + } + + $search = trim($search); + $groupUsers = []; + + if (!empty($search)) { + // only user backends have the capability to do a complex search for users + $searchOffset = 0; + $searchLimit = $limit * 100; + if ($limit === -1) { + $searchLimit = 500; + } + + do { + $filteredUsers = $this->userManager->searchDisplayName($search, $searchLimit, $searchOffset); + foreach ($filteredUsers as $filteredUser) { + if ($group->inGroup($filteredUser)) { + $groupUsers[] = $filteredUser; + } + } + $searchOffset += $searchLimit; + } while (count($groupUsers) < $searchLimit + $offset && count($filteredUsers) >= $searchLimit); + + if ($limit === -1) { + $groupUsers = array_slice($groupUsers, $offset); + } else { + $groupUsers = array_slice($groupUsers, $offset, $limit); + } + } else { + $groupUsers = $group->searchUsers('', $limit, $offset); + } + + $matchingUsers = []; + foreach ($groupUsers as $groupUser) { + $matchingUsers[(string)$groupUser->getUID()] = $groupUser->getDisplayName(); + } + return $matchingUsers; + } + + /** + * @return \OC\SubAdmin + */ + public function getSubAdmin() { + if (!$this->subAdmin) { + $this->subAdmin = new \OC\SubAdmin( + $this->userManager, + $this, + \OC::$server->getDatabaseConnection(), + $this->dispatcher + ); + } + + return $this->subAdmin; + } +} |