summaryrefslogtreecommitdiffstats
path: root/apps/settings
diff options
context:
space:
mode:
authorLouis <louis@chmn.me>2024-07-24 11:15:54 +0200
committerGitHub <noreply@github.com>2024-07-24 11:15:54 +0200
commit7266a9ef333b47f4ec6dd16f48227fd4b4e862d4 (patch)
treeadb2b808e653b2ea1d0255ae774e97241d8c25c6 /apps/settings
parentf3a2806b691543ba48968f875ad381d53f68ba35 (diff)
parent7f0f671417f6de083827327d72fa7f8a21c7a950 (diff)
downloadnextcloud-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.xml1
-rw-r--r--apps/settings/composer/composer/autoload_classmap.php1
-rw-r--r--apps/settings/composer/composer/autoload_static.php1
-rw-r--r--apps/settings/lib/Controller/UsersController.php8
-rw-r--r--apps/settings/lib/Settings/Admin/Users.php61
-rw-r--r--apps/settings/src/components/GroupListItem.vue4
-rw-r--r--apps/settings/src/components/UserList.vue4
-rw-r--r--apps/settings/src/components/Users/NewUserDialog.vue4
-rw-r--r--apps/settings/src/components/Users/UserListHeader.vue2
-rw-r--r--apps/settings/src/components/Users/UserRow.vue64
-rw-r--r--apps/settings/src/store/users.js27
-rw-r--r--apps/settings/tests/Controller/AdminSettingsControllerTest.php2
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();
}