Various updates.

This commit is contained in:
Julien Fontanet 2015-04-30 17:45:53 +02:00
parent 13f36b3f79
commit ad2de95f32
7 changed files with 289 additions and 280 deletions

View File

@ -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)
}
}

View File

@ -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) {

View File

@ -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_

View File

@ -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
View 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 }
}

View File

@ -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
View File

@ -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
})
}
})
}
}