Various updates.
This commit is contained in:
parent
13f36b3f79
commit
ad2de95f32
@ -17,8 +17,8 @@ export class NotImplemented extends JsonRpcError {
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export class NoSuchObject extends JsonRpcError {
|
||||
constructor () {
|
||||
super('no such object', 1)
|
||||
constructor (data) {
|
||||
super('no such object', 1, data)
|
||||
}
|
||||
}
|
||||
|
||||
|
74
src/api.js
74
src/api.js
@ -271,54 +271,46 @@ export default class Api {
|
||||
}, this)
|
||||
}
|
||||
|
||||
call (session, name, params) {
|
||||
async call (session, name, params) {
|
||||
debug('%s(...)', name)
|
||||
|
||||
let method
|
||||
let context
|
||||
const method = this.getMethod(name)
|
||||
if (!method) {
|
||||
throw new MethodNotFound(name)
|
||||
}
|
||||
|
||||
return Bluebird.try(() => {
|
||||
method = this.getMethod(name)
|
||||
if (!method) {
|
||||
throw new MethodNotFound(name)
|
||||
const context = Object.create(this.context)
|
||||
context.api = this // Used by system.*().
|
||||
context.session = session
|
||||
|
||||
// FIXME: too coupled with XO.
|
||||
// Fetch and inject the current user.
|
||||
const userId = session.get('user_id', undefined)
|
||||
if (userId) {
|
||||
context.user = await context._getUser(userId)
|
||||
}
|
||||
|
||||
await checkPermission.call(context, method)
|
||||
checkParams(method, params)
|
||||
|
||||
await resolveParams.call(context, method, params)
|
||||
try {
|
||||
let result = method.call(context, params)
|
||||
|
||||
// If nothing was returned, consider this operation a success
|
||||
// and return true.
|
||||
if (result === undefined) {
|
||||
result = true
|
||||
}
|
||||
|
||||
context = Object.create(this.context)
|
||||
context.api = this // Used by system.*().
|
||||
context.session = session
|
||||
debug('%s(...) → %s', name, typeof result)
|
||||
|
||||
// FIXME: too coupled with XO.
|
||||
// Fetch and inject the current user.
|
||||
const userId = session.get('user_id', undefined)
|
||||
return userId === undefined ? null : context.users.first(userId)
|
||||
}).then(function (user) {
|
||||
context.user = user
|
||||
return result
|
||||
} catch (error) {
|
||||
debug('Error: %s(...) → %s', name, error)
|
||||
|
||||
return checkPermission.call(context, method)
|
||||
}).then(() => {
|
||||
checkParams(method, params)
|
||||
|
||||
return resolveParams.call(context, method, params)
|
||||
}).then(params => {
|
||||
return method.call(context, params)
|
||||
}).then(
|
||||
result => {
|
||||
// If nothing was returned, consider this operation a success
|
||||
// and return true.
|
||||
if (result === undefined) {
|
||||
result = true
|
||||
}
|
||||
|
||||
debug('%s(...) → %s', name, typeof result)
|
||||
|
||||
return result
|
||||
},
|
||||
error => {
|
||||
debug('Error: %s(...) → %s', name, error)
|
||||
|
||||
throw error
|
||||
}
|
||||
)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
getMethod (name) {
|
||||
|
@ -1,38 +0,0 @@
|
||||
{$coroutine, $wait} = require '../fibers-utils'
|
||||
|
||||
#=====================================================================
|
||||
|
||||
# Creates a new token.
|
||||
#
|
||||
# TODO: Token permission.
|
||||
exports.create = $coroutine ->
|
||||
userId = @session.get 'user_id'
|
||||
|
||||
# The user MUST be signed in and not with a token
|
||||
@throw 'UNAUTHORIZED' if not userId? or @session.has 'token_id'
|
||||
|
||||
# Creates the token.
|
||||
token = $wait @tokens.generate userId
|
||||
|
||||
return token.get('id')
|
||||
|
||||
#---------------------------------------------------------------------
|
||||
|
||||
# Deletes a token.
|
||||
delete_ = $coroutine ({token: tokenId}) ->
|
||||
# Gets the token.
|
||||
token = $wait @tokens.first tokenId
|
||||
@throw 'NO_SUCH_OBJECT' unless token?
|
||||
|
||||
# Deletes the token.
|
||||
$wait @tokens.remove tokenId
|
||||
|
||||
return true
|
||||
|
||||
delete_.permission = 'admin'
|
||||
|
||||
delete_.params = {
|
||||
token: { type: 'string' }
|
||||
}
|
||||
|
||||
exports.delete = delete_
|
@ -1,106 +0,0 @@
|
||||
{$coroutine, $wait} = require '../fibers-utils'
|
||||
|
||||
#=====================================================================
|
||||
|
||||
# Creates a new user.
|
||||
exports.create = $coroutine ({email, password, permission}) ->
|
||||
# Creates the user.
|
||||
user = $wait @users.create email, password, permission
|
||||
|
||||
return user.get('id')
|
||||
exports.create.permission = 'admin'
|
||||
exports.create.params = {
|
||||
email: { type: 'string' }
|
||||
password: { type: 'string' }
|
||||
permission: { type: 'string', optional: true}
|
||||
}
|
||||
|
||||
# Deletes an existing user.
|
||||
#
|
||||
# FIXME: a user should not be able to delete itself.
|
||||
exports.delete = $coroutine ({id}) ->
|
||||
# The user cannot delete himself.
|
||||
@throw 'INVALID_PARAMS' if id is @session.get 'user_id'
|
||||
|
||||
# Throws an error if the user did not exist.
|
||||
@throw 'NO_SUCH_OBJECT' unless $wait @users.remove id
|
||||
|
||||
return true
|
||||
exports.delete.permission = 'admin'
|
||||
exports.delete.params = {
|
||||
id: { type: 'string' }
|
||||
}
|
||||
|
||||
# Changes the password of the current user.
|
||||
exports.changePassword = $coroutine ({old, new: newP}) ->
|
||||
# Gets the current user (which MUST exist).
|
||||
user = $wait @users.first @session.get 'user_id'
|
||||
|
||||
# Checks its old password.
|
||||
@throw 'INVALID_CREDENTIAL' unless $wait user.checkPassword old
|
||||
|
||||
# Sets the new password.
|
||||
$wait user.setPassword newP
|
||||
|
||||
# Updates the user.
|
||||
$wait @users.update user
|
||||
|
||||
return true
|
||||
exports.changePassword.permission = '' # Signed in.
|
||||
exports.changePassword.params = {
|
||||
old: { type: 'string' }
|
||||
new: { type: 'string' }
|
||||
}
|
||||
|
||||
# Returns the user with a given identifier.
|
||||
exports.get = $coroutine ({id}) ->
|
||||
# Only an administrator can see another user.
|
||||
@checkPermission 'admin' unless @session.get 'user_id' is id
|
||||
|
||||
# Retrieves the user.
|
||||
user = $wait @users.first id
|
||||
|
||||
# Throws an error if it did not exist.
|
||||
@throw 'NO_SUCH_OBJECT' unless user
|
||||
|
||||
return @getUserPublicProperties user
|
||||
exports.get.params = {
|
||||
id: { type: 'string' }
|
||||
}
|
||||
|
||||
# Returns all users.
|
||||
exports.getAll = $coroutine ->
|
||||
# Retrieves the users.
|
||||
users = $wait @users.get()
|
||||
|
||||
# Filters out private properties.
|
||||
for user, i in users
|
||||
users[i] = @getUserPublicProperties user
|
||||
|
||||
return users
|
||||
exports.getAll.permission = 'admin'
|
||||
|
||||
# Changes the properties of an existing user.
|
||||
exports.set = $coroutine ({id, email, password, permission}) ->
|
||||
# Retrieves the user.
|
||||
user = $wait @users.first id
|
||||
|
||||
# Throws an error if it did not exist.
|
||||
@throw 'NO_SUCH_OBJECT' unless user
|
||||
|
||||
# Updates the provided properties.
|
||||
user.set {email} if email?
|
||||
user.set {permission} if permission?
|
||||
$wait user.setPassword password if password?
|
||||
|
||||
# Updates the user.
|
||||
$wait @users.update user
|
||||
|
||||
return true
|
||||
exports.set.permission = 'admin'
|
||||
exports.set.params = {
|
||||
id: { type: 'string' }
|
||||
email: { type: 'string', optional: true }
|
||||
password: { type: 'string', optional: true }
|
||||
permission: { type: 'string', optional: true }
|
||||
}
|
74
src/api/user.js
Normal file
74
src/api/user.js
Normal file
@ -0,0 +1,74 @@
|
||||
import map from 'lodash.map'
|
||||
|
||||
import {InvalidParameters} from '../api-errors'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export async function create ({email, password, permission}) {
|
||||
return (await this.createUser({email, password, permission})).id
|
||||
}
|
||||
|
||||
create.description = 'creates a new user'
|
||||
|
||||
create.permission = 'admin'
|
||||
|
||||
create.params = {
|
||||
email: { type: 'string' },
|
||||
password: { type: 'string' },
|
||||
permission: { type: 'string', optional: true}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// Deletes an existing user.
|
||||
async function delete_ ({id}) {
|
||||
if (id === this.session.get('user_id')) {
|
||||
throw new InvalidParameters('an user cannot delete itself')
|
||||
}
|
||||
|
||||
await this.deleteUser(id)
|
||||
}
|
||||
|
||||
// delete is not a valid identifier.
|
||||
export {delete_ as delete}
|
||||
|
||||
delete_.description = 'deletes an existing user'
|
||||
|
||||
delete_.permission = 'admin'
|
||||
|
||||
delete_.params = {
|
||||
id: { type: 'string' }
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// TODO: remove this function when users are integrated to the main
|
||||
// collection.
|
||||
export async function getAll () {
|
||||
// Retrieves the users.
|
||||
const users = await this._users.get()
|
||||
|
||||
// Filters out private properties.
|
||||
return map(users, this.getUserPublicProperties)
|
||||
}
|
||||
|
||||
getAll.description = 'returns all the existing users'
|
||||
|
||||
getAll.permission = 'admin'
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export async function set ({id, email, password, permission}) {
|
||||
await this.updateUser(id, {email, password, permission})
|
||||
}
|
||||
|
||||
set.description = 'changes the properties of an existing user'
|
||||
|
||||
set.permission = 'admin'
|
||||
|
||||
set.params = {
|
||||
id: { type: 'string' },
|
||||
email: { type: 'string', optional: true },
|
||||
password: { type: 'string', optional: true },
|
||||
permission: { type: 'string', optional: true }
|
||||
}
|
12
src/index.js
12
src/index.js
@ -309,7 +309,8 @@ const registerPasswordAuthenticationProvider = (xo) => {
|
||||
throw null
|
||||
}
|
||||
|
||||
const user = await xo.users.first({email})
|
||||
// TODO: this is deprecated and should be removed.
|
||||
const user = await xo._users.first({email})
|
||||
if (!user || !(await user.checkPassword(password))) {
|
||||
throw null
|
||||
}
|
||||
@ -329,12 +330,7 @@ const registerTokenAuthenticationProvider = (xo) => {
|
||||
throw null
|
||||
}
|
||||
|
||||
const token = await xo.tokens.first(tokenId)
|
||||
if (!token) {
|
||||
throw null
|
||||
}
|
||||
|
||||
return token.get('user_id')
|
||||
return (await xo.getAuthenticationToken(tokenId)).user_id
|
||||
}
|
||||
|
||||
xo.registerAuthenticationProvider(tokenAuthenticationProvider)
|
||||
@ -404,7 +400,7 @@ export default async function main (args) {
|
||||
|
||||
setUpStaticFiles(connect, config.http.mounts)
|
||||
|
||||
if (!(await xo.users.exists())) {
|
||||
if (!(await xo._users.exists())) {
|
||||
const email = 'admin@admin.net'
|
||||
const password = 'admin'
|
||||
|
||||
|
261
src/xo.js
261
src/xo.js
@ -12,12 +12,33 @@ import {parse as parseUrl} from 'url'
|
||||
|
||||
import Connection from './connection'
|
||||
import spec from './spec'
|
||||
import User, {Users} from './models/user'
|
||||
import {$MappedCollection as MappedCollection} from './MappedCollection'
|
||||
import {Acls} from './models/acl'
|
||||
import {generateToken} from './utils'
|
||||
import {NoSuchObject} from './api-errors'
|
||||
import {Servers} from './models/server'
|
||||
import {Tokens} from './models/token'
|
||||
import User, {Users} from './models/user'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
class NoSuchAuthenticationToken extends NoSuchObject {
|
||||
constructor (id) {
|
||||
super({
|
||||
type: 'authentication token',
|
||||
id
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
class NoSuchUser extends NoSuchObject {
|
||||
constructor (id) {
|
||||
super({
|
||||
type: 'user',
|
||||
id
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@ -26,21 +47,25 @@ export default class Xo extends EventEmitter {
|
||||
super()
|
||||
|
||||
// These will be initialized in start()
|
||||
//
|
||||
// TODO: remove and put everything in the `_objects` collection.
|
||||
this._tokens = null
|
||||
this._users = null
|
||||
this._UUIDsToKeys = null
|
||||
this.acls = null
|
||||
this.servers = null
|
||||
this.tokens = null
|
||||
this.users = null
|
||||
|
||||
// Connections to Xen servers.
|
||||
this._xapis = Object.create(null)
|
||||
|
||||
// Connections to users.
|
||||
this._nextConId = 0
|
||||
this.connections = Object.create(null)
|
||||
this._connections = Object.create(null)
|
||||
|
||||
// Collections of XAPI objects mapped to XO Api.
|
||||
this._xobjs = new MappedCollection()
|
||||
spec.call(this._xobjs)
|
||||
this._watchXobjs()
|
||||
|
||||
this._proxyRequests = Object.create(null)
|
||||
|
||||
@ -87,12 +112,12 @@ export default class Xo extends EventEmitter {
|
||||
prefix: 'xo:server',
|
||||
indexes: ['host']
|
||||
})
|
||||
this.tokens = new Tokens({
|
||||
this._tokens = new Tokens({
|
||||
connection: redis,
|
||||
prefix: 'xo:token',
|
||||
indexes: ['user_id']
|
||||
})
|
||||
this.users = new Users({
|
||||
this._users = new Users({
|
||||
connection: redis,
|
||||
prefix: 'xo:user',
|
||||
indexes: ['email']
|
||||
@ -100,91 +125,21 @@ export default class Xo extends EventEmitter {
|
||||
|
||||
// Proxies tokens/users related events to XO and removes tokens
|
||||
// when their related user is removed.
|
||||
this.tokens.on('remove', ids => {
|
||||
this._tokens.on('remove', ids => {
|
||||
for (let id of ids) {
|
||||
this.emit(`token.revoked:${id}`)
|
||||
}
|
||||
})
|
||||
this.users.on('remove', async function (ids) {
|
||||
this._users.on('remove', async function (ids) {
|
||||
for (let id of ids) {
|
||||
this.emit(`user.revoked:${id}`)
|
||||
}
|
||||
|
||||
const tokens = await this.tokens.get({ user_id: id })
|
||||
for (let token of tokens) {
|
||||
this.tokens.remove(token.id)
|
||||
const tokens = await this._tokens.get({ user_id: id })
|
||||
for (let token of tokens) {
|
||||
this._tokens.remove(token.id)
|
||||
}
|
||||
}
|
||||
}.bind(this))
|
||||
|
||||
// When objects enter or exists, sends a notification to all
|
||||
// connected clients.
|
||||
{
|
||||
let entered = {}
|
||||
let exited = {}
|
||||
|
||||
let dispatcherRegistered = false
|
||||
const dispatcher = Bluebird.method(() => {
|
||||
dispatcherRegistered = false
|
||||
|
||||
const {connections} = this
|
||||
|
||||
if (!isEmpty(entered)) {
|
||||
const enterParams = {
|
||||
type: 'enter',
|
||||
items: pluck(entered, 'val')
|
||||
}
|
||||
entered = {}
|
||||
|
||||
for (let id in connections) {
|
||||
const connection = connections[id]
|
||||
|
||||
if (connection.has('user_id')) {
|
||||
connection.notify('all', enterParams)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isEmpty(exited)) {
|
||||
const exitParams = {
|
||||
type: 'exit',
|
||||
items: pluck(exited, 'val')
|
||||
}
|
||||
exited = {}
|
||||
|
||||
for (let id in connections) {
|
||||
const connection = connections[id]
|
||||
|
||||
if (connection.has('user_id')) {
|
||||
connection.notify('all', exitParams)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
this._xobjs.on('any', (event, items) => {
|
||||
if (!dispatcherRegistered) {
|
||||
dispatcherRegistered = true
|
||||
process.nextTick(dispatcher)
|
||||
}
|
||||
|
||||
if (event === 'exit') {
|
||||
forEach(items, item => {
|
||||
const {key} = item
|
||||
|
||||
delete entered[key]
|
||||
exited[key] = item
|
||||
})
|
||||
} else {
|
||||
forEach(items, item => {
|
||||
const {key} = item
|
||||
|
||||
delete exited[key]
|
||||
entered[key] = item
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Exports the map from UUIDs to keys.
|
||||
this._UUIDsToKeys = this._xobjs.get('xo').$UUIDsToKeys
|
||||
|
||||
@ -201,6 +156,66 @@ export default class Xo extends EventEmitter {
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
async createUser ({email, password, permission}) {
|
||||
// TODO: use plain objects
|
||||
const user = await this._users.create(email, password, permission)
|
||||
|
||||
return user.properties
|
||||
}
|
||||
|
||||
async deleteUser (id) {
|
||||
if (!await this._users.remove(id)) {
|
||||
throw new NoSuchUser(id)
|
||||
}
|
||||
}
|
||||
|
||||
async updateUser(id, {email, password, permission}) {
|
||||
const user = await this._getUser(id)
|
||||
|
||||
if (email) user.set('email', email)
|
||||
if (password) user.setPassword(password)
|
||||
if (permission) user.set('permission', permission)
|
||||
|
||||
await this._users.update(user)
|
||||
}
|
||||
|
||||
// TODO: this method will no longer be async when users are
|
||||
// integrated to the main collection.
|
||||
async _getUser (id) {
|
||||
const user = await this._users.first(id)
|
||||
if (!user) {
|
||||
throw new NoSuchUser(id)
|
||||
}
|
||||
|
||||
return user
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
async createAuthenticationToken (userId) {
|
||||
// TODO: use plain objects
|
||||
const token = await this._tokens.generate(userId)
|
||||
|
||||
return token.properties
|
||||
}
|
||||
|
||||
async deleteAuthenticationToken (id) {
|
||||
if (!await this._token.remove(id)) {
|
||||
throw new NoSuchAuthenticationToken(id)
|
||||
}
|
||||
}
|
||||
|
||||
async getAuthenticationToken (id) {
|
||||
const token = await this._tokens.first(id)
|
||||
if (!token) {
|
||||
throw new NoSuchAuthenticationToken(id)
|
||||
}
|
||||
|
||||
return token.properties
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
connectServer (server) {
|
||||
if (server.properties) {
|
||||
server = server.properties
|
||||
@ -327,7 +342,7 @@ export default class Xo extends EventEmitter {
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
createUserConnection () {
|
||||
const {connections} = this
|
||||
const {_connections: connections} = this
|
||||
|
||||
const connection = new Connection()
|
||||
const id = connection.id = this._nextConId++
|
||||
@ -461,10 +476,10 @@ export default class Xo extends EventEmitter {
|
||||
delete result.username
|
||||
}
|
||||
|
||||
const user = await this.users.first(result)
|
||||
const user = await this._users.first(result)
|
||||
if (user) return user
|
||||
|
||||
return this.users.create(result.email)
|
||||
return this._users.create(result.email)
|
||||
} catch (error) {
|
||||
// Authentication providers may just throw `null` to indicate
|
||||
// they could not authenticate the user without any special
|
||||
@ -475,4 +490,80 @@ export default class Xo extends EventEmitter {
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
// When objects enter or exists, sends a notification to all
|
||||
// connected clients.
|
||||
//
|
||||
// TODO: remove when all objects are in `this._objects`.
|
||||
_watchXobjs () {
|
||||
const {
|
||||
_connections: connections,
|
||||
_xobjs: xobjs
|
||||
} = this
|
||||
|
||||
let entered = {}
|
||||
let exited = {}
|
||||
|
||||
let dispatcherRegistered = false
|
||||
const dispatcher = Bluebird.method(() => {
|
||||
dispatcherRegistered = false
|
||||
|
||||
if (!isEmpty(entered)) {
|
||||
const enterParams = {
|
||||
type: 'enter',
|
||||
items: pluck(entered, 'val')
|
||||
}
|
||||
entered = {}
|
||||
|
||||
for (let id in connections) {
|
||||
const connection = connections[id]
|
||||
|
||||
if (connection.has('user_id')) {
|
||||
connection.notify('all', enterParams)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isEmpty(exited)) {
|
||||
const exitParams = {
|
||||
type: 'exit',
|
||||
items: pluck(exited, 'val')
|
||||
}
|
||||
exited = {}
|
||||
|
||||
for (let id in connections) {
|
||||
const connection = connections[id]
|
||||
|
||||
if (connection.has('user_id')) {
|
||||
connection.notify('all', exitParams)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
xobjs.on('any', (event, items) => {
|
||||
if (!dispatcherRegistered) {
|
||||
dispatcherRegistered = true
|
||||
process.nextTick(dispatcher)
|
||||
}
|
||||
|
||||
if (event === 'exit') {
|
||||
forEach(items, item => {
|
||||
const {key} = item
|
||||
|
||||
delete entered[key]
|
||||
exited[key] = item
|
||||
})
|
||||
} else {
|
||||
forEach(items, item => {
|
||||
const {key} = item
|
||||
|
||||
delete exited[key]
|
||||
entered[key] = item
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user