From a4e03daeeeadd1c539a631944a004404d79e3356 Mon Sep 17 00:00:00 2001 From: Julien Fontanet Date: Thu, 28 May 2015 22:14:20 +0200 Subject: [PATCH] Various updates. --- src/api.js | 38 ++++++++----- src/api/acl.js | 14 +++-- src/api/role.js | 15 +---- src/api/sr.js | 2 +- src/models/acl.js | 30 +++++----- src/xo.js | 136 +++++++++++++++++++++++++++++++++++++++++----- 6 files changed, 171 insertions(+), 64 deletions(-) diff --git a/src/api.js b/src/api.js index 7245987bf..6af27c793 100644 --- a/src/api.js +++ b/src/api.js @@ -74,9 +74,9 @@ function authorized () {} // throw new Unauthorized() // } function checkMemberAuthorization (member) { - return function (userId, object) { + return function (userId, object, permission) { const memberObject = this.getObject(object[member]) - return checkAuthorization.call(this, userId, memberObject) + return checkAuthorization.call(this, userId, memberObject, permission) } } @@ -93,38 +93,44 @@ const checkAuthorizationByTypes = { // Access to a VDI is granted if the user has access to the // containing SR or to a linked VM. - VDI (userId, vdi) { + VDI (userId, vdi, permission) { // Check authorization for each of the connected VMs. const promises = map(this.getObjects(vdi.$VBDs, 'VBD'), vbd => { const vm = this.getObject(vbd.VM, 'VM') - return checkAuthorization.call(this, userId, vm) + return checkAuthorization.call(this, userId, vm, permission) }) // Check authorization for the containing SR. const sr = this.getObject(vdi.$SR, 'SR') - promises.push(checkAuthorization.call(this, userId, sr)) + promises.push(checkAuthorization.call(this, userId, sr, permission)) // We need at least one success - return Bluebird.any(promises).catch(function (aggregateError) { - throw aggregateError[0] - }) + return Bluebird.any(promises) }, - VIF (userId, vif) { + VIF (userId, vif, permission) { const network = this.getObject(vif.$network) const vm = this.getObject(vif.$VM) return Bluebird.any([ - checkAuthorization.call(this, userId, network), - checkAuthorization.call(this, userId, vm) + checkAuthorization.call(this, userId, network, permission), + checkAuthorization.call(this, userId, vm, permission) ]) }, 'VM-snapshot': checkMemberAuthorization('$snapshot_of') } +function throwIfFail (success) { + if (!success) { + // We don't care about an error object. + /* eslint no-throw-literal: 0 */ + throw null + } +} + function defaultCheckAuthorization (userId, object) { - return this.canAccess(userId, object.id) + return this.canAccess(userId, object.id).then(throwIfFail) } checkAuthorization = Bluebird.method(function (userId, object) { @@ -147,7 +153,7 @@ function resolveParams (method, params) { const isAdmin = this.user.hasPermission('admin') const promises = [] - forEach(resolve, ([param, types], key) => { + forEach(resolve, ([param, types, permission], key) => { const id = params[param] if (id === undefined) { return @@ -162,11 +168,13 @@ function resolveParams (method, params) { params[key] = object if (!isAdmin) { - promises.push(checkAuthorization.call(this, userId, object)) + promises.push(checkAuthorization.call(this, userId, object, permission)) } }) - return Bluebird.all(promises).return(params) + return Bluebird.all(promises).catch(() => { + throw new Unauthorized() + }).return(params) } // =================================================================== diff --git a/src/api/acl.js b/src/api/acl.js index f9e4c1896..d153f155c 100644 --- a/src/api/acl.js +++ b/src/api/acl.js @@ -18,30 +18,32 @@ getCurrent.description = 'get existing ACLs concerning current user' // ------------------------------------------------------------------- -export async function add ({subject, object, role}) { - await this.addAcl(subject, object, role) +export async function add ({subject, object, action = 'view'}) { + await this.addAcl(subject, object, action) } add.permission = 'admin' add.params = { subject: { type: 'string' }, - object: { type: 'string' } + object: { type: 'string' }, + // action: { type: 'string' } } add.description = 'add a new ACL entry' // ------------------------------------------------------------------- -export async function remove ({subject, object, role}) { - await this.removeAcl(subject, object, role) +export async function remove ({subject, object, action}) { + await this.removeAcl(subject, object, action) } remove.permission = 'admin' remove.params = { subject: { type: 'string' }, - object: { type: 'string' } + object: { type: 'string' }, + action: { type: 'string' } } remove.description = 'remove an existing ACL entry' diff --git a/src/api/role.js b/src/api/role.js index 01fd30311..6e5d13b14 100644 --- a/src/api/role.js +++ b/src/api/role.js @@ -1,16 +1,3 @@ export async function getAll () { - return [ - { - id: 'viewer', - name: 'Viewer' - }, - { - id: 'operator', - name: 'Operator' - }, - { - id: 'admin', - name: 'Admin' - } - ] + return await this.getRoles() } diff --git a/src/api/sr.js b/src/api/sr.js index 664b76042..5f2f6fa49 100644 --- a/src/api/sr.js +++ b/src/api/sr.js @@ -20,7 +20,7 @@ set.params = { } set.resolve = { - sr: ['id', 'SR'] + sr: ['id', 'SR', 'operate'] } // ------------------------------------------------------------------- diff --git a/src/models/acl.js b/src/models/acl.js index 0af9046b6..284282144 100644 --- a/src/models/acl.js +++ b/src/models/acl.js @@ -7,24 +7,24 @@ import {multiKeyHash} from '../utils' // =================================================================== -// Up until now, there were no roles, therefore the default role is -// used for existing entries. -const DEFAULT_ROLE = 'admin' +// Up until now, there were no actions, therefore the default +// action is used to update existing entries. +const DEFAULT_ACTION = 'admin' // =================================================================== export default class Acl extends Model {} -Acl.create = (subject, object, role) => { - return Acl.hash(subject, object, role).then(hash => new Acl({ +Acl.create = (subject, object, action) => { + return Acl.hash(subject, object, action).then(hash => new Acl({ id: hash, subject, object, - role + action })) } -Acl.hash = (subject, object, role) => multiKeyHash(subject, object, role) +Acl.hash = (subject, object, action) => multiKeyHash(subject, object, action) // ------------------------------------------------------------------- @@ -33,22 +33,22 @@ export class Acls extends Collection { return Acl } - create (subject, object, role) { - return Acl.create(subject, object, role).then(acl => this.add(acl)) + create (subject, object, action) { + return Acl.create(subject, object, action).then(acl => this.add(acl)) } - delete (subject, object, role = DEFAULT_ROLE) { - return Acl.hash(subject, object, role).then(hash => this.remove(hash)) + delete (subject, object, action) { + return Acl.hash(subject, object, action).then(hash => this.remove(hash)) } async get (properties) { const acls = await super.get(properties) - // Finds all records that are missing a role and need to be updated. + // Finds all records that are missing a action and need to be updated. const toUpdate = [] forEach(acls, acl => { - if (!acl.role) { - acl.role = DEFAULT_ROLE + if (!acl.action) { + acl.action = DEFAULT_ACTION toUpdate.push(acl) } }) @@ -60,7 +60,7 @@ export class Acls extends Collection { const {hash} = Acl await Promise.all(map( toUpdate, - (acl) => hash(acl.subject, acl.object, acl.role).then(id => { + (acl) => hash(acl.subject, acl.object, acl.action).then(id => { acl.id = id }) )) diff --git a/src/xo.js b/src/xo.js index 737855875..b1d302236 100644 --- a/src/xo.js +++ b/src/xo.js @@ -4,6 +4,7 @@ import forEach from 'lodash.foreach' import includes from 'lodash.includes' import isEmpty from 'lodash.isempty' import isString from 'lodash.isstring' +import keys from 'lodash.keys' import map from 'lodash.map' import proxyRequest from 'proxy-http-request' import XoCollection from 'xo-collection' @@ -21,7 +22,7 @@ import {Acls} from './models/acl' import {autobind} from './decorators' import {generateToken} from './utils' import {Groups} from './models/group' -import {JsonRpcError, NoSuchObject, Unauthorized} from './api-errors' +import {JsonRpcError, NoSuchObject} from './api-errors' import {ModelAlreadyExists} from './collection' import {Servers} from './models/server' import {Tokens} from './models/token' @@ -176,9 +177,44 @@ export default class Xo extends EventEmitter { return this._acls.get() } - async canAccess (userId, objectId) { - if (!await this._acls.exists({subject: userId, object: objectId})) { - throw new Unauthorized() + async hasPermission (userId, objectId, permission) { + const user = await this.getUser() + + // Special case for super XO administrators. + // + // TODO: restore when necessary, for now it is already implemented + // in resolveParams(). + // if (user.permission === 'admin') { + // return true + // } + + const subjects = user.groups.concat(userId) + const actions = (await this.getRolesForPermission(permission)).concat(permission) + + const promises = [] + { + const {_acls: acls} = this + const throwIfFail = function (success) { + if (!success) { + // We don't care about an error object. + /* eslint no-throw-literal: 0 */ + throw null + } + } + forEach(subjects, subject => { + forEach(actions, action => { + promises.push( + acls.exists({subject, object: objectId, action}).then(throwIfFail) + ) + }) + }) + } + + try { + await Bluebird.any(promises) + return true + } catch (_) { + return false } } @@ -262,8 +298,15 @@ export default class Xo extends EventEmitter { this.getGroup(groupId) ]) - user.groups.push(groupId) - group.users.push(userId) + const {groups} = user + if (!includes(groups, groupId)) { + user.groups.push(groupId) + } + + const {users} = group + if (!includes(users, userId)) { + group.users.push(userId) + } await Promise.all([ this._users.save(user), @@ -277,6 +320,7 @@ export default class Xo extends EventEmitter { this.getGroup(groupId) ]) + // TODO: maybe not iterating through the whole arrays? user.groups = filter(user.groups, id => id !== groupId) group.users = filter(group.users, id => id !== userId) @@ -287,24 +331,90 @@ export default class Xo extends EventEmitter { } async setGroupUsers (groupId, userIds) { - const [users, group] = await Promise.all([ - Promise.all(map(userIds, this.getUser, this)), - this.getGroup(groupId) + const group = await this.getGroup(groupId) + + const newUsersIds = Object.create(null) + const oldUsersIds = Object.create(null) + forEach(userIds, id => { + newUsersIds[id] = null + }) + forEach(group.users, id => { + if (id in newUsersIds) { + delete newUsersIds[id] + } else { + oldUsersIds[id] = null + } + }) + + const [newUsers, oldUsers] = await Promise.all([ + Promise.all(map(newUsersIds, (_, id) => this.getUser(id))), + Promise.all(map(oldUsersIds, (_, id) => this.getUser(id))) ]) - forEach(users, user => { - user.groups.push(groupId) + forEach(newUsers, user => { + const {groups} = user + if (!includes(groups, groupId)) { + user.groups.push(groupId) + } }) - group.users = userIds + forEach(oldUsers, user => { + user.groups = filter(user.groups, id => id !== groupId) + }) + + // Better than using userIds because we avoid duplicates + group.users = keys(newUsersIds) await Promise.all([ - Promise.all(map(users, this._users.save, this._users)), + Promise.all(map(newUsers, this._users.save, this._users)), + Promise.all(map(oldUsers, this._users.save, this._users)), this._groups.save(group) ]) } // ----------------------------------------------------------------- + // TODO: delete when merged with the new collection. + async getRoles () { + return [ + { + id: 'viewer', + name: 'Viewer', + permissions: [ + 'view' + ] + }, + { + id: 'operator', + name: 'Operator', + permissions: [ + 'view', + 'operate' + ] + }, + { + id: 'admin', + name: 'Admin', + permissions: [ + 'view', + 'operate', + 'administrate' + ] + } + ] + } + + // Returns an array of permission for a role. + // + // If not a role, it will return undefined. + async resolveRolePermissions (id) { + const role = (await this.getRoles())[id] + if (role) { + return role.permissions + } + } + + // ----------------------------------------------------------------- + async createAuthenticationToken ({userId}) { // TODO: use plain objects const token = await this._tokens.generate(userId)