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

feat(xo-web/user): user tokens management through XO interface #6276

Merged
merged 28 commits into from
Jun 28, 2022
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion CHANGELOG.unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

> Users must be able to say: “Nice enhancement, I'm eager to test it”

- [User] User tokens management through XO interface (PR [#6276](https://github.com/vatesfr/xen-orchestra/pull/6276))

### Bug fixes

> Users must be able to say: “I had this issue, happy to know it's fixed”
Expand Down Expand Up @@ -34,6 +36,7 @@
- @vates/read-chunk major
- @xen-orchestra/xapi minor
- xo-remote-parser minor
- xo-server patch
- xo-server minor
- xo-web minor

<!--packages-end-->
26 changes: 4 additions & 22 deletions packages/xo-server/src/api/token.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -25,35 +25,17 @@ create.params = {

// -------------------------------------------------------------------

async function delete_({ token: id }) {
await this.deleteAuthenticationToken(id)
async function delete_({ pattern, tokens }) {
await this.deleteAuthenticationTokens({ filter: pattern ?? { id: { __or: tokens } } })
julien-f marked this conversation as resolved.
Show resolved Hide resolved
}

export { delete_ as delete }

delete_.description = 'delete an existing authentication token'

delete_.params = {
token: { type: 'string' },
}

// -------------------------------------------------------------------

export async function deleteAll({ except }) {
await this.deleteAuthenticationTokens({
filter: {
user_id: this.apiContext.user.id,
id: {
__not: except,
},
},
})
}

deleteAll.description = 'delete all tokens of the current user except the current one'

deleteAll.params = {
except: { type: 'string', optional: true },
tokens: { type: 'array', optional: true, items: { type: 'string' } },
pattern: { type: 'object', optional: true },
}

// -------------------------------------------------------------------
Expand Down
14 changes: 14 additions & 0 deletions packages/xo-web/src/common/intl/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@
const forEach = require('lodash/forEach')

const messages = {
creation: 'Creation',
description: 'Description',
expiration: 'Expiration',
keyValue: '{key}: {value}',

notDefined: 'Not defined',
statusConnecting: 'Connecting',
statusDisconnected: 'Disconnected',
statusLoading: 'Loading…',
Expand Down Expand Up @@ -2045,6 +2049,8 @@ const messages = {
pifPhysicallyDisconnected: 'Physically disconnected',

// ----- User -----
authToken: 'Token',
authTokens: 'Authentication tokens',
username: 'Username',
password: 'Password',
language: 'Language',
Expand All @@ -2064,15 +2070,23 @@ const messages = {
forgetTokensSuccess: 'Successfully forgot connection tokens',
forgetTokensError: 'Error while forgetting connection tokens',
sshKeys: 'SSH keys',
newAuthToken: 'New token',
newSshKey: 'New SSH key',
deleteAuthTokens: 'Delete selected authentication tokens',
deleteSshKey: 'Delete',
deleteSshKeys: 'Delete selected SSH keys',
newAuthTokenModalTitle: 'New authentication token',
newSshKeyModalTitle: 'New SSH key',
sshKeyAlreadyExists: 'SSH key already exists!',
sshKeyErrorTitle: 'Invalid key',
sshKeyErrorMessage: 'An SSH key requires both a title and a key.',
title: 'Title',
key: 'Key',
deleteAuthTokenConfirm: 'Delete authentication token',
deleteAuthTokenConfirmMessage: 'Are you sure you want to delete the authentication token: {id}?',
deleteAuthTokensConfirm: 'Delete authentication token{nTokens, plural, one {} other {s}}',
deleteAuthTokensConfirmMessage:
'Are you sure you want to delete {nTokens, number} autentication token{nTokens, plural, one {} other {s}}?',
deleteSshKeyConfirm: 'Delete SSH key',
deleteSshKeyConfirmMessage: 'Are you sure you want to delete the SSH key {title}?',
deleteSshKeysConfirm: 'Delete SSH key{nKeys, plural, one {} other {s}}',
Expand Down
4 changes: 4 additions & 0 deletions packages/xo-web/src/common/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,10 @@ export const createFakeProgress = (() => {
}
})()

export const NumericDate = ({ timestamp }) => (
<FormattedDate day='2-digit' hour='numeric' minute='numeric' month='2-digit' value={timestamp} year='numeric' />
)

export const ShortDate = ({ timestamp }) => (
<FormattedDate value={timestamp} month='short' day='numeric' year='numeric' />
)
Expand Down
53 changes: 52 additions & 1 deletion packages/xo-web/src/common/xo/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import fetch, { post } from '../fetch'
import invoke from '../invoke'
import Icon from '../icon'
import logError from '../log-error'
import NewAuthTokenModal from './new-auth-token-modal'
import renderXoItem, { renderXoItemFromId } from '../render-xo-item'
import store from 'store'
import { alert, chooseAction, confirm } from '../modal'
Expand Down Expand Up @@ -539,6 +540,8 @@ export const createSrUnhealthyVdiChainsLengthSubscription = sr => {
return subscription
}

export const subscribeUserAuthTokens = createSubscription(() => _call('user.getAuthenticationTokens'))

// System ============================================================

export const apiMethods = _call('system.getMethodsInfo')
Expand Down Expand Up @@ -2782,7 +2785,14 @@ export const deleteUsers = users =>
export const editUser = (user, { email, password, permission }) =>
_call('user.set', { id: resolveId(user), email, password, permission })::tap(subscribeUsers.forceRefresh)

const _signOutFromEverywhereElse = () => _call('token.deleteAll', { except: cookies.get('token') })
const _signOutFromEverywhereElse = () =>
_call('token.delete', {
pattern: {
id: {
__not: cookies.get('token'),
},
},
})

export const signOutFromEverywhereElse = () =>
_signOutFromEverywhereElse().then(
Expand Down Expand Up @@ -2890,6 +2900,47 @@ export const deleteSshKeys = keys =>
})
}, noop)

export const addAuthToken = async () => {
const { description, expiration } = await confirm({
body: <NewAuthTokenModal />,
icon: 'user',
title: _('newAuthTokenModalTitle'),
})
const expires = new Date(expiration).setHours(23, 59, 59)
return _call('token.create', {
description,
expiresIn: Number.isNaN(expires) ? undefined : expires - new Date().getTime(),
julien-f marked this conversation as resolved.
Show resolved Hide resolved
})::tap(subscribeUserAuthTokens.forceRefresh)
}

export const deleteAuthToken = async ({ id }) => {
await confirm({
body: _('deleteAuthTokenConfirmMessage', {
id,
}),
icon: 'user',
title: _('deleteAuthTokenConfirm'),
})
return _call('token.delete', { tokens: [id] })::tap(subscribeUserAuthTokens.forceRefresh)
}

export const deleteAuthTokens = async tokens => {
await confirm({
body: _('deleteAuthTokensConfirmMessage', {
nTokens: tokens.length,
}),
icon: 'user',
title: _('deleteAuthTokensConfirm', { nTokens: tokens.length }),
})
return _call('token.delete', { tokens: tokens.map(token => token.id) })::tap(subscribeUserAuthTokens.forceRefresh)
}

export const editAuthToken = ({ description, id }) =>
_call('token.set', {
description,
id,
})::tap(subscribeUserAuthTokens.forceRefresh)

// User filters --------------------------------------------------

import AddUserFilterModalBody from './add-user-filter-modal' // eslint-disable-line import/first
Expand Down
48 changes: 48 additions & 0 deletions packages/xo-web/src/common/xo/new-auth-token-modal/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import BaseComponent from 'base-component'
import React from 'react'

import _ from '../../intl'
import SingleLineRow from '../../single-line-row'
import { Col } from '../../grid'

export default class NewAuthTokenModal extends BaseComponent {
get value() {
return this.state
}

render() {
const { description, expiration } = this.state

return (
<div>
<div className='pb-1'>
<SingleLineRow>
<Col size={4}>{_('expiration')}</Col>
<Col size={8}>
<input
className='form-control'
min={new Date().toISOString().split('T')[0]}
onChange={this.linkState('expiration')}
type='date'
value={expiration ?? ''}
/>
</Col>
</SingleLineRow>
</div>
<div className='pb-1'>
<SingleLineRow>
<Col size={4}>{_('description')}</Col>
<Col size={8}>
<textarea
className='form-control'
onChange={this.linkState('description')}
rows={10}
value={description ?? ''}
/>
</Col>
</SingleLineRow>
</div>
</div>
)
}
}
87 changes: 86 additions & 1 deletion packages/xo-web/src/xo-app/user/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as FormGrid from 'form-grid'
import _, { messages } from 'intl'
import ActionButton from 'action-button'
import Component from 'base-component'
import Copiable from 'copiable'
import homeFilters from 'home-filters'
import Icon from 'icon'
import PropTypes from 'prop-types'
Expand All @@ -16,16 +17,21 @@ import { isEmpty, map } from 'lodash'
import { injectIntl } from 'react-intl'
import { Select } from 'form'
import { Card, CardBlock, CardHeader } from 'card'
import { addSubscriptions, connectStore, noop } from 'utils'
import { addSubscriptions, connectStore, noop, NumericDate } from 'utils'
import {
addAuthToken,
addSshKey,
changePassword,
deleteAuthToken,
deleteAuthTokens,
deleteSshKey,
deleteSshKeys,
editAuthToken,
editCustomFilter,
removeCustomFilter,
setDefaultHomeFilter,
signOutFromEverywhereElse,
subscribeUserAuthTokens,
subscribeCurrentUser,
} from 'xo'

Expand Down Expand Up @@ -282,6 +288,83 @@ const SshKeys = addSubscriptions({
)
})

// ===================================================================
const COLUMNS_AUTH_TOKENS = [
{
itemRenderer: ({ id }) => (
<Copiable tagName='pre' data={id}>
{id.slice(0, 5)}…
</Copiable>
),
name: _('authToken'),
},
{
itemRenderer: token => (
<Text value={token.description ?? ''} onChange={description => editAuthToken({ ...token, description })} />
),
name: _('description'),
sortCriteria: 'description',
},
{
itemRenderer: ({ created_at }) => {
if (created_at !== undefined) {
return <NumericDate timestamp={created_at} />
}
return _('notDefined')
MathieuRA marked this conversation as resolved.
Show resolved Hide resolved
},
name: _('creation'),
sortCriteria: 'created_at',
},
{
default: true,
itemRenderer: ({ expiration }) => <NumericDate timestamp={expiration} />,
name: _('expiration'),
sortCriteria: 'expiration',
},
]
julien-f marked this conversation as resolved.
Show resolved Hide resolved

const INDIVIDUAL_ACTIONS_AUTH_TOKENS = [
{
handler: deleteAuthToken,
icon: 'delete',
label: _('delete'),
level: 'danger',
},
]

const GROUPED_ACTIONS_AUTH_TOKENS = [
{
handler: deleteAuthTokens,
icon: 'delete',
label: _('deleteAuthTokens'),
level: 'danger',
},
]

const UserAuthTokens = addSubscriptions({
userAuthTokens: subscribeUserAuthTokens,
})(({ userAuthTokens }) => (
<div>
<Card>
<CardHeader>
<Icon icon='user' /> {_('authTokens')}
<ActionButton className='btn-success pull-right' icon='add' handler={addAuthToken}>
{_('newAuthToken')}
</ActionButton>
</CardHeader>
<CardBlock>
<SortedTable
collection={userAuthTokens}
columns={COLUMNS_AUTH_TOKENS}
stateUrlParam='s_auth_tokens'
groupedActions={GROUPED_ACTIONS_AUTH_TOKENS}
individualActions={INDIVIDUAL_ACTIONS_AUTH_TOKENS}
/>
</CardBlock>
</Card>
</div>
))

// ===================================================================

@addSubscriptions({
Expand Down Expand Up @@ -412,6 +495,8 @@ export default class User extends Component {
]}
<SshKeys />
<hr />
<UserAuthTokens />
<hr />
<UserFilters user={user} />
</Page>
)
Expand Down