Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

User management login allowed #8433

Merged
merged 23 commits into from
Feb 23, 2023
Merged
6 changes: 6 additions & 0 deletions changelog/unreleased/enhancement-user-settings-login-field
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Enhancement: User settings login field

We've introduced the new login field in the user settings,
where the admin can allow or disallow the login for the respective user.

https://github.com/owncloud/web/pull/8433
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Enhancement: Align disabled state on oc-select component

We've align the visual disabled state on the oc-select component,
so it unifies with the disabled state of the oc-text-input component.
4 changes: 3 additions & 1 deletion packages/design-system/src/components/OcSelect/OcSelect.vue
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,9 @@ export default defineComponent({
.vs__open-indicator,
.vs__search,
.vs__selected {
background-color: var(--oc-color-input-bg) !important;
background-color: var(--oc-color-background-muted) !important;
color: var(--oc-color-text-muted) !important;
cursor: default;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,8 +214,11 @@ export default defineComponent({
return this.$gettext('%{groupCount} matching groups', { groupCount: this.data.length })
},
data() {
const orderedGroups = this.orderBy(this.groups, this.sortBy, this.sortDir === 'desc')
return this.filter(orderedGroups, this.filterTerm)
return this.orderBy(
this.filter(this.groups, this.filterTerm),
this.sortBy,
this.sortDir === 'desc'
)
},
highlighted() {
return this.selectedGroups.map((group) => group.id)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ export default defineComponent({
})
}
const orderedSpaces = computed(() =>
filter(orderBy(props.spaces, unref(sortBy), unref(sortDir) === 'desc'), unref(filterTerm))
orderBy(filter(props.spaces, unref(filterTerm)), unref(sortBy), unref(sortDir) === 'desc')
)
const handleSort = (event) => {
sortBy.value = event.sortBy
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@
<span v-if="user.appRoleAssignments" v-text="roleDisplayName" />
</td>
</tr>
<tr>
<th scope="col" class="oc-pr-s" v-text="$gettext('Login')" />
<td>
<span v-text="loginDisplayValue" />
</td>
</tr>
<tr>
<th scope="col" class="oc-pr-s" v-text="$gettext('Quota')" />
<td>
Expand Down Expand Up @@ -129,6 +135,11 @@ export default defineComponent({
return this.user.drive.quota.total === 0
? this.$gettext('No restriction')
: formatFileSize(this.user.drive.quota.total, this.currentLanguage)
},
loginDisplayValue() {
return this.user.accountEnabled === false
? this.$gettext('Forbidden')
: this.$gettext('Allowed')
}
}
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,22 @@
</oc-select>
<div class="oc-text-input-message"></div>
</div>
<div class="oc-mb-s">
<oc-select
:disabled="isLoginInputDisabled"
id="login-input"
:model-value="editUser"
:label="$gettext('Login')"
:options="loginOptions"
:clearable="false"
@update:model-value="onUpdateLogin"
>
<template #selected-option>
{{ selectedLoginLabel }}
</template>
</oc-select>
<div class="oc-text-input-message"></div>
</div>
<quota-select
v-if="showQuota"
:key="'quota-select-' + user.id"
Expand Down Expand Up @@ -92,7 +108,7 @@ import GroupSelect from './GroupSelect.vue'
import QuotaSelect from 'web-pkg/src/components/QuotaSelect.vue'
import { cloneDeep } from 'lodash-es'
import { Group, User } from 'web-client/src/generated'
import { MaybeRef, useGraphClient } from 'web-pkg'
import { MaybeRef, useGraphClient, useStore } from 'web-pkg'

export default defineComponent({
name: 'EditPanel',
Expand All @@ -118,6 +134,8 @@ export default defineComponent({
},
emits: ['confirm'],
setup(props) {
const store = useStore()
const currentUser = store.getters.user
const editUser: MaybeRef<User> = ref({})
const formData = ref({
displayName: {
Expand All @@ -133,16 +151,38 @@ export default defineComponent({
valid: true
}
})

const groupOptions = computed(() => {
const { memberOf: selectedGroups } = unref(editUser)
return props.groups
.filter((g) => !selectedGroups.some((s) => s.id === g.id))
.sort((a, b) => a.displayName.localeCompare(b.displayName))
})

return { editUser, formData, groupOptions, ...useGraphClient() }
const isLoginInputDisabled = computed(
() => currentUser.uuid === (props.user.id as PropType<User>)
)

return { isLoginInputDisabled, editUser, formData, groupOptions, ...useGraphClient() }
},
computed: {
loginOptions() {
return [
{
label: this.$gettext('Allowed'),
value: true
},
{
label: this.$gettext('Forbidden'),
value: false
}
]
},
selectedLoginLabel() {
return this.editUser.accountEnabled === false
? this.$gettext('Forbidden')
: this.$gettext('Allowed')
},
translatedRoleOptions() {
return this.roles.map((role) => {
return { ...role, displayName: this.$gettext(role.displayName) }
Expand Down Expand Up @@ -173,6 +213,19 @@ export default defineComponent({
},
deep: true,
immediate: true
},
editUser: {
handler: function () {
/**
* Property accountEnabled won't be always set, but this still means, that login is allowed.
* So we actually don't need to change the property if missing and not set to forbidden in the UI.
* This also avoids the compare save dialog from displaying that there are unsaved changes.
*/
if (this.editUser.accountEnabled === true && this.user.accountEnabled !== false) {
delete this.editUser.accountEnabled
}
},
deep: true
}
},
methods: {
Expand Down Expand Up @@ -261,6 +314,9 @@ export default defineComponent({
this.editUser.passwordProfile = {
password
}
},
onUpdateLogin({ value }) {
this.editUser.accountEnabled = value
}
}
})
Expand Down
43 changes: 35 additions & 8 deletions packages/web-app-admin-settings/src/components/Users/UsersList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,18 @@
<template #role="{ item }">
<template v-if="item.appRoleAssignments">{{ getRoleDisplayNameByUser(item) }}</template>
</template>
<template #accountEnabled="{ item }">
<span v-if="item.accountEnabled === false" class="oc-flex oc-flex-middle">
<oc-icon name="stop-circle" fill-type="line" class="oc-mr-s" /><span
v-text="$gettext('Forbidden')"
/>
</span>
<span v-else class="oc-flex oc-flex-middle">
<oc-icon name="play-circle" fill-type="line" class="oc-mr-s" /><span
v-text="$gettext('Allowed')"
/>
</span>
</template>
<template #actions="{ item }">
<oc-button
v-oc-tooltip="$gettext('Details')"
Expand Down Expand Up @@ -242,6 +254,12 @@ export default defineComponent({
type: 'slot',
sortable: true
},
{
name: 'accountEnabled',
title: this.$gettext('Login'),
type: 'slot',
sortable: true
},
{
name: 'actions',
title: this.$gettext('Actions'),
Expand All @@ -252,8 +270,11 @@ export default defineComponent({
]
},
data() {
const orderedUsers = this.orderBy(this.users, this.sortBy, this.sortDir === 'desc')
return this.filter(orderedUsers, this.filterTerm)
return this.orderBy(
this.filter(this.users, this.filterTerm),
this.sortBy,
this.sortDir === 'desc'
)
},
highlighted() {
return this.selectedUsers.map((user) => user.id)
Expand Down Expand Up @@ -290,12 +311,18 @@ export default defineComponent({
return [...list].sort((user1, user2) => {
let a, b

if (prop === 'role') {
a = this.getRoleDisplayNameByUser(user1)
b = this.getRoleDisplayNameByUser(user2)
} else {
a = user1[prop] || ''
b = user2[prop] || ''
switch (prop) {
case 'role':
a = this.getRoleDisplayNameByUser(user1)
b = this.getRoleDisplayNameByUser(user2)
break
case 'accountEnabled':
a = ('accountEnabled' in user1 ? user1.accountEnabled : true).toString()
b = ('accountEnabled' in user2 ? user2.accountEnabled : true).toString()
break
default:
a = user1[prop] || ''
b = user2[prop] || ''
}

return desc ? b.localeCompare(a) : a.localeCompare(b)
Expand Down
3 changes: 2 additions & 1 deletion packages/web-app-admin-settings/src/views/Users.vue
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ export default defineComponent({

const { data } = yield unref(graphClient).users.getUser(user.id)
unref(additionalUserDataLoadedForUserIds).push(user.id)

Object.assign(user, data)
})

Expand Down Expand Up @@ -435,8 +436,8 @@ export default defineComponent({
}

const { data: updatedUser } = await this.graphClient.users.getUser(user.id)

const userIndex = this.users.findIndex((user) => user.id === updatedUser.id)

this.users[userIndex] = updatedUser

eventBus.publish('sidebar.entity.saved')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ exports[`EditPanel renders all available inputs 1`] = `
<oc-select-stub clearable="false" disabled="false" filter="[Function]" id="oc-select-5" label="Role" loading="false" model-value="[object Object]" optionlabel="displayName" options="[object Object]" searchable="true"></oc-select-stub>
<div class="oc-text-input-message"></div>
</div>
<div class="oc-mb-s">
<oc-select-stub clearable="false" disabled="false" filter="[Function]" id="login-input" label="Login" loading="false" model-value="[object Object]" options="[object Object],[object Object]" searchable="true"></oc-select-stub>
<div class="oc-text-input-message"></div>
</div>
<quota-select-stub class="oc-mb-s" maxquota="0" title="Personal quota" totalquota="0"></quota-select-stub>
<group-select-stub class="oc-mb-s" groupoptions="undefined,undefined" selectedgroups=""></group-select-stub>
</div>
Expand Down
11 changes: 1 addition & 10 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
Feature: spaces management

Scenario: user login can be managed in the admin settings
Given "Admin" creates following users
| id |
| Alice |
When "Admin" logs in
And "Admin" opens the "admin-settings" app
And "Admin" navigates to the users management page
And "Admin" forbids the login for the following user "Alice" using the sidebar panel
And "Admin" logs out
Then "Alice" fails to log in
When "Admin" logs in
And "Admin" opens the "admin-settings" app
And "Admin" navigates to the users management page
And "Admin" allows the login for the following user "Alice" using the sidebar panel
And "Admin" logs out
Then "Alice" logs in
28 changes: 28 additions & 0 deletions tests/e2e/cucumber/steps/ui/adminSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,31 @@ When(
}
}
)

When(
'{string} navigates to the users management page',
async function (this: World, stepUser: string): Promise<void> {
const { page } = this.actorsEnvironment.getActor({ key: stepUser })
const pageObject = new objects.applicationAdminSettings.page.Users({ page })
await pageObject.navigate()
}
)

When(
/^"([^"]*)" (allows|forbids) the login for the following user "([^"]*)" using the sidebar panel$/,
async function (this: World, stepUser: string, action: string, key: string): Promise<void> {
const { page } = this.actorsEnvironment.getActor({ key: stepUser })
const usersObject = new objects.applicationAdminSettings.Users({ page })

switch (action) {
case 'allows':
await usersObject.allowLogin({ key })
break
case 'forbids':
await usersObject.forbidLogin({ key })
break
default:
throw new Error(`${action} not implemented`)
}
}
)
12 changes: 12 additions & 0 deletions tests/e2e/cucumber/steps/ui/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ async function LogInUser(this: World, stepUser: string): Promise<void> {
const user = this.usersEnvironment.getUser({ key: stepUser })
await page.goto(config.frontendUrl)
await sessionObject.login({ user })
await page.waitForSelector('#web')
}

Given('{string} has logged in', LogInUser)
Expand All @@ -40,11 +41,22 @@ Given('{string} has logged out', LogOutUser)

When('{string} logs out', LogOutUser)

When('{string} fails to log in', async function (this: World, stepUser: string): Promise<void> {
const sessionObject = await createNewSession(this, stepUser)
const { page } = this.actorsEnvironment.getActor({ key: stepUser })
const user = this.usersEnvironment.getUser({ key: stepUser })
await page.goto(config.frontendUrl)
await sessionObject.login({ user: { ...user, password: 'fail' } })
await page.waitForSelector('#oc-login-error-message')
})

When(
'{string} logs in from the internal link',
async function (this: World, stepUser: string): Promise<void> {
const sessionObject = await createNewSession(this, stepUser)
const { page } = this.actorsEnvironment.getActor({ key: stepUser })
const user = this.usersEnvironment.getUser({ key: stepUser })
await sessionObject.login({ user })
await page.waitForSelector('#web')
}
)
4 changes: 4 additions & 0 deletions tests/e2e/support/api/graph/userManagement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ export const createUser = async ({ user, admin }: { user: User; admin: User }):
})

checkResponseStatus(response, 'Failed while creating user')

const responseData = await response.json()
user.uuid = responseData.id

return user
}

Expand Down
Loading