ACLs inheritance (fix vatesfr/xo-web#279).
This commit is contained in:
parent
5eb40d2299
commit
62067e0801
123
src/acl.js
Normal file
123
src/acl.js
Normal 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
|
||||
}
|
||||
}
|
105
src/api.js
105
src/api.js
@ -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() }
|
||||
)
|
||||
|
@ -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'
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
|
@ -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
28
src/schemas/acl.js
Normal 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
111
src/xo.js
@ -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 [
|
||||
|
Loading…
Reference in New Issue
Block a user