feat(xo-server): add groupIds and userIds to acl-role object

This commit is contained in:
MathieuRA
2026-07-07 10:23:40 -04:00
committed by Mathieu
parent 1455801282
commit fee9bf8334
4 changed files with 83 additions and 25 deletions

View File

@@ -1,19 +1,24 @@
import { Branded } from '../common.mjs'
import type { XoGroup, XoUser } from '../xo.mjs'
export type XoAclRole =
| {
id: Branded<'acl-v2-role'>
name: string
description?: string
}
| {
id: Branded<'acl-v2-role'>
name: string
description?: string
isTemplate: true
roleTemplateId: number
}
type XoAclBaseRole = {
id: Branded<'acl-v2-role'>
name: string
description?: string
privilegeIds: XoAclBasePrivilege['id'][]
}
type XoAclRegularRole = XoAclBaseRole & {
groupIds: XoGroup['id'][]
userIds: XoUser['id'][]
}
type XoAclTemplateRole = XoAclBaseRole & {
isTemplate: true
roleTemplateId: number
}
export type XoAclRole = XoAclRegularRole | XoAclTemplateRole
export type XoAclSupportedActionsByResource = {
[resource: string]: Record<string, unknown>

View File

@@ -21,4 +21,19 @@ export const aclRole = {
name: 'mra-test-read-only',
description: 'Access the whole infra in read-only mode',
id: '784bd959-08de-4b26-b575-92ded5aef872',
userIds: ['6b017bcf-99e4-40a2-bf64-bf723d6c1994'],
groupIds: [],
privilegeIds: [
'fe6c806d-c993-4144-9018-d3d0c057fbc1',
'a4e0c8a4-0fe7-47e4-a630-06707b10e672',
'bfce6c26-966c-4a59-a9ca-e11e66139850',
'e759f285-de67-44bf-b030-158211b25504',
'b086d21e-7fbd-46e7-9ae6-60ad77c905eb',
'88d49d33-319a-4af2-866b-1835cda254e6',
'bd59c0bc-2db5-4ccf-a50c-8be94e43324a',
'9e3376ef-cc54-4480-b7b1-cc8b802ed8c0',
'59026b9b-0f07-4735-92b1-fd98fb95306c',
'e395bfc9-747c-43a5-a46d-6abd01a0e96e',
'69fd4212-e0d5-4bac-b453-08377aba48f5',
],
}

View File

@@ -40,6 +40,7 @@
- [Pool] Add new VM and disconnect actions to the pool infrastructure tree (PR [#10046](https://github.com/vatesfr/xen-orchestra/pull/10046))
- [REST API] Expose `/rest/v0/groups/:id/acl-roles` (PR [#10085](https://github.com/vatesfr/xen-orchestra/pull/10085))
- [XO server] Add `aclRoleIds` property to the `group` objects (PR [#10085](https://github.com/vatesfr/xen-orchestra/pull/10085))
- [XO server] Add `groupIds`, `userIds` and `privilegeIds` properties to the `acl-role` objects (PR [#10085](https://github.com/vatesfr/xen-orchestra/pull/10085))
### Bug fixes

View File

@@ -236,6 +236,21 @@ export default class {
return this.#roleDb.add({ ...role, isTemplate: true })
}
/**
* @param {XoAclRole['id']} id
* @returns {Promise<Omit<XoAclRole, 'privilegeIds' | 'groupIds' | 'userIds'>>}
*/
async #getRawAclV2Role(id) {
await this._app.checkFeatureAuthorization('RBAC')
const role = await this.#roleDb.first(id)
if (role === undefined) {
throw noSuchObject(id, 'role')
}
return role
}
/**
* @param {object} role
* @param {XoAclRole['name']} role.name
@@ -257,7 +272,7 @@ export default class {
async deleteAclV2Role(id, { force = false } = {}) {
await this._app.checkFeatureAuthorization('RBAC')
const role = await this.getAclV2Role(id)
const role = await this.#getRawAclV2Role(id)
if (!force && 'isTemplate' in role) {
throw forbiddenOperation('delete ACL V2 role', 'role is a template')
@@ -290,7 +305,7 @@ export default class {
async updateAclV2Role(id, { name, description }, { force = false } = {}) {
await this._app.checkFeatureAuthorization('RBAC')
const role = await this.getAclV2Role(id)
const role = await this.#getRawAclV2Role(id)
if (!force && 'isTemplate' in role) {
throw forbiddenOperation('update ACL V2 role', 'role is a template')
}
@@ -309,6 +324,28 @@ export default class {
return this.#roleDb.update(role)
}
/**
* @param {XoAclRole} role
*/
async #normalizeAclV2Role(role) {
if (!('isTemplate' in role)) {
/** @type {[UserRole[], GroupRole[]]} */
const [userRoles, groupRoles] = await Promise.all([
this.#userRoleDb._get({ roleId: role.id }),
this.#groupRoleDb._get({ roleId: role.id }),
])
role.userIds = userRoles.map(userRole => userRole.userId)
role.groupIds = groupRoles.map(groupRole => groupRole.groupId)
}
/** @type {Privilege[]} */
const privileges = await this.#privilegeDb._get({ roleId: role.id })
role.privilegeIds = privileges.map(privilege => privilege.id)
return role
}
/**
* @param {XoAclRole['id']} id
* @returns {Promise<XoAclRole>}
@@ -321,7 +358,7 @@ export default class {
throw noSuchObject(id, 'role')
}
return role
return this.#normalizeAclV2Role(role)
}
/**
@@ -329,9 +366,9 @@ export default class {
*/
async getAclV2Roles() {
await this._app.checkFeatureAuthorization('RBAC')
const roles = await this.#roleDb.get()
// @ts-ignore typed as Promise<void>...
return this.#roleDb.get()
return Promise.all(roles.map(role => this.#normalizeAclV2Role(role)))
}
// === Role
// === Privilege
@@ -350,7 +387,7 @@ export default class {
async createAclV2Privilege({ action, selector, effect = 'allow', resource, roleId }, { force = false } = {}) {
await this._app.checkFeatureAuthorization('RBAC')
const role = await this.getAclV2Role(roleId)
const role = await this.#getRawAclV2Role(roleId)
if (!force && 'isTemplate' in role) {
throw forbiddenOperation('create ACL V2 privilege', 'role is a template')
}
@@ -369,7 +406,7 @@ export default class {
await this._app.checkFeatureAuthorization('RBAC')
const privilege = await this.getAclV2Privilege(id)
const role = await this.getAclV2Role(privilege.roleId)
const role = await this.#getRawAclV2Role(privilege.roleId)
if (!force && 'isTemplate' in role) {
throw forbiddenOperation('delete ACL V2 privilege', 'role is a template')
@@ -392,7 +429,7 @@ export default class {
await this._app.checkFeatureAuthorization('RBAC')
const privilege = await this.getAclV2Privilege(id)
const role = await this.getAclV2Role(privilege.roleId)
const role = await this.#getRawAclV2Role(privilege.roleId)
if ('isTemplate' in role) {
throw forbiddenOperation('update ACL V2 privilege', 'role is a template')
@@ -472,7 +509,7 @@ export default class {
throw objectAlreadyExists({ objectId: userRole.id, objectType: 'userRole' })
}
const role = await this.getAclV2Role(roleId)
const role = await this.#getRawAclV2Role(roleId)
if ('isTemplate' in role) {
throw forbiddenOperation('attach ACL V2 role to user', 'role is a template')
}
@@ -532,7 +569,7 @@ export default class {
throw objectAlreadyExists({ objectId: groupRole.id, objectType: 'groupRole' })
}
const role = await this.getAclV2Role(roleId)
const role = await this.#getRawAclV2Role(roleId)
if ('isTemplate' in role) {
throw forbiddenOperation('attach ACL V2 role to group', 'role is a template')
}
@@ -574,7 +611,7 @@ export default class {
async getAclV2RolePrivileges(roleId) {
await this._app.checkFeatureAuthorization('RBAC')
const role = await this.getAclV2Role(roleId)
const role = await this.#getRawAclV2Role(roleId)
return this.#privilegeDb._get({ roleId: role.id })
}
@@ -631,7 +668,7 @@ export default class {
async copyAclV2Role(roleId, params = {}) {
await this._app.checkFeatureAuthorization('RBAC')
const role = await this.getAclV2Role(roleId)
const role = await this.#getRawAclV2Role(roleId)
const privileges = await this.getAclV2RolePrivileges(roleId)
const replicaRole = await this.createAclV2Role({