diff --git a/@xen-orchestra/acl/src/actions/sr.mts b/@xen-orchestra/acl/src/actions/sr.mts index be96b32d21..be0e34abd2 100644 --- a/@xen-orchestra/acl/src/actions/sr.mts +++ b/@xen-orchestra/acl/src/actions/sr.mts @@ -5,6 +5,7 @@ export default { vdi: true, vm: true, }, + 'migrate-receive': true, read: true, 'reclaim-space': true, scan: true, diff --git a/@xen-orchestra/acl/src/actions/vdi.mts b/@xen-orchestra/acl/src/actions/vdi.mts index 2e29c0f30e..170235979d 100644 --- a/@xen-orchestra/acl/src/actions/vdi.mts +++ b/@xen-orchestra/acl/src/actions/vdi.mts @@ -4,6 +4,7 @@ export default { delete: true, 'export-content': true, 'import-content': true, + 'migrate-send': true, read: true, update: { tags: true, diff --git a/@xen-orchestra/rest-api/src/vdis/vdi.controller.mts b/@xen-orchestra/rest-api/src/vdis/vdi.controller.mts index 58227f1de4..acc2b1ab8c 100644 --- a/@xen-orchestra/rest-api/src/vdis/vdi.controller.mts +++ b/@xen-orchestra/rest-api/src/vdis/vdi.controller.mts @@ -213,13 +213,24 @@ export class VdiController extends XapiXoController { /** * Create an empty VDI. * + * Required privileges: + * - resource: sr, action: import:vdi (on the target SR) + * - resource: vdi, action: create + * * @example body { "srId": "c4284e12-37c9-7967-b9e8-83ef229c3e03", "virtual_size": 10737418240, "name_label": "test VDI" } */ @Example(vdiId) @Extension('x-mcp-exposure', 'confirm') @Post('') - @Middlewares(json()) + @Middlewares([ + json(), + acl([ + { resource: 'sr', action: 'import:vdi', objectId: 'body.srId' }, + { resource: 'vdi', action: 'create', object: ({ req }) => req.body }, + ]), + ]) @SuccessResponse(createdResp.status, createdResp.description) + @Response(forbiddenOperationResp.status, forbiddenOperationResp.description) @Response(notFoundResp.status, notFoundResp.description) @Response(internalServerErrorResp.status, internalServerErrorResp.description) async createVdi(@Body() body: CreateVdiBody): Promise<{ id: string }> { @@ -331,6 +342,10 @@ export class VdiController extends XapiXoController { /** * Migrate a VDI to another SR. * + * Required privileges: + * - resource: vdi, action: migrate-send + * - resource: sr, action: migrate-receive (on the target SR) + * * Note: After migration, the VDI will have a new ID. The new ID is returned in the response. * * @example id "c77f9955-c1d2-4b39-aa1c-73cdb2dacb7e" @@ -340,8 +355,17 @@ export class VdiController extends XapiXoController { @Example(vdiId) @Extension('x-mcp-exposure', 'confirm') @Post('{id}/actions/migrate') - @Middlewares(json()) + @Middlewares([ + json(), + // Two separate checks allow independent control: a user can be allowed to migrate a VDI away + // without being allowed to place VDIs on any specific SR, and vice versa. + acl([ + { resource: 'vdi', action: 'migrate-send', objectId: 'params.id' }, + { resource: 'sr', action: 'migrate-receive', objectId: 'body.srId' }, + ]), + ]) @SuccessResponse(asynchronousActionResp.status, asynchronousActionResp.description) + @Response(forbiddenOperationResp.status, forbiddenOperationResp.description) @Response(200, 'Ok') @Response(notFoundResp.status, notFoundResp.description) @Response(internalServerErrorResp.status, internalServerErrorResp.description) diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md index d2d9d31b2c..b97d81bc93 100644 --- a/CHANGELOG.unreleased.md +++ b/CHANGELOG.unreleased.md @@ -35,6 +35,8 @@ - `DELETE /vdi-snapshots/{id}` (PR [#9906](https://github.com/vatesfr/xen-orchestra/pull/9906)) - `PUT /vdi-snapshots/{id}/tags/{tag}` (PR [#9906](https://github.com/vatesfr/xen-orchestra/pull/9906)) - `DELETE /vdi-snapshots/{id}/tags/{tag}` (PR [#9906](https://github.com/vatesfr/xen-orchestra/pull/9906))/vdi-snapshots/{id}.{format}` (PR [#9906](https://github.com/vatesfr/xen-orchestra/pull/9906)) + - `POST /vdis` (PR [#9908](https://github.com/vatesfr/xen-orchestra/pull/9908)) + - `/vdis/:id/actions/migrate` (PR [#9908](https://github.com/vatesfr/xen-orchestra/pull/9908)) - [XO6] live update XO tasks (PR [#9901](https://github.com/vatesfr/xen-orchestra/pull/9901)) - [XO6/Backup] add progress for backups tasks(PR [#9901](https://github.com/vatesfr/xen-orchestra/pull/9901)) diff --git a/docs/docs/xo6/acl-v2.md b/docs/docs/xo6/acl-v2.md index da687839a5..c68ddebc46 100644 --- a/docs/docs/xo6/acl-v2.md +++ b/docs/docs/xo6/acl-v2.md @@ -110,14 +110,14 @@ Actions are written using the exact string you pass in a privilege. A parent act | `vm-snapshot` | `read`, `delete`, `export`, `update:tags` | | `vm-template` | `read`, `delete`, `export`, `instantiate`, `update:tags` | | `vm-controller` | `read`, `update:tags` | -| `vdi` | `read`, `create`, `delete`, `boot`, `export-content`, `import-content`, `update:tags` | +| `vdi` | `read`, `create`, `delete`, `boot`, `export-content`, `import-content`, `migrate-send`, `update:tags` | | `vdi-snapshot` | `read`, `delete`, `export`, `update:tags` | | `vdi-unmanaged` | `read` | | `vif` | `connect`, `create`, `delete`, `disconnect`,`read` | | `vbd` | `read`, `create`, `delete`, `connect`, `disconnect` | -| `sr` | `read`, `delete`, `forget`, `reclaim-space`, `scan`, `import:vdi`, `import:vm`, `update:tags` | -| `host` | `read`, `allow-vm`, `join-pool`, `export:logs`, `update:tags`, `disable`, `enable`, `evacuate` | -| `pool` | `add-host`, `read`, `emergency-shutdown`, `rolling-reboot`, `rolling-update`, `create:network`, `create:vm`, `update:tags` | +| `sr` | `read`, `delete`, `forget`, `migrate-receive`, `reclaim-space`, `scan`, `import:vdi`, `import:vm`, `update:tags` | +| `host` | `read`, `allow-vm`, `join-pool`, `export:logs`, `update:tags`, `disable`, `enable`, `evacuate` | +| `pool` | `add-host`, `read`, `emergency-shutdown`, `rolling-reboot`, `rolling-update`, `create:network`, `create:vm`, `update:tags` | | `network` | `read`, `create`, `delete`, `update:tags` | | `pif` | `read`, `update:management` | | `pbd` | `read`, `plug`, `unplug` | @@ -261,4 +261,4 @@ A selector is evaluated against each object's properties. If it matches, the pri - **Use groups.** Assigning a role to a group avoids repeating the same assignment for every user. - **Combine `allow` and `deny`.** When a user needs broad access with specific exceptions, grant with a wildcard privilege and carve out exceptions with `deny` + a selector. - **Template roles are immutable.** Copy them first before customizing. -- **Endpoint permissions** are visible in the Swagger UI (`/rest/v0/swagger`). Endpoints without a declared privilege require admin access. \ No newline at end of file +- **Endpoint permissions** are visible in the Swagger UI (`/rest/v0/swagger`). Endpoints without a declared privilege require admin access.