feat(users): Add support for admin delegation for users and groups management

Signed-off-by: Louis Chemineau <louis@chmn.me>
pull/46418/head
Louis Chemineau 2024-07-11 12:09:39 +07:00
parent 1af827fdb3
commit dff8815449
No known key found for this signature in database
13 changed files with 182 additions and 53 deletions

@ -98,7 +98,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 {
@ -116,7 +118,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;
} }
@ -941,7 +956,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;
} }
@ -949,7 +966,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
@ -1204,7 +1222,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);
} }
@ -1240,7 +1260,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);
} }
@ -1300,7 +1322,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);
} }
@ -1329,7 +1353,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)
@ -1388,7 +1414,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);
} }
@ -1429,13 +1457,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);
} }
@ -1443,7 +1473,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) {
@ -1475,6 +1505,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);
@ -1515,6 +1546,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);
@ -1547,6 +1579,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);
@ -1574,9 +1607,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);

@ -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;
@ -200,7 +204,8 @@ class UsersController extends Controller {
// groups // groups
$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 || $isDelegatedAdmin;
$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 [];
}
}

@ -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
); );