@lamalibre/create-lamaste 4.6.0 → 4.7.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/bundle-vendor.js +20 -3
- package/scripts/lamaste-priv +22 -3
- package/scripts/pin-release.js +98 -0
- package/src/index.js +8 -1
- package/src/lib/env.js +19 -0
- package/src/lib/service-config.js +7 -0
- package/src/tasks/redeploy.js +8 -0
- package/vendor/docs/00-introduction/how-it-works.md +2 -2
- package/vendor/docs/00-introduction/quickstart.md +5 -5
- package/vendor/docs/00-introduction/what-is-lamaste.md +5 -5
- package/vendor/docs/01-concepts/authentication.md +6 -2
- package/vendor/docs/01-concepts/authorization.md +4 -0
- package/vendor/docs/01-concepts/certificates.md +3 -3
- package/vendor/docs/01-concepts/dns-and-domains.md +9 -0
- package/vendor/docs/01-concepts/mtls.md +2 -2
- package/vendor/docs/01-concepts/security-model.md +28 -3
- 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 +5 -11
- package/vendor/docs/02-guides/onboarding.md +2 -2
- package/vendor/docs/02-guides/second-domain.md +82 -0
- package/vendor/docs/02-guides/standalone-plugin-with-tickets.md +2 -2
- package/vendor/docs/03-architecture/installer-flow.md +2 -2
- package/vendor/docs/03-architecture/installer.md +4 -4
- package/vendor/docs/03-architecture/management-flow.md +1 -1
- package/vendor/docs/03-architecture/nginx-configuration.md +15 -5
- package/vendor/docs/03-architecture/overview.md +5 -5
- package/vendor/docs/03-architecture/panel-client.md +6 -6
- package/vendor/docs/03-architecture/panel-server.md +2 -2
- package/vendor/docs/03-architecture/state-management.md +3 -3
- package/vendor/docs/03-architecture/system-overview.md +1 -1
- package/vendor/docs/04-api-reference/agent-hosted-plugins.md +17 -13
- package/vendor/docs/04-api-reference/app-host.md +3 -3
- package/vendor/docs/04-api-reference/domains.md +192 -0
- package/vendor/docs/04-api-reference/gatekeeper.md +35 -7
- package/vendor/docs/04-api-reference/lifecycle-hooks.md +4 -4
- package/vendor/docs/04-api-reference/onboarding.md +1 -1
- package/vendor/docs/04-api-reference/overview.md +9 -0
- package/vendor/docs/04-api-reference/registry-and-oidc.md +26 -13
- package/vendor/docs/04-api-reference/sites.md +5 -0
- package/vendor/docs/04-api-reference/tunnels.md +15 -10
- package/vendor/docs/04-api-reference/users.md +41 -29
- package/vendor/docs/05-operations/backup-and-restore.md +1 -1
- package/vendor/docs/05-operations/uninstalling.md +1 -1
- package/vendor/docs/05-operations/upgrades.md +6 -6
- package/vendor/docs/06-reference/config-files.md +1 -1
- package/vendor/docs/06-reference/glossary.md +3 -3
- package/vendor/docs/06-reference/installer-flags.md +13 -13
- package/vendor/docs/06-reference/ports-and-services.md +1 -1
- package/vendor/docs/06-reference/troubleshooting.md +8 -8
- package/vendor/docs/_index.json +105 -19
- package/vendor/docs/external-members-design.md +816 -0
- package/vendor/docs/gatekeeper-design.md +8 -8
- package/vendor/docs/identity-directory-design.md +3 -3
- package/vendor/docs/multi-domain-relay-design.md +836 -0
- 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/lib/grants.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/lib/grants.js +20 -13
- package/vendor/gatekeeper/dist/lib/grants.js.map +1 -1
- package/vendor/gatekeeper/dist/lib/migrations/gatekeeper/0004_unconsume_access_grants.sql +8 -0
- package/vendor/gatekeeper/dist/lib/secret.d.ts +6 -0
- package/vendor/gatekeeper/dist/lib/secret.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/secret.js +12 -0
- package/vendor/gatekeeper/dist/lib/secret.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/templates.d.ts +20 -0
- package/vendor/gatekeeper/dist/lib/templates.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/lib/templates.js +80 -14
- package/vendor/gatekeeper/dist/lib/templates.js.map +1 -1
- package/vendor/gatekeeper/dist/server/index.d.ts +5 -1
- package/vendor/gatekeeper/dist/server/index.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/index.js +28 -2
- package/vendor/gatekeeper/dist/server/index.js.map +1 -1
- package/vendor/gatekeeper/dist/server/routes/authz.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/routes/authz.js +225 -36
- 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 +5 -2
- package/vendor/gatekeeper/dist/server/routes/diagnostic.js.map +1 -1
- package/vendor/gatekeeper/package.json +2 -2
- package/vendor/server/bin/lamaste-server.js +7 -0
- package/vendor/server/package.json +2 -2
- package/vendor/server/src/commands/domains.js +214 -0
- package/vendor/server/src/commands/plugin-identity.js +2 -16
- package/vendor/server/src/commands/plugins.js +112 -8
- package/vendor/server/src/commands/registry-credential.js +10 -3
- package/vendor/server/src/commands/sites.js +3 -0
- package/vendor/server/src/commands/status.js +19 -0
- package/vendor/server/src/commands/tunnels.js +4 -1
- package/vendor/server/src/commands/users.js +8 -19
- package/vendor/server/src/domains.js +92 -0
- package/vendor/server/src/fail.js +21 -0
- package/vendor/server/src/oidc-client-grants.js +63 -0
- package/vendor/server/src/panel-api.js +15 -0
- package/vendor/server-ui/dist/assets/index-B8Tv_ocF.js +84 -0
- package/vendor/server-ui/dist/assets/{index-BZwQ_aQc.css → index-Cb-ADsbB.css} +1 -1
- package/vendor/server-ui/dist/index.html +2 -2
- package/vendor/serverd/package.json +2 -2
- package/vendor/serverd/src/index.js +42 -16
- package/vendor/serverd/src/lib/app-error.js +24 -0
- package/vendor/serverd/src/lib/audience.js +12 -3
- package/vendor/serverd/src/lib/authelia.js +341 -35
- package/vendor/serverd/src/lib/directory.js +67 -52
- package/vendor/serverd/src/lib/domains.js +1792 -0
- package/vendor/serverd/src/lib/gatekeeper-client.js +20 -4
- package/vendor/serverd/src/lib/identity-sync.js +35 -0
- package/vendor/serverd/src/lib/migrations/state/0016_domains.sql +53 -0
- package/vendor/serverd/src/lib/migrations/state/0017_membership_kind.sql +15 -0
- package/vendor/serverd/src/lib/migrations/state/0018_domain_group_keys.sql +23 -0
- package/vendor/serverd/src/lib/nginx.js +318 -140
- package/vendor/serverd/src/lib/owned-groups.js +326 -0
- package/vendor/serverd/src/lib/plugin-domain.js +86 -0
- package/vendor/serverd/src/lib/plugin-lifecycle.js +1 -0
- package/vendor/serverd/src/lib/plugin-oidc.js +144 -42
- package/vendor/serverd/src/lib/plugins.js +45 -5
- package/vendor/serverd/src/lib/registry-credential.js +39 -6
- package/vendor/serverd/src/lib/services.js +9 -1
- package/vendor/serverd/src/lib/state.js +11 -0
- package/vendor/serverd/src/lib/tunnel-deps.js +2 -4
- package/vendor/serverd/src/lib/user-surface.js +29 -4
- package/vendor/serverd/src/lib/users-lock.js +23 -0
- package/vendor/serverd/src/lib/vhost-reconcile.js +149 -41
- package/vendor/serverd/src/lib/web-surface.js +49 -32
- package/vendor/serverd/src/middleware/user-access-session.js +11 -4
- package/vendor/serverd/src/routes/invite.js +15 -5
- package/vendor/serverd/src/routes/management/domains.js +154 -0
- package/vendor/serverd/src/routes/management/gatekeeper-proxy.js +44 -8
- package/vendor/serverd/src/routes/management/identity.js +21 -7
- package/vendor/serverd/src/routes/management/invitations.js +33 -4
- package/vendor/serverd/src/routes/management/plugins.js +4 -2
- package/vendor/serverd/src/routes/management/registry-credential.js +86 -57
- package/vendor/serverd/src/routes/management/sites.js +50 -11
- package/vendor/serverd/src/routes/management/tunnels.js +29 -5
- package/vendor/serverd/src/routes/management/users.js +184 -64
- package/vendor/serverd/src/routes/management.js +2 -0
- package/vendor/serverd/src/routes/onboarding/dns.js +3 -17
- package/vendor/serverd/src/routes/onboarding/provision.js +22 -13
- package/vendor/serverd/src/routes/user-access.js +13 -1
- package/vendor/gatekeeper/dist/lib/migrations/migrations/gatekeeper/0001_groups.sql +0 -7
- package/vendor/gatekeeper/dist/lib/migrations/migrations/gatekeeper/0002_access_grants.sql +0 -14
- package/vendor/gatekeeper/dist/lib/migrations/migrations/gatekeeper/0003_access_request_log.sql +0 -9
- package/vendor/server-ui/dist/assets/index-BvEfCZYS.js +0 -84
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lamaste-server domains — the relay's base domains and their members
|
|
3
|
+
* (the multi-domain relay).
|
|
4
|
+
*
|
|
5
|
+
* Subcommands:
|
|
6
|
+
* list Domains with their state, member count and what uses them
|
|
7
|
+
* add <domain> Add a domain (pending until activated)
|
|
8
|
+
* verify-dns <domain> The records it needs, and whether they point here
|
|
9
|
+
* activate <domain> Issue auth.<domain>, serve its sign-in, admit its members
|
|
10
|
+
* remove <domain> Remove an unused, non-primary domain (its memberships end)
|
|
11
|
+
* members <domain> Its members, each with the kind of the membership
|
|
12
|
+
* members <domain> add <user> [--external]
|
|
13
|
+
* Make a user (directory id or username) a member —
|
|
14
|
+
* an external one with --external
|
|
15
|
+
* members <domain> kind <user> member|external
|
|
16
|
+
* Change the kind of a membership
|
|
17
|
+
* members <domain> remove <user> End a membership
|
|
18
|
+
*
|
|
19
|
+
* Also `plugins domain <name> [<domain>]` ({@link pluginDomainCmd}).
|
|
20
|
+
*
|
|
21
|
+
* Every subcommand goes through the running panel (`/api/domains`, admin
|
|
22
|
+
* certificate), which decides every rule. What the panel says is printed
|
|
23
|
+
* only as `terminalText`; `--json` emits it as it came.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import chalk from 'chalk';
|
|
27
|
+
import { panelRequest } from '../panel-api.js';
|
|
28
|
+
import { emit, emitComplete } from '../ndjson.js';
|
|
29
|
+
import { fail } from '../fail.js';
|
|
30
|
+
import { terminalText } from '../plugin-settings.js';
|
|
31
|
+
import { domainPath, domainRows, memberRequest } from '../domains.js';
|
|
32
|
+
|
|
33
|
+
/** A panel request, or exit with its refusal. */
|
|
34
|
+
const call = (json, ...args) => panelRequest(...args).catch((err) => fail(err.message, json, err));
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {string[]} args
|
|
38
|
+
* @param {{ json: boolean }} options
|
|
39
|
+
*/
|
|
40
|
+
export async function runDomains(args, { json }) {
|
|
41
|
+
const [sub, name, ...rest] = args;
|
|
42
|
+
if (sub === 'list') return listDomains({ json });
|
|
43
|
+
if (sub === 'members' && name) return membersCmd(name.toLowerCase(), rest, { json });
|
|
44
|
+
const command = DOMAIN_COMMANDS[sub];
|
|
45
|
+
if (!command || !name) return usage(sub ? 1 : 0);
|
|
46
|
+
const domain = name.toLowerCase();
|
|
47
|
+
const result = await call(json, ...command.request(domain), { timeoutMs: command.timeoutMs });
|
|
48
|
+
if (json) emitComplete(result);
|
|
49
|
+
else command.print(terminalText(domain), result);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function usage(code) {
|
|
53
|
+
const b = chalk.bold;
|
|
54
|
+
const c = chalk.cyan;
|
|
55
|
+
console.log(`
|
|
56
|
+
${b('Usage:')} lamaste-server domains <subcommand>
|
|
57
|
+
|
|
58
|
+
${b('Subcommands:')}
|
|
59
|
+
${c('list')} Domains: state, members, what uses each
|
|
60
|
+
${c('add <domain>')} Add a domain (pending until activated)
|
|
61
|
+
${c('verify-dns <domain>')} The DNS records it needs, and whether they point here
|
|
62
|
+
${c('activate <domain>')} Issue auth.<domain> and serve its sign-in
|
|
63
|
+
${c('remove <domain>')} Remove an unused, non-primary domain
|
|
64
|
+
${c('members <domain>')} Its members and external members
|
|
65
|
+
${c('members <domain> add <user> [--external]')}
|
|
66
|
+
Make a user (directory id or username) a member
|
|
67
|
+
${c('members <domain> kind <user> member|external')}
|
|
68
|
+
Change the kind of a membership
|
|
69
|
+
${c('members <domain> remove <user>')} End a membership
|
|
70
|
+
|
|
71
|
+
Every domain has its own sign-in (auth.<domain>) and member list; a user reaches a
|
|
72
|
+
domain's protected hosts only as its member. An external member reaches only what
|
|
73
|
+
names them — restricted hosts that grant them, sites that list them, OIDC clients
|
|
74
|
+
granted to them — and holds no Authelia groups. The panel and tunnel. stay on the primary.
|
|
75
|
+
`);
|
|
76
|
+
process.exit(code);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function listDomains({ json }) {
|
|
80
|
+
const { domains } = await call(json, 'GET', '/api/domains');
|
|
81
|
+
if (json) {
|
|
82
|
+
emit({ domains });
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const d = chalk.dim;
|
|
86
|
+
console.log('');
|
|
87
|
+
console.log(chalk.bold(' Domains'));
|
|
88
|
+
console.log(d(' ' + '─'.repeat(50)));
|
|
89
|
+
for (const row of domainRows(domains)) {
|
|
90
|
+
const state = row.state === 'pending' ? chalk.yellow(row.state) : chalk.green(row.state);
|
|
91
|
+
const used = row.usedBy ? d(` used by ${row.usedBy}`) : '';
|
|
92
|
+
const external = row.externalMembers > 0 ? `, ${row.externalMembers} external` : '';
|
|
93
|
+
console.log(` ${chalk.cyan(row.name)} ${state} ${row.members} member(s)${external}${used}`);
|
|
94
|
+
}
|
|
95
|
+
console.log('');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** One entry per domain subcommand: its request, its time budget, how it is printed. */
|
|
99
|
+
const DOMAIN_COMMANDS = {
|
|
100
|
+
add: {
|
|
101
|
+
request: (name) => ['POST', '/api/domains', { name }],
|
|
102
|
+
timeoutMs: 30000,
|
|
103
|
+
print: (domain) => {
|
|
104
|
+
console.log(`\n ${chalk.green('✓')} ${domain} added (pending).`);
|
|
105
|
+
console.log(
|
|
106
|
+
` Create its DNS records (${chalk.cyan(`lamaste-server domains verify-dns ${domain}`)}),`,
|
|
107
|
+
);
|
|
108
|
+
console.log(` then ${chalk.cyan(`lamaste-server domains activate ${domain}`)}\n`);
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
'verify-dns': {
|
|
112
|
+
request: (name) => ['POST', domainPath(name, '/verify-dns'), null],
|
|
113
|
+
timeoutMs: 30000,
|
|
114
|
+
print: (domain, result) => {
|
|
115
|
+
const mark = result.ok ? chalk.green('✓') : chalk.yellow('!');
|
|
116
|
+
console.log(
|
|
117
|
+
`\n ${mark} ${domain}: ${result.ok ? 'its sign-in points at this relay' : 'records still missing'}`,
|
|
118
|
+
);
|
|
119
|
+
for (const r of result.records ?? []) {
|
|
120
|
+
const state = r.ok
|
|
121
|
+
? chalk.green('in place')
|
|
122
|
+
: chalk.yellow(r.required ? 'missing' : 'optional, absent');
|
|
123
|
+
console.log(
|
|
124
|
+
` ${terminalText(r.type)} ${chalk.cyan(terminalText(r.name))} → ${terminalText(r.value)} ${state}`,
|
|
125
|
+
);
|
|
126
|
+
console.log(chalk.dim(` ${terminalText(r.purpose)}`));
|
|
127
|
+
}
|
|
128
|
+
console.log('');
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
activate: {
|
|
132
|
+
request: (name) => ['POST', domainPath(name, '/activate'), null],
|
|
133
|
+
// Activation issues a certificate: give it time.
|
|
134
|
+
timeoutMs: 180000,
|
|
135
|
+
print: (domain) =>
|
|
136
|
+
console.log(`\n ${chalk.green('✓')} ${domain} is active — its sign-in is auth.${domain}\n`),
|
|
137
|
+
},
|
|
138
|
+
remove: {
|
|
139
|
+
request: (name) => ['DELETE', domainPath(name), null],
|
|
140
|
+
timeoutMs: 30000,
|
|
141
|
+
print: (domain) =>
|
|
142
|
+
console.log(`\n ${chalk.green('✓')} ${domain} removed; its memberships ended.\n`),
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
async function membersCmd(name, rest, { json }) {
|
|
147
|
+
if (rest.length === 0) {
|
|
148
|
+
const { members } = await call(json, 'GET', domainPath(name, '/members'));
|
|
149
|
+
if (json) {
|
|
150
|
+
emit({ members });
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
console.log('');
|
|
154
|
+
for (const m of members) {
|
|
155
|
+
const status = m.status === 'active' ? '' : chalk.yellow(` (${m.status})`);
|
|
156
|
+
const kind = m.kind === 'external' ? chalk.magenta(' external') : '';
|
|
157
|
+
console.log(
|
|
158
|
+
` ${chalk.cyan(terminalText(m.username ?? m.userId))}${kind}${status} ${chalk.dim(m.userId)}`,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
if (members.length === 0) console.log(chalk.dim(' No members.'));
|
|
162
|
+
console.log('');
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const request = memberRequest(name, rest);
|
|
166
|
+
if (!request) return usage(1);
|
|
167
|
+
// The panel takes a directory id or a username.
|
|
168
|
+
const result = await call(json, request.method, request.path, request.body);
|
|
169
|
+
if (json) {
|
|
170
|
+
emitComplete(result);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const who = terminalText(request.user);
|
|
174
|
+
const where = terminalText(name);
|
|
175
|
+
console.log(
|
|
176
|
+
request.kind === null
|
|
177
|
+
? `\n ${chalk.green('✓')} ${who} is no longer a member of ${where}\n`
|
|
178
|
+
: `\n ${chalk.green('✓')} ${who} is now ${request.kind === 'external' ? 'an external member' : 'a member'} of ${where}\n`,
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* `plugins domain <name> [<domain>]`: the domain a plugin belongs to, or
|
|
184
|
+
* move it to another active one.
|
|
185
|
+
*
|
|
186
|
+
* @param {string[]} args
|
|
187
|
+
* @param {{ json: boolean }} options
|
|
188
|
+
*/
|
|
189
|
+
export async function pluginDomainCmd(args, { json }) {
|
|
190
|
+
const [name, domain] = args;
|
|
191
|
+
if (!name) {
|
|
192
|
+
console.error('\n Usage: lamaste-server plugins domain <name> [<domain>]\n');
|
|
193
|
+
process.exit(1);
|
|
194
|
+
}
|
|
195
|
+
if (domain === undefined) {
|
|
196
|
+
const { plugin } = await call(json, 'GET', `/api/plugins/${encodeURIComponent(name)}`);
|
|
197
|
+
if (json) {
|
|
198
|
+
emit({ name, domain: plugin.domain });
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
console.log(`\n ${terminalText(name)}: ${terminalText(plugin.domain)}\n`);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const result = await call(json, 'PUT', `/api/plugins/${encodeURIComponent(name)}/domain`, {
|
|
205
|
+
domain: domain.toLowerCase(),
|
|
206
|
+
});
|
|
207
|
+
if (json) {
|
|
208
|
+
emitComplete(result);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
console.log(
|
|
212
|
+
`\n ${chalk.green('✓')} ${terminalText(name)} ${result.changed ? 'now belongs to' : 'already belongs to'} ${terminalText(result.domain)}\n`,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
@@ -28,7 +28,8 @@
|
|
|
28
28
|
|
|
29
29
|
import chalk from 'chalk';
|
|
30
30
|
import { panelRequest } from '../panel-api.js';
|
|
31
|
-
import { emit, emitComplete
|
|
31
|
+
import { emit, emitComplete } from '../ndjson.js';
|
|
32
|
+
import { fail } from '../fail.js';
|
|
32
33
|
import { identityBlockLines } from '../plugin-approval.js';
|
|
33
34
|
import {
|
|
34
35
|
audienceRowText,
|
|
@@ -45,21 +46,6 @@ function pluginPath(name, rest) {
|
|
|
45
46
|
return `/api/plugins/${encodeURIComponent(name)}/${rest}`;
|
|
46
47
|
}
|
|
47
48
|
|
|
48
|
-
/**
|
|
49
|
-
* Exit with a usage error or a refused request, as text or NDJSON (with
|
|
50
|
-
* the refusal's `code`).
|
|
51
|
-
*
|
|
52
|
-
* @param {string} message
|
|
53
|
-
* @param {boolean} json
|
|
54
|
-
* @param {{ code?: string }} [err]
|
|
55
|
-
* @returns {never}
|
|
56
|
-
*/
|
|
57
|
-
function fail(message, json, err = {}) {
|
|
58
|
-
if (json) emitError(message, false, typeof err.code === 'string' ? { code: err.code } : {});
|
|
59
|
-
else console.error(`\n ${chalk.red(terminalText(message))}\n`);
|
|
60
|
-
process.exit(1);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
49
|
/** Print a change's warnings: what the panel could not complete beside it. */
|
|
64
50
|
function printWarnings(warnings) {
|
|
65
51
|
for (const warning of warnings ?? []) {
|
|
@@ -10,7 +10,12 @@
|
|
|
10
10
|
* Remove a plugin (must be disabled first); a hosted one
|
|
11
11
|
* through its uninstall pipeline (an operation); --purge also
|
|
12
12
|
* deletes the data of the agent half Lamaste installed
|
|
13
|
-
* oidc-clients <name>
|
|
13
|
+
* oidc-clients <name> [grants]
|
|
14
|
+
* The OIDC clients a plugin declares on Authelia, and who is
|
|
15
|
+
* granted each user-facing one
|
|
16
|
+
* oidc-clients <name> grant|revoke <clientId> --user <u>|--group <g>
|
|
17
|
+
* Grant (or revoke) a user-facing client to a user or a Lamaste
|
|
18
|
+
* group: what admits an external member to its sign-in
|
|
14
19
|
* rotate-secret <name> <clientId>
|
|
15
20
|
* New secret for a confidential OIDC client (shown once)
|
|
16
21
|
* hosting <name> Which agent hosts an agent-hosted plugin's agent half
|
|
@@ -59,6 +64,7 @@
|
|
|
59
64
|
* Who holds which of its roles; give or take them away
|
|
60
65
|
* identity-sync <name> [--reconcile]
|
|
61
66
|
* Sync its audience to its agent half now
|
|
67
|
+
* domain <name> [<domain>] The domain a plugin belongs to; move it to another active one
|
|
62
68
|
* web-surface <name> [create [--subdomain <s>] [--access-mode <m>] | move --tunnel <t> | delete [--yes]]
|
|
63
69
|
* The site an agent-hosted plugin owns for its web surface: what it
|
|
64
70
|
* declares, whether it is served, its trees; create, move a bound
|
|
@@ -82,6 +88,14 @@ import { emit, emitStep, emitError, emitComplete, emitLog } from '../ndjson.js';
|
|
|
82
88
|
import { configureCmd, settingsCmd } from './plugin-settings.js';
|
|
83
89
|
import { audienceCmd, entitlementsCmd, identityCmd, identitySyncCmd } from './plugin-identity.js';
|
|
84
90
|
import { webSurfaceCmd } from './plugin-web-surface.js';
|
|
91
|
+
import { pluginDomainCmd } from './domains.js';
|
|
92
|
+
import { fail as failPanel } from '../fail.js';
|
|
93
|
+
import {
|
|
94
|
+
OIDC_CLIENT_RESOURCE,
|
|
95
|
+
findClientGrants,
|
|
96
|
+
parsePrincipal,
|
|
97
|
+
principalText,
|
|
98
|
+
} from '../oidc-client-grants.js';
|
|
85
99
|
import { settingsDiffLines, terminalText } from '../plugin-settings.js';
|
|
86
100
|
import {
|
|
87
101
|
APPROVE_HOOKS_USAGE,
|
|
@@ -116,7 +130,7 @@ export async function runPlugins(args, { json }) {
|
|
|
116
130
|
case 'uninstall':
|
|
117
131
|
return uninstallPluginCmd(args[1], args.slice(2), { json });
|
|
118
132
|
case 'oidc-clients':
|
|
119
|
-
return oidcClientsCmd(args[1], { json });
|
|
133
|
+
return oidcClientsCmd(args[1], args.slice(2), { json });
|
|
120
134
|
case 'rotate-secret':
|
|
121
135
|
return rotateSecretCmd(args[1], args[2], { json });
|
|
122
136
|
case 'hosting':
|
|
@@ -161,6 +175,8 @@ export async function runPlugins(args, { json }) {
|
|
|
161
175
|
return identitySyncCmd(args.slice(1), { json });
|
|
162
176
|
case 'web-surface':
|
|
163
177
|
return webSurfaceCmd(args.slice(1), { json });
|
|
178
|
+
case 'domain':
|
|
179
|
+
return pluginDomainCmd(args.slice(1), { json });
|
|
164
180
|
default:
|
|
165
181
|
printPluginUsage();
|
|
166
182
|
process.exit(sub ? 1 : 0);
|
|
@@ -181,7 +197,11 @@ ${b('Subcommands:')}
|
|
|
181
197
|
${c('uninstall <name> [--force] [--purge]')}
|
|
182
198
|
Remove a disabled plugin (a hosted one through its uninstall pipeline;
|
|
183
199
|
--purge deletes the data of the agent half Lamaste installed)
|
|
184
|
-
${c('oidc-clients <name>')}
|
|
200
|
+
${c('oidc-clients <name> [grants]')}
|
|
201
|
+
OIDC clients the plugin declares on Authelia (no secrets), with grants
|
|
202
|
+
${c('oidc-clients <name> grant|revoke <clientId> --user <u>|--group <g>')}
|
|
203
|
+
Admit (or no longer) an external member to a client's sign-in —
|
|
204
|
+
members of the plugin's domain need no grant
|
|
185
205
|
${c('rotate-secret <name> <clientId>')}
|
|
186
206
|
Replace a confidential client's secret — printed once
|
|
187
207
|
${c('hosting <name>')} Which agent hosts the plugin's agent half, port, health
|
|
@@ -228,6 +248,8 @@ ${b('Subcommands:')}
|
|
|
228
248
|
Take every role away
|
|
229
249
|
${c('identity-sync <name> [--reconcile]')}
|
|
230
250
|
Sync its audience to its agent half now; --reconcile replays all of it
|
|
251
|
+
${c('domain <name> [<d>]')} The domain a plugin belongs to (its OIDC sign-in, surfaces, audience);
|
|
252
|
+
with <d>, move it to that active domain (refused while a surface is served)
|
|
231
253
|
${c('web-surface <name>')} The site a plugin owns for its web surface: what it declares, its site,
|
|
232
254
|
whether it is served (and why not), its trees, the bound tunnel, the offer
|
|
233
255
|
${c('web-surface <name> create [--subdomain <label>] [--access-mode authenticated|restricted]')}
|
|
@@ -1433,13 +1455,25 @@ async function uninstallPluginCmd(name, rest, { json }) {
|
|
|
1433
1455
|
* @param {string | undefined} name
|
|
1434
1456
|
* @param {{ json: boolean }} options
|
|
1435
1457
|
*/
|
|
1436
|
-
async function oidcClientsCmd(name, { json }) {
|
|
1437
|
-
|
|
1438
|
-
const msg =
|
|
1458
|
+
async function oidcClientsCmd(name, rest, { json }) {
|
|
1459
|
+
const usage = () => {
|
|
1460
|
+
const msg =
|
|
1461
|
+
'Usage: lamaste-server plugins oidc-clients <name> [grants | grant|revoke <clientId> --user <u>|--group <g>]';
|
|
1439
1462
|
if (json) emitError(msg);
|
|
1440
1463
|
else console.error(`\n ${msg}\n`);
|
|
1441
1464
|
process.exit(1);
|
|
1442
|
-
}
|
|
1465
|
+
};
|
|
1466
|
+
if (!name) return usage();
|
|
1467
|
+
const [action, clientId, ...principalArgs] = rest;
|
|
1468
|
+
if (action === 'grant' || action === 'revoke') {
|
|
1469
|
+
const principal = parsePrincipal(principalArgs);
|
|
1470
|
+
if (!clientId || !principal) return usage();
|
|
1471
|
+
return action === 'grant'
|
|
1472
|
+
? grantOidcClientCmd(clientId, principal, { json })
|
|
1473
|
+
: revokeOidcClientCmd(name, clientId, principal, { json });
|
|
1474
|
+
}
|
|
1475
|
+
if (action !== undefined && (action !== 'grants' || clientId !== undefined)) return usage();
|
|
1476
|
+
|
|
1443
1477
|
const result = await panelRequest('GET', `/api/plugins/${encodeURIComponent(name)}/oidc-clients`);
|
|
1444
1478
|
if (json) {
|
|
1445
1479
|
emit(result);
|
|
@@ -1459,7 +1493,7 @@ async function oidcClientsCmd(name, { json }) {
|
|
|
1459
1493
|
const kind = c.confidential ? chalk.yellow('confidential') : chalk.green('public');
|
|
1460
1494
|
const served = c.enabled ? '' : d(' (plugin disabled — not served)');
|
|
1461
1495
|
console.log(` ${chalk.cyan(c.id)} ${kind}${served}`);
|
|
1462
|
-
console.log(`
|
|
1496
|
+
console.log(` flows: ${c.grantTypes.join(', ')}`);
|
|
1463
1497
|
console.log(` scopes: ${c.scopes.join(', ')}`);
|
|
1464
1498
|
if (c.audience.length > 0) console.log(` audience: ${c.audience.join(', ')}`);
|
|
1465
1499
|
if (c.accessTokenClaims.length > 0) {
|
|
@@ -1472,10 +1506,80 @@ async function oidcClientsCmd(name, { json }) {
|
|
|
1472
1506
|
}`,
|
|
1473
1507
|
);
|
|
1474
1508
|
}
|
|
1509
|
+
if (c.userFacing) {
|
|
1510
|
+
const granted = Array.isArray(c.grants)
|
|
1511
|
+
? c.grants.length > 0
|
|
1512
|
+
? c.grants.map(principalText).join(', ')
|
|
1513
|
+
: d('none — external members do not sign in')
|
|
1514
|
+
: chalk.yellow('unknown (Gatekeeper could not be read)');
|
|
1515
|
+
console.log(` granted: ${granted}`);
|
|
1516
|
+
}
|
|
1475
1517
|
}
|
|
1476
1518
|
console.log('');
|
|
1477
1519
|
}
|
|
1478
1520
|
|
|
1521
|
+
/** Grant a user-facing client to a user or a Lamaste group (a Gatekeeper grant the panel checks). */
|
|
1522
|
+
async function grantOidcClientCmd(clientId, principal, { json }) {
|
|
1523
|
+
let result;
|
|
1524
|
+
try {
|
|
1525
|
+
result = await panelRequest('POST', '/api/gatekeeper/grants', {
|
|
1526
|
+
...principal,
|
|
1527
|
+
resourceType: OIDC_CLIENT_RESOURCE,
|
|
1528
|
+
resourceId: clientId,
|
|
1529
|
+
});
|
|
1530
|
+
} catch (err) {
|
|
1531
|
+
failPanel(err.message, json, err);
|
|
1532
|
+
}
|
|
1533
|
+
if (json) {
|
|
1534
|
+
emitComplete(result);
|
|
1535
|
+
return;
|
|
1536
|
+
}
|
|
1537
|
+
console.log(
|
|
1538
|
+
`\n ${chalk.green('✓')} ${principalText(principal)} may sign in with ${terminalText(clientId)}\n`,
|
|
1539
|
+
);
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
/** Revoke the grant of a user-facing client to a user or a Lamaste group. */
|
|
1543
|
+
async function revokeOidcClientCmd(name, clientId, principal, { json }) {
|
|
1544
|
+
let found;
|
|
1545
|
+
try {
|
|
1546
|
+
const { clients } = await panelRequest(
|
|
1547
|
+
'GET',
|
|
1548
|
+
`/api/plugins/${encodeURIComponent(name)}/oidc-clients`,
|
|
1549
|
+
);
|
|
1550
|
+
found = findClientGrants(clients ?? [], clientId, principal);
|
|
1551
|
+
} catch (err) {
|
|
1552
|
+
failPanel(err.message, json, err);
|
|
1553
|
+
}
|
|
1554
|
+
if ('error' in found) {
|
|
1555
|
+
const reason = {
|
|
1556
|
+
no_client: `Plugin ${name} declares no user-facing OIDC client ${clientId}`,
|
|
1557
|
+
unreadable: "Gatekeeper's grants could not be read — try again",
|
|
1558
|
+
no_grant: `${clientId} is not granted to ${principalText(principal)}`,
|
|
1559
|
+
}[found.error];
|
|
1560
|
+
failPanel(reason, json);
|
|
1561
|
+
}
|
|
1562
|
+
// Every grant naming the principal goes: one left behind would still let
|
|
1563
|
+
// them sign in.
|
|
1564
|
+
const results = [];
|
|
1565
|
+
try {
|
|
1566
|
+
for (const grantId of found.grantIds) {
|
|
1567
|
+
results.push(
|
|
1568
|
+
await panelRequest('DELETE', `/api/gatekeeper/grants/${encodeURIComponent(grantId)}`),
|
|
1569
|
+
);
|
|
1570
|
+
}
|
|
1571
|
+
} catch (err) {
|
|
1572
|
+
failPanel(err.message, json, err);
|
|
1573
|
+
}
|
|
1574
|
+
if (json) {
|
|
1575
|
+
emitComplete({ revoked: found.grantIds.length, results });
|
|
1576
|
+
return;
|
|
1577
|
+
}
|
|
1578
|
+
console.log(
|
|
1579
|
+
`\n ${chalk.green('✓')} ${principalText(principal)} no longer signs in with ${terminalText(clientId)}\n`,
|
|
1580
|
+
);
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1479
1583
|
/**
|
|
1480
1584
|
* @param {string | undefined} name
|
|
1481
1585
|
* @param {string | undefined} clientId
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* for agents.
|
|
5
5
|
*
|
|
6
6
|
* registry-credential show
|
|
7
|
-
* registry-credential set --url <registry> --oidc
|
|
7
|
+
* registry-credential set --url <registry> --oidc [--issuer-domain <domain>]
|
|
8
8
|
* The panel authenticates as its own `lamaste-server` OIDC client on
|
|
9
9
|
* the relay's Authelia (client-credentials grant, Feria login). Nothing
|
|
10
10
|
* to paste; Feria's auth.json must grant the relay's lamaste-server client
|
|
@@ -35,7 +35,7 @@ export async function runRegistryCredential(args, { json }) {
|
|
|
35
35
|
return clear({ json });
|
|
36
36
|
default:
|
|
37
37
|
process.stderr.write(
|
|
38
|
-
`Usage: lamaste-server registry-credential <show | set --url <registry> (--oidc | --token-stdin) | clear>\n`,
|
|
38
|
+
`Usage: lamaste-server registry-credential <show | set --url <registry> (--oidc [--issuer-domain <domain>] | --token-stdin) | clear>\n`,
|
|
39
39
|
);
|
|
40
40
|
process.exit(1);
|
|
41
41
|
}
|
|
@@ -68,6 +68,7 @@ async function set(args, { json }) {
|
|
|
68
68
|
let url;
|
|
69
69
|
let tokenFromStdin = false;
|
|
70
70
|
let oidc = false;
|
|
71
|
+
let issuerDomain;
|
|
71
72
|
for (let i = 0; i < args.length; i += 1) {
|
|
72
73
|
const arg = args[i];
|
|
73
74
|
if (arg === '--url') {
|
|
@@ -78,6 +79,8 @@ async function set(args, { json }) {
|
|
|
78
79
|
tokenFromStdin = true;
|
|
79
80
|
} else if (arg === '--oidc') {
|
|
80
81
|
oidc = true;
|
|
82
|
+
} else if (arg === '--issuer-domain') {
|
|
83
|
+
issuerDomain = args[++i]?.toLowerCase();
|
|
81
84
|
} else if (arg === '--token' || arg.startsWith('--token=')) {
|
|
82
85
|
throw new Error(
|
|
83
86
|
'The token is never accepted on the command line — set LAMASTE_REGISTRY_TOKEN or use --token-stdin',
|
|
@@ -87,13 +90,17 @@ async function set(args, { json }) {
|
|
|
87
90
|
}
|
|
88
91
|
}
|
|
89
92
|
if (!url) throw new Error('registry-credential set: --url <registry> is required');
|
|
93
|
+
if (issuerDomain !== undefined && !oidc) {
|
|
94
|
+
throw new Error('registry-credential set: --issuer-domain goes with --oidc');
|
|
95
|
+
}
|
|
90
96
|
if (oidc && tokenFromStdin) {
|
|
91
97
|
throw new Error('registry-credential set: choose --oidc or --token-stdin, not both');
|
|
92
98
|
}
|
|
93
99
|
|
|
94
100
|
let auth;
|
|
95
101
|
if (oidc) {
|
|
96
|
-
auth
|
|
102
|
+
// The domain whose issuer (https://auth.<domain>) the registry trusts; the primary when absent.
|
|
103
|
+
auth = { kind: 'oidc', ...(issuerDomain ? { issuerDomain } : {}) };
|
|
97
104
|
} else {
|
|
98
105
|
let token = tokenFromStdin ? (await readStdin()).trim() : process.env.LAMASTE_REGISTRY_TOKEN;
|
|
99
106
|
if (!token) {
|
|
@@ -61,6 +61,7 @@ ${b('Create options:')}
|
|
|
61
61
|
--type <type> Site type: managed (default) or custom
|
|
62
62
|
--custom-domain <d> Custom domain (required if type is custom)
|
|
63
63
|
--aliases <a,b> Hostnames that redirect to the custom domain (e.g. www.<domain>)
|
|
64
|
+
--domain <domain> A managed site's base domain, an active one (default: the primary)
|
|
64
65
|
--spa Enable SPA mode (single-page application routing)
|
|
65
66
|
--authelia Enable Authelia protection
|
|
66
67
|
`);
|
|
@@ -124,6 +125,7 @@ async function createSite(args, { json }) {
|
|
|
124
125
|
const type = getArg(args, 'type') || 'managed';
|
|
125
126
|
const customDomain = getArg(args, 'custom-domain') || undefined;
|
|
126
127
|
const aliasArg = getArg(args, 'aliases');
|
|
128
|
+
const domain = getArg(args, 'domain');
|
|
127
129
|
const spaMode = args.includes('--spa');
|
|
128
130
|
const autheliaProtected = args.includes('--authelia');
|
|
129
131
|
|
|
@@ -152,6 +154,7 @@ async function createSite(args, { json }) {
|
|
|
152
154
|
const body = { name, type, spaMode, autheliaProtected };
|
|
153
155
|
if (customDomain) body.customDomain = customDomain;
|
|
154
156
|
if (aliasArg) body.aliases = splitAliases(aliasArg);
|
|
157
|
+
if (domain) body.domain = domain.toLowerCase();
|
|
155
158
|
|
|
156
159
|
if (!json) process.stderr.write(` Creating site ${chalk.cyan(name)}...`);
|
|
157
160
|
|
|
@@ -10,6 +10,8 @@ import { readConfigSafe, PANEL_SERVICE, GATEKEEPER_SERVICE, PKI_DIR } from '../c
|
|
|
10
10
|
import { getMtlsCerts } from '@lamalibre/lamaste/server';
|
|
11
11
|
import { exec } from '../exec.js';
|
|
12
12
|
import { emit } from '../ndjson.js';
|
|
13
|
+
import { panelRequest } from '../panel-api.js';
|
|
14
|
+
import { domainRows } from '../domains.js';
|
|
13
15
|
|
|
14
16
|
/**
|
|
15
17
|
* @param {{ json: boolean }} options
|
|
@@ -20,6 +22,15 @@ export async function runStatus({ json }) {
|
|
|
20
22
|
|
|
21
23
|
// Collect service statuses
|
|
22
24
|
const panelStatus = await getServiceStatus(PANEL_SERVICE);
|
|
25
|
+
// The relay's domains, from the running panel — asked while the other
|
|
26
|
+
// checks run, briefly; none while it is down or not onboarded.
|
|
27
|
+
const domainsRequest =
|
|
28
|
+
domain && panelStatus.active
|
|
29
|
+
? panelRequest('GET', '/api/domains', null, { timeoutMs: 4000 }).then(
|
|
30
|
+
(r) => r.domains ?? [],
|
|
31
|
+
() => null,
|
|
32
|
+
)
|
|
33
|
+
: Promise.resolve(null);
|
|
23
34
|
const gatekeeperStatus = await getServiceStatus(GATEKEEPER_SERVICE);
|
|
24
35
|
const nginxStatus = await getServiceStatus('nginx');
|
|
25
36
|
const autheliaStatus = await getServiceStatus('authelia');
|
|
@@ -35,10 +46,12 @@ export async function runStatus({ json }) {
|
|
|
35
46
|
// Collect system info
|
|
36
47
|
const uptime = await getUptime();
|
|
37
48
|
const memory = await getMemoryUsage();
|
|
49
|
+
const domains = await domainsRequest;
|
|
38
50
|
|
|
39
51
|
if (json) {
|
|
40
52
|
emit({
|
|
41
53
|
domain,
|
|
54
|
+
domains,
|
|
42
55
|
services: {
|
|
43
56
|
panel: panelStatus,
|
|
44
57
|
gatekeeper: gatekeeperStatus,
|
|
@@ -66,6 +79,12 @@ export async function runStatus({ json }) {
|
|
|
66
79
|
if (domain) {
|
|
67
80
|
console.log(` ${b('Domain:')} ${c(String(domain))}`);
|
|
68
81
|
}
|
|
82
|
+
if (domains && domains.length > 1) {
|
|
83
|
+
console.log(` ${b('Domains:')}`);
|
|
84
|
+
for (const row of domainRows(domains)) {
|
|
85
|
+
console.log(` ${c(row.name)} ${d(`${row.state}, ${row.members} member(s)`)}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
69
88
|
|
|
70
89
|
if (uptime) {
|
|
71
90
|
console.log(` ${b('Uptime:')} ${d(uptime)}`);
|
|
@@ -89,6 +89,7 @@ ${b('Create options:')}
|
|
|
89
89
|
--description <text> Optional description
|
|
90
90
|
--access-mode <mode> Access mode: public, authenticated, restricted (default: restricted)
|
|
91
91
|
--max-body-mb <n> Largest request body in MiB, 1-10240 (default: 10)
|
|
92
|
+
--domain <domain> Base domain of the hostname, an active one (default: the primary)
|
|
92
93
|
`);
|
|
93
94
|
}
|
|
94
95
|
|
|
@@ -125,7 +126,7 @@ async function listTunnels({ json }) {
|
|
|
125
126
|
const carrier = t.agentLabel ? d(`via ${t.agentLabel}`) : r('no agent');
|
|
126
127
|
const bodyLimit = t.type !== 'panel' ? d(`${t.maxBodySizeMb ?? 1} MiB`) : '';
|
|
127
128
|
console.log(
|
|
128
|
-
` ${c(t.subdomain)} \u2192 :${t.port} ${enabled} ${carrier} ${accessMode} ${bodyLimit} ${type}`.trimEnd(),
|
|
129
|
+
` ${c(t.fqdn ?? t.subdomain)} \u2192 :${t.port} ${enabled} ${carrier} ${accessMode} ${bodyLimit} ${type}`.trimEnd(),
|
|
129
130
|
);
|
|
130
131
|
if (t.description) {
|
|
131
132
|
console.log(` ${d(t.description)}`);
|
|
@@ -150,6 +151,7 @@ async function createTunnel(args, { json }) {
|
|
|
150
151
|
const accessMode = getArg(args, 'access-mode') || undefined;
|
|
151
152
|
const agentLabel = getArg(args, 'agent');
|
|
152
153
|
const maxBodyStr = getArg(args, 'max-body-mb');
|
|
154
|
+
const domain = getArg(args, 'domain');
|
|
153
155
|
|
|
154
156
|
if (!subdomain || !portStr || !agentLabel) {
|
|
155
157
|
const msg = 'Error: --subdomain, --port and --agent are required';
|
|
@@ -180,6 +182,7 @@ async function createTunnel(args, { json }) {
|
|
|
180
182
|
if (description) body.description = description;
|
|
181
183
|
if (accessMode) body.accessMode = accessMode;
|
|
182
184
|
if (maxBodySizeMb !== undefined) body.maxBodySizeMb = maxBodySizeMb;
|
|
185
|
+
if (domain) body.domain = domain.toLowerCase();
|
|
183
186
|
|
|
184
187
|
if (!json) process.stderr.write(` Creating tunnel ${chalk.cyan(subdomain)} \u2192 :${port}...`);
|
|
185
188
|
|
|
@@ -31,9 +31,11 @@
|
|
|
31
31
|
|
|
32
32
|
import chalk from 'chalk';
|
|
33
33
|
import { panelRequest } from '../panel-api.js';
|
|
34
|
-
import { emit, emitComplete
|
|
34
|
+
import { emit, emitComplete } from '../ndjson.js';
|
|
35
|
+
import { fail } from '../fail.js';
|
|
35
36
|
import { parseUserArgs, warningLine } from '../plugin-identity.js';
|
|
36
37
|
import { formatMoment, terminalText } from '../plugin-settings.js';
|
|
38
|
+
import { membershipsText } from '../domains.js';
|
|
37
39
|
|
|
38
40
|
/**
|
|
39
41
|
* @param {string[]} args Subcommand args: ['list'] | ['disable', '<u>'] | ...
|
|
@@ -78,21 +80,6 @@ ${b('Subcommands:')}
|
|
|
78
80
|
`);
|
|
79
81
|
}
|
|
80
82
|
|
|
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
83
|
/** Print a change's warnings: what the panel could not complete beside it. */
|
|
97
84
|
function printWarnings(warnings) {
|
|
98
85
|
for (const warning of warnings ?? []) {
|
|
@@ -107,7 +94,8 @@ function statusCell(status) {
|
|
|
107
94
|
|
|
108
95
|
/**
|
|
109
96
|
* The rows `users list` prints: each user's username, status, directory id
|
|
110
|
-
* (a dash until the directory has seen them)
|
|
97
|
+
* (a dash until the directory has seen them), groups and domain memberships
|
|
98
|
+
* (external ones marked), as terminal text.
|
|
111
99
|
* Pure, exported for tests.
|
|
112
100
|
*
|
|
113
101
|
* @param {Array<Record<string, any>>} users as `GET /api/users` lists them
|
|
@@ -118,6 +106,7 @@ export function userRows(users) {
|
|
|
118
106
|
status: u.status === 'disabled' ? 'disabled' : 'active',
|
|
119
107
|
userId: u.userId ? terminalText(u.userId) : '—',
|
|
120
108
|
groups: terminalText((u.groups ?? []).join(', ')),
|
|
109
|
+
domains: membershipsText(u.domains),
|
|
121
110
|
}));
|
|
122
111
|
}
|
|
123
112
|
|
|
@@ -142,11 +131,11 @@ async function listUsers({ json }) {
|
|
|
142
131
|
console.log('');
|
|
143
132
|
console.log(chalk.bold(' Users'));
|
|
144
133
|
console.log(
|
|
145
|
-
chalk.dim(` ${'USERNAME'.padEnd(width)} STATUS ${'USER ID'.padEnd(36)} GROUPS`),
|
|
134
|
+
chalk.dim(` ${'USERNAME'.padEnd(width)} STATUS ${'USER ID'.padEnd(36)} DOMAINS GROUPS`),
|
|
146
135
|
);
|
|
147
136
|
for (const r of rows) {
|
|
148
137
|
console.log(
|
|
149
|
-
` ${chalk.cyan(r.username.padEnd(width))} ${statusCell(r.status)} ${r.userId.padEnd(36)} ${chalk.dim(r.groups)}`,
|
|
138
|
+
` ${chalk.cyan(r.username.padEnd(width))} ${statusCell(r.status)} ${r.userId.padEnd(36)} ${r.domains} ${chalk.dim(r.groups)}`,
|
|
150
139
|
);
|
|
151
140
|
}
|
|
152
141
|
if (rows.some((r) => r.userId === '—')) {
|