diff options
author | Louis <louis@chmn.me> | 2024-07-24 11:15:54 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-07-24 11:15:54 +0200 |
commit | 7266a9ef333b47f4ec6dd16f48227fd4b4e862d4 (patch) | |
tree | adb2b808e653b2ea1d0255ae774e97241d8c25c6 /apps/settings | |
parent | f3a2806b691543ba48968f875ad381d53f68ba35 (diff) | |
parent | 7f0f671417f6de083827327d72fa7f8a21c7a950 (diff) | |
download | nextcloud-server-7266a9ef333b47f4ec6dd16f48227fd4b4e862d4.tar.gz nextcloud-server-7266a9ef333b47f4ec6dd16f48227fd4b4e862d4.zip |
Merge pull request #46418 from nextcloud/artonge/feat/user_admin_delegation
feat(users): Add users and group management to admin delegation
Diffstat (limited to 'apps/settings')
-rw-r--r-- | apps/settings/appinfo/info.xml | 1 | ||||
-rw-r--r-- | apps/settings/composer/composer/autoload_classmap.php | 1 | ||||
-rw-r--r-- | apps/settings/composer/composer/autoload_static.php | 1 | ||||
-rw-r--r-- | apps/settings/lib/Controller/UsersController.php | 8 | ||||
-rw-r--r-- | apps/settings/lib/Settings/Admin/Users.php | 61 | ||||
-rw-r--r-- | apps/settings/src/components/GroupListItem.vue | 4 | ||||
-rw-r--r-- | apps/settings/src/components/UserList.vue | 4 | ||||
-rw-r--r-- | apps/settings/src/components/Users/NewUserDialog.vue | 4 | ||||
-rw-r--r-- | apps/settings/src/components/Users/UserListHeader.vue | 2 | ||||
-rw-r--r-- | apps/settings/src/components/Users/UserRow.vue | 64 | ||||
-rw-r--r-- | apps/settings/src/store/users.js | 27 | ||||
-rw-r--r-- | apps/settings/tests/Controller/AdminSettingsControllerTest.php | 2 |
12 files changed, 130 insertions, 49 deletions
diff --git a/apps/settings/appinfo/info.xml b/apps/settings/appinfo/info.xml index a87e459aaf7..2674b97b57d 100644 --- a/apps/settings/appinfo/info.xml +++ b/apps/settings/appinfo/info.xml @@ -34,6 +34,7 @@ <admin>OCA\Settings\Settings\Admin\Sharing</admin> <admin>OCA\Settings\Settings\Admin\Security</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\Delegation</admin-section> <admin-section>OCA\Settings\Sections\Admin\Groupware</admin-section> diff --git a/apps/settings/composer/composer/autoload_classmap.php b/apps/settings/composer/composer/autoload_classmap.php index 27c1496008e..41f70c3a8e6 100644 --- a/apps/settings/composer/composer/autoload_classmap.php +++ b/apps/settings/composer/composer/autoload_classmap.php @@ -71,6 +71,7 @@ return array( '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\\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\\PersonalInfo' => $baseDir . '/../lib/Settings/Personal/PersonalInfo.php', 'OCA\\Settings\\Settings\\Personal\\Security\\Authtokens' => $baseDir . '/../lib/Settings/Personal/Security/Authtokens.php', diff --git a/apps/settings/composer/composer/autoload_static.php b/apps/settings/composer/composer/autoload_static.php index 14e4c362887..4fa905b55bb 100644 --- a/apps/settings/composer/composer/autoload_static.php +++ b/apps/settings/composer/composer/autoload_static.php @@ -86,6 +86,7 @@ class ComposerStaticInitSettings '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\\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\\PersonalInfo' => __DIR__ . '/..' . '/../lib/Settings/Personal/PersonalInfo.php', 'OCA\\Settings\\Settings\\Personal\\Security\\Authtokens' => __DIR__ . '/..' . '/../lib/Settings/Personal/Security/Authtokens.php', diff --git a/apps/settings/lib/Controller/UsersController.php b/apps/settings/lib/Controller/UsersController.php index 999f883bad8..823d3d4cb8b 100644 --- a/apps/settings/lib/Controller/UsersController.php +++ b/apps/settings/lib/Controller/UsersController.php @@ -19,12 +19,14 @@ use OC\Security\IdentityProof\Manager; use OC\User\Manager as UserManager; use OCA\Settings\BackgroundJobs\VerifyUserData; use OCA\Settings\Events\BeforeTemplateRenderedEvent; +use OCA\Settings\Settings\Admin\Users; use OCA\User_LDAP\User_Proxy; use OCP\Accounts\IAccount; use OCP\Accounts\IAccountManager; use OCP\Accounts\PropertyDoesNotExistException; use OCP\App\IAppManager; use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\Attribute\AuthorizedAdminSetting; use OCP\AppFramework\Http\Attribute\OpenAPI; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\Http\JSONResponse; @@ -93,6 +95,7 @@ class UsersController extends Controller { $user = $this->userSession->getUser(); $uid = $user->getUID(); $isAdmin = $this->groupManager->isAdmin($uid); + $isDelegatedAdmin = $this->groupManager->isDelegatedAdmin($uid); \OC::$server->getNavigationManager()->setActiveEntry('core_users'); @@ -118,6 +121,7 @@ class UsersController extends Controller { $groupsInfo = new \OC\Group\MetaData( $uid, $isAdmin, + $isDelegatedAdmin, $this->groupManager, $this->userSession ); @@ -135,7 +139,7 @@ class UsersController extends Controller { $userCount = 0; if (!$isLDAPUsed) { - if ($isAdmin) { + if ($isAdmin || $isDelegatedAdmin) { $disabledUsers = $this->userManager->countDisabledUsers(); $userCount = array_reduce($this->userManager->countUsers(), function ($v, $w) { return $v + (int)$w; @@ -201,6 +205,7 @@ class UsersController extends Controller { $serverData['groups'] = array_merge_recursive($adminGroup, [$recentUsersGroup, $disabledUsersGroup], $groups); // Various data $serverData['isAdmin'] = $isAdmin; + $serverData['isDelegatedAdmin'] = $isDelegatedAdmin; $serverData['sortGroups'] = $forceSortGroupByName ? \OC\Group\MetaData::SORT_GROUPNAME : (int)$this->config->getAppValue('core', 'group.sortBy', (string)\OC\Group\MetaData::SORT_USERCOUNT); @@ -232,6 +237,7 @@ class UsersController extends Controller { * * @return JSONResponse */ + #[AuthorizedAdminSetting(settings:Users::class)] public function setPreference(string $key, string $value): JSONResponse { $allowed = ['newUser.sendEmail', 'group.sortBy']; if (!in_array($key, $allowed, true)) { diff --git a/apps/settings/lib/Settings/Admin/Users.php b/apps/settings/lib/Settings/Admin/Users.php new file mode 100644 index 00000000000..3af018e0cf1 --- /dev/null +++ b/apps/settings/lib/Settings/Admin/Users.php @@ -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 []; + } +} diff --git a/apps/settings/src/components/GroupListItem.vue b/apps/settings/src/components/GroupListItem.vue index 98a96c9b4ef..44b0605c9de 100644 --- a/apps/settings/src/components/GroupListItem.vue +++ b/apps/settings/src/components/GroupListItem.vue @@ -45,7 +45,7 @@ </NcCounterBubble> </template> <template #actions> - <NcActionInput v-if="id !== 'admin' && id !== 'disabled' && settings.isAdmin" + <NcActionInput v-if="id !== 'admin' && id !== 'disabled' && (settings.isAdmin || settings.isDelegatedAdmin)" ref="displayNameInput" :trailing-button-label="t('settings', 'Submit')" type="text" @@ -56,7 +56,7 @@ <Pencil :size="20" /> </template> </NcActionInput> - <NcActionButton v-if="id !== 'admin' && id !== 'disabled' && settings.isAdmin" + <NcActionButton v-if="id !== 'admin' && id !== 'disabled' && (settings.isAdmin || settings.isDelegatedAdmin)" @click="showRemoveGroupModal = true"> <template #icon> <Delete :size="20" /> diff --git a/apps/settings/src/components/UserList.vue b/apps/settings/src/components/UserList.vue index 1fcfe8e6e1b..b417043a270 100644 --- a/apps/settings/src/components/UserList.vue +++ b/apps/settings/src/components/UserList.vue @@ -169,10 +169,6 @@ export default { if (this.selectedGroup === 'disabled') { 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) }, diff --git a/apps/settings/src/components/Users/NewUserDialog.vue b/apps/settings/src/components/Users/NewUserDialog.vue index 5547fed2216..9cb28ab9a18 100644 --- a/apps/settings/src/components/Users/NewUserDialog.vue +++ b/apps/settings/src/components/Users/NewUserDialog.vue @@ -61,7 +61,7 @@ :required="newUser.password === '' || settings.newUserRequireEmail" /> <div class="dialog__item"> <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')" :disabled="loading.groups || loading.all" :options="canAddGroups" @@ -70,7 +70,7 @@ :close-on-select="false" :multiple="true" :taggable="true" - :required="!settings.isAdmin" + :required="!settings.isAdmin && !settings.isDelegatedAdmin" @input="handleGroupInput" @option:created="createGroup" /> <!-- If user is not admin, he is a subadmin. diff --git a/apps/settings/src/components/Users/UserListHeader.vue b/apps/settings/src/components/Users/UserListHeader.vue index 44eb1a6b7ae..dbf60a523a0 100644 --- a/apps/settings/src/components/Users/UserListHeader.vue +++ b/apps/settings/src/components/Users/UserListHeader.vue @@ -42,7 +42,7 @@ scope="col"> <span>{{ t('settings', 'Groups') }}</span> </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" data-cy-user-list-header-subadmins scope="col"> diff --git a/apps/settings/src/components/Users/UserRow.vue b/apps/settings/src/components/Users/UserRow.vue index 39ecebb968f..1644fc89a55 100644 --- a/apps/settings/src/components/Users/UserRow.vue +++ b/apps/settings/src/components/Users/UserRow.vue @@ -112,7 +112,7 @@ :append-to-body="false" :options="availableGroups" :placeholder="t('settings', 'Add account to group')" - :taggable="settings.isAdmin" + :taggable="settings.isAdmin || settings.isDelegatedAdmin" :value="userGroups" label="name" :no-wrap="true" @@ -127,10 +127,10 @@ </span> </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 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" :for="'subadmins' + uniqueId"> {{ t('settings', 'Set account as admin for') }} @@ -424,7 +424,7 @@ export default { }, canEdit() { - return getCurrentUser().uid !== this.user.id || this.settings.isAdmin + return getCurrentUser().uid !== this.user.id || this.settings.isAdmin || this.settings.isDelegatedAdmin }, userQuota() { @@ -624,18 +624,21 @@ export default { * * @param {string} displayName The display name */ - updateDisplayName() { + async updateDisplayName() { this.loading.displayName = true - this.$store.dispatch('setUserData', { - userid: this.user.id, - key: 'displayname', - value: this.editedDisplayName, - }).then(() => { - this.loading.displayName = false + try { + await this.$store.dispatch('setUserData', { + userid: this.user.id, + key: 'displayname', + value: this.editedDisplayName, + }) + if (this.editedDisplayName === this.user.displayname) { 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 */ - updatePassword() { + async updatePassword() { this.loading.password = true if (this.editedPassword.length === 0) { showError(t('setting', "Password can't be empty")) this.loading.password = false } else { - this.$store.dispatch('setUserData', { - userid: this.user.id, - key: 'password', - value: this.editedPassword, - }).then(() => { - this.loading.password = false + try { + await this.$store.dispatch('setUserData', { + userid: this.user.id, + key: 'password', + value: this.editedPassword, + }) this.editedPassword = '' showSuccess(t('setting', 'Password was successfully changed')) - }) + } finally { + this.loading.password = false + } } }, @@ -666,23 +671,26 @@ export default { * * @param {string} mailAddress The email address */ - updateEmail() { + async updateEmail() { this.loading.mailAddress = true if (this.editedMail === '') { showError(t('setting', "Email can't be empty")) this.loading.mailAddress = false this.editedMail = this.user.email } else { - this.$store.dispatch('setUserData', { - userid: this.user.id, - key: 'email', - value: this.editedMail, - }).then(() => { - this.loading.mailAddress = false + try { + await this.$store.dispatch('setUserData', { + userid: this.user.id, + key: 'email', + value: this.editedMail, + }) + if (this.editedMail === this.user.email) { showSuccess(t('setting', 'Email was successfully changed')) } - }) + } finally { + this.loading.mailAddress = false + } } }, diff --git a/apps/settings/src/store/users.js b/apps/settings/src/store/users.js index 8b60b3ab328..15c40d77072 100644 --- a/apps/settings/src/store/users.js +++ b/apps/settings/src/store/users.js @@ -641,11 +641,14 @@ const actions = { * @param {string} userid User id * @return {Promise} */ - wipeUserDevices(context, userid) { - return api.requireAdmin().then((response) => { - return api.post(generateOcsUrl('cloud/users/{userid}/wipe', { userid })) - .catch((error) => { throw error }) - }).catch((error) => context.commit('API_FAILURE', { userid, error })) + async wipeUserDevices(context, userid) { + try { + await api.requireAdmin() + return await api.post(generateOcsUrl('cloud/users/{userid}/wipe', { userid })) + } 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 * @return {Promise} */ - setUserData(context, { userid, key, value }) { + async setUserData(context, { userid, key, value }) { const allowedEmpty = ['email', 'displayname', 'manager'] if (['email', 'language', 'quota', 'displayname', 'password', 'manager'].indexOf(key) !== -1) { // We allow empty email or displayname @@ -745,11 +748,13 @@ const actions = { || allowedEmpty.indexOf(key) !== -1 ) ) { - return api.requireAdmin().then((response) => { - return api.put(generateOcsUrl('cloud/users/{userid}', { userid }), { key, value }) - .then((response) => context.commit('setUserData', { userid, key, value })) - .catch((error) => { throw error }) - }).catch((error) => context.commit('API_FAILURE', { userid, error })) + try { + await api.requireAdmin() + await api.put(generateOcsUrl('cloud/users/{userid}', { userid }), { key, value }) + return context.commit('setUserData', { userid, key, value }) + } catch (error) { + context.commit('API_FAILURE', { userid, error }) + } } } return Promise.reject(new Error('Invalid request data')) diff --git a/apps/settings/tests/Controller/AdminSettingsControllerTest.php b/apps/settings/tests/Controller/AdminSettingsControllerTest.php index 6f4a941011e..578348a3031 100644 --- a/apps/settings/tests/Controller/AdminSettingsControllerTest.php +++ b/apps/settings/tests/Controller/AdminSettingsControllerTest.php @@ -81,6 +81,8 @@ class AdminSettingsControllerTest extends TestCase { protected function tearDown(): void { \OC::$server->getUserManager()->get($this->adminUid)->delete(); + \OC_User::setUserId(null); + \OC::$server->getUserSession()->setUser(null); parent::tearDown(); } |