@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.
- package/README.md +108 -1
- package/dist/abstract-classes/base-controller.mjs +28 -3
- package/dist/abstract-classes/listener.mjs +124 -15
- package/dist/acl-privileges/acl-privilege.controller.mjs +172 -0
- package/dist/acl-roles/acl-role.controller.mjs +384 -0
- package/dist/alarms/alarm.controller.mjs +25 -11
- package/dist/alarms/alarm.service.mjs +8 -0
- package/dist/backup-archives/backup-archive.controller.mjs +33 -23
- package/dist/backup-archives/backup-archive.service.mjs +21 -0
- package/dist/backup-jobs/backup-job.controller.mjs +74 -25
- package/dist/backup-jobs/backup-job.service.mjs +7 -0
- package/dist/backup-logs/backup-log.controller.mjs +28 -13
- package/dist/backup-logs/backup-log.service.mjs +19 -0
- package/dist/backup-repositories/backup-repositories.controller.mjs +24 -5
- package/dist/events/event.class.mjs +36 -18
- package/dist/events/event.controller.mjs +3 -0
- package/dist/events/event.service.mjs +4 -4
- package/dist/groups/group.controller.mjs +99 -12
- package/dist/helpers/markdown.helper.mjs +20 -0
- package/dist/helpers/object-wrapper.helper.mjs +3 -3
- package/dist/hosts/host.controller.mjs +90 -15
- package/dist/ioc/ioc.mjs +13 -4
- package/dist/messages/message.controller.mjs +32 -10
- package/dist/middlewares/acl.middleware.mjs +202 -0
- package/dist/middlewares/authentication.middleware.mjs +15 -6
- package/dist/middlewares/tsoa-to-xo-error.middleware.mjs +19 -1
- package/dist/networks/network.controller.mjs +72 -17
- package/dist/open-api/oa-examples/acl-privilege.oa-example.mjs +25 -0
- package/dist/open-api/oa-examples/acl-role.oa-example.mjs +22 -0
- package/dist/open-api/oa-examples/backup-archive.oa-example.mjs +6 -6
- package/dist/open-api/oa-examples/common.oa-example.mjs +3 -0
- package/dist/open-api/routes/routes.js +856 -172
- package/dist/pbds/pbd.controller.mjs +20 -5
- package/dist/pcis/pci.controller.mjs +19 -5
- package/dist/pgpus/pgpu.controller.mjs +19 -5
- package/dist/pifs/pif.controller.mjs +56 -16
- package/dist/pools/pool.controller.mjs +166 -17
- package/dist/proxies/proxy.controller.mjs +25 -6
- package/dist/restore-logs/restore-log.controller.mjs +42 -23
- package/dist/schedules/schedule.controller.mjs +36 -5
- package/dist/servers/server.controller.mjs +71 -9
- package/dist/sms/sm.controller.mjs +17 -4
- package/dist/srs/sr.controller.mjs +74 -18
- package/dist/tasks/task.controller.mjs +74 -13
- package/dist/users/user.controller.mjs +124 -22
- package/dist/vbds/vbd.controller.mjs +76 -38
- package/dist/vdi-snapshots/vdi-snapshot.controller.mjs +48 -14
- package/dist/vdis/vdi.controller.mjs +81 -16
- package/dist/vifs/vif.controller.mjs +118 -16
- package/dist/vm-controller/vm-controller.controller.mjs +77 -19
- package/dist/vm-snapshots/vm-snapshot.controller.mjs +85 -18
- package/dist/vm-templates/vm-template.controller.mjs +86 -18
- package/dist/vms/vm.controller.mjs +182 -24
- package/open-api/spec/swagger.json +12112 -3537
- 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
|
-
|
|
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
|
|
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 {
|
|
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
|
|
21
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
*
|
|
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
|
|
71
|
-
return this.sendObjects(Object.values(messages), req,
|
|
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
|
|
81
|
-
return this.sendObjects(Object.values(tasks), req,
|
|
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
|
|
3
|
-
'/rest/v0/backup-archives/1af95910-01b4-4e87-9c2f-d895cafe0776
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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',
|