@lamalibre/create-lamaste 4.4.0 → 4.5.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 +200 -16
- package/src/index.js +1 -0
- package/src/lib/backend-cookie.js +65 -0
- package/src/lib/ip-panel-vhost.js +208 -0
- package/src/lib/service-config.js +5 -3
- package/src/tasks/nginx.js +18 -135
- package/src/tasks/redeploy.js +39 -1
- 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 +1 -1
- package/vendor/docs/01-concepts/authorization.md +66 -48
- package/vendor/docs/01-concepts/certificates.md +3 -3
- package/vendor/docs/01-concepts/mtls.md +2 -2
- package/vendor/docs/01-concepts/security-model.md +70 -33
- 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 +5 -5
- 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/managing-users.md +113 -25
- 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 +3 -1
- package/vendor/docs/03-architecture/nginx-configuration.md +165 -18
- package/vendor/docs/03-architecture/overview.md +13 -17
- package/vendor/docs/03-architecture/panel-client.md +6 -6
- package/vendor/docs/03-architecture/panel-server.md +3 -2
- package/vendor/docs/03-architecture/state-management.md +4 -8
- package/vendor/docs/03-architecture/system-overview.md +1 -1
- package/vendor/docs/04-api-reference/agent-hosted-plugins.md +292 -23
- package/vendor/docs/04-api-reference/app-host.md +3 -3
- package/vendor/docs/04-api-reference/gatekeeper.md +60 -5
- package/vendor/docs/04-api-reference/lifecycle-hooks.md +15 -13
- package/vendor/docs/04-api-reference/onboarding.md +1 -1
- package/vendor/docs/04-api-reference/registry-and-oidc.md +6 -4
- package/vendor/docs/04-api-reference/tunnels.md +67 -29
- package/vendor/docs/04-api-reference/users.md +241 -45
- package/vendor/docs/05-operations/backup-and-restore.md +35 -26
- package/vendor/docs/05-operations/monitoring.md +39 -0
- package/vendor/docs/05-operations/uninstalling.md +2 -1
- package/vendor/docs/05-operations/upgrades.md +15 -6
- package/vendor/docs/06-reference/config-files.md +83 -189
- 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 +25 -26
- package/vendor/docs/06-reference/troubleshooting.md +8 -8
- package/vendor/docs/gatekeeper-design.md +10 -8
- package/vendor/docs/identity-directory-design.md +48 -14
- package/vendor/docs/package.json +1 -1
- package/vendor/docs/todo/lib-cli-desktop-refactor.md +27 -27
- package/vendor/gatekeeper/dist/index.d.ts +2 -1
- package/vendor/gatekeeper/dist/index.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/index.js +2 -0
- package/vendor/gatekeeper/dist/index.js.map +1 -1
- package/vendor/gatekeeper/dist/lib/grants.d.ts +6 -0
- package/vendor/gatekeeper/dist/lib/grants.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/lib/grants.js +6 -1
- package/vendor/gatekeeper/dist/lib/grants.js.map +1 -1
- package/vendor/gatekeeper/dist/lib/groups.d.ts +5 -0
- package/vendor/gatekeeper/dist/lib/groups.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/lib/groups.js +5 -1
- package/vendor/gatekeeper/dist/lib/groups.js.map +1 -1
- package/vendor/gatekeeper/dist/lib/principals.d.ts +29 -0
- package/vendor/gatekeeper/dist/lib/principals.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/principals.js +67 -0
- package/vendor/gatekeeper/dist/lib/principals.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/tunnels.d.ts +13 -0
- package/vendor/gatekeeper/dist/lib/tunnels.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/tunnels.js +68 -0
- package/vendor/gatekeeper/dist/lib/tunnels.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/types.d.ts +19 -0
- package/vendor/gatekeeper/dist/lib/types.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/index.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/index.js +22 -48
- package/vendor/gatekeeper/dist/server/index.js.map +1 -1
- package/vendor/gatekeeper/dist/server/routes/authz.d.ts +15 -1
- package/vendor/gatekeeper/dist/server/routes/authz.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/routes/authz.js +250 -103
- package/vendor/gatekeeper/dist/server/routes/authz.js.map +1 -1
- package/vendor/gatekeeper/dist/server/routes/diagnostic.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/routes/diagnostic.js +61 -4
- package/vendor/gatekeeper/dist/server/routes/diagnostic.js.map +1 -1
- package/vendor/gatekeeper/dist/server/routes/principals.d.ts +3 -0
- package/vendor/gatekeeper/dist/server/routes/principals.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/principals.js +27 -0
- package/vendor/gatekeeper/dist/server/routes/principals.js.map +1 -0
- package/vendor/gatekeeper/package.json +2 -2
- package/vendor/server/bin/lamaste-server.js +15 -1
- package/vendor/server/package.json +2 -2
- package/vendor/server/src/commands/plugin-identity.js +335 -0
- package/vendor/server/src/commands/plugins.js +99 -11
- package/vendor/server/src/commands/registry-credential.js +2 -1
- package/vendor/server/src/commands/tunnels.js +102 -2
- package/vendor/server/src/commands/uninstall.js +3 -1
- package/vendor/server/src/commands/users.js +358 -0
- package/vendor/server/src/plugin-approval.js +107 -5
- package/vendor/server/src/plugin-identity.js +382 -0
- package/vendor/server-ui/dist/assets/index-D-fer5ef.css +1 -0
- package/vendor/server-ui/dist/assets/index-DSaDPJaw.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 +25 -1
- package/vendor/serverd/src/lib/audience.js +576 -0
- package/vendor/serverd/src/lib/authelia.js +78 -7
- package/vendor/serverd/src/lib/config.js +68 -43
- package/vendor/serverd/src/lib/directory.js +1113 -0
- package/vendor/serverd/src/lib/entitlements.js +517 -0
- package/vendor/serverd/src/lib/gatekeeper-client.js +215 -10
- package/vendor/serverd/src/lib/hook-pipelines.js +13 -0
- package/vendor/serverd/src/lib/identity-sync.js +905 -0
- package/vendor/serverd/src/lib/migrations/state/0013_identity_directory.sql +158 -0
- package/vendor/serverd/src/lib/migrations/state/0014_identity_ledger.sql +20 -0
- package/vendor/serverd/src/lib/mtls.js +6 -2
- package/vendor/serverd/src/lib/nginx.js +279 -48
- package/vendor/serverd/src/lib/plugin-hooks.js +98 -45
- package/vendor/serverd/src/lib/plugin-hosting.js +33 -5
- package/vendor/serverd/src/lib/plugin-lifecycle.js +100 -9
- package/vendor/serverd/src/lib/plugin-packages.js +30 -22
- package/vendor/serverd/src/lib/plugins.js +94 -15
- package/vendor/serverd/src/lib/registry-credential.js +46 -6
- package/vendor/serverd/src/lib/request-logging.js +19 -0
- package/vendor/serverd/src/lib/retained-data.js +50 -4
- package/vendor/serverd/src/lib/state.js +18 -0
- package/vendor/serverd/src/lib/tunnel-hooks.js +4 -1
- package/vendor/serverd/src/lib/user-access-session.js +21 -11
- package/vendor/serverd/src/lib/user-caller.js +67 -0
- package/vendor/serverd/src/lib/user-surface.js +733 -0
- package/vendor/serverd/src/lib/vhost-reconcile.js +136 -34
- package/vendor/serverd/src/middleware/user-access-session.js +9 -0
- package/vendor/serverd/src/routes/agent-forwarder.js +4 -2
- package/vendor/serverd/src/routes/invite.js +53 -12
- package/vendor/serverd/src/routes/management/gatekeeper-proxy.js +107 -16
- package/vendor/serverd/src/routes/management/identity-directory.js +79 -0
- package/vendor/serverd/src/routes/management/invitations.js +78 -68
- package/vendor/serverd/src/routes/management/plugin-identity.js +221 -0
- package/vendor/serverd/src/routes/management/registry-credential.js +1 -1
- package/vendor/serverd/src/routes/management/tunnels.js +98 -2
- package/vendor/serverd/src/routes/management/users.js +290 -208
- package/vendor/serverd/src/routes/management.js +4 -0
- package/vendor/serverd/src/routes/onboarding/provision.js +14 -1
- package/vendor/serverd/src/routes/user-access.js +17 -4
- package/vendor/serverd/src/routes/user-surface.js +69 -0
- package/vendor/server-ui/dist/assets/index-25wo_8av.css +0 -1
- package/vendor/server-ui/dist/assets/index-CESyyK8V.js +0 -84
|
@@ -5,14 +5,17 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Approval binds bytes (see `hook-set.ts` in the core library): one digest
|
|
7
7
|
* (`approvalDigest`) covers the relay hook set — the normalised relay
|
|
8
|
-
* declarations and the SHA-256 of the relay entry module —
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
8
|
+
* declarations and the SHA-256 of the relay entry module — the agent half
|
|
9
|
+
* Lamaste installs onto the hosting agent — its package, version, tarball
|
|
10
|
+
* integrity, service and system packages — and the plugin's identity block
|
|
11
|
+
* — the roles its users hold and the attributes it receives about them.
|
|
12
|
+
* Approval is needed when any exists. Lamaste runs relay hooks, hosts the
|
|
13
|
+
* plugin, installs its agent half and sends it its users only while the
|
|
14
|
+
* installed version's digest is the approved one. Settings are outside the
|
|
15
|
+
* digest, but an update that turns a secret setting into a plain one waits
|
|
16
|
+
* for an administrator's approval all the same (`plugin-lifecycle.js`), and
|
|
17
|
+
* so does one that adds or removes a role or widens the attributes: the
|
|
18
|
+
* same pending update, approved here.
|
|
16
19
|
*
|
|
17
20
|
* The runner is about the relay part alone: it holds a root-owned copy of
|
|
18
21
|
* the approved module that lamaste-priv installed after checking its
|
|
@@ -216,22 +219,27 @@ export function installedAgentHalf(plugin) {
|
|
|
216
219
|
}
|
|
217
220
|
|
|
218
221
|
/**
|
|
219
|
-
* What approving a plugin version covers — its relay hook set
|
|
220
|
-
*
|
|
221
|
-
*
|
|
222
|
+
* What approving a plugin version covers — its relay hook set, the agent
|
|
223
|
+
* half Lamaste installs and its identity block (normalized, as the registry
|
|
224
|
+
* stores it) — and the digest the approval names. `digest` is null when
|
|
225
|
+
* there is nothing to approve. A plugin without an identity block has the
|
|
226
|
+
* digest it had before identity existed.
|
|
222
227
|
*/
|
|
223
228
|
export function installedApproval(plugin) {
|
|
224
229
|
const relay = installedRelayHooks(plugin);
|
|
225
230
|
const agentHalf = installedAgentHalf(plugin);
|
|
226
|
-
const
|
|
231
|
+
const identity = plugin.identity ?? null;
|
|
232
|
+
const required = relay !== null || agentHalf !== null || identity !== null;
|
|
227
233
|
return {
|
|
228
234
|
relay,
|
|
229
235
|
agentHalf,
|
|
236
|
+
identity,
|
|
230
237
|
required,
|
|
231
238
|
digest: required
|
|
232
239
|
? approvalDigest({
|
|
233
240
|
relay: relay ? { set: relay.set, entrySha256: relay.entrySha256 } : null,
|
|
234
241
|
agentHalf,
|
|
242
|
+
identity,
|
|
235
243
|
})
|
|
236
244
|
: null,
|
|
237
245
|
};
|
|
@@ -243,9 +251,10 @@ function isApproved(approval, state) {
|
|
|
243
251
|
}
|
|
244
252
|
|
|
245
253
|
/**
|
|
246
|
-
* Whether the installed version is approved — it brings
|
|
247
|
-
*
|
|
248
|
-
* Relay hooks run,
|
|
254
|
+
* Whether the installed version is approved — it brings no relay hooks, no
|
|
255
|
+
* agent half Lamaste installs and no identity block, or its digest is the
|
|
256
|
+
* approved one. Relay hooks run, the plugin is hosted and its users are
|
|
257
|
+
* sent to it only then.
|
|
249
258
|
*/
|
|
250
259
|
export async function installedVersionApproved(plugin) {
|
|
251
260
|
const approval = installedApproval(plugin);
|
|
@@ -253,17 +262,34 @@ export async function installedVersionApproved(plugin) {
|
|
|
253
262
|
return isApproved(approval, await readHookState(plugin.name));
|
|
254
263
|
}
|
|
255
264
|
|
|
256
|
-
/**
|
|
265
|
+
/**
|
|
266
|
+
* What the panel shows about a plugin's hooks, its agent half, its identity
|
|
267
|
+
* (`identity`: whether the installed version declares it, whether that
|
|
268
|
+
* version is approved, and the normalized block) and their approval.
|
|
269
|
+
*/
|
|
257
270
|
export async function hookStatus(plugin) {
|
|
258
271
|
const approval = installedApproval(plugin);
|
|
259
|
-
const { relay, agentHalf } = approval;
|
|
272
|
+
const { relay, agentHalf, identity } = approval;
|
|
260
273
|
const agent = agentHookSetOf({ hooks: plugin.hooks });
|
|
261
274
|
const state = await readHookState(plugin.name);
|
|
262
275
|
const approved = isApproved(approval, state);
|
|
276
|
+
let pendingUpdate = null;
|
|
277
|
+
if (state?.pendingUpdate) {
|
|
278
|
+
const { withIdentityGrantImpact } = await import('./entitlements.js');
|
|
279
|
+
pendingUpdate = await withIdentityGrantImpact(
|
|
280
|
+
pendingUpdateView(state.pendingUpdate),
|
|
281
|
+
plugin.route,
|
|
282
|
+
);
|
|
283
|
+
}
|
|
263
284
|
return {
|
|
264
285
|
relay: relay ? relayHookSetPreview(relay.set, relay.entrySha256) : null,
|
|
265
286
|
agent,
|
|
266
287
|
agentHalf: agentHalf ? agentHalfPreview(derivePluginRoute(plugin.name), agentHalf) : null,
|
|
288
|
+
identity: {
|
|
289
|
+
declared: identity !== null,
|
|
290
|
+
approved: identity !== null && approved,
|
|
291
|
+
block: identity,
|
|
292
|
+
},
|
|
267
293
|
approval: {
|
|
268
294
|
required: approval.required,
|
|
269
295
|
state: !approval.required ? 'not-required' : approved ? 'approved' : 'pending',
|
|
@@ -279,7 +305,7 @@ export async function hookStatus(plugin) {
|
|
|
279
305
|
installedDigest: state?.installedDigest ?? null,
|
|
280
306
|
current: relay !== null && approved && state?.installedDigest === relay.digest,
|
|
281
307
|
},
|
|
282
|
-
pendingUpdate
|
|
308
|
+
pendingUpdate,
|
|
283
309
|
};
|
|
284
310
|
}
|
|
285
311
|
|
|
@@ -287,22 +313,29 @@ export async function hookStatus(plugin) {
|
|
|
287
313
|
export const DIGEST_UNAPPROVED = 'digest.unapproved';
|
|
288
314
|
/** Why an update waits: it turns secret settings into plain ones. */
|
|
289
315
|
export const SETTINGS_SECRET_TO_PLAIN = 'settings.secret-to-plain';
|
|
316
|
+
/** Why an update waits: it adds or removes a role, or widens the attributes it receives. */
|
|
317
|
+
export const IDENTITY_CHANGED = 'identity.changed';
|
|
290
318
|
|
|
291
319
|
/**
|
|
292
320
|
* The reason a staged version waits because what it brings — its relay
|
|
293
|
-
* hooks (`relay`), the agent half Lamaste installs (`agentHalf`)
|
|
294
|
-
* truthy when it brings one — is not what
|
|
321
|
+
* hooks (`relay`), the agent half Lamaste installs (`agentHalf`), its
|
|
322
|
+
* identity block (`identity`); each truthy when it brings one — is not what
|
|
323
|
+
* was approved.
|
|
295
324
|
*
|
|
296
325
|
* @returns {{ code: string, reason: string }}
|
|
297
326
|
*/
|
|
298
|
-
export function digestUnapprovedReason({ relay, agentHalf }) {
|
|
299
|
-
const
|
|
300
|
-
relay &&
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
327
|
+
export function digestUnapprovedReason({ relay, agentHalf, identity = null }) {
|
|
328
|
+
const parts = [
|
|
329
|
+
...(relay || (!agentHalf && !identity) ? ['relay hooks'] : []),
|
|
330
|
+
...(agentHalf ? ['agent half'] : []),
|
|
331
|
+
...(identity ? ['identity'] : []),
|
|
332
|
+
];
|
|
333
|
+
const named =
|
|
334
|
+
parts.length === 1
|
|
335
|
+
? parts[0]
|
|
336
|
+
: `${parts.slice(0, -1).join(', ')} and ${parts[parts.length - 1]}`;
|
|
337
|
+
const verb = parts.length === 1 && parts[0] !== 'relay hooks' ? 'is' : 'are';
|
|
338
|
+
return { code: DIGEST_UNAPPROVED, reason: `its ${named} ${verb} not what was approved` };
|
|
306
339
|
}
|
|
307
340
|
|
|
308
341
|
/**
|
|
@@ -314,7 +347,11 @@ export function digestUnapprovedReason({ relay, agentHalf }) {
|
|
|
314
347
|
function pendingApprovalRequired(pending) {
|
|
315
348
|
return (
|
|
316
349
|
pending.approvalRequired ?? [
|
|
317
|
-
digestUnapprovedReason({
|
|
350
|
+
digestUnapprovedReason({
|
|
351
|
+
relay: pending.preview,
|
|
352
|
+
agentHalf: pending.agentHalf,
|
|
353
|
+
identity: pending.identity?.after ?? null,
|
|
354
|
+
}),
|
|
318
355
|
]
|
|
319
356
|
);
|
|
320
357
|
}
|
|
@@ -323,14 +360,18 @@ function pendingApprovalRequired(pending) {
|
|
|
323
360
|
* A staged update waiting for approval as clients see it — in the hook
|
|
324
361
|
* status and in the update response alike: its version, the digest to
|
|
325
362
|
* approve it with (null when it brings neither relay hooks nor an agent
|
|
326
|
-
* half Lamaste installs: it is approved by its
|
|
327
|
-
* (its relay hook set and the agent half Lamaste
|
|
328
|
-
* it brings none), how it changes the settings
|
|
329
|
-
* `{ added: [{ key, type }], removed: [{ key, type }],
|
|
330
|
-
* from, to }], newRequired: [key], secretToPlain: [key] }`,
|
|
331
|
-
* neither version declares settings — not part of the digest),
|
|
332
|
-
*
|
|
333
|
-
*
|
|
363
|
+
* half Lamaste installs nor an identity block: it is approved by its
|
|
364
|
+
* version), what it brings (its relay hook set and the agent half Lamaste
|
|
365
|
+
* installs, each null when it brings none), how it changes the settings
|
|
366
|
+
* schema (`settings`: `{ added: [{ key, type }], removed: [{ key, type }],
|
|
367
|
+
* retyped: [{ key, from, to }], newRequired: [key], secretToPlain: [key] }`,
|
|
368
|
+
* null when neither version declares settings — not part of the digest),
|
|
369
|
+
* how it changes the identity block (`identity`: `{ before, after,
|
|
370
|
+
* rolesAdded, rolesRemoved, attributesAdded, attributesRemoved,
|
|
371
|
+
* labelsChanged }` with the normalized blocks, null when neither version
|
|
372
|
+
* declares one — part of the digest), why it waits for an administrator
|
|
373
|
+
* (`approvalRequired`: `[{ code, reason }]`, `digest.unapproved`,
|
|
374
|
+
* `settings.secret-to-plain` and `identity.changed`; never empty), and —
|
|
334
375
|
* when the plugin has auto-approve on — why auto-approve did not cover it
|
|
335
376
|
* (`autoApproveWithheld`, sentences; null otherwise). The stored record
|
|
336
377
|
* names the relay hook set's preview `preview`.
|
|
@@ -343,6 +384,7 @@ export function pendingUpdateView(pending) {
|
|
|
343
384
|
relay: pending.preview ?? null,
|
|
344
385
|
agentHalf: pending.agentHalf ?? null,
|
|
345
386
|
settings: pending.settings ?? null,
|
|
387
|
+
identity: pending.identity ?? null,
|
|
346
388
|
approvalRequired: pendingApprovalRequired(pending),
|
|
347
389
|
autoApproveWithheld: pending.autoApproveWithheld ?? null,
|
|
348
390
|
};
|
|
@@ -462,12 +504,14 @@ export async function restartRelayHookRunner(plugin, logger) {
|
|
|
462
504
|
|
|
463
505
|
/**
|
|
464
506
|
* An administrator approves what a plugin version brings — its relay hook
|
|
465
|
-
* set
|
|
466
|
-
* shown: the installed version's, or a pending update's.
|
|
467
|
-
* is also approved by naming its `version`, with its
|
|
468
|
-
* one — the only way to approve one that brings neither
|
|
469
|
-
*
|
|
470
|
-
* settings change to the release
|
|
507
|
+
* set, the agent half Lamaste installs and its identity block — with the
|
|
508
|
+
* digest they were shown: the installed version's, or a pending update's.
|
|
509
|
+
* A pending update is also approved by naming its `version`, with its
|
|
510
|
+
* digest when it has one — the only way to approve one that brings neither
|
|
511
|
+
* relay hooks nor an agent half nor an identity block (its digest is
|
|
512
|
+
* null), and what binds the approval of a settings change to the release
|
|
513
|
+
* that was shown. Approving an installed version that declares identity
|
|
514
|
+
* computes its audience and sends it to its agent half.
|
|
471
515
|
*
|
|
472
516
|
* The installed version's approval is recorded here, in the hook state and
|
|
473
517
|
* the audit log. A pending update's is checked against the update that is
|
|
@@ -506,7 +550,7 @@ export async function approveHookSet(
|
|
|
506
550
|
const pendingDigest = pending.digest ?? null;
|
|
507
551
|
if (digest === null && pendingDigest !== null) {
|
|
508
552
|
throw httpError(
|
|
509
|
-
`Version ${version} brings relay hooks
|
|
553
|
+
`Version ${version} brings relay hooks, an agent half or an identity block — approve it with the approval digest it shows`,
|
|
510
554
|
400,
|
|
511
555
|
);
|
|
512
556
|
}
|
|
@@ -530,7 +574,7 @@ export async function approveHookSet(
|
|
|
530
574
|
if (pending && pending.digest === digest) return pendingUpdateApproval(pending);
|
|
531
575
|
if (!approval.required) {
|
|
532
576
|
throw httpError(
|
|
533
|
-
`Plugin "${plugin.name}" has no relay hooks
|
|
577
|
+
`Plugin "${plugin.name}" has no relay hooks, no agent half Lamaste installs and no identity block — nothing to approve`,
|
|
534
578
|
400,
|
|
535
579
|
);
|
|
536
580
|
}
|
|
@@ -553,6 +597,15 @@ export async function approveHookSet(
|
|
|
553
597
|
details: { digest, version: plugin.version, for: 'installed', certSerial },
|
|
554
598
|
});
|
|
555
599
|
await ensureRelayHooksInstalled(plugin, logger);
|
|
600
|
+
// An approved identity block lets the plugin's users through: its
|
|
601
|
+
// audience is computed and sent to its agent half (when it is hosted), and
|
|
602
|
+
// the hosts its user surface is bound to start serving it.
|
|
603
|
+
if (approval.identity) {
|
|
604
|
+
const { refreshPluginIdentity } = await import('./identity-sync.js');
|
|
605
|
+
await refreshPluginIdentity(plugin.name, logger);
|
|
606
|
+
const { refreshUserSurfaceVhosts } = await import('./user-surface.js');
|
|
607
|
+
await refreshUserSurfaceVhosts(plugin.route, logger);
|
|
608
|
+
}
|
|
556
609
|
return { target: 'installed', digest };
|
|
557
610
|
}
|
|
558
611
|
|
|
@@ -69,6 +69,8 @@ import { recordAudit } from './audit.js';
|
|
|
69
69
|
import { onHealthTransition } from './tunnel-hooks.js';
|
|
70
70
|
import { agentLastSeenAt, agentPresenceMap } from './agent-presence.js';
|
|
71
71
|
import { refreshPluginSettingsBestEffort } from './plugin-settings.js';
|
|
72
|
+
import { refreshPluginIdentity } from './identity-sync.js';
|
|
73
|
+
import { refreshUserSurfaceVhosts } from './user-surface.js';
|
|
72
74
|
|
|
73
75
|
const STATE_DIR = process.env.LAMALIBRE_LAMASTE_STATE_DIR || '/etc/lamalibre/lamaste';
|
|
74
76
|
|
|
@@ -311,7 +313,7 @@ export async function hostPlugin(name, agentLabel, { actor, logger, sourceIp })
|
|
|
311
313
|
}
|
|
312
314
|
if (!(await installedVersionApproved(plugin))) {
|
|
313
315
|
throw httpError(
|
|
314
|
-
`Plugin "${name}" brings relay hooks
|
|
316
|
+
`Plugin "${name}" brings relay hooks, an agent half or an identity block that are not approved — review and approve them first`,
|
|
315
317
|
409,
|
|
316
318
|
{ code: 'hooks_not_approved' },
|
|
317
319
|
);
|
|
@@ -339,9 +341,17 @@ export async function hostPlugin(name, agentLabel, { actor, logger, sourceIp })
|
|
|
339
341
|
logger,
|
|
340
342
|
run: async (op) => {
|
|
341
343
|
const result = await runInstallPipeline(op, ctx);
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
344
|
+
if (result.status === 'succeeded') {
|
|
345
|
+
// The agent half starts without settings — or with those it kept on
|
|
346
|
+
// this agent from an earlier hosting: the relay's cache follows it.
|
|
347
|
+
await refreshPluginSettingsBestEffort(plugin.name, logger);
|
|
348
|
+
// A plugin that declares identity learns its users now: its first
|
|
349
|
+
// sync replays the whole audience.
|
|
350
|
+
await refreshPluginIdentity(plugin.name, logger);
|
|
351
|
+
// Its user surface's locations take the body limit of the internal
|
|
352
|
+
// tunnel the hosting brought.
|
|
353
|
+
await refreshUserSurfaceVhosts(plugin.route, logger);
|
|
354
|
+
}
|
|
345
355
|
return result;
|
|
346
356
|
},
|
|
347
357
|
});
|
|
@@ -528,6 +538,9 @@ export async function purgeRetainedData(name, agentLabel, { actor, logger, sourc
|
|
|
528
538
|
plugin: name,
|
|
529
539
|
details: { agentLabel, route: retained.route, package: retained.package },
|
|
530
540
|
});
|
|
541
|
+
// The last data of an uninstalled plugin: its identity ledger goes too.
|
|
542
|
+
const { dropIdentityLedgerIfNoData } = await import('./audience.js');
|
|
543
|
+
await dropIdentityLedgerIfNoData(name, logger);
|
|
531
544
|
return { forgotten: retained };
|
|
532
545
|
}
|
|
533
546
|
if (!(await isActiveAgent(agentLabel))) {
|
|
@@ -613,7 +626,19 @@ export async function acknowledgeAgentPluginConfig(label, route, issuedAt) {
|
|
|
613
626
|
// ---------------------------------------------------------------------------
|
|
614
627
|
|
|
615
628
|
/**
|
|
616
|
-
* Resolve a route to a forward target
|
|
629
|
+
* Resolve a route to a forward target — of the admin forwarder
|
|
630
|
+
* (`agentForwarderPlugin`) and the user surface alike.
|
|
631
|
+
*
|
|
632
|
+
* While the hosting's health is `down` — the monitor's probes found nothing
|
|
633
|
+
* on the relay-loopback port that answers with the installation secret —
|
|
634
|
+
* this refuses with `down` and the health's reason (`tunnel` or `app`), so
|
|
635
|
+
* nothing of an administrator's, an agent's or a user's request reaches
|
|
636
|
+
* whatever holds that port: the admin forwarder answers as for a dead
|
|
637
|
+
* tunnel (the `tunnel:down` fallback, or `502` with the reason), the user
|
|
638
|
+
* surface `503`. Lamaste's own calls — the health probe, settings, hooks,
|
|
639
|
+
* the identity feed — do not come this way, so the monitor still finds the
|
|
640
|
+
* agent half when it is back and the hosting comes up again.
|
|
641
|
+
*
|
|
617
642
|
* @param {string} route
|
|
618
643
|
*/
|
|
619
644
|
export async function resolveForwardTarget(route) {
|
|
@@ -623,6 +648,9 @@ export async function resolveForwardTarget(route) {
|
|
|
623
648
|
if (plugin.status !== 'enabled') return { kind: 'disabled' };
|
|
624
649
|
const record = await ownHosting(plugin);
|
|
625
650
|
if (!record || pluginHostingPhase(record) !== 'active') return { kind: 'not_hosted' };
|
|
651
|
+
if (record.health?.state === 'down') {
|
|
652
|
+
return { kind: 'down', reason: record.health.reason === 'app' ? 'app' : 'tunnel' };
|
|
653
|
+
}
|
|
626
654
|
const tunnels = await readTunnels();
|
|
627
655
|
const tunnel = tunnels.find((t) => t.id === record.tunnelId);
|
|
628
656
|
const maxBodyMb = tunnel?.maxBodySizeMb ?? TUNNEL_MAX_BODY_MB.default;
|
|
@@ -18,7 +18,11 @@
|
|
|
18
18
|
* a version that turns a secret setting into a plain one always waits for
|
|
19
19
|
* an administrator's approval, its digest approved or not, and whatever
|
|
20
20
|
* auto-approve says (`pendingUpdate.approvalRequired` says why an update
|
|
21
|
-
* waits).
|
|
21
|
+
* waits). The identity block is inside the digest; a version that adds or
|
|
22
|
+
* removes a role or widens the attributes its users' records carry waits
|
|
23
|
+
* for an administrator too, whatever auto-approve says
|
|
24
|
+
* (`identity.changed`; `pendingUpdate.identity` shows the change), and
|
|
25
|
+
* after the update the audiences are recomputed and the agent half told.
|
|
22
26
|
*/
|
|
23
27
|
|
|
24
28
|
import {
|
|
@@ -27,6 +31,8 @@ import {
|
|
|
27
31
|
derivePluginRoute,
|
|
28
32
|
hasAgentHalf,
|
|
29
33
|
hookEntrySha256,
|
|
34
|
+
identityChangeNeedsApproval,
|
|
35
|
+
identityDiff,
|
|
30
36
|
relayHookSetNeedsEntry,
|
|
31
37
|
relayHookSetOf,
|
|
32
38
|
relayHookSetPreview,
|
|
@@ -39,6 +45,7 @@ import { setPluginCapabilities } from './mtls.js';
|
|
|
39
45
|
import {
|
|
40
46
|
findPlugin,
|
|
41
47
|
getPluginCapabilities,
|
|
48
|
+
identityOf,
|
|
42
49
|
manifestPolicyError,
|
|
43
50
|
replacePluginPackage,
|
|
44
51
|
uninstallPlugin,
|
|
@@ -47,6 +54,7 @@ import {
|
|
|
47
54
|
import { pinAgentHalf, readRegistryPackage } from './plugin-packages.js';
|
|
48
55
|
import {
|
|
49
56
|
DIGEST_UNAPPROVED,
|
|
57
|
+
IDENTITY_CHANGED,
|
|
50
58
|
SETTINGS_SECRET_TO_PLAIN,
|
|
51
59
|
digestUnapprovedReason,
|
|
52
60
|
installRunnerModule,
|
|
@@ -82,6 +90,8 @@ import { getConfig } from './config.js';
|
|
|
82
90
|
import { recordAudit, recordAuditBestEffort } from './audit.js';
|
|
83
91
|
import { notifyAdminsBestEffort } from './notifications.js';
|
|
84
92
|
import { refreshPluginSettingsBestEffort, settingsBlockOf } from './plugin-settings.js';
|
|
93
|
+
import { refreshPluginIdentity } from './identity-sync.js';
|
|
94
|
+
import { refreshUserSurfaceVhosts } from './user-surface.js';
|
|
85
95
|
|
|
86
96
|
/** Replaces the OIDC client sync (tests: there is no Authelia to write to). */
|
|
87
97
|
let oidcSyncOverride = null;
|
|
@@ -163,6 +173,16 @@ export async function uninstallPluginWithHosting(
|
|
|
163
173
|
}
|
|
164
174
|
await op.step('remove the package', () => uninstallPlugin(name, logger));
|
|
165
175
|
setPluginCapabilities(await getPluginCapabilities());
|
|
176
|
+
// With purge nothing of the plugin is kept — not who held its roles.
|
|
177
|
+
if (purge) {
|
|
178
|
+
const { revokePluginRoleGrants } = await import('./entitlements.js');
|
|
179
|
+
const revoked = await revokePluginRoleGrants(plugin, { actor, logger });
|
|
180
|
+
if (!revoked.ok) {
|
|
181
|
+
op.warn(
|
|
182
|
+
`The plugin's role grants were not revoked (${revoked.error}) — revoke them in Gatekeeper`,
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
166
186
|
await recordAuditBestEffort(
|
|
167
187
|
{ actor, action: 'plugin.uninstalled', plugin: name, details: { operation: op.id } },
|
|
168
188
|
logger,
|
|
@@ -261,6 +281,32 @@ export function settingsApprovalRequired(diff) {
|
|
|
261
281
|
];
|
|
262
282
|
}
|
|
263
283
|
|
|
284
|
+
/**
|
|
285
|
+
* Why a version waits for an administrator because of how it changes the
|
|
286
|
+
* plugin's identity block (`identity.changed`): it adds or removes a role —
|
|
287
|
+
* entitlements name roles, and a new role is a new power over its users —
|
|
288
|
+
* or widens the attributes it receives about them. Narrowing the
|
|
289
|
+
* attributes and editing labels only change the digest. Auto-approve never
|
|
290
|
+
* covers it.
|
|
291
|
+
*
|
|
292
|
+
* @param {(import('@lamalibre/lamaste').IdentityDiff & { before: object | null, after: object | null }) | null} identity
|
|
293
|
+
* @returns {Array<{ code: string, reason: string }>}
|
|
294
|
+
*/
|
|
295
|
+
function identityApprovalRequired(identity) {
|
|
296
|
+
if (!identity || !identityChangeNeedsApproval(identity)) return [];
|
|
297
|
+
const changes = [];
|
|
298
|
+
if (identity.rolesAdded.length > 0) {
|
|
299
|
+
changes.push(`adds roles (${identity.rolesAdded.join(', ')})`);
|
|
300
|
+
}
|
|
301
|
+
if (identity.rolesRemoved.length > 0) {
|
|
302
|
+
changes.push(`removes roles (${identity.rolesRemoved.join(', ')})`);
|
|
303
|
+
}
|
|
304
|
+
if (identity.attributesAdded.length > 0) {
|
|
305
|
+
changes.push(`receives more about its users (${identity.attributesAdded.join(', ')})`);
|
|
306
|
+
}
|
|
307
|
+
return [{ code: IDENTITY_CHANGED, reason: `it ${changes.join(' and ')}` }];
|
|
308
|
+
}
|
|
309
|
+
|
|
264
310
|
/**
|
|
265
311
|
* Why a staged version waits for an administrator's approval (`[{ code,
|
|
266
312
|
* reason }]`; empty: it needs none and runs at once) — what the approval of
|
|
@@ -268,21 +314,30 @@ export function settingsApprovalRequired(diff) {
|
|
|
268
314
|
* in for an administrator on the first reason alone:
|
|
269
315
|
*
|
|
270
316
|
* - `digest.unapproved` — what it brings (relay hooks, the agent half
|
|
271
|
-
* Lamaste installs) is not what was approved;
|
|
272
|
-
* - `settings.secret-to-plain` — {@link settingsApprovalRequired}
|
|
317
|
+
* Lamaste installs, its identity block) is not what was approved;
|
|
318
|
+
* - `settings.secret-to-plain` — {@link settingsApprovalRequired};
|
|
319
|
+
* - `identity.changed` — {@link identityApprovalRequired}.
|
|
273
320
|
*
|
|
274
|
-
* @param {{ approvalRequired: boolean, digest: string | null, relaySet: object | null, agentHalf: object | null }} staged
|
|
321
|
+
* @param {{ approvalRequired: boolean, digest: string | null, relaySet: object | null, agentHalf: object | null, identity: object | null }} staged
|
|
275
322
|
* @param {{ approvedDigest: string | null } | null} state
|
|
276
323
|
* @param {import('@lamalibre/lamaste').SettingsSchemaDiff | null} settings
|
|
324
|
+
* @param {(import('@lamalibre/lamaste').IdentityDiff & { before: object | null, after: object | null }) | null} identity
|
|
277
325
|
* @returns {Array<{ code: string, reason: string }>}
|
|
278
326
|
*/
|
|
279
|
-
function approvalRequiredFor(staged, state, settings) {
|
|
327
|
+
function approvalRequiredFor(staged, state, settings, identity) {
|
|
280
328
|
const digestApproved = !staged.approvalRequired || state?.approvedDigest === staged.digest;
|
|
281
329
|
return [
|
|
282
330
|
...(digestApproved
|
|
283
331
|
? []
|
|
284
|
-
: [
|
|
332
|
+
: [
|
|
333
|
+
digestUnapprovedReason({
|
|
334
|
+
relay: staged.relaySet,
|
|
335
|
+
agentHalf: staged.agentHalf,
|
|
336
|
+
identity: staged.identity,
|
|
337
|
+
}),
|
|
338
|
+
]),
|
|
285
339
|
...settingsApprovalRequired(settings),
|
|
340
|
+
...identityApprovalRequired(identity),
|
|
286
341
|
];
|
|
287
342
|
}
|
|
288
343
|
|
|
@@ -401,6 +456,7 @@ async function stageVersion(plugin, requestedVersion, record) {
|
|
|
401
456
|
agent: manifest.agent,
|
|
402
457
|
agentHalfPin,
|
|
403
458
|
settings: manifest.settings,
|
|
459
|
+
identity: identityOf(manifest),
|
|
404
460
|
};
|
|
405
461
|
const approval = installedApproval(entry);
|
|
406
462
|
return {
|
|
@@ -412,6 +468,8 @@ async function stageVersion(plugin, requestedVersion, record) {
|
|
|
412
468
|
entrySha256,
|
|
413
469
|
agentHalfPin,
|
|
414
470
|
agentHalf: approval.agentHalf,
|
|
471
|
+
// The staged identity block, normalized (null: it declares none).
|
|
472
|
+
identity: approval.identity,
|
|
415
473
|
// The relay hook set's own digest — what the runner holds.
|
|
416
474
|
relayDigest: approval.relay?.digest ?? null,
|
|
417
475
|
approvalRequired: approval.required,
|
|
@@ -439,6 +497,23 @@ function settingsPreview(plugin, manifest) {
|
|
|
439
497
|
return settingsSchemaDiff(installed, staged);
|
|
440
498
|
}
|
|
441
499
|
|
|
500
|
+
/**
|
|
501
|
+
* How a staged version changes the identity block, for the update's
|
|
502
|
+
* preview: both blocks, normalized (null: that version declares none), and
|
|
503
|
+
* core's `identityDiff` — roles added and removed, attributes added and
|
|
504
|
+
* removed, roles whose label or description changed. Null when neither
|
|
505
|
+
* version declares one.
|
|
506
|
+
*
|
|
507
|
+
* @param {object} plugin the installed registry entry
|
|
508
|
+
* @param {object} manifest the staged version's manifest
|
|
509
|
+
*/
|
|
510
|
+
function identityPreview(plugin, manifest) {
|
|
511
|
+
const before = plugin.identity ?? null;
|
|
512
|
+
const after = identityOf(manifest) ?? null;
|
|
513
|
+
if (!before && !after) return null;
|
|
514
|
+
return { before, after, ...identityDiff(before, after) };
|
|
515
|
+
}
|
|
516
|
+
|
|
442
517
|
/**
|
|
443
518
|
* Update plugin `name` to `requestedVersion` (an exact version or a
|
|
444
519
|
* dist-tag; default `latest`). Returns `{ upToDate }`, `{ pendingUpdate }`
|
|
@@ -489,7 +564,8 @@ export async function updatePlugin(
|
|
|
489
564
|
|
|
490
565
|
const state = await readHookState(name);
|
|
491
566
|
const settings = settingsPreview(plugin, staged.manifest);
|
|
492
|
-
const
|
|
567
|
+
const identity = identityPreview(plugin, staged.manifest);
|
|
568
|
+
const approvalRequired = approvalRequiredFor(staged, state, settings, identity);
|
|
493
569
|
let approvedBy = null;
|
|
494
570
|
let autoApproved = false;
|
|
495
571
|
if (approvedUpdate !== null) {
|
|
@@ -499,7 +575,7 @@ export async function updatePlugin(
|
|
|
499
575
|
// Auto-approve stands in for an administrator on a digest that changed,
|
|
500
576
|
// when the agent half changes nothing that runs as root on the agent —
|
|
501
577
|
// and on nothing else: every other reason (a secret setting turned
|
|
502
|
-
// plain) withholds it.
|
|
578
|
+
// plain, a role added or removed, an attribute widened) withholds it.
|
|
503
579
|
const digestChanged = approvalRequired.some((reason) => reason.code === DIGEST_UNAPPROVED);
|
|
504
580
|
const withheldReasons = state?.autoApprove
|
|
505
581
|
? [
|
|
@@ -514,6 +590,7 @@ export async function updatePlugin(
|
|
|
514
590
|
plugin,
|
|
515
591
|
staged,
|
|
516
592
|
settings,
|
|
593
|
+
identity,
|
|
517
594
|
approvalRequired,
|
|
518
595
|
withheldReasons,
|
|
519
596
|
actor,
|
|
@@ -592,6 +669,7 @@ async function awaitApproval({
|
|
|
592
669
|
plugin,
|
|
593
670
|
staged,
|
|
594
671
|
settings,
|
|
672
|
+
identity,
|
|
595
673
|
approvalRequired,
|
|
596
674
|
withheldReasons,
|
|
597
675
|
actor,
|
|
@@ -609,6 +687,7 @@ async function awaitApproval({
|
|
|
609
687
|
? agentHalfPreview(derivePluginRoute(plugin.name), staged.agentHalf)
|
|
610
688
|
: null,
|
|
611
689
|
settings,
|
|
690
|
+
identity,
|
|
612
691
|
approvalRequired,
|
|
613
692
|
...(withheld.length > 0 ? { autoApproveWithheld: withheld } : {}),
|
|
614
693
|
};
|
|
@@ -654,7 +733,10 @@ async function awaitApproval({
|
|
|
654
733
|
},
|
|
655
734
|
logger,
|
|
656
735
|
);
|
|
657
|
-
|
|
736
|
+
const { withIdentityGrantImpact } = await import('./entitlements.js');
|
|
737
|
+
return {
|
|
738
|
+
pendingUpdate: await withIdentityGrantImpact(pendingUpdateView(pendingUpdate), plugin.route),
|
|
739
|
+
};
|
|
658
740
|
}
|
|
659
741
|
|
|
660
742
|
async function applyUpdate(op, { plugin, staged, approvedBy, actor, logger }) {
|
|
@@ -704,6 +786,7 @@ async function applyUpdate(op, { plugin, staged, approvedBy, actor, logger }) {
|
|
|
704
786
|
hooks: installed.manifest.hooks,
|
|
705
787
|
hooksEntrySha256: installed.hooksEntrySha256,
|
|
706
788
|
agent: installed.manifest.agent,
|
|
789
|
+
identity: identityOf(installed.manifest),
|
|
707
790
|
};
|
|
708
791
|
let approval;
|
|
709
792
|
try {
|
|
@@ -760,6 +843,14 @@ async function applyUpdate(op, { plugin, staged, approvedBy, actor, logger }) {
|
|
|
760
843
|
// The agent half may have dropped values the new version retyped or no
|
|
761
844
|
// longer knows (and keeps that after a revert): the relay re-reads them.
|
|
762
845
|
if (hosted) await refreshPluginSettingsBestEffort(name, logger);
|
|
846
|
+
// The roles and attributes may have changed with the version (or back,
|
|
847
|
+
// after a revert); the agent half of the installed version must answer
|
|
848
|
+
// protocol 1 again before its users reach it.
|
|
849
|
+
await refreshPluginIdentity(name, logger);
|
|
850
|
+
// So may whether it declares identity at all: the hosts its user surface
|
|
851
|
+
// is bound to serve it from the version that does — a plugin's
|
|
852
|
+
// switch-over to the relay's user surface is this update.
|
|
853
|
+
await refreshUserSurfaceVhosts(plugin.route, logger);
|
|
763
854
|
|
|
764
855
|
// Hooks the new version no longer has: the old runner goes once the new
|
|
765
856
|
// version is in place.
|