@lamalibre/create-lamaste 4.5.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 +56 -5
- 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/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 +1 -1
- package/vendor/docs/03-architecture/nginx-configuration.md +80 -12
- 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 +116 -37
- 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 +54 -8
- 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 +9 -0
- package/vendor/docs/04-api-reference/tunnels.md +35 -28
- 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 +15 -6
- package/vendor/docs/06-reference/config-files.md +19 -15
- 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/index.d.ts +3 -3
- package/vendor/gatekeeper/dist/index.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/index.js +2 -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.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/lib/tunnels.d.ts +3 -6
- package/vendor/gatekeeper/dist/lib/tunnels.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/lib/tunnels.js +4 -16
- package/vendor/gatekeeper/dist/lib/tunnels.js.map +1 -1
- package/vendor/gatekeeper/dist/lib/types.d.ts +0 -19
- package/vendor/gatekeeper/dist/lib/types.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/index.d.ts +6 -1
- package/vendor/gatekeeper/dist/server/index.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/index.js +56 -11
- 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 +325 -55
- 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/plugin-web-surface.js +269 -0
- package/vendor/server/src/commands/plugins.js +175 -14
- package/vendor/server/src/commands/registry-credential.js +10 -3
- package/vendor/server/src/commands/sites.js +18 -1
- 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/src/plugin-approval.js +80 -1
- package/vendor/server/src/plugin-web-surface.js +307 -0
- package/vendor/server-ui/dist/assets/index-B8Tv_ocF.js +84 -0
- package/vendor/server-ui/dist/assets/index-Cb-ADsbB.css +1 -0
- package/vendor/server-ui/dist/index.html +2 -2
- package/vendor/serverd/package.json +2 -2
- package/vendor/serverd/src/index.js +50 -19
- 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/chisel-runtime.js +18 -6
- package/vendor/serverd/src/lib/chisel.js +10 -2
- 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 +34 -7
- package/vendor/serverd/src/lib/hook-pipelines.js +8 -3
- package/vendor/serverd/src/lib/identity-sync.js +35 -0
- package/vendor/serverd/src/lib/migrations/state/0015_plugin_web_surface.sql +14 -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 +576 -131
- 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-hooks.js +40 -11
- package/vendor/serverd/src/lib/plugin-lifecycle.js +129 -5
- package/vendor/serverd/src/lib/plugin-oidc.js +144 -42
- package/vendor/serverd/src/lib/plugin-packages.js +74 -2
- package/vendor/serverd/src/lib/plugins.js +82 -9
- 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 +29 -0
- package/vendor/serverd/src/lib/tunnel-deps.js +2 -4
- package/vendor/serverd/src/lib/user-surface.js +77 -31
- package/vendor/serverd/src/lib/users-lock.js +23 -0
- package/vendor/serverd/src/lib/vhost-reconcile.js +151 -41
- package/vendor/serverd/src/lib/web-surface.js +783 -0
- 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/plugin-web-surface.js +151 -0
- package/vendor/serverd/src/routes/management/plugins.js +6 -2
- package/vendor/serverd/src/routes/management/registry-credential.js +86 -57
- package/vendor/serverd/src/routes/management/sites.js +188 -71
- package/vendor/serverd/src/routes/management/tunnels.js +37 -22
- package/vendor/serverd/src/routes/management/users.js +184 -64
- package/vendor/serverd/src/routes/management.js +4 -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-D-fer5ef.css +0 -1
- package/vendor/server-ui/dist/assets/index-DSaDPJaw.js +0 -84
|
@@ -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,11 @@
|
|
|
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
|
|
68
|
+
* web-surface <name> [create [--subdomain <s>] [--access-mode <m>] | move --tunnel <t> | delete [--yes]]
|
|
69
|
+
* The site an agent-hosted plugin owns for its web surface: what it
|
|
70
|
+
* declares, whether it is served, its trees; create, move a bound
|
|
71
|
+
* tunnel's host to it, delete it
|
|
62
72
|
*
|
|
63
73
|
* Every subcommand goes through the running panel (`/api/plugins/*`, admin
|
|
64
74
|
* certificate): the panel owns the plugin registry (SQLite `state.db`), runs
|
|
@@ -77,6 +87,15 @@ import { panelRequest } from '../panel-api.js';
|
|
|
77
87
|
import { emit, emitStep, emitError, emitComplete, emitLog } from '../ndjson.js';
|
|
78
88
|
import { configureCmd, settingsCmd } from './plugin-settings.js';
|
|
79
89
|
import { audienceCmd, entitlementsCmd, identityCmd, identitySyncCmd } from './plugin-identity.js';
|
|
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';
|
|
80
99
|
import { settingsDiffLines, terminalText } from '../plugin-settings.js';
|
|
81
100
|
import {
|
|
82
101
|
APPROVE_HOOKS_USAGE,
|
|
@@ -85,6 +104,8 @@ import {
|
|
|
85
104
|
identityDiffLines,
|
|
86
105
|
parseApproveArgs,
|
|
87
106
|
pendingUpdateReasons,
|
|
107
|
+
webSurfaceBlockLines,
|
|
108
|
+
webTreeDiffLines,
|
|
88
109
|
} from '../plugin-approval.js';
|
|
89
110
|
|
|
90
111
|
/** npm runs inside the panel for install and uninstall (its own limit is 120 s). */
|
|
@@ -109,7 +130,7 @@ export async function runPlugins(args, { json }) {
|
|
|
109
130
|
case 'uninstall':
|
|
110
131
|
return uninstallPluginCmd(args[1], args.slice(2), { json });
|
|
111
132
|
case 'oidc-clients':
|
|
112
|
-
return oidcClientsCmd(args[1], { json });
|
|
133
|
+
return oidcClientsCmd(args[1], args.slice(2), { json });
|
|
113
134
|
case 'rotate-secret':
|
|
114
135
|
return rotateSecretCmd(args[1], args[2], { json });
|
|
115
136
|
case 'hosting':
|
|
@@ -152,6 +173,10 @@ export async function runPlugins(args, { json }) {
|
|
|
152
173
|
return entitlementsCmd(args.slice(1), { json });
|
|
153
174
|
case 'identity-sync':
|
|
154
175
|
return identitySyncCmd(args.slice(1), { json });
|
|
176
|
+
case 'web-surface':
|
|
177
|
+
return webSurfaceCmd(args.slice(1), { json });
|
|
178
|
+
case 'domain':
|
|
179
|
+
return pluginDomainCmd(args.slice(1), { json });
|
|
155
180
|
default:
|
|
156
181
|
printPluginUsage();
|
|
157
182
|
process.exit(sub ? 1 : 0);
|
|
@@ -172,7 +197,11 @@ ${b('Subcommands:')}
|
|
|
172
197
|
${c('uninstall <name> [--force] [--purge]')}
|
|
173
198
|
Remove a disabled plugin (a hosted one through its uninstall pipeline;
|
|
174
199
|
--purge deletes the data of the agent half Lamaste installed)
|
|
175
|
-
${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
|
|
176
205
|
${c('rotate-secret <name> <clientId>')}
|
|
177
206
|
Replace a confidential client's secret — printed once
|
|
178
207
|
${c('hosting <name>')} Which agent hosts the plugin's agent half, port, health
|
|
@@ -219,6 +248,16 @@ ${b('Subcommands:')}
|
|
|
219
248
|
Take every role away
|
|
220
249
|
${c('identity-sync <name> [--reconcile]')}
|
|
221
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)
|
|
253
|
+
${c('web-surface <name>')} The site a plugin owns for its web surface: what it declares, its site,
|
|
254
|
+
whether it is served (and why not), its trees, the bound tunnel, the offer
|
|
255
|
+
${c('web-surface <name> create [--subdomain <label>] [--access-mode authenticated|restricted]')}
|
|
256
|
+
Give it its site (default: the subdomain and access mode it declares)
|
|
257
|
+
${c('web-surface <name> move --tunnel <id | subdomain | fqdn>')}
|
|
258
|
+
Hand the host of the tunnel bound as its user surface to its site
|
|
259
|
+
${c('web-surface <name> delete [--yes]')}
|
|
260
|
+
Delete its site, its trees and the grants on it (typed confirmation)
|
|
222
261
|
`);
|
|
223
262
|
}
|
|
224
263
|
|
|
@@ -796,16 +835,25 @@ function printIdentityBlock(block, title) {
|
|
|
796
835
|
for (const line of identityBlockLines(block)) console.log(` ${line}`);
|
|
797
836
|
}
|
|
798
837
|
|
|
838
|
+
/**
|
|
839
|
+
* Print a web surface — the web tree an approval covers and the site
|
|
840
|
+
* Lamaste offers for it (the hook status's `web`).
|
|
841
|
+
*/
|
|
842
|
+
function printWebSurfaceBlock(web, title) {
|
|
843
|
+
console.log(chalk.bold(` ${title}`));
|
|
844
|
+
for (const line of webSurfaceBlockLines(web)) console.log(` ${line}`);
|
|
845
|
+
}
|
|
846
|
+
|
|
799
847
|
/**
|
|
800
848
|
* Print what a version brings that needs approval — the approval digest
|
|
801
849
|
* (what `approve-hooks` takes; none for an update that brings neither
|
|
802
|
-
* relay hooks, nor an agent half Lamaste installs, nor an identity block
|
|
803
|
-
* its relay hook set, the agent half Lamaste installs
|
|
804
|
-
* block.
|
|
850
|
+
* relay hooks, nor an agent half Lamaste installs, nor an identity block,
|
|
851
|
+
* nor a web surface), its relay hook set, the agent half Lamaste installs,
|
|
852
|
+
* its identity block and its web tree.
|
|
805
853
|
*/
|
|
806
|
-
function printApprovalContents({ digest, relay, agentHalf, identity = null }) {
|
|
854
|
+
function printApprovalContents({ digest, relay, agentHalf, identity = null, web = null }) {
|
|
807
855
|
console.log(
|
|
808
|
-
` approval digest: ${digest ? chalk.cyan(digest) : chalk.dim('none — no relay hooks, no agent half Lamaste installs, no identity block')}`,
|
|
856
|
+
` approval digest: ${digest ? chalk.cyan(digest) : chalk.dim('none — no relay hooks, no agent half Lamaste installs, no identity block, no web surface')}`,
|
|
809
857
|
);
|
|
810
858
|
if (relay) {
|
|
811
859
|
console.log('');
|
|
@@ -822,6 +870,27 @@ function printApprovalContents({ digest, relay, agentHalf, identity = null }) {
|
|
|
822
870
|
'Identity — Lamaste sends its agent half the users entitled to these roles',
|
|
823
871
|
);
|
|
824
872
|
}
|
|
873
|
+
if (web?.declared) {
|
|
874
|
+
console.log('');
|
|
875
|
+
printWebSurfaceBlock(
|
|
876
|
+
web,
|
|
877
|
+
'Web surface — the relay serves this tree on the site the plugin owns',
|
|
878
|
+
);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/**
|
|
883
|
+
* Print how an update changes the plugin's web tree (part of the digest):
|
|
884
|
+
* the tree before and after, its file count and size.
|
|
885
|
+
*/
|
|
886
|
+
function printWebTreeChanges(web) {
|
|
887
|
+
const shown = webTreeDiffLines(web);
|
|
888
|
+
if (!shown) return;
|
|
889
|
+
console.log('');
|
|
890
|
+
console.log(chalk.bold(` Web surface — this version ${shown.headline}`));
|
|
891
|
+
for (const line of shown.lines) {
|
|
892
|
+
console.log(` ${shown.changed ? line : chalk.dim(line)}`);
|
|
893
|
+
}
|
|
825
894
|
}
|
|
826
895
|
|
|
827
896
|
/**
|
|
@@ -874,6 +943,7 @@ function printPendingUpdate(name, update, title) {
|
|
|
874
943
|
agentHalf: update.agentHalf,
|
|
875
944
|
});
|
|
876
945
|
printIdentityChanges(update.identity);
|
|
946
|
+
printWebTreeChanges(update.web);
|
|
877
947
|
const lines = settingsDiffLines(update.settings);
|
|
878
948
|
if (lines.length > 0) {
|
|
879
949
|
console.log('');
|
|
@@ -916,7 +986,7 @@ async function hooksCmd(name, { json }) {
|
|
|
916
986
|
console.log('');
|
|
917
987
|
if (!approval.required && !hooks.agent && !hooks.pendingUpdate) {
|
|
918
988
|
console.log(
|
|
919
|
-
` Plugin ${chalk.cyan(name)} declares no lifecycle hooks, no agent half for Lamaste to install and no
|
|
989
|
+
` Plugin ${chalk.cyan(name)} declares no lifecycle hooks, no agent half for Lamaste to install, no identity and no web surface.\n`,
|
|
920
990
|
);
|
|
921
991
|
return;
|
|
922
992
|
}
|
|
@@ -933,6 +1003,7 @@ async function hooksCmd(name, { json }) {
|
|
|
933
1003
|
relay: hooks.relay,
|
|
934
1004
|
agentHalf: hooks.agentHalf,
|
|
935
1005
|
identity: hooks.identity?.declared ? hooks.identity.block : null,
|
|
1006
|
+
web: hooks.web ?? null,
|
|
936
1007
|
});
|
|
937
1008
|
console.log(
|
|
938
1009
|
`\n auto-approve for later versions: ${hooks.autoApprove ? chalk.yellow('on') : 'off'}`,
|
|
@@ -947,6 +1018,13 @@ async function hooksCmd(name, { json }) {
|
|
|
947
1018
|
),
|
|
948
1019
|
);
|
|
949
1020
|
}
|
|
1021
|
+
if (hooks.web?.declared) {
|
|
1022
|
+
console.log(
|
|
1023
|
+
chalk.dim(
|
|
1024
|
+
` Its site (served, trees, offer): lamaste-server plugins web-surface ${terminalText(name)}`,
|
|
1025
|
+
),
|
|
1026
|
+
);
|
|
1027
|
+
}
|
|
950
1028
|
}
|
|
951
1029
|
if (hooks.agent) {
|
|
952
1030
|
console.log(chalk.bold(`\n Agent hooks (run by the agent half; no approval)`));
|
|
@@ -1262,6 +1340,7 @@ async function installPluginCmd(packageName, { json }) {
|
|
|
1262
1340
|
relay: result.hooks.relay,
|
|
1263
1341
|
agentHalf: result.hooks.agentHalf,
|
|
1264
1342
|
identity: result.hooks.identity?.declared ? result.hooks.identity.block : null,
|
|
1343
|
+
web: result.hooks.web ?? null,
|
|
1265
1344
|
});
|
|
1266
1345
|
if (approval.state === 'pending') {
|
|
1267
1346
|
console.log(
|
|
@@ -1376,13 +1455,25 @@ async function uninstallPluginCmd(name, rest, { json }) {
|
|
|
1376
1455
|
* @param {string | undefined} name
|
|
1377
1456
|
* @param {{ json: boolean }} options
|
|
1378
1457
|
*/
|
|
1379
|
-
async function oidcClientsCmd(name, { json }) {
|
|
1380
|
-
|
|
1381
|
-
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>]';
|
|
1382
1462
|
if (json) emitError(msg);
|
|
1383
1463
|
else console.error(`\n ${msg}\n`);
|
|
1384
1464
|
process.exit(1);
|
|
1385
|
-
}
|
|
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
|
+
|
|
1386
1477
|
const result = await panelRequest('GET', `/api/plugins/${encodeURIComponent(name)}/oidc-clients`);
|
|
1387
1478
|
if (json) {
|
|
1388
1479
|
emit(result);
|
|
@@ -1402,7 +1493,7 @@ async function oidcClientsCmd(name, { json }) {
|
|
|
1402
1493
|
const kind = c.confidential ? chalk.yellow('confidential') : chalk.green('public');
|
|
1403
1494
|
const served = c.enabled ? '' : d(' (plugin disabled — not served)');
|
|
1404
1495
|
console.log(` ${chalk.cyan(c.id)} ${kind}${served}`);
|
|
1405
|
-
console.log(`
|
|
1496
|
+
console.log(` flows: ${c.grantTypes.join(', ')}`);
|
|
1406
1497
|
console.log(` scopes: ${c.scopes.join(', ')}`);
|
|
1407
1498
|
if (c.audience.length > 0) console.log(` audience: ${c.audience.join(', ')}`);
|
|
1408
1499
|
if (c.accessTokenClaims.length > 0) {
|
|
@@ -1415,10 +1506,80 @@ async function oidcClientsCmd(name, { json }) {
|
|
|
1415
1506
|
}`,
|
|
1416
1507
|
);
|
|
1417
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
|
+
}
|
|
1418
1517
|
}
|
|
1419
1518
|
console.log('');
|
|
1420
1519
|
}
|
|
1421
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
|
+
|
|
1422
1583
|
/**
|
|
1423
1584
|
* @param {string | undefined} name
|
|
1424
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) {
|
|
@@ -7,6 +7,10 @@
|
|
|
7
7
|
* delete <id> Delete a site by ID
|
|
8
8
|
* aliases <id> --set <a,b|""> Replace a custom site's redirect aliases
|
|
9
9
|
*
|
|
10
|
+
* A site a plugin owns (its web surface) is marked in the list and managed
|
|
11
|
+
* through `lamaste-server plugins web-surface <plugin>`; the panel refuses
|
|
12
|
+
* to change or delete it here.
|
|
13
|
+
*
|
|
10
14
|
* List reads directly from the state file.
|
|
11
15
|
* Create/delete use the panel REST API (which has the full
|
|
12
16
|
* nginx/certbot/directory dependency stack).
|
|
@@ -16,6 +20,7 @@ import chalk from 'chalk';
|
|
|
16
20
|
import { readSites } from '../state.js';
|
|
17
21
|
import { panelRequest } from '../panel-api.js';
|
|
18
22
|
import { emit, emitError, emitComplete } from '../ndjson.js';
|
|
23
|
+
import { siteOwnerTag } from '../plugin-web-surface.js';
|
|
19
24
|
|
|
20
25
|
/**
|
|
21
26
|
* @param {string[]} args
|
|
@@ -56,6 +61,7 @@ ${b('Create options:')}
|
|
|
56
61
|
--type <type> Site type: managed (default) or custom
|
|
57
62
|
--custom-domain <d> Custom domain (required if type is custom)
|
|
58
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)
|
|
59
65
|
--spa Enable SPA mode (single-page application routing)
|
|
60
66
|
--authelia Enable Authelia protection
|
|
61
67
|
`);
|
|
@@ -92,12 +98,21 @@ async function listSites({ json }) {
|
|
|
92
98
|
const certStatus = s.certIssued ? g('cert issued') : y('no cert');
|
|
93
99
|
const spaTag = s.spaMode ? d('[SPA]') : '';
|
|
94
100
|
const authTag = s.autheliaProtected ? d('[protected]') : '';
|
|
101
|
+
const owner = siteOwnerTag(s);
|
|
102
|
+
const ownerTag = owner ? c(owner) : '';
|
|
95
103
|
|
|
96
104
|
console.log(
|
|
97
|
-
` ${c(s.name)} ${d(s.fqdn)} ${s.type} ${dnsStatus} ${certStatus} ${spaTag} ${authTag}`.trimEnd(),
|
|
105
|
+
` ${c(s.name)} ${d(s.fqdn)} ${s.type} ${dnsStatus} ${certStatus} ${spaTag} ${authTag} ${ownerTag}`.trimEnd(),
|
|
98
106
|
);
|
|
99
107
|
console.log(` ${d(`id: ${s.id}`)}`);
|
|
100
108
|
}
|
|
109
|
+
if (sites.some((s) => siteOwnerTag(s) !== null)) {
|
|
110
|
+
console.log(
|
|
111
|
+
d(
|
|
112
|
+
'\n A [plugin …] site is the web surface of that plugin: manage it with lamaste-server plugins web-surface <plugin>',
|
|
113
|
+
),
|
|
114
|
+
);
|
|
115
|
+
}
|
|
101
116
|
console.log('');
|
|
102
117
|
}
|
|
103
118
|
|
|
@@ -110,6 +125,7 @@ async function createSite(args, { json }) {
|
|
|
110
125
|
const type = getArg(args, 'type') || 'managed';
|
|
111
126
|
const customDomain = getArg(args, 'custom-domain') || undefined;
|
|
112
127
|
const aliasArg = getArg(args, 'aliases');
|
|
128
|
+
const domain = getArg(args, 'domain');
|
|
113
129
|
const spaMode = args.includes('--spa');
|
|
114
130
|
const autheliaProtected = args.includes('--authelia');
|
|
115
131
|
|
|
@@ -138,6 +154,7 @@ async function createSite(args, { json }) {
|
|
|
138
154
|
const body = { name, type, spaMode, autheliaProtected };
|
|
139
155
|
if (customDomain) body.customDomain = customDomain;
|
|
140
156
|
if (aliasArg) body.aliases = splitAliases(aliasArg);
|
|
157
|
+
if (domain) body.domain = domain.toLowerCase();
|
|
141
158
|
|
|
142
159
|
if (!json) process.stderr.write(` Creating site ${chalk.cyan(name)}...`);
|
|
143
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 === '—')) {
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The relay's domains as `lamaste-server domains` prints them — pure
|
|
3
|
+
* helpers, exported for tests (the command is src/commands/domains.js).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { terminalText } from './plugin-settings.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* One row per domain: name, state, members, what uses it — as terminal text.
|
|
10
|
+
*
|
|
11
|
+
* @param {Array<Record<string, any>>} domains as `GET /api/domains` lists them
|
|
12
|
+
*/
|
|
13
|
+
export function domainRows(domains) {
|
|
14
|
+
return domains.map((d) => ({
|
|
15
|
+
name: terminalText(d.name),
|
|
16
|
+
state: d.status === 'active' ? (d.primary ? 'primary' : 'active') : 'pending',
|
|
17
|
+
members: Number.isInteger(d.members) ? d.members : 0,
|
|
18
|
+
externalMembers: Number.isInteger(d.externalMembers) ? d.externalMembers : 0,
|
|
19
|
+
usedBy: usageText(d.usedBy),
|
|
20
|
+
}));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** What uses a domain, in one line (empty when nothing does). */
|
|
24
|
+
export function usageText(usedBy) {
|
|
25
|
+
if (!usedBy) return '';
|
|
26
|
+
const parts = [];
|
|
27
|
+
const count = (label, items) => {
|
|
28
|
+
if (Array.isArray(items) && items.length > 0) parts.push(`${items.length} ${label}`);
|
|
29
|
+
};
|
|
30
|
+
count('tunnel(s)', usedBy.tunnels);
|
|
31
|
+
count('site(s)', usedBy.sites);
|
|
32
|
+
count('plugin(s)', usedBy.plugins);
|
|
33
|
+
if (usedBy.registry) parts.push('the registry credential');
|
|
34
|
+
return parts.join(', ');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** `/api/domains/<name><rest>`. */
|
|
38
|
+
export function domainPath(name, rest = '') {
|
|
39
|
+
return `/api/domains/${encodeURIComponent(name)}${rest}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* A user's memberships in one line — `a.example, b.example (external)` —
|
|
44
|
+
* as terminal text; `—` for none. A bare name (an older panel's answer) is
|
|
45
|
+
* a member.
|
|
46
|
+
*
|
|
47
|
+
* @param {ReadonlyArray<string | { name: string, kind?: string }> | undefined} domains
|
|
48
|
+
*/
|
|
49
|
+
export function membershipsText(domains) {
|
|
50
|
+
const parts = (domains ?? []).flatMap((d) => {
|
|
51
|
+
const name = typeof d === 'string' ? d : d?.name;
|
|
52
|
+
if (typeof name !== 'string') return [];
|
|
53
|
+
return [terminalText(d?.kind === 'external' ? `${name} (external)` : name)];
|
|
54
|
+
});
|
|
55
|
+
return parts.length > 0 ? parts.join(', ') : '—';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Whether any of `domains` is an external membership. */
|
|
59
|
+
export function isExternalAnywhere(domains) {
|
|
60
|
+
return (domains ?? []).some((d) => d?.kind === 'external');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* What `domains members <domain> <action> <user> …` asks the panel, or
|
|
65
|
+
* null for a command line it does not take:
|
|
66
|
+
*
|
|
67
|
+
* - `add <user> [--external]` — a member (an external one with the flag);
|
|
68
|
+
* - `kind <user> member|external` — change the kind of a membership;
|
|
69
|
+
* - `remove <user>` — end it.
|
|
70
|
+
*
|
|
71
|
+
* `user` is a directory id or a username.
|
|
72
|
+
*
|
|
73
|
+
* @param {string} domain
|
|
74
|
+
* @param {string[]} rest what follows `members <domain>`
|
|
75
|
+
* @returns {{ method: 'PUT' | 'DELETE', path: string, body: object | null, user: string, kind: 'member' | 'external' | null } | null}
|
|
76
|
+
*/
|
|
77
|
+
export function memberRequest(domain, rest) {
|
|
78
|
+
const [action, user, ...more] = rest;
|
|
79
|
+
if (!user) return null;
|
|
80
|
+
const path = domainPath(domain, `/members/${encodeURIComponent(user)}`);
|
|
81
|
+
if (action === 'remove' && more.length === 0) {
|
|
82
|
+
return { method: 'DELETE', path, body: null, user, kind: null };
|
|
83
|
+
}
|
|
84
|
+
if (action === 'add' && more.every((a) => a === '--external')) {
|
|
85
|
+
const kind = more.length > 0 ? 'external' : 'member';
|
|
86
|
+
return { method: 'PUT', path, body: { kind }, user, kind };
|
|
87
|
+
}
|
|
88
|
+
if (action === 'kind' && more.length === 1 && (more[0] === 'member' || more[0] === 'external')) {
|
|
89
|
+
return { method: 'PUT', path, body: { kind: more[0] }, user, kind: more[0] };
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|