@lamalibre/create-lamaste 4.2.0 → 4.3.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/package.json +2 -2
- package/scripts/lamaste-priv +205 -2
- package/src/index.js +4 -4
- package/src/lib/node-version.js +114 -0
- package/src/lib/relay-services.js +15 -1
- package/src/lib/service-config.js +113 -0
- package/src/tasks/gatekeeper.js +8 -3
- package/src/tasks/node.js +34 -75
- package/src/tasks/redeploy.js +36 -0
- package/vendor/docs/00-introduction/how-it-works.md +2 -2
- package/vendor/docs/00-introduction/quickstart.md +5 -5
- package/vendor/docs/00-introduction/what-is-lamaste.md +5 -5
- package/vendor/docs/01-concepts/authentication.md +6 -3
- package/vendor/docs/01-concepts/authorization.md +29 -17
- package/vendor/docs/01-concepts/certificates.md +3 -3
- package/vendor/docs/01-concepts/mtls.md +2 -2
- package/vendor/docs/01-concepts/nginx-reverse-proxy.md +8 -3
- package/vendor/docs/01-concepts/security-model.md +98 -34
- package/vendor/docs/01-concepts/tickets.md +2 -2
- package/vendor/docs/01-concepts/tunneling.md +4 -4
- package/vendor/docs/02-guides/agent-setup.md +34 -16
- package/vendor/docs/02-guides/certificate-management.md +2 -2
- package/vendor/docs/02-guides/cloud-provisioning.md +3 -3
- package/vendor/docs/02-guides/desktop-app-setup.md +10 -10
- package/vendor/docs/02-guides/installation.md +3 -3
- package/vendor/docs/02-guides/onboarding.md +2 -2
- package/vendor/docs/02-guides/standalone-plugin-with-tickets.md +2 -2
- package/vendor/docs/03-architecture/installer-flow.md +2 -2
- package/vendor/docs/03-architecture/installer.md +4 -4
- package/vendor/docs/03-architecture/management-flow.md +1 -1
- package/vendor/docs/03-architecture/nginx-configuration.md +91 -26
- package/vendor/docs/03-architecture/overview.md +8 -8
- package/vendor/docs/03-architecture/panel-client.md +6 -6
- package/vendor/docs/03-architecture/panel-server.md +2 -2
- package/vendor/docs/03-architecture/state-management.md +2 -2
- package/vendor/docs/03-architecture/system-overview.md +1 -1
- package/vendor/docs/04-api-reference/agent-hosted-plugins.md +407 -0
- package/vendor/docs/04-api-reference/app-host.md +360 -0
- package/vendor/docs/04-api-reference/audit-and-notifications.md +57 -0
- package/vendor/docs/04-api-reference/certificates.md +95 -38
- package/vendor/docs/04-api-reference/gatekeeper.md +5 -3
- package/vendor/docs/04-api-reference/lifecycle-hooks.md +333 -0
- package/vendor/docs/04-api-reference/onboarding.md +1 -1
- package/vendor/docs/04-api-reference/overview.md +137 -111
- package/vendor/docs/04-api-reference/registry-and-oidc.md +7 -3
- package/vendor/docs/04-api-reference/tunnels.md +15 -13
- package/vendor/docs/05-operations/backup-and-restore.md +1 -1
- package/vendor/docs/05-operations/uninstalling.md +1 -1
- package/vendor/docs/05-operations/upgrades.md +25 -20
- package/vendor/docs/06-reference/config-files.md +190 -97
- package/vendor/docs/06-reference/glossary.md +3 -3
- package/vendor/docs/06-reference/installer-flags.md +13 -13
- package/vendor/docs/06-reference/ports-and-services.md +1 -1
- package/vendor/docs/06-reference/troubleshooting.md +33 -8
- package/vendor/docs/_index.json +20 -0
- package/vendor/docs/gatekeeper-design.md +22 -19
- package/vendor/docs/identity-directory-design.md +57 -56
- package/vendor/docs/package.json +1 -1
- package/vendor/docs/todo/lib-cli-desktop-refactor.md +27 -27
- package/vendor/gatekeeper/dist/lib/types.d.ts +12 -0
- package/vendor/gatekeeper/dist/lib/types.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/index.d.ts +2 -0
- package/vendor/gatekeeper/dist/server/index.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/index.js +47 -8
- package/vendor/gatekeeper/dist/server/index.js.map +1 -1
- package/vendor/gatekeeper/dist/server/routes/authz.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/routes/authz.js +233 -141
- package/vendor/gatekeeper/dist/server/routes/authz.js.map +1 -1
- package/vendor/gatekeeper/package.json +4 -3
- package/vendor/server/bin/lamaste-server.js +18 -1
- package/vendor/server/package.json +2 -2
- package/vendor/server/src/commands/audit.js +60 -0
- package/vendor/server/src/commands/plugins.js +963 -12
- package/vendor/server/src/commands/push-install.js +317 -0
- package/vendor/server/src/commands/registry-credential.js +2 -1
- package/vendor/server/src/commands/uninstall.js +28 -1
- package/vendor/server/src/ndjson.js +4 -2
- package/vendor/server/src/panel-api.js +32 -4
- package/vendor/server-ui/dist/assets/index-Bc5NCMSv.css +1 -0
- package/vendor/server-ui/dist/assets/index-DBp3cmaU.js +84 -0
- package/vendor/server-ui/dist/index.html +2 -2
- package/vendor/serverd/package.json +2 -2
- package/vendor/serverd/src/index.js +56 -4
- package/vendor/serverd/src/lib/actor.js +24 -0
- package/vendor/serverd/src/lib/agent-jobs.js +330 -0
- package/vendor/serverd/src/lib/app-error.js +10 -0
- package/vendor/serverd/src/lib/audit.js +145 -0
- package/vendor/serverd/src/lib/hook-effects.js +123 -0
- package/vendor/serverd/src/lib/hook-pipelines.js +902 -0
- package/vendor/serverd/src/lib/migrations/state/0006_plugin_agent.sql +4 -0
- package/vendor/serverd/src/lib/migrations/state/0007_audit_and_notifications.sql +29 -0
- package/vendor/serverd/src/lib/migrations/state/0008_plugin_hooks.sql +70 -0
- package/vendor/serverd/src/lib/migrations/state/0009_plugin_agent_half.sql +53 -0
- package/vendor/serverd/src/lib/migrations/state/0010_plugin_routes_and_retained_data.sql +47 -0
- package/vendor/serverd/src/lib/migrations/state/0011_agent_job_signature.sql +14 -0
- package/vendor/serverd/src/lib/mtls.js +89 -8
- package/vendor/serverd/src/lib/nginx.js +298 -44
- package/vendor/serverd/src/lib/notifications.js +107 -0
- package/vendor/serverd/src/lib/plugin-hooks.js +560 -0
- package/vendor/serverd/src/lib/plugin-hosting.js +688 -0
- package/vendor/serverd/src/lib/plugin-lifecycle.js +603 -0
- package/vendor/serverd/src/lib/plugin-operations.js +295 -0
- package/vendor/serverd/src/lib/plugin-packages.js +249 -0
- package/vendor/serverd/src/lib/plugins.js +298 -68
- package/vendor/serverd/src/lib/push-install.js +88 -2
- package/vendor/serverd/src/lib/relay-hooks.js +100 -0
- package/vendor/serverd/src/lib/relay-signing.js +43 -0
- package/vendor/serverd/src/lib/request-hooks.js +79 -0
- package/vendor/serverd/src/lib/retained-data.js +99 -0
- package/vendor/serverd/src/lib/trust-proxy.js +34 -0
- package/vendor/serverd/src/lib/tunnel-hooks.js +88 -0
- package/vendor/serverd/src/lib/vhost-reconcile.js +260 -0
- package/vendor/serverd/src/middleware/twofa-session.js +11 -1
- package/vendor/serverd/src/routes/agent-forwarder.js +37 -0
- package/vendor/serverd/src/routes/management/agent-panel-authz.js +97 -0
- package/vendor/serverd/src/routes/management/agents.js +262 -1
- package/vendor/serverd/src/routes/management/audit.js +81 -0
- package/vendor/serverd/src/routes/management/certs.js +1 -15
- package/vendor/serverd/src/routes/management/plugins.js +569 -54
- package/vendor/serverd/src/routes/management/tunnels.js +26 -0
- package/vendor/serverd/src/routes/management.js +4 -0
- package/vendor/server-ui/dist/assets/index-B5Wp8lu-.js +0 -257
- package/vendor/server-ui/dist/assets/index-BqWWFlTU.css +0 -1
|
@@ -1,14 +1,42 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { AGENT_LABEL_REGEX } from '@lamalibre/lamaste';
|
|
2
3
|
import {
|
|
3
4
|
readPlugins,
|
|
4
5
|
installPlugin,
|
|
5
|
-
uninstallPlugin,
|
|
6
6
|
enablePlugin,
|
|
7
7
|
disablePlugin,
|
|
8
8
|
getPluginCapabilities,
|
|
9
|
+
findPlugin,
|
|
9
10
|
} from '../../lib/plugins.js';
|
|
10
11
|
import { setPluginCapabilities, loadAgentRegistry } from '../../lib/mtls.js';
|
|
11
12
|
import { listPluginOidcClients, rotatePluginOidcClientSecret } from '../../lib/plugin-oidc.js';
|
|
13
|
+
import {
|
|
14
|
+
hostPlugin,
|
|
15
|
+
unhostPlugin,
|
|
16
|
+
getHostingView,
|
|
17
|
+
attachHostings,
|
|
18
|
+
rotateHostingSecret,
|
|
19
|
+
hostingEligibility,
|
|
20
|
+
retainedDataOf,
|
|
21
|
+
purgeRetainedData,
|
|
22
|
+
} from '../../lib/plugin-hosting.js';
|
|
23
|
+
import { listRetainedData } from '../../lib/retained-data.js';
|
|
24
|
+
import {
|
|
25
|
+
hookStatus,
|
|
26
|
+
approveHookSet,
|
|
27
|
+
setAutoApprove,
|
|
28
|
+
setPendingUpdate,
|
|
29
|
+
readHookState,
|
|
30
|
+
} from '../../lib/plugin-hooks.js';
|
|
31
|
+
import { uninstallPluginWithHosting, updatePlugin } from '../../lib/plugin-lifecycle.js';
|
|
32
|
+
import {
|
|
33
|
+
getPluginOperation,
|
|
34
|
+
hasOperationReveal,
|
|
35
|
+
listPluginOperations,
|
|
36
|
+
takeOperationReveal,
|
|
37
|
+
} from '../../lib/plugin-operations.js';
|
|
38
|
+
import { actorFromRequest, certSerialFromRequest } from '../../lib/actor.js';
|
|
39
|
+
import { recordAudit } from '../../lib/audit.js';
|
|
12
40
|
import {
|
|
13
41
|
readPushInstallConfig,
|
|
14
42
|
readPushInstallSessions,
|
|
@@ -20,6 +48,9 @@ import {
|
|
|
20
48
|
disableAgentPushInstall,
|
|
21
49
|
logPushInstallSession,
|
|
22
50
|
validatePushInstallAccess,
|
|
51
|
+
pushInstallPolicyRefusal,
|
|
52
|
+
PUSH_INSTALL_ACTIONS,
|
|
53
|
+
PUSH_INSTALL_WINDOW_MINUTES,
|
|
23
54
|
} from '../../lib/push-install.js';
|
|
24
55
|
|
|
25
56
|
// --- Zod schemas ---
|
|
@@ -40,6 +71,66 @@ const OidcClientIdParamSchema = z.object({
|
|
|
40
71
|
.regex(/^[a-z0-9][a-z0-9-]*$/, 'Client id must be lowercase letters, numbers and hyphens'),
|
|
41
72
|
});
|
|
42
73
|
|
|
74
|
+
const AgentLabelSchema = z
|
|
75
|
+
.string()
|
|
76
|
+
.min(1)
|
|
77
|
+
.max(63)
|
|
78
|
+
.regex(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/, 'Invalid agent label');
|
|
79
|
+
|
|
80
|
+
const HostingBodySchema = z.object({
|
|
81
|
+
agentLabel: AgentLabelSchema,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const RetainedDataQuerySchema = z.object({
|
|
85
|
+
agentLabel: AgentLabelSchema,
|
|
86
|
+
// Drop the relay's record only; the agent is not touched.
|
|
87
|
+
forget: z.enum(['true', 'false']).optional(),
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const ForceQuerySchema = z.object({
|
|
91
|
+
force: z.enum(['true', 'false']).optional(),
|
|
92
|
+
// Remove the data directory and service account of an agent half Lamaste installed.
|
|
93
|
+
purge: z.enum(['true', 'false']).optional(),
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The error body of a refused plugin operation: the message, and what a
|
|
98
|
+
* client acts on — `code` (`hooks_not_approved`, `agent_not_eligible` with
|
|
99
|
+
* its `reasons`, `push_install_refused`, `route_taken`, `route_conflict`,
|
|
100
|
+
* `hosting_owner_mismatch`, `retained_data_conflict` with the `retained`
|
|
101
|
+
* record, `retained_data_in_use`, `agent_not_enrolled`) and a running
|
|
102
|
+
* operation's id.
|
|
103
|
+
*/
|
|
104
|
+
function operationErrorBody(err, fallback) {
|
|
105
|
+
return {
|
|
106
|
+
error: err.message || fallback,
|
|
107
|
+
...(err.code ? { code: err.code } : {}),
|
|
108
|
+
...(Array.isArray(err.reasons) ? { reasons: err.reasons } : {}),
|
|
109
|
+
...(err.retained ? { retained: err.retained } : {}),
|
|
110
|
+
...(err.owner ? { route: err.route, owner: err.owner } : {}),
|
|
111
|
+
...(err.operationId ? { operationId: err.operationId } : {}),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const OperationParamSchema = z.object({
|
|
116
|
+
id: z.string().uuid(),
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const ApproveHooksBodySchema = z
|
|
120
|
+
.object({ digest: z.string().regex(/^[a-f0-9]{64}$/, 'digest is a SHA-256 hex string') })
|
|
121
|
+
.strict();
|
|
122
|
+
|
|
123
|
+
const AutoApproveBodySchema = z.object({ enabled: z.boolean() }).strict();
|
|
124
|
+
|
|
125
|
+
const UpdateBodySchema = z
|
|
126
|
+
.object({
|
|
127
|
+
version: z
|
|
128
|
+
.string()
|
|
129
|
+
.regex(/^[0-9A-Za-z.+-]{1,64}$/, 'version is an exact version or a dist-tag')
|
|
130
|
+
.optional(),
|
|
131
|
+
})
|
|
132
|
+
.strict();
|
|
133
|
+
|
|
43
134
|
const InstallBodySchema = z.object({
|
|
44
135
|
packageName: z
|
|
45
136
|
.string()
|
|
@@ -88,9 +179,9 @@ const CreatePolicySchema = z.object({
|
|
|
88
179
|
deniedIps: z.array(IpEntrySchema).optional().default([]),
|
|
89
180
|
allowedPlugins: z.array(ScopedPackageSchema).optional().default([]),
|
|
90
181
|
allowedActions: z
|
|
91
|
-
.array(z.enum(
|
|
182
|
+
.array(z.enum(PUSH_INSTALL_ACTIONS))
|
|
92
183
|
.optional()
|
|
93
|
-
.default([
|
|
184
|
+
.default([...PUSH_INSTALL_ACTIONS]),
|
|
94
185
|
});
|
|
95
186
|
|
|
96
187
|
const UpdatePolicySchema = z.object({
|
|
@@ -99,9 +190,7 @@ const UpdatePolicySchema = z.object({
|
|
|
99
190
|
allowedIps: z.array(IpEntrySchema).optional(),
|
|
100
191
|
deniedIps: z.array(IpEntrySchema).optional(),
|
|
101
192
|
allowedPlugins: z.array(ScopedPackageSchema).optional(),
|
|
102
|
-
allowedActions: z
|
|
103
|
-
.array(z.enum(['install', 'update', 'uninstall', 'check-prerequisites']))
|
|
104
|
-
.optional(),
|
|
193
|
+
allowedActions: z.array(z.enum(PUSH_INSTALL_ACTIONS)).optional(),
|
|
105
194
|
});
|
|
106
195
|
|
|
107
196
|
const UpdatePushInstallConfigSchema = z.object({
|
|
@@ -109,22 +198,30 @@ const UpdatePushInstallConfigSchema = z.object({
|
|
|
109
198
|
defaultPolicy: PolicyIdSchema.optional(),
|
|
110
199
|
});
|
|
111
200
|
|
|
201
|
+
const WINDOW_BOUNDS = `A push-install window is open ${PUSH_INSTALL_WINDOW_MINUTES.min} to ${PUSH_INSTALL_WINDOW_MINUTES.max} minutes`;
|
|
202
|
+
|
|
112
203
|
const EnablePushInstallSchema = z.object({
|
|
113
|
-
durationMinutes: z
|
|
204
|
+
durationMinutes: z
|
|
205
|
+
.number({ invalid_type_error: WINDOW_BOUNDS, required_error: WINDOW_BOUNDS })
|
|
206
|
+
.int(WINDOW_BOUNDS)
|
|
207
|
+
.min(PUSH_INSTALL_WINDOW_MINUTES.min, WINDOW_BOUNDS)
|
|
208
|
+
.max(PUSH_INSTALL_WINDOW_MINUTES.max, WINDOW_BOUNDS),
|
|
114
209
|
policyId: PolicyIdSchema.optional(),
|
|
115
210
|
});
|
|
116
211
|
|
|
212
|
+
/** A machine agent's label (a DNS label of up to 63 characters, as enrolment allows). */
|
|
117
213
|
const AgentLabelParamSchema = z.object({
|
|
118
|
-
label: z
|
|
119
|
-
.string()
|
|
120
|
-
.min(1)
|
|
121
|
-
.max(50)
|
|
122
|
-
.regex(/^[a-z0-9-]+$/, 'Invalid agent label'),
|
|
214
|
+
label: z.string().regex(AGENT_LABEL_REGEX, 'Invalid agent label'),
|
|
123
215
|
});
|
|
124
216
|
|
|
217
|
+
/** The push-install config as the panel shows it, with the window bounds. */
|
|
218
|
+
function pushInstallConfigView(config) {
|
|
219
|
+
return { ...config, windowMinutes: PUSH_INSTALL_WINDOW_MINUTES };
|
|
220
|
+
}
|
|
221
|
+
|
|
125
222
|
const PushInstallCommandSchema = z
|
|
126
223
|
.object({
|
|
127
|
-
action: z.enum(
|
|
224
|
+
action: z.enum(PUSH_INSTALL_ACTIONS),
|
|
128
225
|
packageName: z
|
|
129
226
|
.string()
|
|
130
227
|
.min(1)
|
|
@@ -145,6 +242,26 @@ const PushInstallCommandSchema = z
|
|
|
145
242
|
},
|
|
146
243
|
);
|
|
147
244
|
|
|
245
|
+
/** A plugin entry with a summary of its hooks for list views. */
|
|
246
|
+
async function withHooks(plugin) {
|
|
247
|
+
const status = await hookStatus(plugin);
|
|
248
|
+
return {
|
|
249
|
+
...plugin,
|
|
250
|
+
hooks: {
|
|
251
|
+
relay: status.relay !== null,
|
|
252
|
+
agent: status.agent !== null,
|
|
253
|
+
// Lamaste installs the agent half (agent.package); false: none, or installed by hand.
|
|
254
|
+
agentHalf: status.agentHalf !== null,
|
|
255
|
+
approval: status.approval.state,
|
|
256
|
+
autoApprove: status.autoApprove,
|
|
257
|
+
failOpenCount: status.relay?.failOpenCount ?? 0,
|
|
258
|
+
pendingUpdate: status.pendingUpdate
|
|
259
|
+
? { version: status.pendingUpdate.version, digest: status.pendingUpdate.digest }
|
|
260
|
+
: null,
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
148
265
|
export default async function pluginRoutes(fastify, _opts) {
|
|
149
266
|
// ===========================================================================
|
|
150
267
|
// Plugin management routes
|
|
@@ -160,7 +277,17 @@ export default async function pluginRoutes(fastify, _opts) {
|
|
|
160
277
|
},
|
|
161
278
|
async (_request, _reply) => {
|
|
162
279
|
const registry = await readPlugins();
|
|
163
|
-
|
|
280
|
+
// `agentHosting`: where an agent-hosted plugin's agent half runs, or null.
|
|
281
|
+
// `retainedData`: what agents keep of its agent half after it was
|
|
282
|
+
// removed without purge. `hooks`: its lifecycle hooks and their
|
|
283
|
+
// approval (see GET /plugins/:name/hooks).
|
|
284
|
+
const plugins = await attachHostings(registry.plugins);
|
|
285
|
+
return {
|
|
286
|
+
plugins: await Promise.all(plugins.map(withHooks)),
|
|
287
|
+
// Every agent's retained data — of uninstalled plugins too, which
|
|
288
|
+
// no entry of `plugins` lists any more.
|
|
289
|
+
retainedData: await listRetainedData(),
|
|
290
|
+
};
|
|
164
291
|
},
|
|
165
292
|
);
|
|
166
293
|
|
|
@@ -181,7 +308,387 @@ export default async function pluginRoutes(fastify, _opts) {
|
|
|
181
308
|
return reply.code(404).send({ error: `Plugin "${params.name}" not found` });
|
|
182
309
|
}
|
|
183
310
|
|
|
184
|
-
|
|
311
|
+
const [withHosting] = await attachHostings([plugin]);
|
|
312
|
+
return { plugin: await withHooks(withHosting) };
|
|
313
|
+
},
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
// ===========================================================================
|
|
317
|
+
// Hosting an agent-hosted plugin's agent half on an agent
|
|
318
|
+
// ===========================================================================
|
|
319
|
+
|
|
320
|
+
// ------------------------------------------------------------------
|
|
321
|
+
// GET /plugins/:name/hosting — which agent hosts the plugin, port, health
|
|
322
|
+
// ------------------------------------------------------------------
|
|
323
|
+
fastify.get(
|
|
324
|
+
'/plugins/:name/hosting',
|
|
325
|
+
{
|
|
326
|
+
preHandler: fastify.requireRole(['admin']),
|
|
327
|
+
},
|
|
328
|
+
async (request, reply) => {
|
|
329
|
+
const params = PluginNameParamSchema.parse(request.params);
|
|
330
|
+
const registry = await readPlugins();
|
|
331
|
+
if (!registry.plugins.some((p) => p.name === params.name)) {
|
|
332
|
+
return reply.code(404).send({ error: `Plugin "${params.name}" not found` });
|
|
333
|
+
}
|
|
334
|
+
const hosting = await getHostingView(params.name);
|
|
335
|
+
if (!hosting) {
|
|
336
|
+
return reply.code(404).send({ error: `Plugin "${params.name}" is not hosted on an agent` });
|
|
337
|
+
}
|
|
338
|
+
return { hosting };
|
|
339
|
+
},
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
// ------------------------------------------------------------------
|
|
343
|
+
// POST /plugins/:name/hosting — host the plugin on an agent: internal
|
|
344
|
+
// tunnel (port assigned), installation secret, host capability, and the
|
|
345
|
+
// agent half when Lamaste installs it (the agent must be eligible —
|
|
346
|
+
// 409 agent_not_eligible with reasons — and the push-install gates must
|
|
347
|
+
// allow `install` of the agent package — push_install_refused)
|
|
348
|
+
// ------------------------------------------------------------------
|
|
349
|
+
fastify.post(
|
|
350
|
+
'/plugins/:name/hosting',
|
|
351
|
+
{
|
|
352
|
+
preHandler: fastify.requireRole(['admin']),
|
|
353
|
+
},
|
|
354
|
+
async (request, reply) => {
|
|
355
|
+
const params = PluginNameParamSchema.parse(request.params);
|
|
356
|
+
const body = HostingBodySchema.parse(request.body);
|
|
357
|
+
try {
|
|
358
|
+
const operation = await hostPlugin(params.name, body.agentLabel, {
|
|
359
|
+
actor: actorFromRequest(request),
|
|
360
|
+
logger: request.log,
|
|
361
|
+
sourceIp: request.ip,
|
|
362
|
+
});
|
|
363
|
+
return reply.code(202).send({ ok: true, operation });
|
|
364
|
+
} catch (err) {
|
|
365
|
+
const statusCode = err.statusCode || 500;
|
|
366
|
+
return reply.code(statusCode).send(operationErrorBody(err, 'Failed to host plugin'));
|
|
367
|
+
}
|
|
368
|
+
},
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
// ------------------------------------------------------------------
|
|
372
|
+
// GET /plugins/:name/hosting/eligibility — which enrolled agents can host
|
|
373
|
+
// the plugin, and why the others cannot (the host dialog). For a plugin
|
|
374
|
+
// whose agent half Lamaste installs: `agent.requires` and what installing
|
|
375
|
+
// it needs, against the host facts each agent reported.
|
|
376
|
+
// ------------------------------------------------------------------
|
|
377
|
+
fastify.get(
|
|
378
|
+
'/plugins/:name/hosting/eligibility',
|
|
379
|
+
{
|
|
380
|
+
preHandler: fastify.requireRole(['admin']),
|
|
381
|
+
},
|
|
382
|
+
async (request, reply) => {
|
|
383
|
+
const params = PluginNameParamSchema.parse(request.params);
|
|
384
|
+
try {
|
|
385
|
+
return { agents: await hostingEligibility(params.name) };
|
|
386
|
+
} catch (err) {
|
|
387
|
+
const statusCode = err.statusCode || 500;
|
|
388
|
+
return reply
|
|
389
|
+
.code(statusCode)
|
|
390
|
+
.send({ error: err.message || 'Failed to check which agents can host the plugin' });
|
|
391
|
+
}
|
|
392
|
+
},
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
// ------------------------------------------------------------------
|
|
396
|
+
// DELETE /plugins/:name/hosting?force=&purge= — remove tunnel, secret,
|
|
397
|
+
// capability and the agent half Lamaste installed (a push: `uninstall`);
|
|
398
|
+
// `purge=true` removes the agent half's data and service account too
|
|
399
|
+
// ------------------------------------------------------------------
|
|
400
|
+
fastify.delete(
|
|
401
|
+
'/plugins/:name/hosting',
|
|
402
|
+
{
|
|
403
|
+
preHandler: fastify.requireRole(['admin']),
|
|
404
|
+
},
|
|
405
|
+
async (request, reply) => {
|
|
406
|
+
const params = PluginNameParamSchema.parse(request.params);
|
|
407
|
+
const query = ForceQuerySchema.parse(request.query);
|
|
408
|
+
try {
|
|
409
|
+
const operation = await unhostPlugin(params.name, {
|
|
410
|
+
actor: actorFromRequest(request),
|
|
411
|
+
logger: request.log,
|
|
412
|
+
force: query.force === 'true',
|
|
413
|
+
purge: query.purge === 'true',
|
|
414
|
+
sourceIp: request.ip,
|
|
415
|
+
});
|
|
416
|
+
return reply.code(202).send({ ok: true, operation });
|
|
417
|
+
} catch (err) {
|
|
418
|
+
const statusCode = err.statusCode || 500;
|
|
419
|
+
return reply
|
|
420
|
+
.code(statusCode)
|
|
421
|
+
.send(operationErrorBody(err, 'Failed to remove plugin hosting'));
|
|
422
|
+
}
|
|
423
|
+
},
|
|
424
|
+
);
|
|
425
|
+
|
|
426
|
+
// ------------------------------------------------------------------
|
|
427
|
+
// POST /plugins/:name/hosting/rotate-secret — a new installation secret;
|
|
428
|
+
// the previous one stays accepted until the hosting agent acknowledges
|
|
429
|
+
// the new one (its sync does that within about 30 seconds)
|
|
430
|
+
// ------------------------------------------------------------------
|
|
431
|
+
fastify.post(
|
|
432
|
+
'/plugins/:name/hosting/rotate-secret',
|
|
433
|
+
{
|
|
434
|
+
preHandler: fastify.requireRole(['admin']),
|
|
435
|
+
config: { rateLimit: { max: 10, timeWindow: '1 minute' } },
|
|
436
|
+
},
|
|
437
|
+
async (request, reply) => {
|
|
438
|
+
const params = PluginNameParamSchema.parse(request.params);
|
|
439
|
+
try {
|
|
440
|
+
const hosting = await rotateHostingSecret(params.name, request.log);
|
|
441
|
+
return { ok: true, hosting };
|
|
442
|
+
} catch (err) {
|
|
443
|
+
const statusCode = err.statusCode || 500;
|
|
444
|
+
return reply
|
|
445
|
+
.code(statusCode)
|
|
446
|
+
.send({ error: err.message || 'Failed to rotate the installation secret' });
|
|
447
|
+
}
|
|
448
|
+
},
|
|
449
|
+
);
|
|
450
|
+
|
|
451
|
+
// ------------------------------------------------------------------
|
|
452
|
+
// GET /plugins/:name/retained-data — the data agents keep of the plugin's
|
|
453
|
+
// agent half after it was removed without purge (an unhost, an uninstall,
|
|
454
|
+
// a rolled-back install): [{ agentLabel, route, plugin, package, since,
|
|
455
|
+
// operation }]. By the plugin's name, so it answers after the plugin is
|
|
456
|
+
// uninstalled too.
|
|
457
|
+
// ------------------------------------------------------------------
|
|
458
|
+
fastify.get(
|
|
459
|
+
'/plugins/:name/retained-data',
|
|
460
|
+
{
|
|
461
|
+
preHandler: fastify.requireRole(['admin']),
|
|
462
|
+
},
|
|
463
|
+
async (request) => {
|
|
464
|
+
const params = PluginNameParamSchema.parse(request.params);
|
|
465
|
+
return { retainedData: await retainedDataOf(params.name) };
|
|
466
|
+
},
|
|
467
|
+
);
|
|
468
|
+
|
|
469
|
+
// ------------------------------------------------------------------
|
|
470
|
+
// DELETE /plugins/:name/retained-data?agentLabel=<label>[&forget=true] —
|
|
471
|
+
// purge the data agent <label> keeps of the plugin's agent half (its data
|
|
472
|
+
// directory and service account): 202 with the operation (kind
|
|
473
|
+
// `purge-data`), an app-remove job with purge, push-install gated as
|
|
474
|
+
// `uninstall` of the agent package. `forget=true` drops the record only
|
|
475
|
+
// (the agent is not touched — it is gone, or was cleaned by hand): 200.
|
|
476
|
+
// ------------------------------------------------------------------
|
|
477
|
+
fastify.delete(
|
|
478
|
+
'/plugins/:name/retained-data',
|
|
479
|
+
{
|
|
480
|
+
preHandler: fastify.requireRole(['admin']),
|
|
481
|
+
config: { rateLimit: { max: 10, timeWindow: '1 minute' } },
|
|
482
|
+
},
|
|
483
|
+
async (request, reply) => {
|
|
484
|
+
const params = PluginNameParamSchema.parse(request.params);
|
|
485
|
+
const query = RetainedDataQuerySchema.parse(request.query);
|
|
486
|
+
try {
|
|
487
|
+
const result = await purgeRetainedData(params.name, query.agentLabel, {
|
|
488
|
+
actor: actorFromRequest(request),
|
|
489
|
+
logger: request.log,
|
|
490
|
+
sourceIp: request.ip,
|
|
491
|
+
forget: query.forget === 'true',
|
|
492
|
+
});
|
|
493
|
+
if (result.forgotten) return { ok: true, forgotten: result.forgotten };
|
|
494
|
+
return reply.code(202).send({ ok: true, operation: result.operation });
|
|
495
|
+
} catch (err) {
|
|
496
|
+
const statusCode = err.statusCode || 500;
|
|
497
|
+
return reply
|
|
498
|
+
.code(statusCode)
|
|
499
|
+
.send(operationErrorBody(err, 'Failed to purge the retained data'));
|
|
500
|
+
}
|
|
501
|
+
},
|
|
502
|
+
);
|
|
503
|
+
|
|
504
|
+
// ------------------------------------------------------------------
|
|
505
|
+
// GET /plugins/:name/operations — the plugin's recent operations
|
|
506
|
+
// (hosting, unhosting, update, uninstall), newest first; `revealPending`
|
|
507
|
+
// marks one whose secrets wait for that first read below
|
|
508
|
+
// GET /plugins/:name/operations/:id — one of them. Once it finished, the
|
|
509
|
+
// first read also carries `oidcClients`: the secrets of confidential OIDC
|
|
510
|
+
// clients it created (an update adding one), shown this once — as
|
|
511
|
+
// POST /plugins/install returns them — and never stored.
|
|
512
|
+
// ------------------------------------------------------------------
|
|
513
|
+
fastify.get(
|
|
514
|
+
'/plugins/:name/operations',
|
|
515
|
+
{
|
|
516
|
+
preHandler: fastify.requireRole(['admin']),
|
|
517
|
+
},
|
|
518
|
+
async (request) => {
|
|
519
|
+
const params = PluginNameParamSchema.parse(request.params);
|
|
520
|
+
const operations = await listPluginOperations(params.name);
|
|
521
|
+
return {
|
|
522
|
+
operations: operations.map((op) => ({ ...op, revealPending: hasOperationReveal(op.id) })),
|
|
523
|
+
};
|
|
524
|
+
},
|
|
525
|
+
);
|
|
526
|
+
|
|
527
|
+
fastify.get(
|
|
528
|
+
'/plugins/:name/operations/:id',
|
|
529
|
+
{
|
|
530
|
+
preHandler: fastify.requireRole(['admin']),
|
|
531
|
+
},
|
|
532
|
+
async (request, reply) => {
|
|
533
|
+
const params = PluginNameParamSchema.merge(OperationParamSchema).parse(request.params);
|
|
534
|
+
const operation = await getPluginOperation(params.name, params.id);
|
|
535
|
+
if (!operation) return reply.code(404).send({ error: 'Operation not found' });
|
|
536
|
+
const reveal = operation.status === 'running' ? null : takeOperationReveal(operation.id);
|
|
537
|
+
return { operation: reveal ? { ...operation, oidcClients: reveal.oidcClients } : operation };
|
|
538
|
+
},
|
|
539
|
+
);
|
|
540
|
+
|
|
541
|
+
// ===========================================================================
|
|
542
|
+
// Lifecycle hooks: review, approval, auto-approve
|
|
543
|
+
// ===========================================================================
|
|
544
|
+
|
|
545
|
+
// ------------------------------------------------------------------
|
|
546
|
+
// GET /plugins/:name/hooks — the relay hook set (entry, stages, routes,
|
|
547
|
+
// fail-open flags, effects, digest), the agent hooks, the agent half
|
|
548
|
+
// Lamaste installs (package, version, integrity, service, system
|
|
549
|
+
// packages), the approval (`approval.digest` is what to approve) and a
|
|
550
|
+
// pending update's relay hook set and agent half
|
|
551
|
+
// ------------------------------------------------------------------
|
|
552
|
+
fastify.get(
|
|
553
|
+
'/plugins/:name/hooks',
|
|
554
|
+
{
|
|
555
|
+
preHandler: fastify.requireRole(['admin']),
|
|
556
|
+
},
|
|
557
|
+
async (request, reply) => {
|
|
558
|
+
const params = PluginNameParamSchema.parse(request.params);
|
|
559
|
+
const plugin = await findPlugin(params.name);
|
|
560
|
+
if (!plugin) return reply.code(404).send({ error: `Plugin "${params.name}" not found` });
|
|
561
|
+
return { hooks: await hookStatus(plugin) };
|
|
562
|
+
},
|
|
563
|
+
);
|
|
564
|
+
|
|
565
|
+
// ------------------------------------------------------------------
|
|
566
|
+
// POST /plugins/:name/hooks/approve — approve what the plugin brings
|
|
567
|
+
// (relay hooks, an agent half Lamaste installs) with the digest that was
|
|
568
|
+
// shown. Approving a pending update's digest applies the update (202
|
|
569
|
+
// with its operation).
|
|
570
|
+
// ------------------------------------------------------------------
|
|
571
|
+
fastify.post(
|
|
572
|
+
'/plugins/:name/hooks/approve',
|
|
573
|
+
{
|
|
574
|
+
preHandler: fastify.requireRole(['admin']),
|
|
575
|
+
config: { rateLimit: { max: 20, timeWindow: '1 minute' } },
|
|
576
|
+
},
|
|
577
|
+
async (request, reply) => {
|
|
578
|
+
const params = PluginNameParamSchema.parse(request.params);
|
|
579
|
+
const body = ApproveHooksBodySchema.parse(request.body);
|
|
580
|
+
const plugin = await findPlugin(params.name);
|
|
581
|
+
if (!plugin) return reply.code(404).send({ error: `Plugin "${params.name}" not found` });
|
|
582
|
+
const actor = actorFromRequest(request);
|
|
583
|
+
const certSerial = certSerialFromRequest(request);
|
|
584
|
+
try {
|
|
585
|
+
const approved = await approveHookSet(plugin, body.digest, {
|
|
586
|
+
actor,
|
|
587
|
+
certSerial,
|
|
588
|
+
logger: request.log,
|
|
589
|
+
});
|
|
590
|
+
if (approved.target === 'update') {
|
|
591
|
+
const state = await readHookState(plugin.name);
|
|
592
|
+
const result = await updatePlugin(plugin.name, state.pendingUpdate.version, {
|
|
593
|
+
actor,
|
|
594
|
+
certSerial,
|
|
595
|
+
logger: request.log,
|
|
596
|
+
approvedDigest: body.digest,
|
|
597
|
+
sourceIp: request.ip,
|
|
598
|
+
});
|
|
599
|
+
return reply.code(202).send({ ok: true, ...result });
|
|
600
|
+
}
|
|
601
|
+
return { ok: true, hooks: await hookStatus(plugin) };
|
|
602
|
+
} catch (err) {
|
|
603
|
+
const statusCode = err.statusCode || 500;
|
|
604
|
+
return reply.code(statusCode).send({
|
|
605
|
+
...operationErrorBody(err, 'Failed to approve the hook set'),
|
|
606
|
+
...(err.currentDigest ? { currentDigest: err.currentDigest } : {}),
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
},
|
|
610
|
+
);
|
|
611
|
+
|
|
612
|
+
// ------------------------------------------------------------------
|
|
613
|
+
// PUT /plugins/:name/hooks/auto-approve — approve later hook-set changes
|
|
614
|
+
// of this plugin automatically (off by default; every use is audited)
|
|
615
|
+
// ------------------------------------------------------------------
|
|
616
|
+
fastify.put(
|
|
617
|
+
'/plugins/:name/hooks/auto-approve',
|
|
618
|
+
{
|
|
619
|
+
preHandler: fastify.requireRole(['admin']),
|
|
620
|
+
},
|
|
621
|
+
async (request, reply) => {
|
|
622
|
+
const params = PluginNameParamSchema.parse(request.params);
|
|
623
|
+
const body = AutoApproveBodySchema.parse(request.body);
|
|
624
|
+
const plugin = await findPlugin(params.name);
|
|
625
|
+
if (!plugin) return reply.code(404).send({ error: `Plugin "${params.name}" not found` });
|
|
626
|
+
const result = await setAutoApprove(plugin.name, body.enabled, {
|
|
627
|
+
actor: actorFromRequest(request),
|
|
628
|
+
certSerial: certSerialFromRequest(request),
|
|
629
|
+
});
|
|
630
|
+
return { ok: true, ...result };
|
|
631
|
+
},
|
|
632
|
+
);
|
|
633
|
+
|
|
634
|
+
// ===========================================================================
|
|
635
|
+
// Updating an agent-hosted plugin in place
|
|
636
|
+
// ===========================================================================
|
|
637
|
+
|
|
638
|
+
// ------------------------------------------------------------------
|
|
639
|
+
// POST /plugins/:name/update { version? } — fetch the version (default
|
|
640
|
+
// latest) and read it without installing it; pin its agent half. A
|
|
641
|
+
// changed relay hook set or agent half waits for approval
|
|
642
|
+
// (202 { pendingUpdate }); otherwise the update runs as an operation
|
|
643
|
+
// (202 { operation }). 200 { upToDate } when there is nothing to do.
|
|
644
|
+
// ------------------------------------------------------------------
|
|
645
|
+
fastify.post(
|
|
646
|
+
'/plugins/:name/update',
|
|
647
|
+
{
|
|
648
|
+
preHandler: fastify.requireRole(['admin']),
|
|
649
|
+
config: { rateLimit: { max: 10, timeWindow: '1 minute' } },
|
|
650
|
+
},
|
|
651
|
+
async (request, reply) => {
|
|
652
|
+
const params = PluginNameParamSchema.parse(request.params);
|
|
653
|
+
const body = UpdateBodySchema.parse(request.body ?? {});
|
|
654
|
+
try {
|
|
655
|
+
const result = await updatePlugin(params.name, body.version ?? 'latest', {
|
|
656
|
+
actor: actorFromRequest(request),
|
|
657
|
+
certSerial: certSerialFromRequest(request),
|
|
658
|
+
logger: request.log,
|
|
659
|
+
sourceIp: request.ip,
|
|
660
|
+
});
|
|
661
|
+
if (result.upToDate) return { ok: true, ...result };
|
|
662
|
+
return reply.code(202).send({ ok: true, ...result });
|
|
663
|
+
} catch (err) {
|
|
664
|
+
const statusCode = err.statusCode || 500;
|
|
665
|
+
return reply.code(statusCode).send(operationErrorBody(err, 'Plugin update failed'));
|
|
666
|
+
}
|
|
667
|
+
},
|
|
668
|
+
);
|
|
669
|
+
|
|
670
|
+
// ------------------------------------------------------------------
|
|
671
|
+
// DELETE /plugins/:name/update — discard an update that waits for approval
|
|
672
|
+
// ------------------------------------------------------------------
|
|
673
|
+
fastify.delete(
|
|
674
|
+
'/plugins/:name/update',
|
|
675
|
+
{
|
|
676
|
+
preHandler: fastify.requireRole(['admin']),
|
|
677
|
+
},
|
|
678
|
+
async (request, reply) => {
|
|
679
|
+
const params = PluginNameParamSchema.parse(request.params);
|
|
680
|
+
const state = await readHookState(params.name);
|
|
681
|
+
if (!state?.pendingUpdate) {
|
|
682
|
+
return reply.code(404).send({ error: `Plugin "${params.name}" has no pending update` });
|
|
683
|
+
}
|
|
684
|
+
await setPendingUpdate(params.name, null);
|
|
685
|
+
await recordAudit({
|
|
686
|
+
actor: actorFromRequest(request),
|
|
687
|
+
action: 'plugin.update.discarded',
|
|
688
|
+
plugin: params.name,
|
|
689
|
+
details: { version: state.pendingUpdate.version },
|
|
690
|
+
});
|
|
691
|
+
return { ok: true };
|
|
185
692
|
},
|
|
186
693
|
);
|
|
187
694
|
|
|
@@ -202,12 +709,18 @@ export default async function pluginRoutes(fastify, _opts) {
|
|
|
202
709
|
const { oidcClients, ...plugin } = await installPlugin(body.packageName, request.log);
|
|
203
710
|
// Refresh in-memory capability list so new plugin caps are valid immediately
|
|
204
711
|
setPluginCapabilities(await getPluginCapabilities());
|
|
205
|
-
|
|
712
|
+
await recordAudit({
|
|
713
|
+
actor: actorFromRequest(request),
|
|
714
|
+
action: 'plugin.installed',
|
|
715
|
+
plugin: plugin.name,
|
|
716
|
+
details: { packageName: plugin.packageName, version: plugin.version },
|
|
717
|
+
});
|
|
718
|
+
// `hooks`: the relay hook set to review (and approve) before hosting.
|
|
719
|
+
return { ok: true, plugin, oidcClients, hooks: await hookStatus(plugin) };
|
|
206
720
|
} catch (err) {
|
|
207
721
|
const statusCode = err.statusCode || 500;
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
});
|
|
722
|
+
// `route_taken`: another installed plugin holds the route (`route`, `owner`).
|
|
723
|
+
return reply.code(statusCode).send(operationErrorBody(err, 'Plugin installation failed'));
|
|
211
724
|
}
|
|
212
725
|
},
|
|
213
726
|
);
|
|
@@ -261,7 +774,9 @@ export default async function pluginRoutes(fastify, _opts) {
|
|
|
261
774
|
);
|
|
262
775
|
|
|
263
776
|
// ------------------------------------------------------------------
|
|
264
|
-
// DELETE /plugins/:name — uninstall a plugin (must be
|
|
777
|
+
// DELETE /plugins/:name?force=&purge= — uninstall a plugin (must be
|
|
778
|
+
// disabled); a hosted one's hosting is removed first (see
|
|
779
|
+
// DELETE /plugins/:name/hosting)
|
|
265
780
|
// ------------------------------------------------------------------
|
|
266
781
|
fastify.delete(
|
|
267
782
|
'/plugins/:name',
|
|
@@ -270,16 +785,24 @@ export default async function pluginRoutes(fastify, _opts) {
|
|
|
270
785
|
},
|
|
271
786
|
async (request, reply) => {
|
|
272
787
|
const params = PluginNameParamSchema.parse(request.params);
|
|
788
|
+
const query = ForceQuerySchema.parse(request.query);
|
|
273
789
|
|
|
274
790
|
try {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
791
|
+
// A hosted plugin's hosting is removed first, by an operation
|
|
792
|
+
// (its uninstall pipeline); any other plugin goes at once.
|
|
793
|
+
const result = await uninstallPluginWithHosting(params.name, {
|
|
794
|
+
actor: actorFromRequest(request),
|
|
795
|
+
logger: request.log,
|
|
796
|
+
force: query.force === 'true',
|
|
797
|
+
purge: query.purge === 'true',
|
|
798
|
+
sourceIp: request.ip,
|
|
799
|
+
});
|
|
800
|
+
if (result.operation)
|
|
801
|
+
return reply.code(202).send({ ok: true, operation: result.operation });
|
|
802
|
+
return result.removed;
|
|
278
803
|
} catch (err) {
|
|
279
804
|
const statusCode = err.statusCode || 500;
|
|
280
|
-
return reply.code(statusCode).send(
|
|
281
|
-
error: err.message || 'Plugin uninstall failed',
|
|
282
|
-
});
|
|
805
|
+
return reply.code(statusCode).send(operationErrorBody(err, 'Plugin uninstall failed'));
|
|
283
806
|
}
|
|
284
807
|
},
|
|
285
808
|
);
|
|
@@ -341,7 +864,9 @@ export default async function pluginRoutes(fastify, _opts) {
|
|
|
341
864
|
// ===========================================================================
|
|
342
865
|
|
|
343
866
|
// ------------------------------------------------------------------
|
|
344
|
-
// GET /plugins/push-install/config — get push install config
|
|
867
|
+
// GET /plugins/push-install/config — get push install config: the
|
|
868
|
+
// global switch, the policies, the default policy and the bounds of an
|
|
869
|
+
// agent's window (`windowMinutes`)
|
|
345
870
|
// ------------------------------------------------------------------
|
|
346
871
|
fastify.get(
|
|
347
872
|
'/plugins/push-install/config',
|
|
@@ -350,7 +875,7 @@ export default async function pluginRoutes(fastify, _opts) {
|
|
|
350
875
|
},
|
|
351
876
|
async (_request, _reply) => {
|
|
352
877
|
const config = await readPushInstallConfig();
|
|
353
|
-
return config;
|
|
878
|
+
return pushInstallConfigView(config);
|
|
354
879
|
},
|
|
355
880
|
);
|
|
356
881
|
|
|
@@ -367,7 +892,7 @@ export default async function pluginRoutes(fastify, _opts) {
|
|
|
367
892
|
|
|
368
893
|
try {
|
|
369
894
|
const config = await updatePushInstallConfigFields(body);
|
|
370
|
-
return { ok: true, ...config };
|
|
895
|
+
return { ok: true, ...pushInstallConfigView(config) };
|
|
371
896
|
} catch (err) {
|
|
372
897
|
const statusCode = err.statusCode || 500;
|
|
373
898
|
return reply.code(statusCode).send({
|
|
@@ -513,7 +1038,12 @@ export default async function pluginRoutes(fastify, _opts) {
|
|
|
513
1038
|
);
|
|
514
1039
|
|
|
515
1040
|
// ------------------------------------------------------------------
|
|
516
|
-
// POST /plugins/push-install/:label —
|
|
1041
|
+
// POST /plugins/push-install/:label — record a push-install command
|
|
1042
|
+
// (legacy). It checks the push-install gates and logs the command in the
|
|
1043
|
+
// push-install session log, and that is all: no agent polls for it and
|
|
1044
|
+
// nothing delivers it. What Lamaste really pushes — installing, updating
|
|
1045
|
+
// and removing an agent half — runs as plugin operations (hosting, update,
|
|
1046
|
+
// unhosting, uninstall, purging retained data), gated the same way.
|
|
517
1047
|
// ------------------------------------------------------------------
|
|
518
1048
|
fastify.post(
|
|
519
1049
|
'/plugins/push-install/:label',
|
|
@@ -530,27 +1060,14 @@ export default async function pluginRoutes(fastify, _opts) {
|
|
|
530
1060
|
return reply.code(access.statusCode).send({ error: access.error });
|
|
531
1061
|
}
|
|
532
1062
|
|
|
533
|
-
// Check
|
|
534
|
-
|
|
535
|
-
access.policy
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
)
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
});
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
// Check if the package is allowed by the policy
|
|
545
|
-
if (
|
|
546
|
-
body.packageName &&
|
|
547
|
-
access.policy.allowedPlugins &&
|
|
548
|
-
access.policy.allowedPlugins.length > 0 &&
|
|
549
|
-
!access.policy.allowedPlugins.includes(body.packageName)
|
|
550
|
-
) {
|
|
551
|
-
return reply.code(403).send({
|
|
552
|
-
error: `Package "${body.packageName}" is not allowed by the assigned policy`,
|
|
553
|
-
});
|
|
1063
|
+
// Check the action and the package against the policy
|
|
1064
|
+
const refusal = pushInstallPolicyRefusal(
|
|
1065
|
+
access.policy,
|
|
1066
|
+
body.action,
|
|
1067
|
+
body.packageName ?? null,
|
|
1068
|
+
);
|
|
1069
|
+
if (refusal) {
|
|
1070
|
+
return reply.code(refusal.statusCode).send({ error: refusal.error });
|
|
554
1071
|
}
|
|
555
1072
|
|
|
556
1073
|
// Log the session
|
|
@@ -563,14 +1080,12 @@ export default async function pluginRoutes(fastify, _opts) {
|
|
|
563
1080
|
status: 'sent',
|
|
564
1081
|
});
|
|
565
1082
|
|
|
566
|
-
// The actual command execution happens on the agent side.
|
|
567
|
-
// This endpoint signals intent; the agent polls or receives via WebSocket.
|
|
568
1083
|
return {
|
|
569
1084
|
ok: true,
|
|
570
1085
|
label: params.label,
|
|
571
1086
|
action: body.action,
|
|
572
1087
|
packageName: body.packageName || null,
|
|
573
|
-
message: `Push install command "${body.action}"
|
|
1088
|
+
message: `Push install command "${body.action}" for agent "${params.label}" recorded in the push-install log; nothing delivers it to the agent`,
|
|
574
1089
|
};
|
|
575
1090
|
},
|
|
576
1091
|
);
|