Merge pull request #46418 from nextcloud/artonge/feat/user_admin_delegation

feat(users): Add users and group management to admin delegation
pull/46689/head
Louis 2024-07-24 11:15:54 +07:00 committed by GitHub
commit 7266a9ef33
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 297 additions and 135 deletions

@ -99,7 +99,9 @@ abstract class AUserData extends OCSController {
} }
$isAdmin = $this->groupManager->isAdmin($currentLoggedInUser->getUID()); $isAdmin = $this->groupManager->isAdmin($currentLoggedInUser->getUID());
$isDelegatedAdmin = $this->groupManager->isDelegatedAdmin($currentLoggedInUser->getUID());
if ($isAdmin if ($isAdmin
|| $isDelegatedAdmin
|| $this->groupManager->getSubAdmin()->isUserAccessible($currentLoggedInUser, $targetUserObject)) { || $this->groupManager->getSubAdmin()->isUserAccessible($currentLoggedInUser, $targetUserObject)) {
$data['enabled'] = $this->config->getUserValue($targetUserObject->getUID(), 'core', 'enabled', 'true') === 'true'; $data['enabled'] = $this->config->getUserValue($targetUserObject->getUID(), 'core', 'enabled', 'true') === 'true';
} else { } else {
@ -117,7 +119,7 @@ abstract class AUserData extends OCSController {
$gids[] = $group->getGID(); $gids[] = $group->getGID();
} }
if ($isAdmin) { if ($isAdmin || $isDelegatedAdmin) {
try { try {
# might be thrown by LDAP due to handling of users disappears # might be thrown by LDAP due to handling of users disappears
# from the external source (reasons unknown to us) # from the external source (reasons unknown to us)

@ -9,8 +9,10 @@ declare(strict_types=1);
namespace OCA\Provisioning_API\Controller; namespace OCA\Provisioning_API\Controller;
use OCA\Provisioning_API\ResponseDefinitions; use OCA\Provisioning_API\ResponseDefinitions;
use OCA\Settings\Settings\Admin\Users;
use OCP\Accounts\IAccountManager; use OCP\Accounts\IAccountManager;
use OCP\AppFramework\Http; use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\AuthorizedAdminSetting;
use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\OCS\OCSException; use OCP\AppFramework\OCS\OCSException;
use OCP\AppFramework\OCS\OCSForbiddenException; use OCP\AppFramework\OCS\OCSForbiddenException;
@ -154,8 +156,9 @@ class GroupsController extends AUserData {
} }
// Check subadmin has access to this group // Check subadmin has access to this group
if ($this->groupManager->isAdmin($user->getUID()) $isAdmin = $this->groupManager->isAdmin($user->getUID());
|| $isSubadminOfGroup) { $isDelegatedAdmin = $this->groupManager->isDelegatedAdmin($user->getUID());
if ($isAdmin || $isDelegatedAdmin || $isSubadminOfGroup) {
$users = $this->groupManager->get($groupId)->getUsers(); $users = $this->groupManager->get($groupId)->getUsers();
$users = array_map(function ($user) { $users = array_map(function ($user) {
/** @var IUser $user */ /** @var IUser $user */
@ -197,7 +200,9 @@ class GroupsController extends AUserData {
} }
// Check subadmin has access to this group // Check subadmin has access to this group
if ($this->groupManager->isAdmin($currentUser->getUID()) || $isSubadminOfGroup) { $isAdmin = $this->groupManager->isAdmin($currentUser->getUID());
$isDelegatedAdmin = $this->groupManager->isDelegatedAdmin($currentUser->getUID());
if ($isAdmin || $isDelegatedAdmin || $isSubadminOfGroup) {
$users = $group->searchUsers($search, $limit, $offset); $users = $group->searchUsers($search, $limit, $offset);
// Extract required number // Extract required number
@ -237,6 +242,7 @@ class GroupsController extends AUserData {
* *
* 200: Group created successfully * 200: Group created successfully
*/ */
#[AuthorizedAdminSetting(settings:Users::class)]
public function addGroup(string $groupid, string $displayname = ''): DataResponse { public function addGroup(string $groupid, string $displayname = ''): DataResponse {
// Validate name // Validate name
if (empty($groupid)) { if (empty($groupid)) {
@ -270,6 +276,7 @@ class GroupsController extends AUserData {
* *
* 200: Group updated successfully * 200: Group updated successfully
*/ */
#[AuthorizedAdminSetting(settings:Users::class)]
public function updateGroup(string $groupId, string $key, string $value): DataResponse { public function updateGroup(string $groupId, string $key, string $value): DataResponse {
$groupId = urldecode($groupId); $groupId = urldecode($groupId);
@ -299,6 +306,7 @@ class GroupsController extends AUserData {
* *
* 200: Group deleted successfully * 200: Group deleted successfully
*/ */
#[AuthorizedAdminSetting(settings:Users::class)]
public function deleteGroup(string $groupId): DataResponse { public function deleteGroup(string $groupId): DataResponse {
$groupId = urldecode($groupId); $groupId = urldecode($groupId);
@ -322,6 +330,7 @@ class GroupsController extends AUserData {
* *
* 200: Sub admins returned * 200: Sub admins returned
*/ */
#[AuthorizedAdminSetting(settings:Users::class)]
public function getSubAdminsOfGroup(string $groupId): DataResponse { public function getSubAdminsOfGroup(string $groupId): DataResponse {
// Check group exists // Check group exists
$targetGroup = $this->groupManager->get($groupId); $targetGroup = $this->groupManager->get($groupId);

@ -16,10 +16,12 @@ use OC\KnownUser\KnownUserService;
use OC\User\Backend; use OC\User\Backend;
use OCA\Provisioning_API\ResponseDefinitions; use OCA\Provisioning_API\ResponseDefinitions;
use OCA\Settings\Mailer\NewUserMailHelper; use OCA\Settings\Mailer\NewUserMailHelper;
use OCA\Settings\Settings\Admin\Users;
use OCP\Accounts\IAccountManager; use OCP\Accounts\IAccountManager;
use OCP\Accounts\IAccountProperty; use OCP\Accounts\IAccountProperty;
use OCP\Accounts\PropertyDoesNotExistException; use OCP\Accounts\PropertyDoesNotExistException;
use OCP\AppFramework\Http; use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\AuthorizedAdminSetting;
use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\OCS\OCSException; use OCP\AppFramework\OCS\OCSException;
use OCP\AppFramework\OCS\OCSForbiddenException; use OCP\AppFramework\OCS\OCSForbiddenException;
@ -101,7 +103,9 @@ class UsersController extends AUserData {
// Admin? Or SubAdmin? // Admin? Or SubAdmin?
$uid = $user->getUID(); $uid = $user->getUID();
$subAdminManager = $this->groupManager->getSubAdmin(); $subAdminManager = $this->groupManager->getSubAdmin();
if ($this->groupManager->isAdmin($uid)) { $isAdmin = $this->groupManager->isAdmin($uid);
$isDelegatedAdmin = $this->groupManager->isDelegatedAdmin($uid);
if ($isAdmin || $isDelegatedAdmin) {
$users = $this->userManager->search($search, $limit, $offset); $users = $this->userManager->search($search, $limit, $offset);
} elseif ($subAdminManager->isSubAdmin($user)) { } elseif ($subAdminManager->isSubAdmin($user)) {
$subAdminOfGroups = $subAdminManager->getSubAdminsGroups($user); $subAdminOfGroups = $subAdminManager->getSubAdminsGroups($user);
@ -142,7 +146,9 @@ class UsersController extends AUserData {
// Admin? Or SubAdmin? // Admin? Or SubAdmin?
$uid = $currentUser->getUID(); $uid = $currentUser->getUID();
$subAdminManager = $this->groupManager->getSubAdmin(); $subAdminManager = $this->groupManager->getSubAdmin();
if ($this->groupManager->isAdmin($uid)) { $isAdmin = $this->groupManager->isAdmin($uid);
$isDelegatedAdmin = $this->groupManager->isDelegatedAdmin($uid);
if ($isAdmin || $isDelegatedAdmin) {
$users = $this->userManager->search($search, $limit, $offset); $users = $this->userManager->search($search, $limit, $offset);
$users = array_keys($users); $users = array_keys($users);
} elseif ($subAdminManager->isSubAdmin($currentUser)) { } elseif ($subAdminManager->isSubAdmin($currentUser)) {
@ -213,7 +219,9 @@ class UsersController extends AUserData {
// Admin? Or SubAdmin? // Admin? Or SubAdmin?
$uid = $currentUser->getUID(); $uid = $currentUser->getUID();
$subAdminManager = $this->groupManager->getSubAdmin(); $subAdminManager = $this->groupManager->getSubAdmin();
if ($this->groupManager->isAdmin($uid)) { $isAdmin = $this->groupManager->isAdmin($uid);
$isDelegatedAdmin = $this->groupManager->isDelegatedAdmin($uid);
if ($isAdmin || $isDelegatedAdmin) {
$users = $this->userManager->getDisabledUsers($limit, $offset, $search); $users = $this->userManager->getDisabledUsers($limit, $offset, $search);
$users = array_map(fn (IUser $user): string => $user->getUID(), $users); $users = array_map(fn (IUser $user): string => $user->getUID(), $users);
} elseif ($subAdminManager->isSubAdmin($currentUser)) { } elseif ($subAdminManager->isSubAdmin($currentUser)) {
@ -275,6 +283,7 @@ class UsersController extends AUserData {
* *
* 200: Users details returned based on last logged in information * 200: Users details returned based on last logged in information
*/ */
#[AuthorizedAdminSetting(settings:Users::class)]
public function getLastLoggedInUsers(string $search = '', public function getLastLoggedInUsers(string $search = '',
?int $limit = null, ?int $limit = null,
int $offset = 0, int $offset = 0,
@ -447,6 +456,7 @@ class UsersController extends AUserData {
): DataResponse { ): DataResponse {
$user = $this->userSession->getUser(); $user = $this->userSession->getUser();
$isAdmin = $this->groupManager->isAdmin($user->getUID()); $isAdmin = $this->groupManager->isAdmin($user->getUID());
$isDelegatedAdmin = $this->groupManager->isDelegatedAdmin($user->getUID());
$subAdminManager = $this->groupManager->getSubAdmin(); $subAdminManager = $this->groupManager->getSubAdmin();
if (empty($userid) && $this->config->getAppValue('core', 'newUser.generateUserID', 'no') === 'yes') { if (empty($userid) && $this->config->getAppValue('core', 'newUser.generateUserID', 'no') === 'yes') {
@ -463,12 +473,12 @@ class UsersController extends AUserData {
if (!$this->groupManager->groupExists($group)) { if (!$this->groupManager->groupExists($group)) {
throw new OCSException($this->l10n->t('Group %1$s does not exist', [$group]), 104); throw new OCSException($this->l10n->t('Group %1$s does not exist', [$group]), 104);
} }
if (!$isAdmin && !$subAdminManager->isSubAdminOfGroup($user, $this->groupManager->get($group))) { if (!$isAdmin && !($isDelegatedAdmin && $group !== 'admin') && !$subAdminManager->isSubAdminOfGroup($user, $this->groupManager->get($group))) {
throw new OCSException($this->l10n->t('Insufficient privileges for group %1$s', [$group]), 105); throw new OCSException($this->l10n->t('Insufficient privileges for group %1$s', [$group]), 105);
} }
} }
} else { } else {
if (!$isAdmin) { if (!$isAdmin && !$isDelegatedAdmin) {
throw new OCSException($this->l10n->t('No group specified (required for sub-admins)'), 106); throw new OCSException($this->l10n->t('No group specified (required for sub-admins)'), 106);
} }
} }
@ -486,7 +496,7 @@ class UsersController extends AUserData {
throw new OCSException($this->l10n->t('Cannot create sub-admins for admin group'), 103); throw new OCSException($this->l10n->t('Cannot create sub-admins for admin group'), 103);
} }
// Check if has permission to promote subadmins // Check if has permission to promote subadmins
if (!$subAdminManager->isSubAdminOfGroup($user, $group) && !$isAdmin) { if (!$subAdminManager->isSubAdminOfGroup($user, $group) && !$isAdmin && !$isDelegatedAdmin) {
throw new OCSForbiddenException($this->l10n->t('No permissions to promote sub-admins')); throw new OCSForbiddenException($this->l10n->t('No permissions to promote sub-admins'));
} }
$subadminGroups[] = $group; $subadminGroups[] = $group;
@ -718,8 +728,10 @@ class UsersController extends AUserData {
} }
$subAdminManager = $this->groupManager->getSubAdmin(); $subAdminManager = $this->groupManager->getSubAdmin();
$isAdmin = $this->groupManager->isAdmin($currentLoggedInUser->getUID());
$isDelegatedAdmin = $this->groupManager->isDelegatedAdmin($currentLoggedInUser->getUID());
if ( if (
!$this->groupManager->isAdmin($currentLoggedInUser->getUID()) !($isAdmin || $isDelegatedAdmin)
&& !$subAdminManager->isUserAccessible($currentLoggedInUser, $targetUser) && !$subAdminManager->isUserAccessible($currentLoggedInUser, $targetUser)
) { ) {
throw new OCSException('', OCSController::RESPOND_NOT_FOUND); throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
@ -788,6 +800,7 @@ class UsersController extends AUserData {
} }
$subAdminManager = $this->groupManager->getSubAdmin(); $subAdminManager = $this->groupManager->getSubAdmin();
$isDelegatedAdmin = $this->groupManager->isDelegatedAdmin($currentLoggedInUser->getUID());
$isAdminOrSubadmin = $this->groupManager->isAdmin($currentLoggedInUser->getUID()) $isAdminOrSubadmin = $this->groupManager->isAdmin($currentLoggedInUser->getUID())
|| $subAdminManager->isUserAccessible($currentLoggedInUser, $targetUser); || $subAdminManager->isUserAccessible($currentLoggedInUser, $targetUser);
@ -798,7 +811,7 @@ class UsersController extends AUserData {
$permittedFields[] = IAccountManager::COLLECTION_EMAIL . self::SCOPE_SUFFIX; $permittedFields[] = IAccountManager::COLLECTION_EMAIL . self::SCOPE_SUFFIX;
} else { } else {
// Check if admin / subadmin // Check if admin / subadmin
if ($isAdminOrSubadmin) { if ($isAdminOrSubadmin || $isDelegatedAdmin && !$this->groupManager->isInGroup($targetUser->getUID(), 'admin')) {
// They have permissions over the user // They have permissions over the user
$permittedFields[] = IAccountManager::COLLECTION_EMAIL; $permittedFields[] = IAccountManager::COLLECTION_EMAIL;
} else { } else {
@ -903,14 +916,16 @@ class UsersController extends AUserData {
$permittedFields[] = self::USER_FIELD_NOTIFICATION_EMAIL; $permittedFields[] = self::USER_FIELD_NOTIFICATION_EMAIL;
if ( if (
$this->config->getSystemValue('force_language', false) === false || $this->config->getSystemValue('force_language', false) === false ||
$this->groupManager->isAdmin($currentLoggedInUser->getUID()) $this->groupManager->isAdmin($currentLoggedInUser->getUID()) ||
$this->groupManager->isDelegatedAdmin($currentLoggedInUser->getUID())
) { ) {
$permittedFields[] = self::USER_FIELD_LANGUAGE; $permittedFields[] = self::USER_FIELD_LANGUAGE;
} }
if ( if (
$this->config->getSystemValue('force_locale', false) === false || $this->config->getSystemValue('force_locale', false) === false ||
$this->groupManager->isAdmin($currentLoggedInUser->getUID()) $this->groupManager->isAdmin($currentLoggedInUser->getUID()) ||
$this->groupManager->isDelegatedAdmin($currentLoggedInUser->getUID())
) { ) {
$permittedFields[] = self::USER_FIELD_LOCALE; $permittedFields[] = self::USER_FIELD_LOCALE;
$permittedFields[] = self::USER_FIELD_FIRST_DAY_OF_WEEK; $permittedFields[] = self::USER_FIELD_FIRST_DAY_OF_WEEK;
@ -942,7 +957,9 @@ class UsersController extends AUserData {
$permittedFields[] = IAccountManager::PROPERTY_AVATAR . self::SCOPE_SUFFIX; $permittedFields[] = IAccountManager::PROPERTY_AVATAR . self::SCOPE_SUFFIX;
// If admin they can edit their own quota and manager // If admin they can edit their own quota and manager
if ($this->groupManager->isAdmin($currentLoggedInUser->getUID())) { $isAdmin = $this->groupManager->isAdmin($currentLoggedInUser->getUID());
$isDelegatedAdmin = $this->groupManager->isDelegatedAdmin($currentLoggedInUser->getUID());
if ($isAdmin || $isDelegatedAdmin) {
$permittedFields[] = self::USER_FIELD_QUOTA; $permittedFields[] = self::USER_FIELD_QUOTA;
$permittedFields[] = self::USER_FIELD_MANAGER; $permittedFields[] = self::USER_FIELD_MANAGER;
} }
@ -950,7 +967,8 @@ class UsersController extends AUserData {
// Check if admin / subadmin // Check if admin / subadmin
$subAdminManager = $this->groupManager->getSubAdmin(); $subAdminManager = $this->groupManager->getSubAdmin();
if ( if (
$this->groupManager->isAdmin($currentLoggedInUser->getUID()) $this->groupManager->isAdmin($currentLoggedInUser->getUID()) ||
$this->groupManager->isDelegatedAdmin($currentLoggedInUser->getUID()) && !$this->groupManager->isInGroup($targetUser->getUID(), 'admin')
|| $subAdminManager->isUserAccessible($currentLoggedInUser, $targetUser) || $subAdminManager->isUserAccessible($currentLoggedInUser, $targetUser)
) { ) {
// They have permissions over the user // They have permissions over the user
@ -1217,7 +1235,9 @@ class UsersController extends AUserData {
// If not permitted // If not permitted
$subAdminManager = $this->groupManager->getSubAdmin(); $subAdminManager = $this->groupManager->getSubAdmin();
if (!$this->groupManager->isAdmin($currentLoggedInUser->getUID()) && !$subAdminManager->isUserAccessible($currentLoggedInUser, $targetUser)) { $isAdmin = $this->groupManager->isAdmin($currentLoggedInUser->getUID());
$isDelegatedAdmin = $this->groupManager->isDelegatedAdmin($currentLoggedInUser->getUID());
if (!$isAdmin && !($isDelegatedAdmin && !$this->groupManager->isInGroup($targetUser->getUID(), 'admin')) && !$subAdminManager->isUserAccessible($currentLoggedInUser, $targetUser)) {
throw new OCSException('', OCSController::RESPOND_NOT_FOUND); throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
} }
@ -1253,7 +1273,9 @@ class UsersController extends AUserData {
// If not permitted // If not permitted
$subAdminManager = $this->groupManager->getSubAdmin(); $subAdminManager = $this->groupManager->getSubAdmin();
if (!$this->groupManager->isAdmin($currentLoggedInUser->getUID()) && !$subAdminManager->isUserAccessible($currentLoggedInUser, $targetUser)) { $isAdmin = $this->groupManager->isAdmin($currentLoggedInUser->getUID());
$isDelegatedAdmin = $this->groupManager->isDelegatedAdmin($currentLoggedInUser->getUID());
if (!$isAdmin && !($isDelegatedAdmin && !$this->groupManager->isInGroup($targetUser->getUID(), 'admin')) && !$subAdminManager->isUserAccessible($currentLoggedInUser, $targetUser)) {
throw new OCSException('', OCSController::RESPOND_NOT_FOUND); throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
} }
@ -1313,7 +1335,9 @@ class UsersController extends AUserData {
// If not permitted // If not permitted
$subAdminManager = $this->groupManager->getSubAdmin(); $subAdminManager = $this->groupManager->getSubAdmin();
if (!$this->groupManager->isAdmin($currentLoggedInUser->getUID()) && !$subAdminManager->isUserAccessible($currentLoggedInUser, $targetUser)) { $isAdmin = $this->groupManager->isAdmin($currentLoggedInUser->getUID());
$isDelegatedAdmin = $this->groupManager->isDelegatedAdmin($currentLoggedInUser->getUID());
if (!$isAdmin && !($isDelegatedAdmin && !$this->groupManager->isInGroup($targetUser->getUID(), 'admin')) && !$subAdminManager->isUserAccessible($currentLoggedInUser, $targetUser)) {
throw new OCSException('', OCSController::RESPOND_NOT_FOUND); throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
} }
@ -1342,7 +1366,9 @@ class UsersController extends AUserData {
throw new OCSException('', OCSController::RESPOND_NOT_FOUND); throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
} }
if ($targetUser->getUID() === $loggedInUser->getUID() || $this->groupManager->isAdmin($loggedInUser->getUID())) { $isAdmin = $this->groupManager->isAdmin($loggedInUser->getUID());
$isDelegatedAdmin = $this->groupManager->isDelegatedAdmin($loggedInUser->getUID());
if ($targetUser->getUID() === $loggedInUser->getUID() || $isAdmin || $isDelegatedAdmin) {
// Self lookup or admin lookup // Self lookup or admin lookup
return new DataResponse([ return new DataResponse([
'groups' => $this->groupManager->getUserGroupIds($targetUser) 'groups' => $this->groupManager->getUserGroupIds($targetUser)
@ -1401,7 +1427,9 @@ class UsersController extends AUserData {
// If they're not an admin, check they are a subadmin of the group in question // If they're not an admin, check they are a subadmin of the group in question
$loggedInUser = $this->userSession->getUser(); $loggedInUser = $this->userSession->getUser();
$subAdminManager = $this->groupManager->getSubAdmin(); $subAdminManager = $this->groupManager->getSubAdmin();
if (!$this->groupManager->isAdmin($loggedInUser->getUID()) && !$subAdminManager->isSubAdminOfGroup($loggedInUser, $group)) { $isAdmin = $this->groupManager->isAdmin($loggedInUser->getUID());
$isDelegatedAdmin = $this->groupManager->isDelegatedAdmin($loggedInUser->getUID());
if (!$isAdmin && !($isDelegatedAdmin && $groupid !== 'admin') && !$subAdminManager->isSubAdminOfGroup($loggedInUser, $group)) {
throw new OCSException('', 104); throw new OCSException('', 104);
} }
@ -1442,13 +1470,15 @@ class UsersController extends AUserData {
// If they're not an admin, check they are a subadmin of the group in question // If they're not an admin, check they are a subadmin of the group in question
$subAdminManager = $this->groupManager->getSubAdmin(); $subAdminManager = $this->groupManager->getSubAdmin();
if (!$this->groupManager->isAdmin($loggedInUser->getUID()) && !$subAdminManager->isSubAdminOfGroup($loggedInUser, $group)) { $isAdmin = $this->groupManager->isAdmin($loggedInUser->getUID());
$isDelegatedAdmin = $this->groupManager->isDelegatedAdmin($loggedInUser->getUID());
if (!$isAdmin && !($isDelegatedAdmin && $groupid !== 'admin') && !$subAdminManager->isSubAdminOfGroup($loggedInUser, $group)) {
throw new OCSException('', 104); throw new OCSException('', 104);
} }
// Check they aren't removing themselves from 'admin' or their 'subadmin; group // Check they aren't removing themselves from 'admin' or their 'subadmin; group
if ($targetUser->getUID() === $loggedInUser->getUID()) { if ($targetUser->getUID() === $loggedInUser->getUID()) {
if ($this->groupManager->isAdmin($loggedInUser->getUID())) { if ($isAdmin || $isDelegatedAdmin) {
if ($group->getGID() === 'admin') { if ($group->getGID() === 'admin') {
throw new OCSException($this->l10n->t('Cannot remove yourself from the admin group'), 105); throw new OCSException($this->l10n->t('Cannot remove yourself from the admin group'), 105);
} }
@ -1456,7 +1486,7 @@ class UsersController extends AUserData {
// Not an admin, so the user must be a subadmin of this group, but that is not allowed. // Not an admin, so the user must be a subadmin of this group, but that is not allowed.
throw new OCSException($this->l10n->t('Cannot remove yourself from this group as you are a sub-admin'), 105); throw new OCSException($this->l10n->t('Cannot remove yourself from this group as you are a sub-admin'), 105);
} }
} elseif (!$this->groupManager->isAdmin($loggedInUser->getUID())) { } elseif (!($isAdmin || $isDelegatedAdmin)) {
/** @var IGroup[] $subAdminGroups */ /** @var IGroup[] $subAdminGroups */
$subAdminGroups = $subAdminManager->getSubAdminsGroups($loggedInUser); $subAdminGroups = $subAdminManager->getSubAdminsGroups($loggedInUser);
$subAdminGroups = array_map(function (IGroup $subAdminGroup) { $subAdminGroups = array_map(function (IGroup $subAdminGroup) {
@ -1488,6 +1518,7 @@ class UsersController extends AUserData {
* *
* 200: User added as group subadmin successfully * 200: User added as group subadmin successfully
*/ */
#[AuthorizedAdminSetting(settings:Users::class)]
public function addSubAdmin(string $userId, string $groupid): DataResponse { public function addSubAdmin(string $userId, string $groupid): DataResponse {
$group = $this->groupManager->get($groupid); $group = $this->groupManager->get($groupid);
$user = $this->userManager->get($userId); $user = $this->userManager->get($userId);
@ -1528,6 +1559,7 @@ class UsersController extends AUserData {
* *
* 200: User removed as group subadmin successfully * 200: User removed as group subadmin successfully
*/ */
#[AuthorizedAdminSetting(settings:Users::class)]
public function removeSubAdmin(string $userId, string $groupid): DataResponse { public function removeSubAdmin(string $userId, string $groupid): DataResponse {
$group = $this->groupManager->get($groupid); $group = $this->groupManager->get($groupid);
$user = $this->userManager->get($userId); $user = $this->userManager->get($userId);
@ -1560,6 +1592,7 @@ class UsersController extends AUserData {
* *
* 200: User subadmin groups returned * 200: User subadmin groups returned
*/ */
#[AuthorizedAdminSetting(settings:Users::class)]
public function getUserSubAdminGroups(string $userId): DataResponse { public function getUserSubAdminGroups(string $userId): DataResponse {
$groups = $this->getUserSubAdminGroupsData($userId); $groups = $this->getUserSubAdminGroupsData($userId);
return new DataResponse($groups); return new DataResponse($groups);
@ -1587,9 +1620,11 @@ class UsersController extends AUserData {
// Check if admin / subadmin // Check if admin / subadmin
$subAdminManager = $this->groupManager->getSubAdmin(); $subAdminManager = $this->groupManager->getSubAdmin();
$isAdmin = $this->groupManager->isAdmin($currentLoggedInUser->getUID());
$isDelegatedAdmin = $this->groupManager->isDelegatedAdmin($currentLoggedInUser->getUID());
if ( if (
!$subAdminManager->isUserAccessible($currentLoggedInUser, $targetUser) !$subAdminManager->isUserAccessible($currentLoggedInUser, $targetUser)
&& !$this->groupManager->isAdmin($currentLoggedInUser->getUID()) && !($isAdmin || $isDelegatedAdmin)
) { ) {
// No rights // No rights
throw new OCSException('', OCSController::RESPOND_NOT_FOUND); throw new OCSException('', OCSController::RESPOND_NOT_FOUND);

@ -45,13 +45,17 @@ class AppsControllerTest extends \OCA\Provisioning_API\Tests\TestCase {
); );
} }
protected function tearDown(): void {
$this->userSession->setUser(null);
}
public function testGetAppInfo() { public function testGetAppInfo() {
$result = $this->api->getAppInfo('provisioning_api'); $result = $this->api->getAppInfo('provisioning_api');
$expected = $this->appManager->getAppInfo('provisioning_api'); $expected = $this->appManager->getAppInfo('provisioning_api');
$this->assertEquals($expected, $result->getData()); $this->assertEquals($expected, $result->getData());
} }
public function testGetAppInfoOnBadAppID() { public function testGetAppInfoOnBadAppID() {
$this->expectException(\OCP\AppFramework\OCS\OCSException::class); $this->expectException(\OCP\AppFramework\OCS\OCSException::class);
$this->expectExceptionCode(998); $this->expectExceptionCode(998);

@ -232,7 +232,7 @@ class UsersControllerTest extends TestCase {
->disableOriginalConstructor() ->disableOriginalConstructor()
->getMock(); ->getMock();
$loggedInUser $loggedInUser
->expects($this->once()) ->expects($this->exactly(2))
->method('getUID') ->method('getUID')
->willReturn('adminUser'); ->willReturn('adminUser');
$this->userSession $this->userSession
@ -263,7 +263,7 @@ class UsersControllerTest extends TestCase {
->disableOriginalConstructor() ->disableOriginalConstructor()
->getMock(); ->getMock();
$loggedInUser $loggedInUser
->expects($this->once()) ->expects($this->exactly(2))
->method('getUID') ->method('getUID')
->willReturn('adminUser'); ->willReturn('adminUser');
$this->userSession $this->userSession
@ -299,7 +299,7 @@ class UsersControllerTest extends TestCase {
->disableOriginalConstructor() ->disableOriginalConstructor()
->getMock(); ->getMock();
$loggedInUser $loggedInUser
->expects($this->once()) ->expects($this->exactly(2))
->method('getUID') ->method('getUID')
->willReturn('adminUser'); ->willReturn('adminUser');
$this->userSession $this->userSession
@ -344,7 +344,7 @@ class UsersControllerTest extends TestCase {
->disableOriginalConstructor() ->disableOriginalConstructor()
->getMock(); ->getMock();
$loggedInUser $loggedInUser
->expects($this->once()) ->expects($this->exactly(2))
->method('getUID') ->method('getUID')
->willReturn('adminUser'); ->willReturn('adminUser');
$this->userSession $this->userSession
@ -456,7 +456,7 @@ class UsersControllerTest extends TestCase {
->disableOriginalConstructor() ->disableOriginalConstructor()
->getMock(); ->getMock();
$loggedInUser $loggedInUser
->expects($this->once()) ->expects($this->exactly(2))
->method('getUID') ->method('getUID')
->willReturn('adminUser'); ->willReturn('adminUser');
$this->userSession $this->userSession
@ -502,7 +502,7 @@ class UsersControllerTest extends TestCase {
->disableOriginalConstructor() ->disableOriginalConstructor()
->getMock(); ->getMock();
$loggedInUser $loggedInUser
->expects($this->once()) ->expects($this->exactly(2))
->method('getUID') ->method('getUID')
->willReturn('adminUser'); ->willReturn('adminUser');
$this->userSession $this->userSession
@ -552,7 +552,7 @@ class UsersControllerTest extends TestCase {
->disableOriginalConstructor() ->disableOriginalConstructor()
->getMock(); ->getMock();
$loggedInUser $loggedInUser
->expects($this->once()) ->expects($this->exactly(2))
->method('getUID') ->method('getUID')
->willReturn('adminUser'); ->willReturn('adminUser');
$this->userSession $this->userSession
@ -595,7 +595,7 @@ class UsersControllerTest extends TestCase {
->disableOriginalConstructor() ->disableOriginalConstructor()
->getMock(); ->getMock();
$loggedInUser $loggedInUser
->expects($this->once()) ->expects($this->exactly(2))
->method('getUID') ->method('getUID')
->willReturn('adminUser'); ->willReturn('adminUser');
$this->userSession $this->userSession
@ -624,7 +624,7 @@ class UsersControllerTest extends TestCase {
->disableOriginalConstructor() ->disableOriginalConstructor()
->getMock(); ->getMock();
$loggedInUser $loggedInUser
->expects($this->once()) ->expects($this->exactly(2))
->method('getUID') ->method('getUID')
->willReturn('adminUser'); ->willReturn('adminUser');
$this->userSession $this->userSession
@ -706,7 +706,7 @@ class UsersControllerTest extends TestCase {
->disableOriginalConstructor() ->disableOriginalConstructor()
->getMock(); ->getMock();
$loggedInUser $loggedInUser
->expects($this->once()) ->expects($this->exactly(2))
->method('getUID') ->method('getUID')
->willReturn('adminUser'); ->willReturn('adminUser');
$this->userSession $this->userSession
@ -732,7 +732,7 @@ class UsersControllerTest extends TestCase {
->disableOriginalConstructor() ->disableOriginalConstructor()
->getMock(); ->getMock();
$loggedInUser $loggedInUser
->expects($this->once()) ->expects($this->exactly(2))
->method('getUID') ->method('getUID')
->willReturn('regularUser'); ->willReturn('regularUser');
$this->userSession $this->userSession
@ -765,7 +765,7 @@ class UsersControllerTest extends TestCase {
->disableOriginalConstructor() ->disableOriginalConstructor()
->getMock(); ->getMock();
$loggedInUser $loggedInUser
->expects($this->once()) ->expects($this->exactly(2))
->method('getUID') ->method('getUID')
->willReturn('regularUser'); ->willReturn('regularUser');
$this->userSession $this->userSession
@ -814,7 +814,7 @@ class UsersControllerTest extends TestCase {
->disableOriginalConstructor() ->disableOriginalConstructor()
->getMock(); ->getMock();
$loggedInUser $loggedInUser
->expects($this->once()) ->expects($this->exactly(2))
->method('getUID') ->method('getUID')
->willReturn('subAdminUser'); ->willReturn('subAdminUser');
$this->userSession $this->userSession
@ -931,7 +931,7 @@ class UsersControllerTest extends TestCase {
->disableOriginalConstructor() ->disableOriginalConstructor()
->getMock(); ->getMock();
$loggedInUser $loggedInUser
->expects($this->once()) ->expects($this->exactly(2))
->method('getUID') ->method('getUID')
->willReturn('admin'); ->willReturn('admin');
$targetUser = $this->getMockBuilder(IUser::class) $targetUser = $this->getMockBuilder(IUser::class)
@ -1077,7 +1077,7 @@ class UsersControllerTest extends TestCase {
->disableOriginalConstructor() ->disableOriginalConstructor()
->getMock(); ->getMock();
$loggedInUser $loggedInUser
->expects($this->once()) ->expects($this->exactly(2))
->method('getUID') ->method('getUID')
->willReturn('subadmin'); ->willReturn('subadmin');
$targetUser = $this->getMockBuilder(IUser::class) $targetUser = $this->getMockBuilder(IUser::class)
@ -1223,7 +1223,7 @@ class UsersControllerTest extends TestCase {
->disableOriginalConstructor() ->disableOriginalConstructor()
->getMock(); ->getMock();
$loggedInUser $loggedInUser
->expects($this->exactly(3)) ->expects($this->exactly(4))
->method('getUID') ->method('getUID')
->willReturn('subadmin'); ->willReturn('subadmin');
$targetUser = $this->getMockBuilder(IUser::class) $targetUser = $this->getMockBuilder(IUser::class)
@ -1263,7 +1263,7 @@ class UsersControllerTest extends TestCase {
->disableOriginalConstructor() ->disableOriginalConstructor()
->getMock(); ->getMock();
$loggedInUser $loggedInUser
->expects($this->exactly(2)) ->expects($this->exactly(3))
->method('getUID') ->method('getUID')
->willReturn('UID'); ->willReturn('UID');
$targetUser = $this->getMockBuilder(IUser::class) $targetUser = $this->getMockBuilder(IUser::class)
@ -2662,7 +2662,7 @@ class UsersControllerTest extends TestCase {
public function testGetUsersGroupsSelfTargetted() { public function testGetUsersGroupsSelfTargetted() {
$loggedInUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock(); $loggedInUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
$loggedInUser $loggedInUser
->expects($this->once()) ->expects($this->exactly(3))
->method('getUID') ->method('getUID')
->willReturn('UserToLookup'); ->willReturn('UserToLookup');
$targetUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock(); $targetUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
@ -2691,7 +2691,7 @@ class UsersControllerTest extends TestCase {
public function testGetUsersGroupsForAdminUser() { public function testGetUsersGroupsForAdminUser() {
$loggedInUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock(); $loggedInUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
$loggedInUser $loggedInUser
->expects($this->exactly(2)) ->expects($this->exactly(3))
->method('getUID') ->method('getUID')
->willReturn('admin'); ->willReturn('admin');
$targetUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock(); $targetUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
@ -2725,7 +2725,7 @@ class UsersControllerTest extends TestCase {
public function testGetUsersGroupsForSubAdminUserAndUserIsAccessible() { public function testGetUsersGroupsForSubAdminUserAndUserIsAccessible() {
$loggedInUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock(); $loggedInUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
$loggedInUser $loggedInUser
->expects($this->exactly(2)) ->expects($this->exactly(3))
->method('getUID') ->method('getUID')
->willReturn('subadmin'); ->willReturn('subadmin');
$targetUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock(); $targetUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
@ -2789,7 +2789,7 @@ class UsersControllerTest extends TestCase {
$loggedInUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock(); $loggedInUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
$loggedInUser $loggedInUser
->expects($this->exactly(2)) ->expects($this->exactly(3))
->method('getUID') ->method('getUID')
->willReturn('subadmin'); ->willReturn('subadmin');
$targetUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock(); $targetUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
@ -2873,7 +2873,7 @@ class UsersControllerTest extends TestCase {
$targetUser = $this->createMock(IUser::class); $targetUser = $this->createMock(IUser::class);
$loggedInUser = $this->createMock(IUser::class); $loggedInUser = $this->createMock(IUser::class);
$loggedInUser->expects($this->once()) $loggedInUser->expects($this->exactly(2))
->method('getUID') ->method('getUID')
->willReturn('subadmin'); ->willReturn('subadmin');
@ -2917,7 +2917,7 @@ class UsersControllerTest extends TestCase {
public function testAddToGroupSuccessAsSubadmin() { public function testAddToGroupSuccessAsSubadmin() {
$targetUser = $this->createMock(IUser::class); $targetUser = $this->createMock(IUser::class);
$loggedInUser = $this->createMock(IUser::class); $loggedInUser = $this->createMock(IUser::class);
$loggedInUser->expects($this->once()) $loggedInUser->expects($this->exactly(2))
->method('getUID') ->method('getUID')
->willReturn('subadmin'); ->willReturn('subadmin');
@ -2961,7 +2961,7 @@ class UsersControllerTest extends TestCase {
public function testAddToGroupSuccessAsAdmin() { public function testAddToGroupSuccessAsAdmin() {
$targetUser = $this->createMock(IUser::class); $targetUser = $this->createMock(IUser::class);
$loggedInUser = $this->createMock(IUser::class); $loggedInUser = $this->createMock(IUser::class);
$loggedInUser->expects($this->once()) $loggedInUser->expects($this->exactly(2))
->method('getUID') ->method('getUID')
->willReturn('admin'); ->willReturn('admin');
@ -3079,7 +3079,7 @@ class UsersControllerTest extends TestCase {
$loggedInUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock(); $loggedInUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
$loggedInUser $loggedInUser
->expects($this->once()) ->expects($this->exactly(2))
->method('getUID') ->method('getUID')
->willReturn('unauthorizedUser'); ->willReturn('unauthorizedUser');
$targetUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock(); $targetUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
@ -3600,7 +3600,7 @@ class UsersControllerTest extends TestCase {
->willReturn($targetUser); ->willReturn($targetUser);
$loggedInUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock(); $loggedInUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
$loggedInUser $loggedInUser
->expects($this->exactly(2)) ->expects($this->exactly(3))
->method('getUID') ->method('getUID')
->willReturn('admin'); ->willReturn('admin');
$this->userSession $this->userSession
@ -3627,7 +3627,7 @@ class UsersControllerTest extends TestCase {
->willReturn($targetUser); ->willReturn($targetUser);
$loggedInUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock(); $loggedInUser = $this->getMockBuilder(IUser::class)->disableOriginalConstructor()->getMock();
$loggedInUser $loggedInUser
->expects($this->exactly(2)) ->expects($this->exactly(3))
->method('getUID') ->method('getUID')
->willReturn('admin'); ->willReturn('admin');
$this->userSession $this->userSession
@ -3814,7 +3814,7 @@ class UsersControllerTest extends TestCase {
->disableOriginalConstructor() ->disableOriginalConstructor()
->getMock(); ->getMock();
$loggedInUser $loggedInUser
->expects($this->exactly(1)) ->expects($this->exactly(2))
->method('getUID') ->method('getUID')
->willReturn('subadmin'); ->willReturn('subadmin');
$targetUser = $this->getMockBuilder(IUser::class) $targetUser = $this->getMockBuilder(IUser::class)
@ -3883,6 +3883,10 @@ class UsersControllerTest extends TestCase {
->expects($this->once()) ->expects($this->once())
->method('getSubAdmin') ->method('getSubAdmin')
->willReturn($subAdminManager); ->willReturn($subAdminManager);
$loggedInUser
->expects($this->exactly(2))
->method('getUID')
->willReturn('logged-user-id');
$targetUser $targetUser
->expects($this->once()) ->expects($this->once())
->method('getEmailAddress') ->method('getEmailAddress')
@ -3924,6 +3928,10 @@ class UsersControllerTest extends TestCase {
->expects($this->once()) ->expects($this->once())
->method('getSubAdmin') ->method('getSubAdmin')
->willReturn($subAdminManager); ->willReturn($subAdminManager);
$loggedInUser
->expects($this->exactly(2))
->method('getUID')
->willReturn('logged-user-id');
$targetUser $targetUser
->expects($this->once()) ->expects($this->once())
->method('getEmailAddress') ->method('getEmailAddress')
@ -3939,6 +3947,9 @@ class UsersControllerTest extends TestCase {
$targetUser = $this->getMockBuilder(IUser::class) $targetUser = $this->getMockBuilder(IUser::class)
->disableOriginalConstructor() ->disableOriginalConstructor()
->getMock(); ->getMock();
$loggedInUser
->method('getUID')
->willReturn('logged-user-id');
$targetUser $targetUser
->method('getUID') ->method('getUID')
->willReturn('user-id'); ->willReturn('user-id');
@ -3987,6 +3998,9 @@ class UsersControllerTest extends TestCase {
$targetUser = $this->getMockBuilder(IUser::class) $targetUser = $this->getMockBuilder(IUser::class)
->disableOriginalConstructor() ->disableOriginalConstructor()
->getMock(); ->getMock();
$loggedInUser
->method('getUID')
->willReturn('logged-user-id');
$targetUser $targetUser
->method('getUID') ->method('getUID')
->willReturn('user-id'); ->willReturn('user-id');
@ -4040,6 +4054,10 @@ class UsersControllerTest extends TestCase {
$targetUser = $this->getMockBuilder(IUser::class) $targetUser = $this->getMockBuilder(IUser::class)
->disableOriginalConstructor() ->disableOriginalConstructor()
->getMock(); ->getMock();
$loggedInUser
->expects($this->exactly(2))
->method('getUID')
->willReturn('logged-user-id');
$targetUser $targetUser
->method('getUID') ->method('getUID')
->willReturn('user-id'); ->willReturn('user-id');

@ -34,6 +34,7 @@
<admin>OCA\Settings\Settings\Admin\Sharing</admin> <admin>OCA\Settings\Settings\Admin\Sharing</admin>
<admin>OCA\Settings\Settings\Admin\Security</admin> <admin>OCA\Settings\Settings\Admin\Security</admin>
<admin>OCA\Settings\Settings\Admin\Delegation</admin> <admin>OCA\Settings\Settings\Admin\Delegation</admin>
<admin>OCA\Settings\Settings\Admin\Users</admin>
<admin-section>OCA\Settings\Sections\Admin\Additional</admin-section> <admin-section>OCA\Settings\Sections\Admin\Additional</admin-section>
<admin-section>OCA\Settings\Sections\Admin\Delegation</admin-section> <admin-section>OCA\Settings\Sections\Admin\Delegation</admin-section>
<admin-section>OCA\Settings\Sections\Admin\Groupware</admin-section> <admin-section>OCA\Settings\Sections\Admin\Groupware</admin-section>

@ -71,6 +71,7 @@ return array(
'OCA\\Settings\\Settings\\Admin\\Security' => $baseDir . '/../lib/Settings/Admin/Security.php', 'OCA\\Settings\\Settings\\Admin\\Security' => $baseDir . '/../lib/Settings/Admin/Security.php',
'OCA\\Settings\\Settings\\Admin\\Server' => $baseDir . '/../lib/Settings/Admin/Server.php', 'OCA\\Settings\\Settings\\Admin\\Server' => $baseDir . '/../lib/Settings/Admin/Server.php',
'OCA\\Settings\\Settings\\Admin\\Sharing' => $baseDir . '/../lib/Settings/Admin/Sharing.php', 'OCA\\Settings\\Settings\\Admin\\Sharing' => $baseDir . '/../lib/Settings/Admin/Sharing.php',
'OCA\\Settings\\Settings\\Admin\\Users' => $baseDir . '/../lib/Settings/Admin/Users.php',
'OCA\\Settings\\Settings\\Personal\\Additional' => $baseDir . '/../lib/Settings/Personal/Additional.php', 'OCA\\Settings\\Settings\\Personal\\Additional' => $baseDir . '/../lib/Settings/Personal/Additional.php',
'OCA\\Settings\\Settings\\Personal\\PersonalInfo' => $baseDir . '/../lib/Settings/Personal/PersonalInfo.php', 'OCA\\Settings\\Settings\\Personal\\PersonalInfo' => $baseDir . '/../lib/Settings/Personal/PersonalInfo.php',
'OCA\\Settings\\Settings\\Personal\\Security\\Authtokens' => $baseDir . '/../lib/Settings/Personal/Security/Authtokens.php', 'OCA\\Settings\\Settings\\Personal\\Security\\Authtokens' => $baseDir . '/../lib/Settings/Personal/Security/Authtokens.php',

@ -86,6 +86,7 @@ class ComposerStaticInitSettings
'OCA\\Settings\\Settings\\Admin\\Security' => __DIR__ . '/..' . '/../lib/Settings/Admin/Security.php', 'OCA\\Settings\\Settings\\Admin\\Security' => __DIR__ . '/..' . '/../lib/Settings/Admin/Security.php',
'OCA\\Settings\\Settings\\Admin\\Server' => __DIR__ . '/..' . '/../lib/Settings/Admin/Server.php', 'OCA\\Settings\\Settings\\Admin\\Server' => __DIR__ . '/..' . '/../lib/Settings/Admin/Server.php',
'OCA\\Settings\\Settings\\Admin\\Sharing' => __DIR__ . '/..' . '/../lib/Settings/Admin/Sharing.php', 'OCA\\Settings\\Settings\\Admin\\Sharing' => __DIR__ . '/..' . '/../lib/Settings/Admin/Sharing.php',
'OCA\\Settings\\Settings\\Admin\\Users' => __DIR__ . '/..' . '/../lib/Settings/Admin/Users.php',
'OCA\\Settings\\Settings\\Personal\\Additional' => __DIR__ . '/..' . '/../lib/Settings/Personal/Additional.php', 'OCA\\Settings\\Settings\\Personal\\Additional' => __DIR__ . '/..' . '/../lib/Settings/Personal/Additional.php',
'OCA\\Settings\\Settings\\Personal\\PersonalInfo' => __DIR__ . '/..' . '/../lib/Settings/Personal/PersonalInfo.php', 'OCA\\Settings\\Settings\\Personal\\PersonalInfo' => __DIR__ . '/..' . '/../lib/Settings/Personal/PersonalInfo.php',
'OCA\\Settings\\Settings\\Personal\\Security\\Authtokens' => __DIR__ . '/..' . '/../lib/Settings/Personal/Security/Authtokens.php', 'OCA\\Settings\\Settings\\Personal\\Security\\Authtokens' => __DIR__ . '/..' . '/../lib/Settings/Personal/Security/Authtokens.php',

@ -19,12 +19,14 @@ use OC\Security\IdentityProof\Manager;
use OC\User\Manager as UserManager; use OC\User\Manager as UserManager;
use OCA\Settings\BackgroundJobs\VerifyUserData; use OCA\Settings\BackgroundJobs\VerifyUserData;
use OCA\Settings\Events\BeforeTemplateRenderedEvent; use OCA\Settings\Events\BeforeTemplateRenderedEvent;
use OCA\Settings\Settings\Admin\Users;
use OCA\User_LDAP\User_Proxy; use OCA\User_LDAP\User_Proxy;
use OCP\Accounts\IAccount; use OCP\Accounts\IAccount;
use OCP\Accounts\IAccountManager; use OCP\Accounts\IAccountManager;
use OCP\Accounts\PropertyDoesNotExistException; use OCP\Accounts\PropertyDoesNotExistException;
use OCP\App\IAppManager; use OCP\App\IAppManager;
use OCP\AppFramework\Controller; use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\Attribute\AuthorizedAdminSetting;
use OCP\AppFramework\Http\Attribute\OpenAPI; use OCP\AppFramework\Http\Attribute\OpenAPI;
use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\Http\JSONResponse; use OCP\AppFramework\Http\JSONResponse;
@ -93,6 +95,7 @@ class UsersController extends Controller {
$user = $this->userSession->getUser(); $user = $this->userSession->getUser();
$uid = $user->getUID(); $uid = $user->getUID();
$isAdmin = $this->groupManager->isAdmin($uid); $isAdmin = $this->groupManager->isAdmin($uid);
$isDelegatedAdmin = $this->groupManager->isDelegatedAdmin($uid);
\OC::$server->getNavigationManager()->setActiveEntry('core_users'); \OC::$server->getNavigationManager()->setActiveEntry('core_users');
@ -118,6 +121,7 @@ class UsersController extends Controller {
$groupsInfo = new \OC\Group\MetaData( $groupsInfo = new \OC\Group\MetaData(
$uid, $uid,
$isAdmin, $isAdmin,
$isDelegatedAdmin,
$this->groupManager, $this->groupManager,
$this->userSession $this->userSession
); );
@ -135,7 +139,7 @@ class UsersController extends Controller {
$userCount = 0; $userCount = 0;
if (!$isLDAPUsed) { if (!$isLDAPUsed) {
if ($isAdmin) { if ($isAdmin || $isDelegatedAdmin) {
$disabledUsers = $this->userManager->countDisabledUsers(); $disabledUsers = $this->userManager->countDisabledUsers();
$userCount = array_reduce($this->userManager->countUsers(), function ($v, $w) { $userCount = array_reduce($this->userManager->countUsers(), function ($v, $w) {
return $v + (int)$w; return $v + (int)$w;
@ -201,6 +205,7 @@ class UsersController extends Controller {
$serverData['groups'] = array_merge_recursive($adminGroup, [$recentUsersGroup, $disabledUsersGroup], $groups); $serverData['groups'] = array_merge_recursive($adminGroup, [$recentUsersGroup, $disabledUsersGroup], $groups);
// Various data // Various data
$serverData['isAdmin'] = $isAdmin; $serverData['isAdmin'] = $isAdmin;
$serverData['isDelegatedAdmin'] = $isDelegatedAdmin;
$serverData['sortGroups'] = $forceSortGroupByName $serverData['sortGroups'] = $forceSortGroupByName
? \OC\Group\MetaData::SORT_GROUPNAME ? \OC\Group\MetaData::SORT_GROUPNAME
: (int)$this->config->getAppValue('core', 'group.sortBy', (string)\OC\Group\MetaData::SORT_USERCOUNT); : (int)$this->config->getAppValue('core', 'group.sortBy', (string)\OC\Group\MetaData::SORT_USERCOUNT);
@ -232,6 +237,7 @@ class UsersController extends Controller {
* *
* @return JSONResponse * @return JSONResponse
*/ */
#[AuthorizedAdminSetting(settings:Users::class)]
public function setPreference(string $key, string $value): JSONResponse { public function setPreference(string $key, string $value): JSONResponse {
$allowed = ['newUser.sendEmail', 'group.sortBy']; $allowed = ['newUser.sendEmail', 'group.sortBy'];
if (!in_array($key, $allowed, true)) { if (!in_array($key, $allowed, true)) {

@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Settings\Settings\Admin;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\IL10N;
use OCP\Settings\IDelegatedSettings;
/**
* Empty settings class, used only for admin delegation.
*/
class Users implements IDelegatedSettings {
public function __construct(
protected string $appName,
private IL10N $l10n,
) {
}
/**
* Empty template response
*/
public function getForm(): TemplateResponse {
return new /** @template-extends TemplateResponse<\OCP\AppFramework\Http::STATUS_OK, array{}> */ class($this->appName, '') extends TemplateResponse {
public function render(): string {
return '';
}
};
}
public function getSection(): ?string {
return 'admindelegation';
}
/**
* @return int whether the form should be rather on the top or bottom of
* the admin section. The forms are arranged in ascending order of the
* priority values. It is required to return a value between 0 and 100.
*
* E.g.: 70
*/
public function getPriority(): int {
return 0;
}
public function getName(): string {
return $this->l10n->t('Users');
}
public function getAuthorizedAppConfig(): array {
return [];
}
}

@ -45,7 +45,7 @@
</NcCounterBubble> </NcCounterBubble>
</template> </template>
<template #actions> <template #actions>
<NcActionInput v-if="id !== 'admin' && id !== 'disabled' && settings.isAdmin" <NcActionInput v-if="id !== 'admin' && id !== 'disabled' && (settings.isAdmin || settings.isDelegatedAdmin)"
ref="displayNameInput" ref="displayNameInput"
:trailing-button-label="t('settings', 'Submit')" :trailing-button-label="t('settings', 'Submit')"
type="text" type="text"
@ -56,7 +56,7 @@
<Pencil :size="20" /> <Pencil :size="20" />
</template> </template>
</NcActionInput> </NcActionInput>
<NcActionButton v-if="id !== 'admin' && id !== 'disabled' && settings.isAdmin" <NcActionButton v-if="id !== 'admin' && id !== 'disabled' && (settings.isAdmin || settings.isDelegatedAdmin)"
@click="showRemoveGroupModal = true"> @click="showRemoveGroupModal = true">
<template #icon> <template #icon>
<Delete :size="20" /> <Delete :size="20" />

@ -169,10 +169,6 @@ export default {
if (this.selectedGroup === 'disabled') { if (this.selectedGroup === 'disabled') {
return this.users.filter(user => user.enabled === false) return this.users.filter(user => user.enabled === false)
} }
if (!this.settings.isAdmin) {
// we don't want subadmins to edit themselves
return this.users.filter(user => user.enabled !== false)
}
return this.users.filter(user => user.enabled !== false) return this.users.filter(user => user.enabled !== false)
}, },

@ -61,7 +61,7 @@
:required="newUser.password === '' || settings.newUserRequireEmail" /> :required="newUser.password === '' || settings.newUserRequireEmail" />
<div class="dialog__item"> <div class="dialog__item">
<NcSelect class="dialog__select" <NcSelect class="dialog__select"
:input-label="!settings.isAdmin ? t('settings', 'Member of the following groups (required)') : t('settings', 'Member of the following groups')" :input-label="!settings.isAdmin && !settings.isDelegatedAdmin ? t('settings', 'Member of the following groups (required)') : t('settings', 'Member of the following groups')"
:placeholder="t('settings', 'Set account groups')" :placeholder="t('settings', 'Set account groups')"
:disabled="loading.groups || loading.all" :disabled="loading.groups || loading.all"
:options="canAddGroups" :options="canAddGroups"
@ -70,7 +70,7 @@
:close-on-select="false" :close-on-select="false"
:multiple="true" :multiple="true"
:taggable="true" :taggable="true"
:required="!settings.isAdmin" :required="!settings.isAdmin && !settings.isDelegatedAdmin"
@input="handleGroupInput" @input="handleGroupInput"
@option:created="createGroup" /> @option:created="createGroup" />
<!-- If user is not admin, he is a subadmin. <!-- If user is not admin, he is a subadmin.

@ -42,7 +42,7 @@
scope="col"> scope="col">
<span>{{ t('settings', 'Groups') }}</span> <span>{{ t('settings', 'Groups') }}</span>
</th> </th>
<th v-if="subAdminsGroups.length > 0 && settings.isAdmin" <th v-if="subAdminsGroups.length > 0 && (settings.isAdmin || settings.isDelegatedAdmin)"
class="header__cell header__cell--large" class="header__cell header__cell--large"
data-cy-user-list-header-subadmins data-cy-user-list-header-subadmins
scope="col"> scope="col">

@ -112,7 +112,7 @@
:append-to-body="false" :append-to-body="false"
:options="availableGroups" :options="availableGroups"
:placeholder="t('settings', 'Add account to group')" :placeholder="t('settings', 'Add account to group')"
:taggable="settings.isAdmin" :taggable="settings.isAdmin || settings.isDelegatedAdmin"
:value="userGroups" :value="userGroups"
label="name" label="name"
:no-wrap="true" :no-wrap="true"
@ -127,10 +127,10 @@
</span> </span>
</td> </td>
<td v-if="subAdminsGroups.length > 0 && settings.isAdmin" <td v-if="subAdminsGroups.length > 0 && (settings.isAdmin || settings.isDelegatedAdmin)"
data-cy-user-list-cell-subadmins data-cy-user-list-cell-subadmins
class="row__cell row__cell--large row__cell--multiline"> class="row__cell row__cell--large row__cell--multiline">
<template v-if="editing && settings.isAdmin && subAdminsGroups.length > 0"> <template v-if="editing && (settings.isAdmin || settings.isDelegatedAdmin) && subAdminsGroups.length > 0">
<label class="hidden-visually" <label class="hidden-visually"
:for="'subadmins' + uniqueId"> :for="'subadmins' + uniqueId">
{{ t('settings', 'Set account as admin for') }} {{ t('settings', 'Set account as admin for') }}
@ -424,7 +424,7 @@ export default {
}, },
canEdit() { canEdit() {
return getCurrentUser().uid !== this.user.id || this.settings.isAdmin return getCurrentUser().uid !== this.user.id || this.settings.isAdmin || this.settings.isDelegatedAdmin
}, },
userQuota() { userQuota() {
@ -624,18 +624,21 @@ export default {
* *
* @param {string} displayName The display name * @param {string} displayName The display name
*/ */
updateDisplayName() { async updateDisplayName() {
this.loading.displayName = true this.loading.displayName = true
this.$store.dispatch('setUserData', { try {
userid: this.user.id, await this.$store.dispatch('setUserData', {
key: 'displayname', userid: this.user.id,
value: this.editedDisplayName, key: 'displayname',
}).then(() => { value: this.editedDisplayName,
this.loading.displayName = false })
if (this.editedDisplayName === this.user.displayname) { if (this.editedDisplayName === this.user.displayname) {
showSuccess(t('setting', 'Display name was successfully changed')) showSuccess(t('setting', 'Display name was successfully changed'))
} }
}) } finally {
this.loading.displayName = false
}
}, },
/** /**
@ -643,21 +646,23 @@ export default {
* *
* @param {string} password The email address * @param {string} password The email address
*/ */
updatePassword() { async updatePassword() {
this.loading.password = true this.loading.password = true
if (this.editedPassword.length === 0) { if (this.editedPassword.length === 0) {
showError(t('setting', "Password can't be empty")) showError(t('setting', "Password can't be empty"))
this.loading.password = false this.loading.password = false
} else { } else {
this.$store.dispatch('setUserData', { try {
userid: this.user.id, await this.$store.dispatch('setUserData', {
key: 'password', userid: this.user.id,
value: this.editedPassword, key: 'password',
}).then(() => { value: this.editedPassword,
this.loading.password = false })
this.editedPassword = '' this.editedPassword = ''
showSuccess(t('setting', 'Password was successfully changed')) showSuccess(t('setting', 'Password was successfully changed'))
}) } finally {
this.loading.password = false
}
} }
}, },
@ -666,23 +671,26 @@ export default {
* *
* @param {string} mailAddress The email address * @param {string} mailAddress The email address
*/ */
updateEmail() { async updateEmail() {
this.loading.mailAddress = true this.loading.mailAddress = true
if (this.editedMail === '') { if (this.editedMail === '') {
showError(t('setting', "Email can't be empty")) showError(t('setting', "Email can't be empty"))
this.loading.mailAddress = false this.loading.mailAddress = false
this.editedMail = this.user.email this.editedMail = this.user.email
} else { } else {
this.$store.dispatch('setUserData', { try {
userid: this.user.id, await this.$store.dispatch('setUserData', {
key: 'email', userid: this.user.id,
value: this.editedMail, key: 'email',
}).then(() => { value: this.editedMail,
this.loading.mailAddress = false })
if (this.editedMail === this.user.email) { if (this.editedMail === this.user.email) {
showSuccess(t('setting', 'Email was successfully changed')) showSuccess(t('setting', 'Email was successfully changed'))
} }
}) } finally {
this.loading.mailAddress = false
}
} }
}, },

@ -641,11 +641,14 @@ const actions = {
* @param {string} userid User id * @param {string} userid User id
* @return {Promise} * @return {Promise}
*/ */
wipeUserDevices(context, userid) { async wipeUserDevices(context, userid) {
return api.requireAdmin().then((response) => { try {
return api.post(generateOcsUrl('cloud/users/{userid}/wipe', { userid })) await api.requireAdmin()
.catch((error) => { throw error }) return await api.post(generateOcsUrl('cloud/users/{userid}/wipe', { userid }))
}).catch((error) => context.commit('API_FAILURE', { userid, error })) } catch (error) {
context.commit('API_FAILURE', { userid, error })
return Promise.reject(new Error('Failed to wipe user devices'))
}
}, },
/** /**
@ -735,7 +738,7 @@ const actions = {
* @param {string} options.value Value of the change * @param {string} options.value Value of the change
* @return {Promise} * @return {Promise}
*/ */
setUserData(context, { userid, key, value }) { async setUserData(context, { userid, key, value }) {
const allowedEmpty = ['email', 'displayname', 'manager'] const allowedEmpty = ['email', 'displayname', 'manager']
if (['email', 'language', 'quota', 'displayname', 'password', 'manager'].indexOf(key) !== -1) { if (['email', 'language', 'quota', 'displayname', 'password', 'manager'].indexOf(key) !== -1) {
// We allow empty email or displayname // We allow empty email or displayname
@ -745,11 +748,13 @@ const actions = {
|| allowedEmpty.indexOf(key) !== -1 || allowedEmpty.indexOf(key) !== -1
) )
) { ) {
return api.requireAdmin().then((response) => { try {
return api.put(generateOcsUrl('cloud/users/{userid}', { userid }), { key, value }) await api.requireAdmin()
.then((response) => context.commit('setUserData', { userid, key, value })) await api.put(generateOcsUrl('cloud/users/{userid}', { userid }), { key, value })
.catch((error) => { throw error }) return context.commit('setUserData', { userid, key, value })
}).catch((error) => context.commit('API_FAILURE', { userid, error })) } catch (error) {
context.commit('API_FAILURE', { userid, error })
}
} }
} }
return Promise.reject(new Error('Invalid request data')) return Promise.reject(new Error('Invalid request data'))

@ -81,6 +81,8 @@ class AdminSettingsControllerTest extends TestCase {
protected function tearDown(): void { protected function tearDown(): void {
\OC::$server->getUserManager()->get($this->adminUid)->delete(); \OC::$server->getUserManager()->get($this->adminUid)->delete();
\OC_User::setUserId(null);
\OC::$server->getUserSession()->setUser(null);
parent::tearDown(); parent::tearDown();
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -8,6 +8,7 @@
namespace OC\Group; namespace OC\Group;
use OC\Hooks\PublicEmitter; use OC\Hooks\PublicEmitter;
use OC\Settings\AuthorizedGroupMapper;
use OCP\EventDispatcher\IEventDispatcher; use OCP\EventDispatcher\IEventDispatcher;
use OCP\Group\Backend\IBatchMethodsBackend; use OCP\Group\Backend\IBatchMethodsBackend;
use OCP\Group\Backend\ICreateNamedGroupBackend; use OCP\Group\Backend\ICreateNamedGroupBackend;
@ -333,6 +334,18 @@ class Manager extends PublicEmitter implements IGroupManager {
return $this->isInGroup($userId, 'admin'); 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 * Checks if a userId is in a group
* *

@ -17,33 +17,22 @@ class MetaData {
public const SORT_USERCOUNT = 1; // May have performance issues on LDAP backends public const SORT_USERCOUNT = 1; // May have performance issues on LDAP backends
public const SORT_GROUPNAME = 2; public const SORT_GROUPNAME = 2;
/** @var string */
protected $user;
/** @var bool */
protected $isAdmin;
/** @var array */ /** @var array */
protected $metaData = []; protected $metaData = [];
/** @var GroupManager */
protected $groupManager;
/** @var int */ /** @var int */
protected $sorting = self::SORT_NONE; protected $sorting = self::SORT_NONE;
/** @var IUserSession */
protected $userSession;
/** /**
* @param string $user the uid of the current user * @param string $user the uid of the current user
* @param bool $isAdmin whether the current users is an admin * @param bool $isAdmin whether the current users is an admin
*/ */
public function __construct( public function __construct(
string $user, private string $user,
bool $isAdmin, private bool $isAdmin,
IGroupManager $groupManager, private bool $isDelegatedAdmin,
IUserSession $userSession private IGroupManager $groupManager,
private IUserSession $userSession
) { ) {
$this->user = $user;
$this->isAdmin = $isAdmin;
$this->groupManager = $groupManager;
$this->userSession = $userSession;
} }
/** /**
@ -162,11 +151,11 @@ class MetaData {
* @return IGroup[] * @return IGroup[]
*/ */
public function getGroups(string $search = ''): array { public function getGroups(string $search = ''): array {
if ($this->isAdmin) { if ($this->isAdmin || $this->isDelegatedAdmin) {
return $this->groupManager->search($search); return $this->groupManager->search($search);
} else { } else {
$userObject = $this->userSession->getUser(); $userObject = $this->userSession->getUser();
if ($userObject !== null) { if ($userObject !== null && $this->groupManager instanceof GroupManager) {
$groups = $this->groupManager->getSubAdmin()->getSubAdminsGroups($userObject); $groups = $this->groupManager->getSubAdmin()->getSubAdminsGroups($userObject);
} else { } else {
$groups = []; $groups = [];

@ -233,6 +233,11 @@ class SubAdmin extends PublicEmitter implements ISubAdmin {
return true; return true;
} }
// Check if the user is already an admin
if ($this->groupManager->isDelegatedAdmin($user->getUID())) {
return true;
}
$qb = $this->dbConn->getQueryBuilder(); $qb = $this->dbConn->getQueryBuilder();
$result = $qb->select('gid') $result = $qb->select('gid')

@ -114,6 +114,14 @@ interface IGroupManager {
*/ */
public function isAdmin($userId); public function isAdmin($userId);
/**
* Checks if a userId is eligible to users administration delegation
* @param string $userId
* @return bool if delegated admin
* @since 30.0.0
*/
public function isDelegatedAdmin(string $userId): bool;
/** /**
* Checks if a userId is in a group * Checks if a userId is in a group
* @param string $userId * @param string $userId

@ -10,14 +10,11 @@ namespace Test\Group;
use OCP\IUserSession; use OCP\IUserSession;
class MetaDataTest extends \Test\TestCase { class MetaDataTest extends \Test\TestCase {
/** @var \OC\Group\Manager */ private \OC\Group\Manager $groupManager;
private $groupManager; private IUserSession $userSession;
/** @var \OCP\IUserSession */ private \OC\Group\MetaData $groupMetadata;
private $userSession; private bool $isAdmin = true;
/** @var \OC\Group\MetaData */ private bool $isDelegatedAdmin = true;
private $groupMetadata;
/** @var bool */
private $isAdmin = true;
protected function setUp(): void { protected function setUp(): void {
parent::setUp(); parent::setUp();
@ -28,6 +25,7 @@ class MetaDataTest extends \Test\TestCase {
$this->groupMetadata = new \OC\Group\MetaData( $this->groupMetadata = new \OC\Group\MetaData(
'foo', 'foo',
$this->isAdmin, $this->isAdmin,
$this->isDelegatedAdmin,
$this->groupManager, $this->groupManager,
$this->userSession $this->userSession
); );