Various updates.

This commit is contained in:
Julien Fontanet 2015-05-28 22:14:20 +02:00
parent cbd0b9db1d
commit a4e03daeee
6 changed files with 171 additions and 64 deletions

View File

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

View File

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

View File

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

View File

@ -20,7 +20,7 @@ set.params = {
}
set.resolve = {
sr: ['id', 'SR']
sr: ['id', 'SR', 'operate']
}
// -------------------------------------------------------------------

View File

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

136
src/xo.js
View File

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