ACLs inheritance (fix vatesfr/xo-web#279).

This commit is contained in:
Julien Fontanet 2015-09-07 15:20:31 +02:00
parent 5eb40d2299
commit 62067e0801
6 changed files with 238 additions and 157 deletions

123
src/acl.js Normal file
View File

@ -0,0 +1,123 @@
// These global variables are not a problem because the algorithm is
// synchronous.
let permissionsByObject
let getObject
// -------------------------------------------------------------------
const authorized = () => true // eslint-disable-line no-unused-vars
const forbiddden = () => false // eslint-disable-line no-unused-vars
function and (...checkers) { // eslint-disable-line no-unused-vars
return function (object, permission) {
for (const checker of checkers) {
if (!checker(object, permission)) {
return false
}
}
return true
}
}
function or (...checkers) { // eslint-disable-line no-unused-vars
return function (object, permission) {
for (const checker of checkers) {
if (checker(object, permission)) {
return true
}
}
return false
}
}
// -------------------------------------------------------------------
function checkMember (memberName) {
return function (object, permission) {
const member = object[memberName]
return checkAuthorization(member, permission)
}
}
function checkSelf ({ id }, permission) {
const permissionsForObject = permissionsByObject[id]
return (
permissionsForObject &&
permissionsForObject[permission]
)
}
// ===================================================================
const checkAuthorizationByTypes = {
host: or(checkSelf, checkMember('$poolId')),
message: checkMember('$object'),
network: or(checkSelf, checkMember('$poolId')),
SR: or(checkSelf, checkMember('$poolId')),
task: checkMember('$host'),
VBD: checkMember('VDI'),
// Access to a VDI is granted if the user has access to the
// containing SR or to a linked VM.
VDI (vdi, permission) {
// Check authorization for the containing SR.
if (checkAuthorization(vdi.$SR, permission)) {
return true
}
// Check authorization for each of the connected VMs.
for (const {$VM: vm} of vdi.$VBDs) {
if (checkAuthorization(vm, permission)) {
return true
}
}
return false
},
VIF: or(checkMember('$network'), checkMember('$VM')),
VM: or(checkSelf, checkMember('$container')),
'VM-snapshot': checkMember('snapshot_of'),
'VM-template': authorized
}
function checkAuthorization (objectId, permission) {
const object = getObject(objectId)
const checker = checkAuthorizationByTypes[object.type] || checkSelf
return checker(object, permission)
}
// -------------------------------------------------------------------
export default function (
permissionsByObject_,
getObject_,
permissions
) {
// Assign global variables.
permissionsByObject = permissionsByObject_
getObject = getObject_
try {
for (const [objectId, permission] of permissions) {
if (!checkAuthorization(objectId, permission)) {
return false
}
}
return true
} finally {
// Free the global variables.
permissionsByObject = getObject = null
}
}

View File

@ -2,12 +2,10 @@ import createDebug from 'debug'
const debug = createDebug('xo:api')
import assign from 'lodash.assign'
import Bluebird from 'bluebird'
import forEach from 'lodash.foreach'
import getKeys from 'lodash.keys'
import isFunction from 'lodash.isfunction'
import kindOf from 'kindof'
import map from 'lodash.map'
import ms from 'ms'
import schemaInspector from 'schema-inspector'
@ -68,100 +66,6 @@ function checkParams (method, params) {
// -------------------------------------------------------------------
// Forward declaration.
let checkAuthorization
function authorized () {}
function forbiddden () { // eslint-disable-line no-unused-vars
throw null // eslint-disable-line no-throw-literal
}
function checkMemberAuthorization (member) {
return function (userId, object, permission) {
const memberObject = this.getObject(object[member])
return checkAuthorization.call(this, userId, memberObject, permission)
}
}
const checkAuthorizationByTypes = {
host (userId, host, permission) {
return defaultCheckAuthorization.call(this, userId, host, permission).catch(() => {
return checkAuthorization.call(this, userId, host.$pool, permission)
})
},
message: checkMemberAuthorization('$object'),
network (userId, network, permission) {
return defaultCheckAuthorization.call(this, userId, network, permission).catch(() => {
return checkAuthorization.call(this, userId, network.$pool, permission)
})
},
SR (userId, sr, permission) {
return defaultCheckAuthorization.call(this, userId, sr, permission).catch(() => {
return checkAuthorization.call(this, userId, sr.$pool, permission)
})
},
task: checkMemberAuthorization('$host'),
VBD: checkMemberAuthorization('VDI'),
// Access to a VDI is granted if the user has access to the
// containing SR or to a linked VM.
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, permission)
})
// Check authorization for the containing SR.
const sr = this.getObject(vdi.$SR, 'SR')
promises.push(checkAuthorization.call(this, userId, sr, permission))
// We need at least one success
return Bluebird.any(promises)
},
VIF (userId, vif, permission) {
const network = this.getObject(vif.$network)
const vm = this.getObject(vif.$VM)
return Bluebird.any([
checkAuthorization.call(this, userId, network, permission),
checkAuthorization.call(this, userId, vm, permission)
])
},
VM (userId, vm, permission) {
return defaultCheckAuthorization.call(this, userId, vm, permission).catch(() => {
return checkAuthorization.call(this, userId, vm.$host, permission)
})
},
'VM-snapshot': checkMemberAuthorization('$snapshot_of'),
'VM-template': authorized
}
function throwIfFail (success) {
if (!success) {
// We don't care about an error object.
/* eslint no-throw-literal: 0 */
throw null
}
}
function defaultCheckAuthorization (userId, object, permission) {
return this.hasPermission(userId, object.id, permission).then(throwIfFail)
}
checkAuthorization = async function (userId, object, permission) {
const fn = checkAuthorizationByTypes[object.type] || defaultCheckAuthorization
return fn.call(this, userId, object, permission)
}
function resolveParams (method, params) {
const resolve = method.resolve
if (!resolve) {
@ -174,9 +78,8 @@ function resolveParams (method, params) {
}
const userId = user.get('id')
const isAdmin = this.user.hasPermission('admin')
const promises = []
const permissions = []
forEach(resolve, ([param, types, permission = 'administrate'], key) => {
const id = params[param]
if (id === undefined) {
@ -191,12 +94,10 @@ function resolveParams (method, params) {
// Register this new value.
params[key] = object
if (!isAdmin) {
promises.push(checkAuthorization.call(this, userId, object, permission))
}
permissions.push([ object.id, permission ])
})
return Promise.all(promises).then(
return this.hasPermissions(userId, permissions).then(
() => params,
() => { throw new Unauthorized() }
)

View File

@ -8,13 +8,13 @@ get.description = 'get existing ACLs'
// -------------------------------------------------------------------
export async function getCurrent () {
return await this.getAclsForUser(this.session.get('user_id'))
export async function getCurrentPermissions () {
return await this.getPermissionsForUser(this.session.get('user_id'))
}
getCurrent.permission = ''
getCurrentPermissions.permission = ''
getCurrent.description = 'get existing ACLs concerning current user'
getCurrentPermissions.description = 'get (explicit) permissions by object for the current user'
// -------------------------------------------------------------------

View File

@ -1,5 +1,21 @@
export function hasPermission ({userId, objectId, permission}) {
return this.hasPermission(userId, objectId, permission)
export function getPermissionsForUser ({ userId }) {
return this.getPermissionsForUser(userId)
}
getPermissionsForUser.permission = 'admin'
getPermissionsForUser.params = {
userId: {
type: 'string'
}
}
// -------------------------------------------------------------------
export function hasPermission ({ userId, objectId, permission }) {
return this.hasPermission(userId, [
[ objectId, permission ]
])
}
hasPermission.permission = 'admin'

28
src/schemas/acl.js Normal file
View File

@ -0,0 +1,28 @@
export default {
$schema: 'http://json-schema.org/draft-04/schema#',
type: 'object',
properties: {
id: {
type: 'string',
description: 'unique identifier for this ACL'
},
action: {
type: 'string',
description: 'permission (or role)'
},
object: {
type: 'string',
description: 'item (or set)'
},
subject: {
type: 'string',
description: 'user (or group)'
}
},
required: [
'id',
'action',
'object',
'subject'
]
}

111
src/xo.js
View File

@ -20,6 +20,7 @@ import {createClient as createRedisClient} from 'redis'
import {EventEmitter} from 'events'
import * as xapiObjectsToXo from './xapi-objects-to-xo'
import checkAuthorization from './acl'
import Connection from './connection'
import Xapi from './xapi'
import XapiStats from './xapi-stats'
@ -216,21 +217,7 @@ export default class Xo extends EventEmitter {
// -----------------------------------------------------------------
async addAcl (subjectId, objectId, action) {
try {
await this._acls.create(subjectId, objectId, action)
} catch (error) {
if (!(error instanceof ModelAlreadyExists)) {
throw error
}
}
}
async removeAcl (subjectId, objectId, action) {
await this._acls.delete(subjectId, objectId, action)
}
async getAclsForUser (userId) {
async _getAclsForUser (userId) {
const subjects = (await this.getUser(userId)).groups.concat(userId)
const acls = []
@ -249,50 +236,66 @@ export default class Xo extends EventEmitter {
return acls
}
async addAcl (subjectId, objectId, action) {
try {
await this._acls.create(subjectId, objectId, action)
} catch (error) {
if (!(error instanceof ModelAlreadyExists)) {
throw error
}
}
}
async removeAcl (subjectId, objectId, action) {
await this._acls.delete(subjectId, objectId, action)
}
// TODO: remove when new collection.
async getAllAcls () {
return this._acls.get()
}
async hasPermission (userId, objectId, permission) {
async getPermissionsForUser (userId) {
const [
acls,
permissionsByRole
] = await Promise.all([
this._getAclsForUser(userId),
this._getPermissionsByRole()
])
const permissions = createRawObject()
for (const { action, object: objectId } of acls) {
const current = (
permissions[objectId] ||
(permissions[objectId] = createRawObject())
)
const permissionsForRole = permissionsByRole[action]
if (permissionsForRole) {
for (const permission of permissionsForRole) {
current[permission] = 1
}
} else {
current[action] = 1
}
}
return permissions
}
async hasPermissions (userId, permissions) {
const user = await this.getUser(userId)
// 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)
let 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.aclExists(subject, objectId, action).then(throwIfFail)
)
})
})
}
try {
await Bluebird.any(promises)
if (user.permission === 'admin') {
return true
} catch (_) {
return false
}
return checkAuthorization(
await this.getPermissionsForUser(userId),
id => this.getObject(id),
permissions
)
}
// -----------------------------------------------------------------
@ -495,6 +498,16 @@ export default class Xo extends EventEmitter {
// -----------------------------------------------------------------
async _getPermissionsByRole () {
const roles = await this.getRoles()
const permissions = createRawObject()
for (const role of roles) {
permissions[role.id] = role.permissions
}
return permissions
}
// TODO: delete when merged with the new collection.
async getRoles () {
return [