diff --git a/src/api-errors.js b/src/api-errors.js index 8b6c345c7..5bd0a1b18 100644 --- a/src/api-errors.js +++ b/src/api-errors.js @@ -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) } } diff --git a/src/api.js b/src/api.js index 8e8b95826..87a4b24fc 100644 --- a/src/api.js +++ b/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) { diff --git a/src/api/token.coffee b/src/api/token.coffee deleted file mode 100644 index 86cedc25b..000000000 --- a/src/api/token.coffee +++ /dev/null @@ -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_ diff --git a/src/api/user.coffee b/src/api/user.coffee deleted file mode 100644 index 2cbfc8556..000000000 --- a/src/api/user.coffee +++ /dev/null @@ -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 } -} diff --git a/src/api/user.js b/src/api/user.js new file mode 100644 index 000000000..03d72e4a9 --- /dev/null +++ b/src/api/user.js @@ -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 } +} diff --git a/src/index.js b/src/index.js index 03db2fb72..fbf1aeb18 100644 --- a/src/index.js +++ b/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' diff --git a/src/xo.js b/src/xo.js index 2232e85a7..6ef5b7f64 100644 --- a/src/xo.js +++ b/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 + }) + } + }) + } }