@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.
- package/package.json +2 -2
- package/scripts/lamaste-priv +234 -18
- 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/02-guides/static-sites.md +4 -0
- 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 +229 -24
- 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 +378 -34
- package/vendor/docs/04-api-reference/app-host.md +3 -3
- package/vendor/docs/04-api-reference/gatekeeper.md +78 -5
- package/vendor/docs/04-api-reference/lifecycle-hooks.md +14 -12
- package/vendor/docs/04-api-reference/onboarding.md +1 -1
- package/vendor/docs/04-api-reference/registry-and-oidc.md +3 -3
- package/vendor/docs/04-api-reference/sites.md +4 -0
- package/vendor/docs/04-api-reference/tunnels.md +87 -47
- 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 +24 -6
- package/vendor/docs/06-reference/config-files.md +100 -202
- 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/cli/index.js +0 -0
- package/vendor/gatekeeper/dist/index.d.ts +4 -3
- package/vendor/gatekeeper/dist/index.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/index.js +4 -2
- package/vendor/gatekeeper/dist/index.js.map +1 -1
- package/vendor/gatekeeper/dist/lib/authz.d.ts +9 -0
- package/vendor/gatekeeper/dist/lib/authz.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/lib/authz.js +31 -0
- package/vendor/gatekeeper/dist/lib/authz.js.map +1 -1
- package/vendor/gatekeeper/dist/lib/constants.d.ts +5 -2
- package/vendor/gatekeeper/dist/lib/constants.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/lib/constants.js +5 -2
- package/vendor/gatekeeper/dist/lib/constants.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 +10 -0
- package/vendor/gatekeeper/dist/lib/tunnels.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/tunnels.js +56 -0
- package/vendor/gatekeeper/dist/lib/tunnels.js.map +1 -0
- package/vendor/gatekeeper/dist/server/index.d.ts +2 -1
- package/vendor/gatekeeper/dist/server/index.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/index.js +49 -56
- 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 +332 -104
- 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/plugin-web-surface.js +269 -0
- package/vendor/server/src/commands/plugins.js +156 -11
- package/vendor/server/src/commands/registry-credential.js +2 -1
- package/vendor/server/src/commands/sites.js +15 -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 +186 -5
- package/vendor/server/src/plugin-identity.js +382 -0
- package/vendor/server/src/plugin-web-surface.js +307 -0
- package/vendor/server-ui/dist/assets/index-BZwQ_aQc.css +1 -0
- package/vendor/server-ui/dist/assets/index-BvEfCZYS.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 +34 -5
- package/vendor/serverd/src/lib/audience.js +576 -0
- package/vendor/serverd/src/lib/authelia.js +78 -7
- package/vendor/serverd/src/lib/chisel-runtime.js +18 -6
- package/vendor/serverd/src/lib/chisel.js +10 -2
- 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 +226 -10
- package/vendor/serverd/src/lib/hook-pipelines.js +21 -3
- 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/migrations/state/0015_plugin_web_surface.sql +14 -0
- package/vendor/serverd/src/lib/mtls.js +6 -2
- package/vendor/serverd/src/lib/nginx.js +546 -48
- package/vendor/serverd/src/lib/plugin-hooks.js +127 -45
- package/vendor/serverd/src/lib/plugin-hosting.js +33 -5
- package/vendor/serverd/src/lib/plugin-lifecycle.js +227 -13
- package/vendor/serverd/src/lib/plugin-packages.js +74 -2
- package/vendor/serverd/src/lib/plugins.js +95 -7
- package/vendor/serverd/src/lib/registry-credential.js +9 -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 +36 -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 +754 -0
- package/vendor/serverd/src/lib/vhost-reconcile.js +137 -33
- package/vendor/serverd/src/lib/web-surface.js +766 -0
- 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/plugin-web-surface.js +151 -0
- package/vendor/serverd/src/routes/management/plugins.js +2 -0
- package/vendor/serverd/src/routes/management/registry-credential.js +1 -1
- package/vendor/serverd/src/routes/management/sites.js +143 -65
- package/vendor/serverd/src/routes/management/tunnels.js +109 -22
- package/vendor/serverd/src/routes/management/users.js +290 -208
- package/vendor/serverd/src/routes/management.js +6 -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-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
|
|
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
|
+
}
|