@xen-orchestra/rest-api 0.31.1 → 0.33.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 +4 -4
- package/dist/acl-privileges/acl-privilege.controller.mjs +7 -2
- package/dist/acl-roles/acl-role.controller.mjs +13 -2
- package/dist/alarms/alarm.controller.mjs +3 -1
- package/dist/backup-archives/backup-archive.controller.mjs +3 -1
- package/dist/backup-jobs/backup-job.controller.mjs +12 -1
- package/dist/backup-logs/backup-log.controller.mjs +3 -1
- package/dist/backup-repositories/backup-repositories.controller.mjs +3 -1
- package/dist/events/event.controller.mjs +4 -1
- package/dist/groups/group.controller.mjs +10 -1
- package/dist/hosts/host.controller.mjs +16 -1
- package/dist/index.mjs +2 -0
- package/dist/mcp/mcp.controller.mjs +59 -0
- package/dist/mcp/mcp.helper.mjs +11 -0
- package/dist/messages/message.controller.mjs +3 -1
- package/dist/middlewares/mcp-gate.middleware.mjs +30 -0
- package/dist/networks/network.controller.mjs +9 -1
- package/dist/open-api/routes/routes.js +118 -5
- package/dist/pbds/pbd.controller.mjs +5 -1
- package/dist/pcis/pci.controller.mjs +3 -1
- package/dist/pgpus/pgpu.controller.mjs +3 -1
- package/dist/pifs/pif.controller.mjs +6 -1
- package/dist/pools/pool.controller.mjs +20 -1
- package/dist/proxies/proxy.controller.mjs +3 -1
- package/dist/restore-logs/restore-log.controller.mjs +5 -1
- package/dist/schedules/schedule.controller.mjs +4 -1
- package/dist/servers/server.controller.mjs +8 -1
- package/dist/sms/sm.controller.mjs +3 -1
- package/dist/srs/sr.controller.mjs +13 -1
- package/dist/tasks/task.controller.mjs +6 -1
- package/dist/users/user.controller.mjs +13 -2
- package/dist/vbds/vbd.controller.mjs +10 -1
- package/dist/vdi-snapshots/vdi-snapshot.controller.mjs +10 -1
- package/dist/vdis/vdi.controller.mjs +13 -1
- package/dist/vifs/vif.controller.mjs +10 -1
- package/dist/vm-controller/vm-controller.controller.mjs +9 -1
- package/dist/vm-snapshots/vm-snapshot.controller.mjs +11 -1
- package/dist/vm-templates/vm-template.controller.mjs +11 -1
- package/dist/vms/vm.controller.mjs +77 -2
- package/dist/vms/vm.service.mjs +17 -0
- package/dist/xoa/xoa.controller.mjs +4 -1
- package/eslint-rules/index.cjs +7 -0
- package/eslint-rules/require-mcp-expose.cjs +129 -0
- package/open-api/spec/swagger.json +1764 -495
- package/package.json +3 -3
- package/tsoa.json +2 -1
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
8
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
9
|
+
};
|
|
10
|
+
import { Controller, Example, Get, Response, Route, Security, SuccessResponse, Tags } from 'tsoa';
|
|
11
|
+
import { inject } from 'inversify';
|
|
12
|
+
import { provide } from 'inversify-binding-decorators';
|
|
13
|
+
import { ApiError } from '../helpers/error.helper.mjs';
|
|
14
|
+
import { isMcpEnabled, MCP_DISABLED_ERROR } from './mcp.helper.mjs';
|
|
15
|
+
import { RestApi } from '../rest-api/rest-api.mjs';
|
|
16
|
+
const ENABLED_RESPONSE = {
|
|
17
|
+
status: 200,
|
|
18
|
+
description: 'MCP is enabled',
|
|
19
|
+
};
|
|
20
|
+
const DISABLED_RESPONSE = {
|
|
21
|
+
status: 503,
|
|
22
|
+
description: 'MCP is disabled by administrator',
|
|
23
|
+
};
|
|
24
|
+
let McpController = class McpController extends Controller {
|
|
25
|
+
#restApi;
|
|
26
|
+
constructor(restApi) {
|
|
27
|
+
super();
|
|
28
|
+
this.#restApi = restApi;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Returns whether MCP is currently enabled on this XO server.
|
|
32
|
+
*
|
|
33
|
+
* The route is publicly reachable (no authentication required) so the
|
|
34
|
+
* `@xen-orchestra/mcp` binary can check the kill-switch at startup,
|
|
35
|
+
* before any credentials have been configured.
|
|
36
|
+
*/
|
|
37
|
+
getMcpStatus() {
|
|
38
|
+
if (!isMcpEnabled(this.#restApi)) {
|
|
39
|
+
throw new ApiError(DISABLED_RESPONSE.description, DISABLED_RESPONSE.status, {
|
|
40
|
+
data: { error: MCP_DISABLED_ERROR },
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return { enabled: true };
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
__decorate([
|
|
47
|
+
Security('none'),
|
|
48
|
+
Example({ enabled: true }),
|
|
49
|
+
Get('status'),
|
|
50
|
+
SuccessResponse(ENABLED_RESPONSE.status, ENABLED_RESPONSE.description),
|
|
51
|
+
Response(DISABLED_RESPONSE.status, DISABLED_RESPONSE.description)
|
|
52
|
+
], McpController.prototype, "getMcpStatus", null);
|
|
53
|
+
McpController = __decorate([
|
|
54
|
+
Route('mcp'),
|
|
55
|
+
Tags('mcp'),
|
|
56
|
+
provide(McpController),
|
|
57
|
+
__param(0, inject(RestApi))
|
|
58
|
+
], McpController);
|
|
59
|
+
export { McpController };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Single source of truth for reading the MCP kill-switch flag. Defaults to
|
|
2
|
+
// `true` so legacy configs without a `[mcp]` section keep MCP enabled.
|
|
3
|
+
export function isMcpEnabled(restApi) {
|
|
4
|
+
return restApi.xoApp.config.getOptional('mcp.enabled') ?? true;
|
|
5
|
+
}
|
|
6
|
+
// Wire-level identifier used by the kill-switch contract; shared between the
|
|
7
|
+
// gate middleware (`@xen-orchestra/rest-api`) and the controller response.
|
|
8
|
+
export const MCP_DISABLED_ERROR = 'mcp_disabled';
|
|
9
|
+
// Whitelisted by `mcp-gate` so the MCP binary can probe the kill-switch
|
|
10
|
+
// state at boot even when MCP is globally disabled.
|
|
11
|
+
export const MCP_STATUS_PATH = '/mcp/status';
|
|
@@ -7,7 +7,7 @@ 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, Middlewares, Path, Query, Request, Response, Route, Security, Tags } from 'tsoa';
|
|
10
|
+
import { Example, Extension, 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';
|
|
@@ -73,6 +73,7 @@ let MessageController = class MessageController extends XapiXoController {
|
|
|
73
73
|
__decorate([
|
|
74
74
|
Example(messageIds),
|
|
75
75
|
Example(partialMessages),
|
|
76
|
+
Extension('x-mcp-exposure', 'allow'),
|
|
76
77
|
Get(''),
|
|
77
78
|
Security('*', ['acl']),
|
|
78
79
|
__param(0, Request()),
|
|
@@ -84,6 +85,7 @@ __decorate([
|
|
|
84
85
|
], MessageController.prototype, "getMessages", null);
|
|
85
86
|
__decorate([
|
|
86
87
|
Example(message),
|
|
88
|
+
Extension('x-mcp-exposure', 'allow'),
|
|
87
89
|
Get('{id}'),
|
|
88
90
|
Middlewares(acl({
|
|
89
91
|
resource: 'message',
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { ApiError } from '../helpers/error.helper.mjs';
|
|
2
|
+
import { iocContainer } from '../ioc/ioc.mjs';
|
|
3
|
+
import { isMcpEnabled, MCP_DISABLED_ERROR, MCP_STATUS_PATH } from '../mcp/mcp.helper.mjs';
|
|
4
|
+
import { RestApi } from '../rest-api/rest-api.mjs';
|
|
5
|
+
// Identifies requests originating from the `@xen-orchestra/mcp` binary.
|
|
6
|
+
//
|
|
7
|
+
// SECURITY: the header is NOT authenticated — any client can spoof it. Use it
|
|
8
|
+
// only to RESTRICT behaviour (e.g. the MCP kill-switch), never to grant
|
|
9
|
+
// privileges.
|
|
10
|
+
function isMcpRequest(req) {
|
|
11
|
+
// Node lowercases header names; values keep their original case so we
|
|
12
|
+
// normalize before comparing. `Array.isArray` covers the unusual case of
|
|
13
|
+
// a duplicated header value (a malformed or hostile client).
|
|
14
|
+
const header = req.headers['x-xo-client'];
|
|
15
|
+
const value = Array.isArray(header) ? header[0] : header;
|
|
16
|
+
return typeof value === 'string' && value.toLowerCase() === 'mcp';
|
|
17
|
+
}
|
|
18
|
+
export function mcpGateMiddleware(req, _res, next) {
|
|
19
|
+
if (!isMcpRequest(req)) {
|
|
20
|
+
return next();
|
|
21
|
+
}
|
|
22
|
+
// Always let the kill-switch probe through so the MCP binary can fail-fast.
|
|
23
|
+
if (req.path === MCP_STATUS_PATH) {
|
|
24
|
+
return next();
|
|
25
|
+
}
|
|
26
|
+
if (!isMcpEnabled(iocContainer.get(RestApi))) {
|
|
27
|
+
return next(new ApiError('MCP is disabled by administrator', 503, { data: { error: MCP_DISABLED_ERROR } }));
|
|
28
|
+
}
|
|
29
|
+
next();
|
|
30
|
+
}
|
|
@@ -7,7 +7,7 @@ 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,
|
|
10
|
+
import { Delete, Example, Extension, Get, Middlewares, Path, Put, Query, Request, Response, Route, Security, SuccessResponse, Tags, } 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';
|
|
@@ -139,6 +139,7 @@ let NetworkController = class NetworkController extends XapiXoController {
|
|
|
139
139
|
__decorate([
|
|
140
140
|
Example(networkIds),
|
|
141
141
|
Example(partialNetworks),
|
|
142
|
+
Extension('x-mcp-exposure', 'allow'),
|
|
142
143
|
Get(''),
|
|
143
144
|
Security('*', ['acl']),
|
|
144
145
|
__param(0, Request()),
|
|
@@ -150,6 +151,7 @@ __decorate([
|
|
|
150
151
|
], NetworkController.prototype, "getNetworks", null);
|
|
151
152
|
__decorate([
|
|
152
153
|
Example(network),
|
|
154
|
+
Extension('x-mcp-exposure', 'allow'),
|
|
153
155
|
Get('{id}'),
|
|
154
156
|
Middlewares(acl({ resource: 'network', action: 'read', objectId: 'params.id' })),
|
|
155
157
|
Response(forbiddenOperationResp.status, forbiddenOperationResp.description),
|
|
@@ -157,6 +159,7 @@ __decorate([
|
|
|
157
159
|
__param(0, Path())
|
|
158
160
|
], NetworkController.prototype, "getNetwork", null);
|
|
159
161
|
__decorate([
|
|
162
|
+
Extension('x-mcp-exposure', 'confirm'),
|
|
160
163
|
Delete('{id}'),
|
|
161
164
|
Middlewares(acl({ resource: 'network', action: 'delete', objectId: 'params.id' })),
|
|
162
165
|
Response(forbiddenOperationResp.status, forbiddenOperationResp.description),
|
|
@@ -166,6 +169,7 @@ __decorate([
|
|
|
166
169
|
], NetworkController.prototype, "deleteNetwork", null);
|
|
167
170
|
__decorate([
|
|
168
171
|
Example(genericAlarmsExample),
|
|
172
|
+
Extension('x-mcp-exposure', 'allow'),
|
|
169
173
|
Get('{id}/alarms'),
|
|
170
174
|
Security('*', ['acl']),
|
|
171
175
|
Tags('alarms'),
|
|
@@ -181,6 +185,7 @@ __decorate([
|
|
|
181
185
|
__decorate([
|
|
182
186
|
Example(messageIds),
|
|
183
187
|
Example(partialMessages),
|
|
188
|
+
Extension('x-mcp-exposure', 'allow'),
|
|
184
189
|
Get('{id}/messages'),
|
|
185
190
|
Security('*', ['acl']),
|
|
186
191
|
Tags('messages'),
|
|
@@ -196,6 +201,7 @@ __decorate([
|
|
|
196
201
|
__decorate([
|
|
197
202
|
Example(taskIds),
|
|
198
203
|
Example(partialTasks),
|
|
204
|
+
Extension('x-mcp-exposure', 'allow'),
|
|
199
205
|
Get('{id}/tasks'),
|
|
200
206
|
Security('*', ['acl']),
|
|
201
207
|
Tags('tasks'),
|
|
@@ -209,6 +215,7 @@ __decorate([
|
|
|
209
215
|
__param(6, Query())
|
|
210
216
|
], NetworkController.prototype, "getNetworkTasks", null);
|
|
211
217
|
__decorate([
|
|
218
|
+
Extension('x-mcp-exposure', 'confirm'),
|
|
212
219
|
Put('{id}/tags/{tag}'),
|
|
213
220
|
Middlewares(acl({ resource: 'network', action: 'update:tags', objectId: 'params.id' })),
|
|
214
221
|
SuccessResponse(noContentResp.status, noContentResp.description),
|
|
@@ -218,6 +225,7 @@ __decorate([
|
|
|
218
225
|
__param(1, Path())
|
|
219
226
|
], NetworkController.prototype, "putNetworkTag", null);
|
|
220
227
|
__decorate([
|
|
228
|
+
Extension('x-mcp-exposure', 'confirm'),
|
|
221
229
|
Delete('{id}/tags/{tag}'),
|
|
222
230
|
Middlewares(acl({ resource: 'network', action: 'update:tags', objectId: 'params.id' })),
|
|
223
231
|
SuccessResponse(noContentResp.status, noContentResp.description),
|