@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
|
@@ -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
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `lamaste-server plugins hooks|update|approve-hooks`: why an update waits
|
|
3
|
-
* for approval,
|
|
3
|
+
* for approval, what an identity block lets a plugin learn and how an
|
|
4
|
+
* update changes it, the command that approves it, and the arguments that
|
|
4
5
|
* command takes. Pure — no output — so they are tested as they are.
|
|
5
6
|
*/
|
|
6
7
|
|
|
@@ -10,6 +11,8 @@ const DIGEST_RE = /^[a-f0-9]{64}$/;
|
|
|
10
11
|
const VERSION_RE = /^[0-9A-Za-z.+-]{1,64}$/;
|
|
11
12
|
/** The reason an update turns secret settings into plain ones. */
|
|
12
13
|
const SETTINGS_SECRET_TO_PLAIN = 'settings.secret-to-plain';
|
|
14
|
+
/** The reason an update adds or removes roles, or widens the attributes its users' records carry. */
|
|
15
|
+
const IDENTITY_CHANGED = 'identity.changed';
|
|
13
16
|
|
|
14
17
|
/** How `approve-hooks` is called. */
|
|
15
18
|
export const APPROVE_HOOKS_USAGE =
|
|
@@ -18,12 +21,13 @@ export const APPROVE_HOOKS_USAGE =
|
|
|
18
21
|
/**
|
|
19
22
|
* Why a pending update waits, as the terminal shows it: each reason of
|
|
20
23
|
* `approvalRequired` (`[{ code, reason }]`; a secret turned plain is a
|
|
21
|
-
* warning
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
+
* warning; an identity change carries a note that auto-approve never
|
|
25
|
+
* covers it), and the sentences of `autoApproveWithheld` that do not
|
|
26
|
+
* repeat one of them — what auto-approve, when on, could not cover beyond
|
|
27
|
+
* that. Every text as {@link terminalText}.
|
|
24
28
|
*
|
|
25
29
|
* @param {{ approvalRequired?: unknown, autoApproveWithheld?: unknown }} update
|
|
26
|
-
* @returns {{ required: Array<{ text: string, warning: boolean }>, notCovered: string[] }}
|
|
30
|
+
* @returns {{ required: Array<{ text: string, warning: boolean, note: string | null }>, notCovered: string[] }}
|
|
27
31
|
*/
|
|
28
32
|
export function pendingUpdateReasons(update) {
|
|
29
33
|
const required = (Array.isArray(update?.approvalRequired) ? update.approvalRequired : [])
|
|
@@ -31,6 +35,10 @@ export function pendingUpdateReasons(update) {
|
|
|
31
35
|
.map((entry) => ({
|
|
32
36
|
text: terminalText(entry.reason),
|
|
33
37
|
warning: entry.code === SETTINGS_SECRET_TO_PLAIN,
|
|
38
|
+
note:
|
|
39
|
+
entry.code === IDENTITY_CHANGED
|
|
40
|
+
? 'who its users are and what it learns about them — auto-approve never covers it'
|
|
41
|
+
: null,
|
|
34
42
|
}));
|
|
35
43
|
const said = new Set(required.map((line) => line.text));
|
|
36
44
|
const notCovered = (Array.isArray(update?.autoApproveWithheld) ? update.autoApproveWithheld : [])
|
|
@@ -39,6 +47,100 @@ export function pendingUpdateReasons(update) {
|
|
|
39
47
|
return { required, notCovered };
|
|
40
48
|
}
|
|
41
49
|
|
|
50
|
+
/** What a plugin may declare to receive about its users, in words. */
|
|
51
|
+
const ATTRIBUTE_WORDS = { displayName: 'display name', email: 'email address' };
|
|
52
|
+
|
|
53
|
+
/** An identity attribute in words (`displayName` → display name). */
|
|
54
|
+
function attributeWords(attribute) {
|
|
55
|
+
return ATTRIBUTE_WORDS[attribute] ?? terminalText(attribute);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* An identity block (normalized: `{ roles: [{ id, label, description? }],
|
|
60
|
+
* attributes }`) as two lines — its roles, and what it receives about each
|
|
61
|
+
* member. Empty for anything that is not a block. Every line as
|
|
62
|
+
* {@link terminalText}.
|
|
63
|
+
*
|
|
64
|
+
* @param {unknown} block
|
|
65
|
+
* @returns {string[]}
|
|
66
|
+
*/
|
|
67
|
+
export function identityBlockLines(block) {
|
|
68
|
+
if (block === null || typeof block !== 'object' || !Array.isArray(block.roles)) return [];
|
|
69
|
+
const roles = block.roles
|
|
70
|
+
.filter((role) => role !== null && typeof role === 'object')
|
|
71
|
+
.map((role) => `${role.id} (${role.label}${role.description ? ` — ${role.description}` : ''})`);
|
|
72
|
+
const attributes = Array.isArray(block.attributes) ? block.attributes.map(attributeWords) : [];
|
|
73
|
+
return [
|
|
74
|
+
terminalText(`roles: ${roles.length > 0 ? roles.join(', ') : 'none'}`),
|
|
75
|
+
terminalText(
|
|
76
|
+
`receives: ${['user id', 'username', 'roles', 'status', ...attributes].join(', ')} — of the users entitled to it`,
|
|
77
|
+
),
|
|
78
|
+
];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* How an update changes a plugin's identity block (`pendingUpdate.
|
|
83
|
+
* identity`: `{ before, after, rolesAdded, rolesRemoved, attributesAdded,
|
|
84
|
+
* attributesRemoved, labelsChanged, grantsAffected? }` — `grantsAffected`:
|
|
85
|
+
* how many role grants approving it rewrites or revokes, null when unknown):
|
|
86
|
+
* a headline and a line per kind of
|
|
87
|
+
* change — roles added or removed and attributes widened wait for an
|
|
88
|
+
* administrator (`approval`), narrowing and label edits only change the
|
|
89
|
+
* digest. Null when neither version declares a block, or it is unchanged.
|
|
90
|
+
* Every text as {@link terminalText}.
|
|
91
|
+
*
|
|
92
|
+
* @param {unknown} diff
|
|
93
|
+
* @returns {{ headline: string, lines: Array<{ text: string, approval: boolean }> } | null}
|
|
94
|
+
*/
|
|
95
|
+
export function identityDiffLines(diff) {
|
|
96
|
+
if (diff === null || typeof diff !== 'object') return null;
|
|
97
|
+
const list = (value) => (Array.isArray(value) ? value.map(String) : []);
|
|
98
|
+
const labelOf = (block, id) =>
|
|
99
|
+
Array.isArray(block?.roles) ? (block.roles.find((r) => r?.id === id)?.label ?? null) : null;
|
|
100
|
+
const role = (block) => (id) => {
|
|
101
|
+
const label = labelOf(block, id);
|
|
102
|
+
return label ? `${id} (${label})` : id;
|
|
103
|
+
};
|
|
104
|
+
const lines = [];
|
|
105
|
+
const add = (label, entries, approval) => {
|
|
106
|
+
if (entries.length === 0) return;
|
|
107
|
+
lines.push({
|
|
108
|
+
text: terminalText(
|
|
109
|
+
`${label}: ${entries.join(', ')}${approval ? ' — waits for approval' : ''}`,
|
|
110
|
+
),
|
|
111
|
+
approval,
|
|
112
|
+
});
|
|
113
|
+
};
|
|
114
|
+
add('roles added', list(diff.rolesAdded).map(role(diff.after)), true);
|
|
115
|
+
add('roles removed', list(diff.rolesRemoved).map(role(diff.before)), true);
|
|
116
|
+
add('receives more about its users', list(diff.attributesAdded).map(attributeWords), true);
|
|
117
|
+
add('receives less about its users', list(diff.attributesRemoved).map(attributeWords), false);
|
|
118
|
+
add('role labels changed', list(diff.labelsChanged).map(role(diff.after)), false);
|
|
119
|
+
// A removed role stays removed: approving revokes it from the grants that hold it.
|
|
120
|
+
if (list(diff.rolesRemoved).length > 0 && 'grantsAffected' in diff) {
|
|
121
|
+
const affected = diff.grantsAffected;
|
|
122
|
+
const parts =
|
|
123
|
+
affected === null || typeof affected !== 'object'
|
|
124
|
+
? ['not known (Gatekeeper could not be read)']
|
|
125
|
+
: [
|
|
126
|
+
...(affected.revoked > 0 ? [`${affected.revoked} revoked`] : []),
|
|
127
|
+
...(affected.rewritten > 0
|
|
128
|
+
? [`${affected.rewritten} narrowed to the roles that stay`]
|
|
129
|
+
: []),
|
|
130
|
+
];
|
|
131
|
+
add('role grants affected', parts.length > 0 ? parts : ['none'], true);
|
|
132
|
+
}
|
|
133
|
+
const before = diff.before != null;
|
|
134
|
+
const after = diff.after != null;
|
|
135
|
+
if (lines.length === 0 && before === after) return null;
|
|
136
|
+
const headline = !before
|
|
137
|
+
? 'declares identity for the first time'
|
|
138
|
+
: !after
|
|
139
|
+
? 'no longer declares identity — it is sent no users, its role grants are revoked and its user surface is no longer served'
|
|
140
|
+
: 'changes its identity';
|
|
141
|
+
return { headline, lines };
|
|
142
|
+
}
|
|
143
|
+
|
|
42
144
|
/**
|
|
43
145
|
* The command that approves what plugin `name` brings: the installed
|
|
44
146
|
* version by its digest (`{ digest }`), or a pending update by its version
|