feat(users): Store and load a user's manager

Co-Authored-By: hamza221 <hamzamahjoubi221@gmail.com>
Signed-off-by: Christoph Wurst <christoph@winzerhof-wurst.at>
This commit is contained in:
Christoph Wurst 2023-05-02 08:59:46 +02:00
parent 1399c88ee1
commit 1381c4c157
No known key found for this signature in database
GPG Key ID: CC42AC2A7F0E56D8
19 changed files with 264 additions and 13 deletions

View File

@ -31,15 +31,19 @@ use Exception;
use OCP\Accounts\IAccountManager;
use OCP\IImage;
use OCP\IUser;
use OCP\IUserManager;
use Sabre\VObject\Component\VCard;
use Sabre\VObject\Property\Text;
class Converter {
/** @var IAccountManager */
private $accountManager;
private IUserManager $userManager;
public function __construct(IAccountManager $accountManager) {
public function __construct(IAccountManager $accountManager,
IUserManager $userManager) {
$this->accountManager = $accountManager;
$this->userManager = $userManager;
}
public function createCardFromUser(IUser $user): ?VCard {
@ -102,6 +106,20 @@ class Converter {
}
}
// Local properties
$managers = $user->getManagerUids();
// X-MANAGERSNAME only allows a single value, so we take the first manager
if (isset($managers[0])) {
$displayName = $this->userManager->getDisplayName($managers[0]);
// Only set the manager if a user object is found
if ($displayName !== null) {
$vCard->add(new Text($vCard, 'X-MANAGERSNAME', $displayName, [
'uid' => $managers[0],
'X-NC-SCOPE' => IAccountManager::SCOPE_LOCAL,
]));
}
}
if ($publish && !empty($cloudId)) {
$vCard->add(new Text($vCard, 'CLOUD', $cloudId));
$vCard->validate();

View File

@ -1,4 +1,7 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
@ -33,6 +36,7 @@ use OCP\Accounts\IAccountManager;
use OCP\Accounts\IAccountProperty;
use OCP\IImage;
use OCP\IUser;
use OCP\IUserManager;
use PHPUnit\Framework\MockObject\MockObject;
use Test\TestCase;
@ -40,11 +44,14 @@ class ConverterTest extends TestCase {
/** @var IAccountManager|\PHPUnit\Framework\MockObject\MockObject */
private $accountManager;
/** @var IUserManager|(IUserManager&MockObject)|MockObject */
private IUserManager|MockObject $userManager;
protected function setUp(): void {
parent::setUp();
$this->accountManager = $this->createMock(IAccountManager::class);
$this->userManager = $this->createMock(IUserManager::class);
}
/**
@ -96,7 +103,7 @@ class ConverterTest extends TestCase {
$user = $this->getUserMock((string)$displayName, $eMailAddress, $cloudId);
$accountManager = $this->getAccountManager($user);
$converter = new Converter($accountManager);
$converter = new Converter($accountManager, $this->userManager);
$vCard = $converter->createCardFromUser($user);
if ($expectedVCard !== null) {
$this->assertInstanceOf('Sabre\VObject\Component\VCard', $vCard);
@ -107,6 +114,29 @@ class ConverterTest extends TestCase {
}
}
public function testManagerProp(): void {
$user = $this->getUserMock("user", "user@domain.tld", "user@cloud.domain.tld");
$user->method('getManagerUids')
->willReturn(['mgr']);
$this->userManager->expects(self::once())
->method('getDisplayName')
->with('mgr')
->willReturn('Manager');
$accountManager = $this->getAccountManager($user);
$converter = new Converter($accountManager, $this->userManager);
$vCard = $converter->createCardFromUser($user);
$this->compareData(
[
'cloud' => 'user@cloud.domain.tld',
'email' => 'user@domain.tld',
'x-managersname' => 'Manager',
],
$vCard->jsonSerialize()
);
}
protected function compareData($expected, $data) {
foreach ($expected as $key => $value) {
$found = false;
@ -182,7 +212,7 @@ class ConverterTest extends TestCase {
* @param $fullName
*/
public function testNameSplitter($expected, $fullName): void {
$converter = new Converter($this->accountManager);
$converter = new Converter($this->accountManager, $this->userManager);
$r = $converter->splitFullName($fullName);
$r = implode(';', $r);
$this->assertEquals($expected, $r);

View File

@ -60,6 +60,7 @@ abstract class AUserData extends OCSController {
public const USER_FIELD_LOCALE = 'locale';
public const USER_FIELD_PASSWORD = 'password';
public const USER_FIELD_QUOTA = 'quota';
public const USER_FIELD_MANAGER = 'manager';
public const USER_FIELD_NOTIFICATION_EMAIL = 'notify_email';
/** @var IUserManager */
@ -151,6 +152,8 @@ abstract class AUserData extends OCSController {
$data['backend'] = $targetUserObject->getBackendClassName();
$data['subadmin'] = $this->getUserSubAdminGroupsData($targetUserObject->getUID());
$data[self::USER_FIELD_QUOTA] = $this->fillStorageInfo($targetUserObject->getUID());
$managerUids = $targetUserObject->getManagerUids();
$data[self::USER_FIELD_MANAGER] = empty($managerUids) ? '' : $managerUids[0];
try {
if ($includeScopes) {

View File

@ -338,7 +338,8 @@ class UsersController extends AUserData {
array $groups = [],
array $subadmin = [],
string $quota = '',
string $language = ''
string $language = '',
?string $manager = null,
): DataResponse {
$user = $this->userSession->getUser();
$isAdmin = $this->groupManager->isAdmin($user->getUID());
@ -447,6 +448,15 @@ class UsersController extends AUserData {
$this->editUser($userid, self::USER_FIELD_LANGUAGE, $language);
}
/**
* null -> nothing sent
* '' -> unset manager
* else -> set manager
*/
if ($manager !== null) {
$this->editUser($userid, self::USER_FIELD_MANAGER, $manager);
}
// Send new user mail only if a mail is set
if ($email !== '') {
$newUser->setEMailAddress($email);
@ -800,9 +810,11 @@ class UsersController extends AUserData {
$permittedFields[] = IAccountManager::PROPERTY_AVATAR . self::SCOPE_SUFFIX;
// If admin they can edit their own quota
// If admin they can edit their own quota and manager
if ($this->groupManager->isAdmin($currentLoggedInUser->getUID())) {
$permittedFields[] = self::USER_FIELD_QUOTA;
$permittedFields[] = self::USER_FIELD_MANAGER;
}
} else {
// Check if admin / subadmin
@ -836,6 +848,7 @@ class UsersController extends AUserData {
$permittedFields[] = IAccountManager::PROPERTY_PROFILE_ENABLED;
$permittedFields[] = self::USER_FIELD_QUOTA;
$permittedFields[] = self::USER_FIELD_NOTIFICATION_EMAIL;
$permittedFields[] = self::USER_FIELD_MANAGER;
} else {
// No rights
throw new OCSException('', OCSController::RESPOND_NOT_FOUND);
@ -885,6 +898,9 @@ class UsersController extends AUserData {
}
$targetUser->setQuota($quota);
break;
case self::USER_FIELD_MANAGER:
$targetUser->setManagerUids([$value]);
break;
case self::USER_FIELD_PASSWORD:
try {
if (strlen($value) > IUserManager::MAX_PASSWORD_LENGTH) {

View File

@ -1093,6 +1093,7 @@ class UsersControllerTest extends TestCase {
'biography' => 'biography',
'profile_enabled' => '1',
'notify_email' => null,
'manager' => '',
];
$this->assertEquals($expected, $this->invokePrivate($this->api, 'getUserData', ['UID']));
}
@ -1233,6 +1234,7 @@ class UsersControllerTest extends TestCase {
'biography' => 'biography',
'profile_enabled' => '1',
'notify_email' => null,
'manager' => '',
];
$this->assertEquals($expected, $this->invokePrivate($this->api, 'getUserData', ['UID']));
}
@ -1411,6 +1413,7 @@ class UsersControllerTest extends TestCase {
'biography' => 'biography',
'profile_enabled' => '1',
'notify_email' => null,
'manager' => '',
];
$this->assertEquals($expected, $this->invokePrivate($this->api, 'getUserData', ['UID']));
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1350,6 +1350,8 @@ doesnotexist:-o-prefocus, .strengthify-wrapper {
minmax($grid-col-min-width, 1fr) // email
minmax(1.5*$grid-col-min-width, 1fr) // groups
minmax(1.5*$grid-col-min-width, 1fr) // group admins
minmax($grid-col-min-width, 1fr) // quota
minmax(1.5*$grid-col-min-width, 1fr) // manager
repeat(auto-fit, minmax($grid-col-min-width, 1fr));
border-bottom: var(--color-border) 1px solid;
@ -1394,6 +1396,7 @@ doesnotexist:-o-prefocus, .strengthify-wrapper {
}
}
.managers,
.groups,
.subadmins,
.quota {

View File

@ -146,6 +146,20 @@
<div v-if="showConfig.showStoragePath" class="storageLocation" />
<div v-if="showConfig.showUserBackend" class="userBackend" />
<div v-if="showConfig.showLastLogin" class="lastLogin" />
<div :class="{'icon-loading-small': loading.manager}" class="modal__item managers">
<NcMultiselect ref="manager"
v-model="newUser.manager"
:close-on-select="true"
:user-select="true"
:options="possibleManagers"
:placeholder="t('settings', 'Select user manager')"
class="multiselect-vue"
@search-change="searchUserManager"
label="displayname"
track-by="id">
<span slot="noResult">{{ t('settings', 'No results') }}</span>
</NcMultiselect>
</div>
<div class="user-actions">
<NcButton id="newsubmit"
type="primary"
@ -208,7 +222,9 @@
class="headerLastLogin lastLogin">
{{ t('settings', 'Last login') }}
</th>
<th id="headerManager" class="manager">
{{ t('settings', 'Manager') }}
</th>
<th class="userActions hidden-visually">
{{ t('settings', 'User actions') }}
</th>
@ -224,6 +240,7 @@
:show-config="showConfig"
:sub-admins-groups="subAdminsGroups"
:user="user"
:users="users"
:is-dark-theme="isDarkTheme" />
<InfiniteLoading ref="infiniteLoading" @infinite="infiniteHandler">
@ -268,6 +285,7 @@ const newUser = {
password: '',
mailAddress: '',
groups: [],
manager: '',
subAdminsGroups: [],
quota: defaultQuota,
language: {
@ -312,6 +330,7 @@ export default {
groups: false,
},
scrolled: false,
possibleManagers: [],
searchQuery: '',
newUser: Object.assign({}, newUser),
}
@ -422,6 +441,10 @@ export default {
},
},
async beforeMount() {
await this.searchUserManager()
},
mounted() {
if (!this.settings.canChangePassword) {
OC.Notification.showTemporary(t('settings', 'Password change is disabled because the master key is disabled'))
@ -449,6 +472,14 @@ export default {
},
methods: {
async searchUserManager(query) {
await this.$store.dispatch('searchUsers', { offset: 0, limit: 10, search: query }).then(response => {
const users = response?.data ? Object.values(response?.data.ocs.data.users) : []
if (users.length > 0) {
this.possibleManagers = users
}
})
},
onScroll(event) {
this.scrolled = event.target.scrollTo > 0
},
@ -532,6 +563,7 @@ export default {
subadmin: this.newUser.subAdminsGroups.map(group => group.id),
quota: this.newUser.quota.id,
language: this.newUser.language.code,
manager: this.newUser.manager.id,
})
.then(() => {
this.resetForm()

View File

@ -217,6 +217,22 @@
track-by="code"
@input="setUserLanguage" />
</td>
<td :class="{'icon-loading-small': loading.manager}" class="managers">
<NcMultiselect ref="manager"
v-model="currentManager"
:close-on-select="true"
:user-select="true"
:options="possibleManagers"
:placeholder="t('settings', 'Select manager')"
class="multiselect-vue"
label="displayname"
track-by="id"
@search-change="searchUserManager"
@remove="updateUserManager"
@select="updateUserManager">
<span slot="noResult">{{ t('settings', 'No results') }}</span>
</NcMultiselect>
</td>
<!-- don't show this on edit mode -->
<td v-if="showConfig.showStoragePath || showConfig.showUserBackend"
@ -275,6 +291,10 @@ export default {
},
mixins: [UserRowMixin],
props: {
users: {
type: Array,
required: true,
},
user: {
type: Object,
required: true,
@ -317,6 +337,8 @@ export default {
rand: parseInt(Math.random() * 1000),
openedMenu: false,
feedbackMessage: '',
possibleManagers: [],
currentManager: '',
editing: false,
loading: {
all: false,
@ -330,10 +352,12 @@ export default {
disable: false,
languages: false,
wipe: false,
manager: false,
},
}
},
computed: {
/* USER POPOVERMENU ACTIONS */
userActions() {
const actions = [
@ -363,6 +387,12 @@ export default {
return actions.concat(this.externalActions)
},
},
async beforeMount() {
await this.searchUserManager()
if (this.user.manager) {
await this.initManager(this.user.manager)
}
},
methods: {
/* MENU HANDLING */
@ -399,6 +429,34 @@ export default {
)
},
filterManagers(managers) {
return managers.filter((manager) => manager.id !== this.user.id)
},
async initManager(userId) {
await this.$store.dispatch('getUser', userId).then(response => {
this.currentManager = response?.data.ocs.data
})
},
async searchUserManager(query) {
await this.$store.dispatch('searchUsers', { offset: 0, limit: 10, search: query }).then(response => {
const users = response?.data ? this.filterManagers(Object.values(response?.data.ocs.data.users)) : []
if (users.length > 0) {
this.possibleManagers = users
}
})
},
updateUserManager(manager) {
this.loading.manager = true
this.$store.dispatch('setUserData', {
userid: this.user.id,
key: 'manager',
value: this.currentManager ? this.currentManager.id : '',
}).then(() => {
this.loading.manager = false
})
},
deleteUser() {
const userid = this.user.id
OC.dialogs.confirmDestructive(

View File

@ -55,7 +55,9 @@
<td v-if="showConfig.showLastLogin" :title="userLastLoginTooltip" class="lastLogin">
{{ userLastLogin }}
</td>
<td class="managers">
{{ user.manager }}
</td>
<td class="userActions">
<div v-if="canEdit && !loading.all" class="toggleUserActions">
<NcActions>

View File

@ -253,6 +253,41 @@ let searchRequestCancelSource = null
const actions = {
/**
* search users
*
* @param {object} context store context
* @param {object} options destructuring object
* @param {number} options.offset List offset to request
* @param {number} options.limit List number to return from offset
* @param {string} options.search Search amongst users
* @return {Promise}
*/
searchUsers(context, { offset, limit, search }) {
search = typeof search === 'string' ? search : ''
return api.get(generateOcsUrl('cloud/users/details?offset={offset}&limit={limit}&search={search}', { offset, limit, search })).catch((error) => {
if (!axios.isCancel(error)) {
context.commit('API_FAILURE', error)
}
})
},
/**
* Get user details
*
* @param {object} context store context
* @param {string} userId user id
* @return {Promise}
*/
getUser(context, userId) {
return api.get(generateOcsUrl(`cloud/users/${userId}`)).catch((error) => {
if (!axios.isCancel(error)) {
context.commit('API_FAILURE', error)
}
})
},
/**
* Get all users with full details
*
@ -548,11 +583,12 @@ const actions = {
* @param {string} options.subadmin User subadmin groups
* @param {string} options.quota User email
* @param {string} options.language User language
* @param {string} options.manager User manager
* @return {Promise}
*/
addUser({ commit, dispatch }, { userid, password, displayName, email, groups, subadmin, quota, language }) {
addUser({ commit, dispatch }, { userid, password, displayName, email, groups, subadmin, quota, language, manager }) {
return api.requireAdmin().then((response) => {
return api.post(generateOcsUrl('cloud/users'), { userid, password, displayName, email, groups, subadmin, quota, language })
return api.post(generateOcsUrl('cloud/users'), { userid, password, displayName, email, groups, subadmin, quota, language, manager })
.then((response) => dispatch('addUserData', userid || response.data.ocs.data.id))
.catch((error) => { throw error })
}).catch((error) => {
@ -605,8 +641,8 @@ const actions = {
* @return {Promise}
*/
setUserData(context, { userid, key, value }) {
const allowedEmpty = ['email', 'displayname']
if (['email', 'language', 'quota', 'displayname', 'password'].indexOf(key) !== -1) {
const allowedEmpty = ['email', 'displayname', 'manager']
if (['email', 'language', 'quota', 'displayname', 'password', 'manager'].indexOf(key) !== -1) {
// We allow empty email or displayname
if (typeof value === 'string'
&& (

Binary file not shown.

Binary file not shown.

View File

@ -159,4 +159,12 @@ class LazyUser implements IUser {
public function setQuota($quota) {
$this->getUser()->setQuota($quota);
}
public function getManagerUids(): array {
return $this->getUser()->getManagerUids();
}
public function setManagerUids(array $uids): void {
$this->getUser()->setManagerUids($uids);
}
}

View File

@ -59,8 +59,12 @@ use OCP\User\Backend\IGetHomeBackend;
use OCP\UserInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\GenericEvent;
use function json_decode;
use function json_encode;
class User implements IUser {
private const CONFIG_KEY_MANAGERS = 'manager';
/** @var IAccountManager */
protected $accountManager;
/** @var string */
@ -532,6 +536,27 @@ class User implements IUser {
\OC_Helper::clearStorageInfo('/' . $this->uid . '/files');
}
public function getManagerUids(): array {
$encodedUids = $this->config->getUserValue(
$this->uid,
'settings',
self::CONFIG_KEY_MANAGERS,
'[]'
);
return json_decode($encodedUids, false, 512, JSON_THROW_ON_ERROR);
}
public function setManagerUids(array $uids): void {
$oldUids = $this->getManagerUids();
$this->config->setUserValue(
$this->uid,
'settings',
self::CONFIG_KEY_MANAGERS,
json_encode($uids, JSON_THROW_ON_ERROR)
);
$this->triggerChange('managers', $uids, $oldUids);
}
/**
* get the avatar image if it exists
*

View File

@ -270,4 +270,21 @@ interface IUser {
* @since 9.0.0
*/
public function setQuota($quota);
/**
* Get the user's manager UIDs
*
* @since 27.0.0
* @return string[]
*/
public function getManagerUids(): array;
/**
* Set the user's manager UIDs
*
* @param string[] $uids UIDs of all managers
* @return void
* @since 27.0.0
*/
public function setManagerUids(array $uids): void;
}