Compare commits

...

3 Commits

Author SHA1 Message Date
Julien Fontanet
7da0146d3e feat(xo-server/token): savelog last use info 2023-10-27 11:37:44 +02:00
Julien Fontanet
f3bbcbde08 feat(xo-cli): only create a single token per instance (and user) 2023-10-27 11:28:39 +02:00
Julien Fontanet
0559fe8649 feat(xo-server/token): client info support 2023-10-27 11:28:39 +02:00
5 changed files with 81 additions and 11 deletions

View File

@@ -53,6 +53,7 @@
- @xen-orchestra/fs patch
- @xen-orchestra/mixins minor
- @xen-orchestra/xapi minor
- xo-cli minor
- xo-server minor
- xo-server-backup-reports minor
- xo-server-netbox patch

View File

@@ -13,6 +13,7 @@ import humanFormat from 'human-format'
import identity from 'lodash/identity.js'
import isObject from 'lodash/isObject.js'
import micromatch from 'micromatch'
import os from 'os'
import pairs from 'lodash/toPairs.js'
import pick from 'lodash/pick.js'
import prettyMs from 'pretty-ms'
@@ -47,7 +48,7 @@ async function connect() {
return xo
}
async function parseRegisterArgs(args, tokenDescription, acceptToken = false) {
async function parseRegisterArgs(args, tokenDescription, client, acceptToken = false) {
const {
allowUnauthorized,
expiresIn,
@@ -84,21 +85,21 @@ async function parseRegisterArgs(args, tokenDescription, acceptToken = false) {
pw(resolve)
}),
] = opts
result.token = await _createToken({ ...result, description: tokenDescription, email, password })
result.token = await _createToken({ ...result, client, description: tokenDescription, email, password })
}
return result
}
async function _createToken({ allowUnauthorized, description, email, expiresIn, password, url }) {
async function _createToken({ allowUnauthorized, client, description, email, expiresIn, password, url }) {
const xo = new Xo({ rejectUnauthorized: !allowUnauthorized, url })
await xo.open()
try {
await xo.signIn({ email, password })
console.warn('Successfully logged with', xo.user.email)
return await xo.call('token.create', { description, expiresIn }).catch(error => {
// if invalid parameter error, retry without description for backward compatibility
return await xo.call('token.create', { client, description, expiresIn }).catch(error => {
// if invalid parameter error, retry without client and description for backward compatibility
if (error.code === 10) {
return xo.call('token.create', { expiresIn })
}
@@ -219,6 +220,8 @@ function wrap(val) {
// ===================================================================
const PACKAGE_JSON = JSON.parse(readFileSync(new URL('package.json', import.meta.url)))
const help = wrap(
(function (pkg) {
return `Usage:
@@ -355,7 +358,7 @@ $name v$version`.replace(/<([^>]+)>|\$(\w+)/g, function (_, arg, key) {
return pkg[key]
})
})(JSON.parse(readFileSync(new URL('package.json', import.meta.url))))
})(PACKAGE_JSON)
)
// -------------------------------------------------------------------
@@ -422,9 +425,18 @@ async function createToken(args) {
COMMANDS.createToken = createToken
async function register(args) {
const opts = await parseRegisterArgs(args, 'xo-cli --register', true)
let { clientId } = await config.load()
if (clientId === undefined) {
clientId = Math.random().toString(36).slice(2)
}
const { name, version } = PACKAGE_JSON
const label = `${name}@${version} - ${os.hostname()} - ${os.type()} ${os.machine()}`
const opts = await parseRegisterArgs(args, label, { id: clientId }, true)
await config.set({
allowUnauthorized: opts.allowUnauthorized,
clientId,
server: opts.url,
token: opts.token,
})

View File

@@ -1,8 +1,9 @@
// TODO: Prevent token connections from creating tokens.
// TODO: Token permission.
export async function create({ description, expiresIn }) {
export async function create({ client, description, expiresIn }) {
return (
await this.createAuthenticationToken({
client,
description,
expiresIn,
userId: this.apiContext.user.id,
@@ -17,6 +18,15 @@ create.params = {
optional: true,
type: 'string',
},
client: {
description:
'client this authentication token belongs to, if a previous token exists, it will be updated and returned',
optional: true,
type: 'object',
properties: {
id: { description: 'unique identifier of this client', type: 'string' },
},
},
expiresIn: {
optional: true,
type: ['number', 'string'],

View File

@@ -3,7 +3,25 @@ import Collection from '../collection/redis.mjs'
// ===================================================================
export class Tokens extends Collection {
_serialize(token) {
const { client, lastUse } = token
if (client !== undefined) {
const { id, ...rest } = client
token.client_id = id
token.client = JSON.stringify(rest)
}
}
_unserialize(token) {
const { client, client_id } = token
if (client !== undefined) {
token.client = {
...JSON.parse(client),
id: client_id,
}
delete token.client_id
}
if (token.created_at !== undefined) {
token.created_at = +token.created_at
}

View File

@@ -49,13 +49,23 @@ export default class {
})
// Token authentication provider.
this.registerAuthenticationProvider(async ({ token: tokenId }) => {
this.registerAuthenticationProvider(async ({ token: tokenId }, { ip } = {}) => {
if (!tokenId) {
return
}
try {
const token = await app.getAuthenticationToken(tokenId)
this._tokens.update({
...token,
lastUse: {
ip,
timestamp: Date.now(),
},
})
return { expiration: token.expiration, userId: token.user_id }
} catch (error) {}
})
@@ -79,7 +89,7 @@ export default class {
const tokensDb = (this._tokens = new Tokens({
connection: app._redis,
namespace: 'token',
indexes: ['user_id'],
indexes: ['client_id', 'user_id'],
}))
app.addConfigManager(
@@ -180,7 +190,7 @@ export default class {
// -----------------------------------------------------------------
async createAuthenticationToken({ description, expiresIn, userId }) {
async createAuthenticationToken({ client, description, expiresIn, userId }) {
let duration = this._defaultTokenValidity
if (expiresIn !== undefined) {
duration = parseDuration(expiresIn)
@@ -191,8 +201,27 @@ export default class {
}
}
const tokens = this._tokens
const now = Date.now()
const clientId = client?.id
if (clientId !== undefined) {
const token = await tokens.first({ client_id: clientId, user_id: userId })
if (token !== undefined) {
if (token.expiration > now) {
token.description = description
token.expiration = now + duration
tokens.update(token)::ignoreErrors()
return token
}
tokens.remove(token.id)::ignoreErrors()
}
}
const token = {
client,
created_at: now,
description,
id: await generateToken(),