Compare commits
3 Commits
contributi
...
token-last
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7da0146d3e | ||
|
|
f3bbcbde08 | ||
|
|
0559fe8649 |
@@ -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
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user