@lamalibre/create-lamaste 4.4.1 → 4.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. package/package.json +2 -2
  2. package/scripts/lamaste-priv +234 -18
  3. package/src/index.js +1 -0
  4. package/src/lib/backend-cookie.js +65 -0
  5. package/src/lib/ip-panel-vhost.js +208 -0
  6. package/src/lib/service-config.js +5 -3
  7. package/src/tasks/nginx.js +18 -135
  8. package/src/tasks/redeploy.js +39 -1
  9. package/vendor/docs/00-introduction/how-it-works.md +2 -2
  10. package/vendor/docs/00-introduction/quickstart.md +5 -5
  11. package/vendor/docs/00-introduction/what-is-lamaste.md +5 -5
  12. package/vendor/docs/01-concepts/authentication.md +1 -1
  13. package/vendor/docs/01-concepts/authorization.md +66 -48
  14. package/vendor/docs/01-concepts/certificates.md +3 -3
  15. package/vendor/docs/01-concepts/mtls.md +2 -2
  16. package/vendor/docs/01-concepts/security-model.md +70 -33
  17. package/vendor/docs/01-concepts/tickets.md +2 -2
  18. package/vendor/docs/01-concepts/tunneling.md +4 -4
  19. package/vendor/docs/02-guides/agent-setup.md +5 -5
  20. package/vendor/docs/02-guides/certificate-management.md +2 -2
  21. package/vendor/docs/02-guides/cloud-provisioning.md +3 -3
  22. package/vendor/docs/02-guides/desktop-app-setup.md +10 -10
  23. package/vendor/docs/02-guides/installation.md +3 -3
  24. package/vendor/docs/02-guides/managing-users.md +113 -25
  25. package/vendor/docs/02-guides/onboarding.md +2 -2
  26. package/vendor/docs/02-guides/standalone-plugin-with-tickets.md +2 -2
  27. package/vendor/docs/02-guides/static-sites.md +4 -0
  28. package/vendor/docs/03-architecture/installer-flow.md +2 -2
  29. package/vendor/docs/03-architecture/installer.md +4 -4
  30. package/vendor/docs/03-architecture/management-flow.md +3 -1
  31. package/vendor/docs/03-architecture/nginx-configuration.md +229 -24
  32. package/vendor/docs/03-architecture/overview.md +13 -17
  33. package/vendor/docs/03-architecture/panel-client.md +6 -6
  34. package/vendor/docs/03-architecture/panel-server.md +3 -2
  35. package/vendor/docs/03-architecture/state-management.md +4 -8
  36. package/vendor/docs/03-architecture/system-overview.md +1 -1
  37. package/vendor/docs/04-api-reference/agent-hosted-plugins.md +378 -34
  38. package/vendor/docs/04-api-reference/app-host.md +3 -3
  39. package/vendor/docs/04-api-reference/gatekeeper.md +78 -5
  40. package/vendor/docs/04-api-reference/lifecycle-hooks.md +14 -12
  41. package/vendor/docs/04-api-reference/onboarding.md +1 -1
  42. package/vendor/docs/04-api-reference/registry-and-oidc.md +3 -3
  43. package/vendor/docs/04-api-reference/sites.md +4 -0
  44. package/vendor/docs/04-api-reference/tunnels.md +87 -47
  45. package/vendor/docs/04-api-reference/users.md +241 -45
  46. package/vendor/docs/05-operations/backup-and-restore.md +35 -26
  47. package/vendor/docs/05-operations/monitoring.md +39 -0
  48. package/vendor/docs/05-operations/uninstalling.md +2 -1
  49. package/vendor/docs/05-operations/upgrades.md +24 -6
  50. package/vendor/docs/06-reference/config-files.md +100 -202
  51. package/vendor/docs/06-reference/glossary.md +3 -3
  52. package/vendor/docs/06-reference/installer-flags.md +13 -13
  53. package/vendor/docs/06-reference/ports-and-services.md +25 -26
  54. package/vendor/docs/06-reference/troubleshooting.md +8 -8
  55. package/vendor/docs/gatekeeper-design.md +10 -8
  56. package/vendor/docs/identity-directory-design.md +48 -14
  57. package/vendor/docs/package.json +1 -1
  58. package/vendor/docs/todo/lib-cli-desktop-refactor.md +27 -27
  59. package/vendor/gatekeeper/dist/cli/index.js +0 -0
  60. package/vendor/gatekeeper/dist/index.d.ts +4 -3
  61. package/vendor/gatekeeper/dist/index.d.ts.map +1 -1
  62. package/vendor/gatekeeper/dist/index.js +4 -2
  63. package/vendor/gatekeeper/dist/index.js.map +1 -1
  64. package/vendor/gatekeeper/dist/lib/authz.d.ts +9 -0
  65. package/vendor/gatekeeper/dist/lib/authz.d.ts.map +1 -1
  66. package/vendor/gatekeeper/dist/lib/authz.js +31 -0
  67. package/vendor/gatekeeper/dist/lib/authz.js.map +1 -1
  68. package/vendor/gatekeeper/dist/lib/constants.d.ts +5 -2
  69. package/vendor/gatekeeper/dist/lib/constants.d.ts.map +1 -1
  70. package/vendor/gatekeeper/dist/lib/constants.js +5 -2
  71. package/vendor/gatekeeper/dist/lib/constants.js.map +1 -1
  72. package/vendor/gatekeeper/dist/lib/grants.d.ts +6 -0
  73. package/vendor/gatekeeper/dist/lib/grants.d.ts.map +1 -1
  74. package/vendor/gatekeeper/dist/lib/grants.js +6 -1
  75. package/vendor/gatekeeper/dist/lib/grants.js.map +1 -1
  76. package/vendor/gatekeeper/dist/lib/groups.d.ts +5 -0
  77. package/vendor/gatekeeper/dist/lib/groups.d.ts.map +1 -1
  78. package/vendor/gatekeeper/dist/lib/groups.js +5 -1
  79. package/vendor/gatekeeper/dist/lib/groups.js.map +1 -1
  80. package/vendor/gatekeeper/dist/lib/principals.d.ts +29 -0
  81. package/vendor/gatekeeper/dist/lib/principals.d.ts.map +1 -0
  82. package/vendor/gatekeeper/dist/lib/principals.js +67 -0
  83. package/vendor/gatekeeper/dist/lib/principals.js.map +1 -0
  84. package/vendor/gatekeeper/dist/lib/tunnels.d.ts +10 -0
  85. package/vendor/gatekeeper/dist/lib/tunnels.d.ts.map +1 -0
  86. package/vendor/gatekeeper/dist/lib/tunnels.js +56 -0
  87. package/vendor/gatekeeper/dist/lib/tunnels.js.map +1 -0
  88. package/vendor/gatekeeper/dist/server/index.d.ts +2 -1
  89. package/vendor/gatekeeper/dist/server/index.d.ts.map +1 -1
  90. package/vendor/gatekeeper/dist/server/index.js +49 -56
  91. package/vendor/gatekeeper/dist/server/index.js.map +1 -1
  92. package/vendor/gatekeeper/dist/server/routes/authz.d.ts +15 -1
  93. package/vendor/gatekeeper/dist/server/routes/authz.d.ts.map +1 -1
  94. package/vendor/gatekeeper/dist/server/routes/authz.js +332 -104
  95. package/vendor/gatekeeper/dist/server/routes/authz.js.map +1 -1
  96. package/vendor/gatekeeper/dist/server/routes/diagnostic.d.ts.map +1 -1
  97. package/vendor/gatekeeper/dist/server/routes/diagnostic.js +61 -4
  98. package/vendor/gatekeeper/dist/server/routes/diagnostic.js.map +1 -1
  99. package/vendor/gatekeeper/dist/server/routes/principals.d.ts +3 -0
  100. package/vendor/gatekeeper/dist/server/routes/principals.d.ts.map +1 -0
  101. package/vendor/gatekeeper/dist/server/routes/principals.js +27 -0
  102. package/vendor/gatekeeper/dist/server/routes/principals.js.map +1 -0
  103. package/vendor/gatekeeper/package.json +2 -2
  104. package/vendor/server/bin/lamaste-server.js +15 -1
  105. package/vendor/server/package.json +2 -2
  106. package/vendor/server/src/commands/plugin-identity.js +335 -0
  107. package/vendor/server/src/commands/plugin-web-surface.js +269 -0
  108. package/vendor/server/src/commands/plugins.js +156 -11
  109. package/vendor/server/src/commands/registry-credential.js +2 -1
  110. package/vendor/server/src/commands/sites.js +15 -1
  111. package/vendor/server/src/commands/tunnels.js +102 -2
  112. package/vendor/server/src/commands/uninstall.js +3 -1
  113. package/vendor/server/src/commands/users.js +358 -0
  114. package/vendor/server/src/plugin-approval.js +186 -5
  115. package/vendor/server/src/plugin-identity.js +382 -0
  116. package/vendor/server/src/plugin-web-surface.js +307 -0
  117. package/vendor/server-ui/dist/assets/index-BZwQ_aQc.css +1 -0
  118. package/vendor/server-ui/dist/assets/index-BvEfCZYS.js +84 -0
  119. package/vendor/server-ui/dist/index.html +2 -2
  120. package/vendor/serverd/package.json +2 -2
  121. package/vendor/serverd/src/index.js +34 -5
  122. package/vendor/serverd/src/lib/audience.js +576 -0
  123. package/vendor/serverd/src/lib/authelia.js +78 -7
  124. package/vendor/serverd/src/lib/chisel-runtime.js +18 -6
  125. package/vendor/serverd/src/lib/chisel.js +10 -2
  126. package/vendor/serverd/src/lib/config.js +68 -43
  127. package/vendor/serverd/src/lib/directory.js +1113 -0
  128. package/vendor/serverd/src/lib/entitlements.js +517 -0
  129. package/vendor/serverd/src/lib/gatekeeper-client.js +226 -10
  130. package/vendor/serverd/src/lib/hook-pipelines.js +21 -3
  131. package/vendor/serverd/src/lib/identity-sync.js +905 -0
  132. package/vendor/serverd/src/lib/migrations/state/0013_identity_directory.sql +158 -0
  133. package/vendor/serverd/src/lib/migrations/state/0014_identity_ledger.sql +20 -0
  134. package/vendor/serverd/src/lib/migrations/state/0015_plugin_web_surface.sql +14 -0
  135. package/vendor/serverd/src/lib/mtls.js +6 -2
  136. package/vendor/serverd/src/lib/nginx.js +546 -48
  137. package/vendor/serverd/src/lib/plugin-hooks.js +127 -45
  138. package/vendor/serverd/src/lib/plugin-hosting.js +33 -5
  139. package/vendor/serverd/src/lib/plugin-lifecycle.js +227 -13
  140. package/vendor/serverd/src/lib/plugin-packages.js +74 -2
  141. package/vendor/serverd/src/lib/plugins.js +95 -7
  142. package/vendor/serverd/src/lib/registry-credential.js +9 -6
  143. package/vendor/serverd/src/lib/request-logging.js +19 -0
  144. package/vendor/serverd/src/lib/retained-data.js +50 -4
  145. package/vendor/serverd/src/lib/state.js +36 -0
  146. package/vendor/serverd/src/lib/tunnel-hooks.js +4 -1
  147. package/vendor/serverd/src/lib/user-access-session.js +21 -11
  148. package/vendor/serverd/src/lib/user-caller.js +67 -0
  149. package/vendor/serverd/src/lib/user-surface.js +754 -0
  150. package/vendor/serverd/src/lib/vhost-reconcile.js +137 -33
  151. package/vendor/serverd/src/lib/web-surface.js +766 -0
  152. package/vendor/serverd/src/middleware/user-access-session.js +9 -0
  153. package/vendor/serverd/src/routes/agent-forwarder.js +4 -2
  154. package/vendor/serverd/src/routes/invite.js +53 -12
  155. package/vendor/serverd/src/routes/management/gatekeeper-proxy.js +107 -16
  156. package/vendor/serverd/src/routes/management/identity-directory.js +79 -0
  157. package/vendor/serverd/src/routes/management/invitations.js +78 -68
  158. package/vendor/serverd/src/routes/management/plugin-identity.js +221 -0
  159. package/vendor/serverd/src/routes/management/plugin-web-surface.js +151 -0
  160. package/vendor/serverd/src/routes/management/plugins.js +2 -0
  161. package/vendor/serverd/src/routes/management/registry-credential.js +1 -1
  162. package/vendor/serverd/src/routes/management/sites.js +143 -65
  163. package/vendor/serverd/src/routes/management/tunnels.js +109 -22
  164. package/vendor/serverd/src/routes/management/users.js +290 -208
  165. package/vendor/serverd/src/routes/management.js +6 -0
  166. package/vendor/serverd/src/routes/onboarding/provision.js +14 -1
  167. package/vendor/serverd/src/routes/user-access.js +17 -4
  168. package/vendor/serverd/src/routes/user-surface.js +69 -0
  169. package/vendor/server-ui/dist/assets/index-25wo_8av.css +0 -1
  170. package/vendor/server-ui/dist/assets/index-roDHz3dM.js +0 -84
@@ -9,16 +9,28 @@
9
9
  * assign <id> --agent <label> Move a tunnel to another agent
10
10
  * configure <id> [--access-mode <m>] [--max-body-mb <n>]
11
11
  * Change access mode / body limit
12
+ * bind-user-surface <tunnel> <plugin> Serve an agent-hosted plugin's user
13
+ * surface (/api/<route>/app/*) on the
14
+ * tunnel's host
15
+ * unbind-user-surface <tunnel> <plugin> Stop serving it there
12
16
  *
13
17
  * List reads directly from the state file.
14
- * Create/delete/toggle use the panel REST API (which has the full
15
- * nginx/certbot/chisel dependency stack).
18
+ * Create/delete/toggle and the user-surface binding use the panel REST API
19
+ * (which has the full nginx/certbot/chisel dependency stack); a tunnel is
20
+ * named by its id, subdomain or FQDN there, a plugin by its name or route.
16
21
  */
17
22
 
18
23
  import chalk from 'chalk';
19
24
  import { readTunnels } from '../state.js';
20
25
  import { panelRequest } from '../panel-api.js';
21
26
  import { emit, emitError, emitComplete } from '../ndjson.js';
27
+ import {
28
+ findHostTunnel,
29
+ findPluginByNameOrRoute,
30
+ parseUserSurfaceArgs,
31
+ userSurfaceStatusText,
32
+ } from '../plugin-identity.js';
33
+ import { terminalText } from '../plugin-settings.js';
22
34
 
23
35
  /**
24
36
  * @param {string[]} args
@@ -40,6 +52,9 @@ export async function runTunnels(args, { json }) {
40
52
  return assignTunnel(args.slice(1), { json });
41
53
  case 'configure':
42
54
  return configureTunnel(args.slice(1), { json });
55
+ case 'bind-user-surface':
56
+ case 'unbind-user-surface':
57
+ return userSurfaceCmd(sub, args.slice(1), { json });
43
58
  default:
44
59
  printTunnelUsage();
45
60
  process.exit(sub ? 1 : 0);
@@ -59,6 +74,13 @@ ${b('Subcommands:')}
59
74
  ${c('toggle')} <id> --enable|--disable Enable or disable a tunnel
60
75
  ${c('assign')} <id> --agent <label> Move a tunnel to another agent
61
76
  ${c('configure')} <id> [--access-mode <m>] [--max-body-mb <n>] Change access mode / body limit
77
+ ${c('bind-user-surface')} <tunnel> <plugin> Serve an agent-hosted plugin's user surface
78
+ (/api/<route>/app/*) on the tunnel's host
79
+ ${c('unbind-user-surface')} <tunnel> <plugin> Stop serving it there
80
+
81
+ A tunnel is named by its id, subdomain or FQDN; a plugin by its name or route. A user
82
+ surface needs an enabled tunnel that is authenticated or restricted, carried by the
83
+ agent that hosts the plugin — it is served only while that agent carries the tunnel.
62
84
 
63
85
  ${b('Create options:')}
64
86
  --subdomain <name> Tunnel subdomain (required)
@@ -108,6 +130,10 @@ async function listTunnels({ json }) {
108
130
  if (t.description) {
109
131
  console.log(` ${d(t.description)}`);
110
132
  }
133
+ const userSurface = Array.isArray(t.userSurface?.routes) ? t.userSurface.routes : [];
134
+ if (userSurface.length > 0) {
135
+ console.log(` ${d('user surface of:')} ${terminalText(userSurface.join(', '))}`);
136
+ }
111
137
  console.log(` ${d(`id: ${t.id} fqdn: ${t.fqdn}`)}`);
112
138
  }
113
139
  console.log('');
@@ -363,3 +389,77 @@ function getArg(args, name) {
363
389
  if (idx === -1 || idx + 1 >= args.length) return null;
364
390
  return args[idx + 1];
365
391
  }
392
+
393
+ /**
394
+ * `bind-user-surface|unbind-user-surface <tunnel> <plugin>`: the tunnel's
395
+ * host serves (or stops serving) the plugin's `/api/<route>/app/*` — from
396
+ * when its installed, approved version declares identity; until then its
397
+ * requests go down the tunnel. Prints how each route bound to the tunnel is
398
+ * served afterwards.
399
+ *
400
+ * @param {'bind-user-surface' | 'unbind-user-surface'} sub
401
+ * @param {string[]} args
402
+ * @param {{ json: boolean }} options
403
+ */
404
+ async function userSurfaceCmd(sub, args, { json }) {
405
+ const failWith = (message, code) => {
406
+ if (json) emitError(message, false, code ? { code } : {});
407
+ else console.error(`\n ${chalk.red(terminalText(message))}\n`);
408
+ process.exit(1);
409
+ };
410
+ const parsed = parseUserSurfaceArgs(sub, args);
411
+ if (!parsed.ok) failWith(parsed.error);
412
+
413
+ const found = findHostTunnel(await readTunnels(), parsed.tunnel);
414
+ if (found.error) failWith(found.error);
415
+ const { tunnel } = found;
416
+
417
+ let plugins;
418
+ try {
419
+ ({ plugins = [] } = await panelRequest('GET', '/api/plugins'));
420
+ } catch (err) {
421
+ failWith(err.message, err.code);
422
+ }
423
+ const named = findPluginByNameOrRoute(plugins, parsed.plugin);
424
+ if (named.error) failWith(named.error);
425
+ const { route } = named.plugin;
426
+
427
+ const bind = sub === 'bind-user-surface';
428
+ const surfacePath = `/api/tunnels/${encodeURIComponent(tunnel.id)}/user-surface`;
429
+ let result;
430
+ try {
431
+ result = bind
432
+ ? await panelRequest('PUT', surfacePath, { route })
433
+ : await panelRequest('DELETE', `${surfacePath}/${encodeURIComponent(route)}`);
434
+ } catch (err) {
435
+ failWith(err.message, err.code);
436
+ }
437
+ if (json) {
438
+ emitComplete({ id: tunnel.id, route, ...result });
439
+ return;
440
+ }
441
+ const host = terminalText(result.tunnel?.fqdn ?? tunnel.fqdn);
442
+ if (bind) {
443
+ console.log(
444
+ result.changed
445
+ ? `\n ${chalk.green('✓')} ${chalk.cyan(host)} serves the user surface of ${chalk.cyan(route)}`
446
+ : `\n ${chalk.cyan(host)} already serves the user surface of ${chalk.cyan(route)}`,
447
+ );
448
+ } else {
449
+ console.log(
450
+ `\n ${chalk.green('✓')} ${chalk.cyan(host)} no longer serves the user surface of ${chalk.cyan(route)} — its requests go down the tunnel`,
451
+ );
452
+ }
453
+ const statuses = Array.isArray(result.tunnel?.userSurfaceStatus)
454
+ ? result.tunnel.userSurfaceStatus
455
+ : [];
456
+ for (const status of statuses) {
457
+ const served = status.served === true;
458
+ const word = userSurfaceStatusText(status);
459
+ console.log(
460
+ ` ${terminalText(status.route)}: ${served ? chalk.green(word) : chalk.yellow(word)}` +
461
+ (served ? chalk.dim(` https://${host}/api/${terminalText(status.route)}/app/`) : ''),
462
+ );
463
+ }
464
+ console.log('');
465
+ }
@@ -190,11 +190,13 @@ async function removeServices() {
190
190
  * @param {Record<string, unknown> | null} _config
191
191
  */
192
192
  async function removeNginxConfigs(_config) {
193
- // Remove lamaste-specific sites
193
+ // Remove lamaste-specific sites, then the snippets and http-level maps
194
+ // they use (the sites go first: a site naming a removed map fails nginx)
194
195
  const patterns = [
195
196
  '/etc/nginx/sites-enabled/lamalibre-lamaste-*',
196
197
  '/etc/nginx/sites-available/lamalibre-lamaste-*',
197
198
  '/etc/nginx/snippets/lamalibre-lamaste-*',
199
+ '/etc/nginx/conf.d/lamalibre-lamaste-*',
198
200
  ];
199
201
 
200
202
  for (const pattern of patterns) {
@@ -0,0 +1,358 @@
1
+ /**
2
+ * lamaste-server users — the panel's users and its user directory.
3
+ *
4
+ * Subcommands:
5
+ * list Users (Authelia's users file) with their status and
6
+ * directory id
7
+ * disable <username> The one offboarding action: they cannot sign in, every
8
+ * plugin audience shows them disabled, plugins' user
9
+ * surfaces refuse them, their desktop sessions and
10
+ * registry tokens end (the last enabled administrator
11
+ * cannot be disabled)
12
+ * enable <username> Undo it
13
+ * delete <username> [--erase]
14
+ * Delete: the directory keeps a tombstone (the id and
15
+ * the name and address they last had) so plugins'
16
+ * history stays readable; --erase scrubs it at once
17
+ * erase <userId> Erase a deleted user: the tombstone is scrubbed to the
18
+ * id, their invitations and Gatekeeper's access-request
19
+ * log entries go, plugins are sent the erasure
20
+ * directory Every user the directory knows — deleted and erased
21
+ * ones too — with Gatekeeper's cleanup of deleted
22
+ * usernames and the erasures plugins have not
23
+ * acknowledged
24
+ *
25
+ * Every subcommand goes through the running panel (`/api/users`,
26
+ * `/api/identity/directory`, admin certificate): users.yml is Authelia's
27
+ * credential file and the panel's one lock serializes every write of it.
28
+ * What the panel says is printed only as `terminalText`; `--json` emits it
29
+ * as it came.
30
+ */
31
+
32
+ import chalk from 'chalk';
33
+ import { panelRequest } from '../panel-api.js';
34
+ import { emit, emitComplete, emitError } from '../ndjson.js';
35
+ import { parseUserArgs, warningLine } from '../plugin-identity.js';
36
+ import { formatMoment, terminalText } from '../plugin-settings.js';
37
+
38
+ /**
39
+ * @param {string[]} args Subcommand args: ['list'] | ['disable', '<u>'] | ...
40
+ * @param {{ json: boolean }} options
41
+ */
42
+ export async function runUsers(args, { json }) {
43
+ const sub = args[0];
44
+ switch (sub) {
45
+ case 'list':
46
+ return listUsers({ json });
47
+ case 'disable':
48
+ case 'enable':
49
+ return setDisabledCmd(sub, args.slice(1), { json });
50
+ case 'delete':
51
+ return deleteUserCmd(args.slice(1), { json });
52
+ case 'erase':
53
+ return eraseUserCmd(args.slice(1), { json });
54
+ case 'directory':
55
+ return directoryCmd({ json });
56
+ default:
57
+ printUsersUsage();
58
+ process.exit(sub ? 1 : 0);
59
+ }
60
+ }
61
+
62
+ function printUsersUsage() {
63
+ const b = chalk.bold;
64
+ const c = chalk.cyan;
65
+ console.log(`
66
+ ${b('Usage:')} lamaste-server users <subcommand>
67
+
68
+ ${b('Subcommands:')}
69
+ ${c('list')} Users with their status (active, disabled) and directory id
70
+ ${c('disable <username>')} They cannot sign in; plugins see them disabled and refuse them;
71
+ desktop sessions and registry tokens end
72
+ ${c('enable <username>')} Enable them again
73
+ ${c('delete <username> [--erase]')} Delete; the directory keeps a tombstone (id, last name and
74
+ address) — --erase scrubs it at once (cannot be undone)
75
+ ${c('erase <userId>')} Erase a deleted user (an id from ${c('users directory')})
76
+ ${c('directory')} Every user the directory knows: deleted and erased ones, the
77
+ Gatekeeper cleanup of their usernames, pending erasures
78
+ `);
79
+ }
80
+
81
+ /**
82
+ * Exit with a usage error or a refused request, as text or NDJSON (with
83
+ * the refusal's `code`, e.g. `last_admin`, `user_not_removed`).
84
+ *
85
+ * @param {string} message
86
+ * @param {boolean} json
87
+ * @param {{ code?: string }} [err]
88
+ * @returns {never}
89
+ */
90
+ function fail(message, json, err = {}) {
91
+ if (json) emitError(message, false, typeof err.code === 'string' ? { code: err.code } : {});
92
+ else console.error(`\n ${chalk.red(terminalText(message))}\n`);
93
+ process.exit(1);
94
+ }
95
+
96
+ /** Print a change's warnings: what the panel could not complete beside it. */
97
+ function printWarnings(warnings) {
98
+ for (const warning of warnings ?? []) {
99
+ console.log(` ${chalk.yellow('!')} ${warningLine(warning)}`);
100
+ }
101
+ }
102
+
103
+ /** A user's status in its column: disabled in yellow, anything else in green. */
104
+ function statusCell(status) {
105
+ return (status === 'disabled' ? chalk.yellow : chalk.green)(status.padEnd(8));
106
+ }
107
+
108
+ /**
109
+ * The rows `users list` prints: each user's username, status, directory id
110
+ * (a dash until the directory has seen them) and groups, as terminal text.
111
+ * Pure, exported for tests.
112
+ *
113
+ * @param {Array<Record<string, any>>} users as `GET /api/users` lists them
114
+ */
115
+ export function userRows(users) {
116
+ return users.map((u) => ({
117
+ username: terminalText(u.username),
118
+ status: u.status === 'disabled' ? 'disabled' : 'active',
119
+ userId: u.userId ? terminalText(u.userId) : '—',
120
+ groups: terminalText((u.groups ?? []).join(', ')),
121
+ }));
122
+ }
123
+
124
+ /** @param {{ json: boolean }} options */
125
+ async function listUsers({ json }) {
126
+ let result;
127
+ try {
128
+ result = await panelRequest('GET', '/api/users');
129
+ } catch (err) {
130
+ fail(err.message, json, err);
131
+ }
132
+ if (json) {
133
+ emit(result);
134
+ return;
135
+ }
136
+ const rows = userRows(result.users ?? []);
137
+ if (rows.length === 0) {
138
+ console.log('\n No users.\n');
139
+ return;
140
+ }
141
+ const width = Math.max('USERNAME'.length, ...rows.map((r) => r.username.length));
142
+ console.log('');
143
+ console.log(chalk.bold(' Users'));
144
+ console.log(
145
+ chalk.dim(` ${'USERNAME'.padEnd(width)} STATUS ${'USER ID'.padEnd(36)} GROUPS`),
146
+ );
147
+ for (const r of rows) {
148
+ console.log(
149
+ ` ${chalk.cyan(r.username.padEnd(width))} ${statusCell(r.status)} ${r.userId.padEnd(36)} ${chalk.dim(r.groups)}`,
150
+ );
151
+ }
152
+ if (rows.some((r) => r.userId === '—')) {
153
+ console.log(
154
+ chalk.dim('\n — : not in the user directory yet; it adopts new users within a minute.'),
155
+ );
156
+ }
157
+ console.log('');
158
+ }
159
+
160
+ /**
161
+ * `users disable|enable <username>`.
162
+ *
163
+ * @param {'disable' | 'enable'} sub
164
+ * @param {string[]} args
165
+ * @param {{ json: boolean }} options
166
+ */
167
+ async function setDisabledCmd(sub, args, { json }) {
168
+ const parsed = parseUserArgs(sub, args);
169
+ if (!parsed.ok) fail(parsed.error, json);
170
+ const { username } = parsed;
171
+ const disabled = sub === 'disable';
172
+ let result;
173
+ try {
174
+ result = await panelRequest('PUT', `/api/users/${encodeURIComponent(username)}`, {
175
+ disabled,
176
+ });
177
+ } catch (err) {
178
+ fail(err.message, json, err);
179
+ }
180
+ if (json) {
181
+ emitComplete({ username, status: result.user.status, warnings: result.warnings ?? [] });
182
+ return;
183
+ }
184
+ console.log(
185
+ disabled
186
+ ? `\n ${chalk.green('✓')} ${chalk.cyan(username)} ${chalk.yellow('disabled')} — cannot sign in; plugins see them disabled and refuse them`
187
+ : `\n ${chalk.green('✓')} ${chalk.cyan(username)} ${chalk.green('enabled')}`,
188
+ );
189
+ printWarnings(result.warnings);
190
+ console.log('');
191
+ }
192
+
193
+ /**
194
+ * `users delete <username> [--erase]`.
195
+ *
196
+ * @param {string[]} args
197
+ * @param {{ json: boolean }} options
198
+ */
199
+ async function deleteUserCmd(args, { json }) {
200
+ const parsed = parseUserArgs('delete', args);
201
+ if (!parsed.ok) fail(parsed.error, json);
202
+ const { username, erase } = parsed;
203
+ let result;
204
+ try {
205
+ result = await panelRequest(
206
+ 'DELETE',
207
+ `/api/users/${encodeURIComponent(username)}${erase ? '?erase=true' : ''}`,
208
+ );
209
+ } catch (err) {
210
+ fail(err.message, json, err);
211
+ }
212
+ if (json) {
213
+ emitComplete({ username, ...result });
214
+ return;
215
+ }
216
+ console.log(`\n ${chalk.green('✓')} ${chalk.cyan(username)} deleted`);
217
+ if (result.userId) console.log(chalk.dim(` user id ${terminalText(result.userId)}`));
218
+ if (erase) {
219
+ console.log(
220
+ result.erased
221
+ ? ' and erased — plugins that had them are being sent the erasure'
222
+ : chalk.yellow(
223
+ ' not erased (see below) — erase it later with lamaste-server users erase <userId>',
224
+ ),
225
+ );
226
+ } else {
227
+ console.log(
228
+ chalk.dim(
229
+ ' The directory keeps a tombstone (the id and the name they last had) until it is erased.',
230
+ ),
231
+ );
232
+ }
233
+ if (!result.principalCleared) {
234
+ console.log(
235
+ chalk.yellow(
236
+ ' Gatekeeper has not dropped their grants and group memberships yet — retried every minute;\n' +
237
+ ' nothing is given to the username again until it has.',
238
+ ),
239
+ );
240
+ }
241
+ printWarnings(result.warnings);
242
+ console.log('');
243
+ }
244
+
245
+ /**
246
+ * `users erase <userId>`.
247
+ *
248
+ * @param {string[]} args
249
+ * @param {{ json: boolean }} options
250
+ */
251
+ async function eraseUserCmd(args, { json }) {
252
+ const parsed = parseUserArgs('erase', args);
253
+ if (!parsed.ok) fail(parsed.error, json);
254
+ const { userId } = parsed;
255
+ let result;
256
+ try {
257
+ result = await panelRequest(
258
+ 'POST',
259
+ `/api/identity/directory/${encodeURIComponent(userId)}/erase`,
260
+ );
261
+ } catch (err) {
262
+ fail(err.message, json, err);
263
+ }
264
+ if (json) {
265
+ emitComplete(result);
266
+ return;
267
+ }
268
+ console.log(`\n ${chalk.green('✓')} ${terminalText(userId)} erased`);
269
+ console.log(
270
+ chalk.dim(
271
+ ` ${result.invitationsPurged} invitation(s) and ${result.accessLogPurged} access-request log entr${result.accessLogPurged === 1 ? 'y' : 'ies'} deleted;` +
272
+ '\n every plugin that had them is sent the erasure (lamaste-server users directory shows who has not acknowledged it).' +
273
+ "\n Authelia's own authentication log is outside Lamaste's reach.",
274
+ ),
275
+ );
276
+ console.log('');
277
+ }
278
+
279
+ /**
280
+ * What `users directory` says of a removed user: who they were (or that
281
+ * they were erased), how they were removed, and what is still pending —
282
+ * Gatekeeper's cleanup of their username, or the plugins that have not
283
+ * acknowledged their erasure. Pure, exported for tests.
284
+ *
285
+ * @param {Record<string, any>} user as `GET /api/identity/directory` lists them
286
+ * @param {number} now epoch ms
287
+ * @returns {{ userId: string, who: string, removed: string, pending: string | null }}
288
+ */
289
+ export function removedUserText(user, now) {
290
+ const erased = user.erasedAt != null;
291
+ const who = erased
292
+ ? 'erased'
293
+ : terminalText(
294
+ user.displayName && user.username
295
+ ? `${user.username} (${user.displayName})`
296
+ : (user.username ?? '—'),
297
+ );
298
+ const how =
299
+ user.removedReason === 'deleted-outside'
300
+ ? 'gone from users.yml (not through Lamaste)'
301
+ : 'deleted';
302
+ const removed = user.removedAt ? `${how} ${formatMoment(user.removedAt, now)}` : how;
303
+ let pending = null;
304
+ if (erased) {
305
+ const plugins = Array.isArray(user.pendingErasures) ? user.pendingErasures : [];
306
+ const inactive = Array.isArray(user.pendingErasuresInactive)
307
+ ? user.pendingErasuresInactive
308
+ : [];
309
+ const named = plugins.map((name) =>
310
+ inactive.includes(name) ? `${name} (not taking the feed; an agent keeps its data)` : name,
311
+ );
312
+ pending =
313
+ named.length > 0 ? `erasure not yet acknowledged by ${terminalText(named.join(', '))}` : null;
314
+ } else if (user.principalClearedAt === null) {
315
+ pending = 'Gatekeeper cleanup of the username pending (retried every minute)';
316
+ }
317
+ return { userId: terminalText(user.userId), who, removed, pending };
318
+ }
319
+
320
+ /** @param {{ json: boolean }} options */
321
+ async function directoryCmd({ json }) {
322
+ let result;
323
+ try {
324
+ result = await panelRequest('GET', '/api/identity/directory');
325
+ } catch (err) {
326
+ fail(err.message, json, err);
327
+ }
328
+ if (json) {
329
+ emit(result);
330
+ return;
331
+ }
332
+ const now = Date.now();
333
+ const users = result.users ?? [];
334
+ const holding = users.filter((u) => u.status !== 'removed');
335
+ const removed = users.filter((u) => u.status === 'removed');
336
+ console.log('');
337
+ console.log(chalk.bold(' User directory') + chalk.dim(` ${terminalText(result.directoryId)}`));
338
+ console.log(chalk.dim(' ' + '─'.repeat(40)));
339
+ for (const u of holding) {
340
+ console.log(
341
+ ` ${terminalText(u.userId)} ${statusCell(u.status)} ${chalk.cyan(terminalText(u.username))}`,
342
+ );
343
+ }
344
+ if (holding.length === 0) console.log(chalk.dim(' No users.'));
345
+ if (removed.length > 0) {
346
+ console.log('');
347
+ console.log(chalk.bold(' Deleted users') + chalk.dim(' — tombstones until they are erased'));
348
+ for (const u of removed) {
349
+ const text = removedUserText(u, now);
350
+ console.log(` ${text.userId} ${text.who} ${chalk.dim(text.removed)}`);
351
+ if (text.pending) console.log(` ${chalk.yellow(text.pending)}`);
352
+ }
353
+ if (removed.some((u) => u.erasedAt === null)) {
354
+ console.log(chalk.dim('\n Erase a tombstone: lamaste-server users erase <userId>'));
355
+ }
356
+ }
357
+ console.log('');
358
+ }