@xen-orchestra/rest-api 0.28.2 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +108 -1
  2. package/dist/abstract-classes/base-controller.mjs +28 -3
  3. package/dist/abstract-classes/listener.mjs +124 -15
  4. package/dist/acl-privileges/acl-privilege.controller.mjs +172 -0
  5. package/dist/acl-roles/acl-role.controller.mjs +384 -0
  6. package/dist/alarms/alarm.controller.mjs +25 -11
  7. package/dist/alarms/alarm.service.mjs +8 -0
  8. package/dist/backup-archives/backup-archive.controller.mjs +33 -23
  9. package/dist/backup-archives/backup-archive.service.mjs +21 -0
  10. package/dist/backup-jobs/backup-job.controller.mjs +74 -25
  11. package/dist/backup-jobs/backup-job.service.mjs +7 -0
  12. package/dist/backup-logs/backup-log.controller.mjs +28 -13
  13. package/dist/backup-logs/backup-log.service.mjs +19 -0
  14. package/dist/backup-repositories/backup-repositories.controller.mjs +24 -5
  15. package/dist/events/event.class.mjs +36 -18
  16. package/dist/events/event.controller.mjs +3 -0
  17. package/dist/events/event.service.mjs +4 -4
  18. package/dist/groups/group.controller.mjs +99 -12
  19. package/dist/helpers/markdown.helper.mjs +20 -0
  20. package/dist/helpers/object-wrapper.helper.mjs +3 -3
  21. package/dist/hosts/host.controller.mjs +90 -15
  22. package/dist/ioc/ioc.mjs +13 -4
  23. package/dist/messages/message.controller.mjs +32 -10
  24. package/dist/middlewares/acl.middleware.mjs +202 -0
  25. package/dist/middlewares/authentication.middleware.mjs +15 -6
  26. package/dist/middlewares/tsoa-to-xo-error.middleware.mjs +19 -1
  27. package/dist/networks/network.controller.mjs +72 -17
  28. package/dist/open-api/oa-examples/acl-privilege.oa-example.mjs +25 -0
  29. package/dist/open-api/oa-examples/acl-role.oa-example.mjs +22 -0
  30. package/dist/open-api/oa-examples/backup-archive.oa-example.mjs +6 -6
  31. package/dist/open-api/oa-examples/common.oa-example.mjs +3 -0
  32. package/dist/open-api/routes/routes.js +856 -172
  33. package/dist/pbds/pbd.controller.mjs +20 -5
  34. package/dist/pcis/pci.controller.mjs +19 -5
  35. package/dist/pgpus/pgpu.controller.mjs +19 -5
  36. package/dist/pifs/pif.controller.mjs +56 -16
  37. package/dist/pools/pool.controller.mjs +166 -17
  38. package/dist/proxies/proxy.controller.mjs +25 -6
  39. package/dist/restore-logs/restore-log.controller.mjs +42 -23
  40. package/dist/schedules/schedule.controller.mjs +36 -5
  41. package/dist/servers/server.controller.mjs +71 -9
  42. package/dist/sms/sm.controller.mjs +17 -4
  43. package/dist/srs/sr.controller.mjs +74 -18
  44. package/dist/tasks/task.controller.mjs +74 -13
  45. package/dist/users/user.controller.mjs +124 -22
  46. package/dist/vbds/vbd.controller.mjs +76 -38
  47. package/dist/vdi-snapshots/vdi-snapshot.controller.mjs +48 -14
  48. package/dist/vdis/vdi.controller.mjs +81 -16
  49. package/dist/vifs/vif.controller.mjs +118 -16
  50. package/dist/vm-controller/vm-controller.controller.mjs +77 -19
  51. package/dist/vm-snapshots/vm-snapshot.controller.mjs +85 -18
  52. package/dist/vm-templates/vm-template.controller.mjs +86 -18
  53. package/dist/vms/vm.controller.mjs +182 -24
  54. package/open-api/spec/swagger.json +12112 -3537
  55. package/package.json +12 -11
@@ -7,16 +7,24 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
7
7
  var __param = (this && this.__param) || function (paramIndex, decorator) {
8
8
  return function (target, key) { decorator(target, key, paramIndex); }
9
9
  };
10
- import { Example, Get, Path, Query, Request, Response, Route, Security, Tags } from 'tsoa';
10
+ import { Example, Get, Middlewares, Path, Query, Request, Response, Route, Security, Tags } from 'tsoa';
11
11
  import { inject } from 'inversify';
12
12
  import { noSuchObject } from 'xo-common/api-errors.js';
13
13
  import { provide } from 'inversify-binding-decorators';
14
+ import { acl } from '../middlewares/acl.middleware.mjs';
14
15
  import { alarmPredicate } from '../alarms/alarm.service.mjs';
15
16
  import { message, messageIds, partialMessages } from '../open-api/oa-examples/message.oa-example.mjs';
16
17
  import { RestApi } from '../rest-api/rest-api.mjs';
17
- import { badRequestResp, notFoundResp, unauthorizedResp } from '../open-api/common/response.common.mjs';
18
+ import { badRequestResp, forbiddenOperationResp, notFoundResp, unauthorizedResp, } from '../open-api/common/response.common.mjs';
18
19
  import { XapiXoController } from '../abstract-classes/xapi-xo-controller.mjs';
19
20
  import { safeParseComplexMatcher } from '../helpers/utils.helper.mjs';
21
+ function getMessage(restApi, id) {
22
+ const message = restApi.getObject(id, 'message');
23
+ if (alarmPredicate(message)) {
24
+ throw noSuchObject(id, 'message');
25
+ }
26
+ return message;
27
+ }
20
28
  let MessageController = class MessageController extends XapiXoController {
21
29
  constructor(restapi) {
22
30
  super('message', restapi);
@@ -36,21 +44,26 @@ let MessageController = class MessageController extends XapiXoController {
36
44
  * Override parent getObject in order to exclude`ALARM` message
37
45
  */
38
46
  getObject(id) {
39
- const message = super.getObject(id);
40
- if (alarmPredicate(message)) {
41
- throw noSuchObject(id, 'message');
42
- }
43
- return message;
47
+ return getMessage(this.restApi, id);
44
48
  }
45
49
  /**
50
+ * Returns all messages that match the following privilege:
51
+ * - resource: message, action: read
52
+ *
46
53
  * @example fields "name,body,id,$object"
47
54
  * @example filter "name:VM_STARTED"
48
55
  * @example limit 42
49
56
  */
50
- getMessages(req, fields, ndjson, filter, limit) {
51
- return this.sendObjects(Object.values(this.getObjects({ filter, limit })), req);
57
+ getMessages(req, fields, ndjson, markdown, filter, limit) {
58
+ return this.sendObjects(Object.values(this.getObjects({ filter })), req, {
59
+ limit,
60
+ privilege: { action: 'read', resource: 'message' },
61
+ });
52
62
  }
53
63
  /**
64
+ * Required privilege:
65
+ * - resource: message, action: read
66
+ *
54
67
  * @example id "f775eaeb-abe5-94e0-9682-14c37c3a1dfe"
55
68
  */
56
69
  getMessage(id) {
@@ -61,15 +74,24 @@ __decorate([
61
74
  Example(messageIds),
62
75
  Example(partialMessages),
63
76
  Get(''),
77
+ Security('*', ['acl']),
64
78
  __param(0, Request()),
65
79
  __param(1, Query()),
66
80
  __param(2, Query()),
67
81
  __param(3, Query()),
68
- __param(4, Query())
82
+ __param(4, Query()),
83
+ __param(5, Query())
69
84
  ], MessageController.prototype, "getMessages", null);
70
85
  __decorate([
71
86
  Example(message),
72
87
  Get('{id}'),
88
+ Middlewares(acl({
89
+ resource: 'message',
90
+ action: 'read',
91
+ objectId: 'params.id',
92
+ getObject: ({ restApi }) => id => getMessage(restApi, id),
93
+ })),
94
+ Response(forbiddenOperationResp.status, forbiddenOperationResp.description),
73
95
  Response(notFoundResp.status, notFoundResp.description),
74
96
  __param(0, Path())
75
97
  ], MessageController.prototype, "getMessage", null);
@@ -0,0 +1,202 @@
1
+ import { getMissingPrivileges, } from '@xen-orchestra/acl';
2
+ import { RestApi } from '../rest-api/rest-api.mjs';
3
+ import { iocContainer } from '../ioc/ioc.mjs';
4
+ import { ValidateError } from 'tsoa';
5
+ import { ApiError } from '../helpers/error.helper.mjs';
6
+ export const ACL_MIDDLEWARE_NAME = '_aclMiddleware';
7
+ export function actionsFromBody(actions) {
8
+ return ({ req }) => actions.filter(action => {
9
+ const [, field] = action.split(':');
10
+ return field in req.body;
11
+ });
12
+ }
13
+ export function actionFromBody(action) {
14
+ return opts => actionsFromBody([action])(opts)[0];
15
+ }
16
+ export function actionsIfNotSelfUser(actions) {
17
+ return ({ req, restApi }) => {
18
+ const currentUser = restApi.getCurrentUser();
19
+ const userId = req.params.id;
20
+ if (currentUser.id !== userId) {
21
+ return actions;
22
+ }
23
+ return [];
24
+ };
25
+ }
26
+ export function actionIfNotSelfUser(action) {
27
+ return opts => actionsIfNotSelfUser([action])(opts)[0];
28
+ }
29
+ export function autoBindService(serviceId, method) {
30
+ return ({ restApi }) => {
31
+ const service = restApi.ioc.get(serviceId);
32
+ const fn = service[method];
33
+ if (typeof fn !== 'function') {
34
+ throw new Error(`Invalid method: ${String(method)}`);
35
+ }
36
+ return fn.bind(service);
37
+ };
38
+ }
39
+ export const XAPI_TYPE_BY_ACL_RESOURCE = {
40
+ message: 'message',
41
+ gpuGroup: 'gpuGroup',
42
+ host: 'host',
43
+ network: 'network',
44
+ pbd: 'PBD',
45
+ pci: 'PCI',
46
+ pgpu: 'PGPU',
47
+ pif: 'PIF',
48
+ pool: 'pool',
49
+ sr: 'SR',
50
+ vbd: 'VBD',
51
+ vdi: 'VDI',
52
+ 'vdi-snapshot': 'VDI-snapshot',
53
+ 'vdi-unmanaged': 'VDI-unmanaged',
54
+ vgpu: 'vgpu',
55
+ vgpuType: 'vgpuType',
56
+ vif: 'VIF',
57
+ vm: 'VM',
58
+ 'vm-controller': 'VM-controller',
59
+ 'vm-snapshot': 'VM-snapshot',
60
+ 'vm-template': 'VM-template',
61
+ vtpm: 'VTPM',
62
+ sm: 'SM',
63
+ };
64
+ function normalizeAclEntry(acl) {
65
+ const aclAction = acl.action;
66
+ const aclActions = 'actions' in acl && acl.actions !== undefined
67
+ ? acl.actions
68
+ : typeof aclAction === 'function'
69
+ ? (opts) => [aclAction(opts)]
70
+ : [aclAction];
71
+ const actionsResolver = typeof aclActions === 'function'
72
+ ? (req, restApi) => aclActions({ req, restApi })
73
+ : () => aclActions;
74
+ const base = { resource: acl.resource, actionsResolver, getObject: acl.getObject };
75
+ if ('objects' in acl && acl.objects !== undefined) {
76
+ return { ...base, objects: acl.objects };
77
+ }
78
+ if ('object' in acl && acl.object !== undefined) {
79
+ let objects;
80
+ if (typeof acl.object === 'function') {
81
+ const fn = acl.object;
82
+ objects = async (opts) => [await fn(opts)];
83
+ }
84
+ else {
85
+ objects = [acl.object];
86
+ }
87
+ return { ...base, objects };
88
+ }
89
+ if ('objectIds' in acl && acl.objectIds !== undefined) {
90
+ return { ...base, objectIds: acl.objectIds };
91
+ }
92
+ if ('objectId' in acl && acl.objectId !== undefined) {
93
+ let objectIds;
94
+ if (typeof acl.objectId === 'function') {
95
+ const fn = acl.objectId;
96
+ objectIds = (opts) => [fn(opts)];
97
+ }
98
+ else {
99
+ objectIds = [acl.objectId];
100
+ }
101
+ return { ...base, objectIds };
102
+ }
103
+ throw new Error('invalid ACL entry');
104
+ }
105
+ export function acl(acls) {
106
+ acls = Array.isArray(acls) ? acls : [acls];
107
+ const _acls = acls.map(normalizeAclEntry);
108
+ async function middleware(req, res, next) {
109
+ const restApi = iocContainer.get(RestApi);
110
+ const user = restApi.getCurrentUser();
111
+ const invalidFields = {};
112
+ const missingPrivilegeParams = [];
113
+ for (const acl of _acls) {
114
+ let objects = [];
115
+ if ('objectIds' in acl) {
116
+ let ids = [];
117
+ if (typeof acl.objectIds === 'function') {
118
+ try {
119
+ ids = acl.objectIds({ req, restApi }).map(id => ({ id }));
120
+ }
121
+ catch (error) {
122
+ return next(error);
123
+ }
124
+ }
125
+ else {
126
+ for (const path of acl.objectIds) {
127
+ const id = path.split('.').reduce((obj, part) => obj?.[part], req);
128
+ ids.push({ id, path });
129
+ }
130
+ }
131
+ for (const { id, path } of ids) {
132
+ if (id === undefined) {
133
+ continue;
134
+ }
135
+ if (typeof id !== 'string') {
136
+ invalidFields[path ?? 'unknown'] = {
137
+ message: 'invalid string value',
138
+ value: id,
139
+ };
140
+ continue;
141
+ }
142
+ if (Object.keys(invalidFields).length > 0) {
143
+ // No need to get objects if we already known some params are invalid
144
+ continue;
145
+ }
146
+ try {
147
+ const object = (await acl.getObject?.({ restApi })(id)) ??
148
+ restApi.getObject(id, XAPI_TYPE_BY_ACL_RESOURCE[acl.resource]);
149
+ objects.push(object);
150
+ }
151
+ catch (error) {
152
+ return next(error);
153
+ }
154
+ }
155
+ }
156
+ if ('objects' in acl) {
157
+ if (typeof acl.objects === 'function') {
158
+ const _objects = await acl.objects({ req, restApi });
159
+ if (_objects !== undefined) {
160
+ objects = _objects;
161
+ }
162
+ }
163
+ else {
164
+ objects = acl.objects;
165
+ }
166
+ }
167
+ // We cast here to restore the discriminated union correlation between `resource` and `action`.
168
+ // When rebuilding an object from individual properties of a discriminated union, TypeScript transform it into an union type
169
+ // { resource: SupportedResource, action: SupportedAction<SupportedResource> }
170
+ // This loses the original correlation (e.g. it would allow invalid pairs like { resource: 'vgpu', action: 'snapshot' }).
171
+ // The `as` cast re-asserts the discriminated union member type.
172
+ for (const action of acl.actionsResolver(req, restApi)) {
173
+ if (action === undefined) {
174
+ // action can be undefined, if the action is created from `actionFromBody` or `actionIfNotSelfUser`
175
+ continue;
176
+ }
177
+ missingPrivilegeParams.push({ action, resource: acl.resource, objects, user });
178
+ }
179
+ }
180
+ if (Object.keys(invalidFields).length > 0) {
181
+ return next(new ValidateError(invalidFields, 'invalid parameters'));
182
+ }
183
+ let userPrivileges;
184
+ try {
185
+ userPrivileges = (await restApi.xoApp.getAclV2UserPrivileges(user.id));
186
+ }
187
+ catch (error) {
188
+ return next(error);
189
+ }
190
+ const missingPrivileges = getMissingPrivileges(missingPrivilegeParams, userPrivileges);
191
+ if (missingPrivileges.length > 0) {
192
+ return next(new ApiError('not enough privileges', 403, {
193
+ data: {
194
+ missingPrivileges,
195
+ },
196
+ }));
197
+ }
198
+ next();
199
+ }
200
+ Object.defineProperty(middleware, 'name', { value: ACL_MIDDLEWARE_NAME });
201
+ return middleware;
202
+ }
@@ -1,13 +1,12 @@
1
1
  import { createLogger } from '@xen-orchestra/log';
2
2
  import { unauthorized } from 'xo-common/api-errors.js';
3
+ import { ApiError } from '../helpers/error.helper.mjs';
3
4
  import { iocContainer } from '../ioc/ioc.mjs';
4
5
  import { RestApi } from '../rest-api/rest-api.mjs';
5
- import { ApiError } from '../helpers/error.helper.mjs';
6
+ import { ACL_MIDDLEWARE_NAME } from './acl.middleware.mjs';
6
7
  const log = createLogger('xo:rest-api:authentication');
7
- // TODO: correctly handle ACL/Resource set users
8
- // for now only support "xoa-admin"
9
8
  // TSOA spec require this function to be async
10
- export async function expressAuthentication(req, securityName) {
9
+ export async function expressAuthentication(req, securityName, scopes) {
11
10
  if (securityName === 'none') {
12
11
  return undefined;
13
12
  }
@@ -17,8 +16,18 @@ export async function expressAuthentication(req, securityName) {
17
16
  if (securityName !== '*' && authType !== securityName) {
18
17
  throw new ApiError(`invalid authentification. please use ${securityName} authentication`, 401);
19
18
  }
20
- if (user.permission !== 'admin') {
21
- log.error(`The REST API can only be used by 'xoa-admin' users for now. Your permission: ${user.permission}`);
19
+ if (user.permission === 'admin') {
20
+ return user;
21
+ }
22
+ // This means the route requires authentication, but it doesn't need to be associated with a specific ACL.
23
+ // The route's content will depend on the users' ACLs.
24
+ // (For example: GET /vms -> empty array if no VMs ACL; GET /events -> only changes related to the users' ACLs will be sent)
25
+ if (scopes.includes('acl')) {
26
+ return user;
27
+ }
28
+ const aclMiddleware = req.route.stack.find(layer => layer.name === ACL_MIDDLEWARE_NAME);
29
+ if (aclMiddleware === undefined) {
30
+ log.error(`${req.route.path} can only be used by an administrator`);
22
31
  throw unauthorized();
23
32
  }
24
33
  return user;
@@ -2,7 +2,25 @@ import { invalidParameters } from 'xo-common/api-errors.js';
2
2
  import { ValidateError } from 'tsoa';
3
3
  export default function tsoaToXoErrorHandler(error, _req, _res, next) {
4
4
  if (error instanceof ValidateError) {
5
- throw invalidParameters(error.fields);
5
+ const fields = simplifyUnionValidationErrors(error.fields);
6
+ throw invalidParameters(fields);
6
7
  }
7
8
  return next(error);
8
9
  }
10
+ function simplifyUnionValidationErrors(fields) {
11
+ const result = {};
12
+ for (const [key, field] of Object.entries(fields)) {
13
+ if (isUnionMismatchError(field)) {
14
+ result[key] = {
15
+ ...field,
16
+ message: 'Value does not match any allowed schema',
17
+ };
18
+ continue;
19
+ }
20
+ result[key] = field;
21
+ }
22
+ return result;
23
+ }
24
+ function isUnionMismatchError(field) {
25
+ return field.message.startsWith('Could not match the union against any of the items');
26
+ }
@@ -7,18 +7,19 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
7
7
  var __param = (this && this.__param) || function (paramIndex, decorator) {
8
8
  return function (target, key) { decorator(target, key, paramIndex); }
9
9
  };
10
- import { Example, Get, Path, Query, Response, Request, Route, Security, Tags, Delete, SuccessResponse, Put } from 'tsoa';
10
+ import { Example, Get, Path, Query, Response, Request, Route, Security, Tags, Delete, SuccessResponse, Put, Middlewares, } from 'tsoa';
11
11
  import { inject } from 'inversify';
12
12
  import { provide } from 'inversify-binding-decorators';
13
13
  import { AlarmService } from '../alarms/alarm.service.mjs';
14
14
  import { escapeUnsafeComplexMatcher } from '../helpers/utils.helper.mjs';
15
15
  import { genericAlarmsExample } from '../open-api/oa-examples/alarm.oa-example.mjs';
16
16
  import { network, networkIds, partialNetworks } from '../open-api/oa-examples/network.oa-example.mjs';
17
- import { badRequestResp, noContentResp, notFoundResp, unauthorizedResp, } from '../open-api/common/response.common.mjs';
17
+ import { badRequestResp, forbiddenOperationResp, noContentResp, notFoundResp, unauthorizedResp, } from '../open-api/common/response.common.mjs';
18
18
  import { RestApi } from '../rest-api/rest-api.mjs';
19
19
  import { XapiXoController } from '../abstract-classes/xapi-xo-controller.mjs';
20
20
  import { messageIds, partialMessages } from '../open-api/oa-examples/message.oa-example.mjs';
21
21
  import { partialTasks, taskIds } from '../open-api/oa-examples/task.oa-example.mjs';
22
+ import { acl } from '../middlewares/acl.middleware.mjs';
22
23
  let NetworkController = class NetworkController extends XapiXoController {
23
24
  #alarmService;
24
25
  constructor(restApi, alarmService) {
@@ -26,61 +27,96 @@ let NetworkController = class NetworkController extends XapiXoController {
26
27
  this.#alarmService = alarmService;
27
28
  }
28
29
  /**
30
+ * Returns all networks that match the following privilege:
31
+ * - resource: network, action: read
32
+ *
29
33
  * @example fields "nbd,name_label,id"
30
34
  * @example filter "nbd?"
31
35
  * @example limit 42
32
36
  */
33
- getNetworks(req, fields, ndjson, filter, limit) {
34
- return this.sendObjects(Object.values(this.getObjects({ filter, limit })), req);
37
+ getNetworks(req, fields, ndjson, markdown, filter, limit) {
38
+ return this.sendObjects(Object.values(this.getObjects({ filter })), req, {
39
+ limit,
40
+ privilege: { action: 'read', resource: 'network' },
41
+ });
35
42
  }
36
43
  /**
44
+ * Required privilege:
45
+ * - resource: network, action: read
46
+ *
37
47
  * @example id "9fe12ca3-d75d-cfb0-492e-cfd2bc6c568f"
38
48
  */
39
49
  getNetwork(id) {
40
50
  return this.getObject(id);
41
51
  }
42
52
  /**
43
- * @example id "593c39a5-9c56-28eb-969b-255b2f53791b"
53
+ * Required privilege:
54
+ * - resource: network, action: delete
55
+ *
56
+ * @example id "9fe12ca3-d75d-cfb0-492e-cfd2bc6c568f"
44
57
  */
45
58
  async deleteNetwork(id) {
46
59
  const networkId = id;
47
60
  await this.getXapiObject(networkId).$xapi.deleteNetwork(networkId);
48
61
  }
49
62
  /**
63
+ * Returns all alarms that match the following privilege:
64
+ * - resource: alarm, action: read
65
+ *
50
66
  * @example id "9fe12ca3-d75d-cfb0-492e-cfd2bc6c568f"
51
67
  * @example fields "id,time"
52
68
  * @example filter "time:>1747053793"
53
69
  * @example limit 42
54
70
  */
55
- getNetworkAlarms(req, id, fields, ndjson, filter, limit) {
71
+ getNetworkAlarms(req, id, fields, ndjson, markdown, filter, limit) {
56
72
  const network = this.getObject(id);
57
73
  const alarms = this.#alarmService.getAlarms({
58
74
  filter: `${escapeUnsafeComplexMatcher(filter) ?? ''} object:uuid:${network.uuid}`,
75
+ });
76
+ return this.sendObjects(Object.values(alarms), req, {
77
+ path: 'alarms',
59
78
  limit,
79
+ privilege: { action: 'read', resource: 'alarm' },
60
80
  });
61
- return this.sendObjects(Object.values(alarms), req, 'alarms');
62
81
  }
63
82
  /**
83
+ * Returns all messages that match the following privilege:
84
+ * - resource: message, action: read
85
+ *
64
86
  * @example id "9fe12ca3-d75d-cfb0-492e-cfd2bc6c568f"
65
87
  * @example fields "name,id,$object"
66
88
  * @example filter "name:VM_STARTED"
67
89
  * @example limit 42
68
90
  */
69
- getNetworkMessages(req, id, fields, ndjson, filter, limit) {
70
- const messages = this.getMessagesForObject(id, { filter, limit });
71
- return this.sendObjects(Object.values(messages), req, 'messages');
91
+ getNetworkMessages(req, id, fields, ndjson, markdown, filter, limit) {
92
+ const messages = this.getMessagesForObject(id, { filter });
93
+ return this.sendObjects(Object.values(messages), req, {
94
+ path: 'messages',
95
+ limit,
96
+ privilege: { action: 'read', resource: 'message' },
97
+ });
72
98
  }
73
99
  /**
100
+ * Returns all tasks that match the following privilege:
101
+ * - resource: task, action: read
102
+ *
74
103
  * @example id "9fe12ca3-d75d-cfb0-492e-cfd2bc6c568f"
75
104
  * @example fields "id,status,properties"
76
105
  * @example filter "status:failure"
77
106
  * @example limit 42
78
107
  */
79
- async getNetworkTasks(req, id, fields, ndjson, filter, limit) {
80
- const tasks = await this.getTasksForObject(id, { filter, limit });
81
- return this.sendObjects(Object.values(tasks), req, 'tasks');
108
+ async getNetworkTasks(req, id, fields, ndjson, markdown, filter, limit) {
109
+ const tasks = await this.getTasksForObject(id, { filter });
110
+ return this.sendObjects(Object.values(tasks), req, {
111
+ path: 'tasks',
112
+ limit,
113
+ privilege: { action: 'read', resource: 'task' },
114
+ });
82
115
  }
83
116
  /**
117
+ * Required privilege:
118
+ * - resource: network, action: update:tags
119
+ *
84
120
  * @example id "9fe12ca3-d75d-cfb0-492e-cfd2bc6c568f"
85
121
  * @example tag "from-rest-api"
86
122
  */
@@ -89,6 +125,9 @@ let NetworkController = class NetworkController extends XapiXoController {
89
125
  await network.$call('add_tags', tag);
90
126
  }
91
127
  /**
128
+ * Required privilege:
129
+ * - resource: network, action: update:tags
130
+ *
92
131
  * @example id "9fe12ca3-d75d-cfb0-492e-cfd2bc6c568f"
93
132
  * @example tag "from-rest-api"
94
133
  */
@@ -101,20 +140,26 @@ __decorate([
101
140
  Example(networkIds),
102
141
  Example(partialNetworks),
103
142
  Get(''),
143
+ Security('*', ['acl']),
104
144
  __param(0, Request()),
105
145
  __param(1, Query()),
106
146
  __param(2, Query()),
107
147
  __param(3, Query()),
108
- __param(4, Query())
148
+ __param(4, Query()),
149
+ __param(5, Query())
109
150
  ], NetworkController.prototype, "getNetworks", null);
110
151
  __decorate([
111
152
  Example(network),
112
153
  Get('{id}'),
154
+ Middlewares(acl({ resource: 'network', action: 'read', objectId: 'params.id' })),
155
+ Response(forbiddenOperationResp.status, forbiddenOperationResp.description),
113
156
  Response(notFoundResp.status, notFoundResp.description),
114
157
  __param(0, Path())
115
158
  ], NetworkController.prototype, "getNetwork", null);
116
159
  __decorate([
117
160
  Delete('{id}'),
161
+ Middlewares(acl({ resource: 'network', action: 'delete', objectId: 'params.id' })),
162
+ Response(forbiddenOperationResp.status, forbiddenOperationResp.description),
118
163
  SuccessResponse(noContentResp.status, noContentResp.description),
119
164
  Response(notFoundResp.status, notFoundResp.description),
120
165
  __param(0, Path())
@@ -122,6 +167,7 @@ __decorate([
122
167
  __decorate([
123
168
  Example(genericAlarmsExample),
124
169
  Get('{id}/alarms'),
170
+ Security('*', ['acl']),
125
171
  Tags('alarms'),
126
172
  Response(notFoundResp.status, notFoundResp.description),
127
173
  __param(0, Request()),
@@ -129,12 +175,14 @@ __decorate([
129
175
  __param(2, Query()),
130
176
  __param(3, Query()),
131
177
  __param(4, Query()),
132
- __param(5, Query())
178
+ __param(5, Query()),
179
+ __param(6, Query())
133
180
  ], NetworkController.prototype, "getNetworkAlarms", null);
134
181
  __decorate([
135
182
  Example(messageIds),
136
183
  Example(partialMessages),
137
184
  Get('{id}/messages'),
185
+ Security('*', ['acl']),
138
186
  Tags('messages'),
139
187
  Response(notFoundResp.status, notFoundResp.description),
140
188
  __param(0, Request()),
@@ -142,12 +190,14 @@ __decorate([
142
190
  __param(2, Query()),
143
191
  __param(3, Query()),
144
192
  __param(4, Query()),
145
- __param(5, Query())
193
+ __param(5, Query()),
194
+ __param(6, Query())
146
195
  ], NetworkController.prototype, "getNetworkMessages", null);
147
196
  __decorate([
148
197
  Example(taskIds),
149
198
  Example(partialTasks),
150
199
  Get('{id}/tasks'),
200
+ Security('*', ['acl']),
151
201
  Tags('tasks'),
152
202
  Response(notFoundResp.status, notFoundResp.description),
153
203
  __param(0, Request()),
@@ -155,18 +205,23 @@ __decorate([
155
205
  __param(2, Query()),
156
206
  __param(3, Query()),
157
207
  __param(4, Query()),
158
- __param(5, Query())
208
+ __param(5, Query()),
209
+ __param(6, Query())
159
210
  ], NetworkController.prototype, "getNetworkTasks", null);
160
211
  __decorate([
161
212
  Put('{id}/tags/{tag}'),
213
+ Middlewares(acl({ resource: 'network', action: 'update:tags', objectId: 'params.id' })),
162
214
  SuccessResponse(noContentResp.status, noContentResp.description),
215
+ Response(forbiddenOperationResp.status, forbiddenOperationResp.description),
163
216
  Response(notFoundResp.status, notFoundResp.description),
164
217
  __param(0, Path()),
165
218
  __param(1, Path())
166
219
  ], NetworkController.prototype, "putNetworkTag", null);
167
220
  __decorate([
168
221
  Delete('{id}/tags/{tag}'),
222
+ Middlewares(acl({ resource: 'network', action: 'update:tags', objectId: 'params.id' })),
169
223
  SuccessResponse(noContentResp.status, noContentResp.description),
224
+ Response(forbiddenOperationResp.status, forbiddenOperationResp.description),
170
225
  Response(notFoundResp.status, notFoundResp.description),
171
226
  __param(0, Path()),
172
227
  __param(1, Path())
@@ -0,0 +1,25 @@
1
+ export const aclPrivilegeIds = [
2
+ '/rest/v0/acl-privileges/6d16ac11-ad48-483a-a926-3d9655648b28',
3
+ '/rest/v0/acl-privileges/7a81ac36-264e-468e-ad4d-63d9a10aee6a',
4
+ ];
5
+ export const partialAclPrivileges = [
6
+ {
7
+ id: '6d16ac11-ad48-483a-a926-3d9655648b28',
8
+ action: 'create',
9
+ resource: 'vif',
10
+ href: '/rest/v0/acl-privileges/6d16ac11-ad48-483a-a926-3d9655648b28',
11
+ },
12
+ {
13
+ id: '7a81ac36-264e-468e-ad4d-63d9a10aee6a',
14
+ action: 'create',
15
+ resource: 'vdi',
16
+ href: '/rest/v0/acl-privileges/7a81ac36-264e-468e-ad4d-63d9a10aee6a',
17
+ },
18
+ ];
19
+ export const aclPrivilege = {
20
+ action: 'create',
21
+ effect: 'allow',
22
+ resource: 'vif',
23
+ roleId: 'ccaab6e6-bae6-4d9c-9866-911dfc88bdc6',
24
+ id: '6d16ac11-ad48-483a-a926-3d9655648b28',
25
+ };
@@ -0,0 +1,22 @@
1
+ export const aclRoleIds = [
2
+ '/rest/v0/acl-roles/9cac02e2-f612-4b71-851e-d28b9c0cda88',
3
+ '/rest/v0/acl-roles/784bd959-08de-4b26-b575-92ded5aef872',
4
+ ];
5
+ export const partialAclRoles = [
6
+ {
7
+ id: '9cac02e2-f612-4b71-851e-d28b9c0cda88',
8
+ name: 'VMs read only',
9
+ isTemplate: true,
10
+ href: '/rest/v0/acl-roles/9cac02e2-f612-4b71-851e-d28b9c0cda88',
11
+ },
12
+ {
13
+ id: '784bd959-08de-4b26-b575-92ded5aef872',
14
+ name: 'mra-test-read-only',
15
+ href: '/rest/v0/acl-roles/784bd959-08de-4b26-b575-92ded5aef872',
16
+ },
17
+ ];
18
+ export const aclRole = {
19
+ name: 'mra-test-read-only',
20
+ description: 'Access the whole infra in read-only mode',
21
+ id: '784bd959-08de-4b26-b575-92ded5aef872',
22
+ };
@@ -1,10 +1,10 @@
1
1
  export const backupArchiveIds = [
2
- '/rest/v0/backup-archives/231264c3-af43-4ec0-a3be-394c5b1fdbfc//xo-vm-backups/6ef7c09e-677b-1e6f-0546-7ab30413c61c/20250801T080832Z.json',
3
- '/rest/v0/backup-archives/1af95910-01b4-4e87-9c2f-d895cafe0776//xo-vm-backups/7cf6150f-a978-09e6-6b41-0d1d41967bdc/20250918T132942Z.json',
2
+ '/rest/v0/backup-archives/231264c3-af43-4ec0-a3be-394c5b1fdbfc/xo-vm-backups/6ef7c09e-677b-1e6f-0546-7ab30413c61c/20250801T080832Z.json',
3
+ '/rest/v0/backup-archives/1af95910-01b4-4e87-9c2f-d895cafe0776/xo-vm-backups/7cf6150f-a978-09e6-6b41-0d1d41967bdc/20250918T132942Z.json',
4
4
  ];
5
5
  export const partialBackupArchives = [
6
6
  {
7
- id: '231264c3-af43-4ec0-a3be-394c5b1fdbfc//xo-vm-backups/6ef7c09e-677b-1e6f-0546-7ab30413c61c/20250801T080832Z.json',
7
+ id: '231264c3-af43-4ec0-a3be-394c5b1fdbfc/xo-vm-backups/6ef7c09e-677b-1e6f-0546-7ab30413c61c/20250801T080832Z.json',
8
8
  backupRepository: '231264c3-af43-4ec0-a3be-394c5b1fdbfc',
9
9
  disks: [
10
10
  {
@@ -21,7 +21,7 @@ export const partialBackupArchives = [
21
21
  href: '/rest/v0/backup-archives/231264c3-af43-4ec0-a3be-394c5b1fdbfc/xo-vm-backups/6ef7c09e-677b-1e6f-0546-7ab30413c61c/20250801T080832Z.json',
22
22
  },
23
23
  {
24
- id: '1af95910-01b4-4e87-9c2f-d895cafe0776//xo-vm-backups/7cf6150f-a978-09e6-6b41-0d1d41967bdc/20250918T132942Z.json',
24
+ id: '1af95910-01b4-4e87-9c2f-d895cafe0776/xo-vm-backups/7cf6150f-a978-09e6-6b41-0d1d41967bdc/20250918T132942Z.json',
25
25
  backupRepository: '1af95910-01b4-4e87-9c2f-d895cafe0776',
26
26
  disks: [
27
27
  {
@@ -30,7 +30,7 @@ export const partialBackupArchives = [
30
30
  uuid: '73ed06ed-fdc8-43ef-a1c4-253e9005fbe0',
31
31
  },
32
32
  ],
33
- href: '/rest/v0/backup-archives/1af95910-01b4-4e87-9c2f-d895cafe0776//xo-vm-backups/7cf6150f-a978-09e6-6b41-0d1d41967bdc/20250918T132942Z.json',
33
+ href: '/rest/v0/backup-archives/1af95910-01b4-4e87-9c2f-d895cafe0776/xo-vm-backups/7cf6150f-a978-09e6-6b41-0d1d41967bdc/20250918T132942Z.json',
34
34
  },
35
35
  ];
36
36
  export const backupArchive = {
@@ -43,7 +43,7 @@ export const backupArchive = {
43
43
  uuid: 'c922ef3c-9d76-4482-87f8-a4da5849ee45',
44
44
  },
45
45
  ],
46
- id: '1af95910-01b4-4e87-9c2f-d895cafe0776//xo-vm-backups/7cf6150f-a978-09e6-6b41-0d1d41967bdc/20250918T132942Z.json',
46
+ id: '1af95910-01b4-4e87-9c2f-d895cafe0776/xo-vm-backups/7cf6150f-a978-09e6-6b41-0d1d41967bdc/20250918T132942Z.json',
47
47
  jobId: 'f2599aa4-7bb4-434b-bf71-cf0ebe1e06a4',
48
48
  mode: 'delta',
49
49
  scheduleId: '8db1c2da-2635-436f-8f78-62079fea3aa6',