Various updates.
This commit is contained in:
parent
cbd0b9db1d
commit
a4e03daeee
38
src/api.js
38
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)
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
@ -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'
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ set.params = {
|
||||
}
|
||||
|
||||
set.resolve = {
|
||||
sr: ['id', 'SR']
|
||||
sr: ['id', 'SR', 'operate']
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
@ -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
136
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)
|
||||
|
Loading…
Reference in New Issue
Block a user