@lamalibre/create-lamaste 4.0.0 → 4.1.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 +21 -2
- package/src/lib/oidc-issuer-key.js +124 -0
- package/src/lib/relay-services.js +4 -0
- package/src/lib/service-config.js +16 -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 +45 -1
- package/vendor/docs/01-concepts/certificates.md +3 -3
- package/vendor/docs/01-concepts/mtls.md +2 -2
- 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 +3 -3
- 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/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 +6 -5
- package/vendor/docs/03-architecture/management-flow.md +1 -1
- 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 +2 -2
- package/vendor/docs/03-architecture/system-overview.md +1 -1
- package/vendor/docs/04-api-reference/onboarding.md +1 -1
- package/vendor/docs/04-api-reference/overview.md +111 -105
- package/vendor/docs/04-api-reference/registry-and-oidc.md +182 -0
- 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 -5
- package/vendor/docs/06-reference/config-files.md +103 -45
- 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 +5 -0
- package/vendor/docs/gatekeeper-design.md +8 -8
- package/vendor/docs/identity-directory-design.md +443 -0
- package/vendor/docs/package.json +1 -1
- package/vendor/docs/todo/lib-cli-desktop-refactor.md +27 -27
- package/vendor/gatekeeper/package.json +1 -1
- package/vendor/server/bin/lamaste-server.js +10 -1
- package/vendor/server/package.json +4 -3
- package/vendor/server/src/commands/plugins.js +87 -0
- package/vendor/server/src/commands/registry-credential.js +142 -0
- package/vendor/server/src/panel-api.js +83 -38
- package/vendor/serverd/package.json +5 -2
- package/vendor/serverd/src/index.js +11 -0
- package/vendor/serverd/src/lib/authelia.js +11 -0
- package/vendor/serverd/src/lib/plugin-oidc.js +216 -0
- package/vendor/serverd/src/lib/plugins.js +26 -2
- package/vendor/serverd/src/lib/registry-credential.js +145 -0
- package/vendor/serverd/src/routes/management/agents.js +81 -0
- package/vendor/serverd/src/routes/management/plugins.js +65 -2
- package/vendor/serverd/src/routes/management/registry-credential.js +170 -0
- package/vendor/serverd/src/routes/management/users.js +31 -0
- package/vendor/serverd/src/routes/management.js +2 -0
|
@@ -7,6 +7,12 @@
|
|
|
7
7
|
* enable <name> Enable a plugin
|
|
8
8
|
* disable <name> Disable a plugin
|
|
9
9
|
* uninstall <name> Remove a plugin (must be disabled first)
|
|
10
|
+
* oidc-clients <name> The OIDC clients a plugin declares on Authelia
|
|
11
|
+
* rotate-secret <name> <clientId>
|
|
12
|
+
* New secret for a confidential OIDC client (shown once)
|
|
13
|
+
*
|
|
14
|
+
* The OIDC subcommands go through the running panel: writing a client to
|
|
15
|
+
* Authelia is the panel's job.
|
|
10
16
|
*/
|
|
11
17
|
|
|
12
18
|
import chalk from 'chalk';
|
|
@@ -20,6 +26,7 @@ import {
|
|
|
20
26
|
} from '@lamalibre/lamaste/server';
|
|
21
27
|
import { STATE_DIR, PANEL_SERVICE } from '../config.js';
|
|
22
28
|
import { exec } from '../exec.js';
|
|
29
|
+
import { panelRequest } from '../panel-api.js';
|
|
23
30
|
import { createLogger } from '../logger.js';
|
|
24
31
|
import { emit, emitStep, emitError, emitComplete, emitLog } from '../ndjson.js';
|
|
25
32
|
|
|
@@ -47,6 +54,10 @@ export async function runPlugins(args, { json }) {
|
|
|
47
54
|
return disablePluginCmd(args[1], { json });
|
|
48
55
|
case 'uninstall':
|
|
49
56
|
return uninstallPluginCmd(args[1], { json });
|
|
57
|
+
case 'oidc-clients':
|
|
58
|
+
return oidcClientsCmd(args[1], { json });
|
|
59
|
+
case 'rotate-secret':
|
|
60
|
+
return rotateSecretCmd(args[1], args[2], { json });
|
|
50
61
|
default:
|
|
51
62
|
printPluginUsage();
|
|
52
63
|
process.exit(sub ? 1 : 0);
|
|
@@ -65,6 +76,9 @@ ${b('Subcommands:')}
|
|
|
65
76
|
${c('enable <name>')} Enable a plugin
|
|
66
77
|
${c('disable <name>')} Disable a plugin
|
|
67
78
|
${c('uninstall <name>')} Remove a disabled plugin
|
|
79
|
+
${c('oidc-clients <name>')} OIDC clients the plugin declares on Authelia (no secrets)
|
|
80
|
+
${c('rotate-secret <name> <clientId>')}
|
|
81
|
+
Replace a confidential client's secret — printed once
|
|
68
82
|
`);
|
|
69
83
|
}
|
|
70
84
|
|
|
@@ -252,6 +266,79 @@ async function uninstallPluginCmd(name, { json }) {
|
|
|
252
266
|
}
|
|
253
267
|
}
|
|
254
268
|
|
|
269
|
+
/**
|
|
270
|
+
* @param {string | undefined} name
|
|
271
|
+
* @param {{ json: boolean }} options
|
|
272
|
+
*/
|
|
273
|
+
async function oidcClientsCmd(name, { json }) {
|
|
274
|
+
if (!name) {
|
|
275
|
+
const msg = 'Usage: lamaste-server plugins oidc-clients <name>';
|
|
276
|
+
if (json) emitError(msg);
|
|
277
|
+
else console.error(`\n ${msg}\n`);
|
|
278
|
+
process.exit(1);
|
|
279
|
+
}
|
|
280
|
+
const result = await panelRequest('GET', `/api/plugins/${encodeURIComponent(name)}/oidc-clients`);
|
|
281
|
+
if (json) {
|
|
282
|
+
emit(result);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
const clients = result.clients ?? [];
|
|
286
|
+
if (clients.length === 0) {
|
|
287
|
+
console.log(`\n Plugin ${chalk.cyan(name)} declares no OIDC clients.\n`);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
const b = chalk.bold;
|
|
291
|
+
const d = chalk.dim;
|
|
292
|
+
console.log('');
|
|
293
|
+
console.log(b(` OIDC clients of ${name}`));
|
|
294
|
+
console.log(d(' ' + '\u2500'.repeat(40)));
|
|
295
|
+
for (const c of clients) {
|
|
296
|
+
const kind = c.confidential ? chalk.yellow('confidential') : chalk.green('public');
|
|
297
|
+
const served = c.enabled ? '' : d(' (plugin disabled — not served)');
|
|
298
|
+
console.log(` ${chalk.cyan(c.id)} ${kind}${served}`);
|
|
299
|
+
console.log(` grants: ${c.grantTypes.join(', ')}`);
|
|
300
|
+
console.log(` scopes: ${c.scopes.join(', ')}`);
|
|
301
|
+
if (c.audience.length > 0) console.log(` audience: ${c.audience.join(', ')}`);
|
|
302
|
+
if (c.accessTokenClaims.length > 0) {
|
|
303
|
+
console.log(` claims: ${c.accessTokenClaims.join(', ')} (in access tokens)`);
|
|
304
|
+
}
|
|
305
|
+
if (c.confidential) {
|
|
306
|
+
console.log(
|
|
307
|
+
` secret: created ${c.secretCreatedAt ?? d('(not yet)')}${
|
|
308
|
+
c.secretRotatedAt ? `, rotated ${c.secretRotatedAt}` : ''
|
|
309
|
+
}`,
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
console.log('');
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* @param {string | undefined} name
|
|
318
|
+
* @param {string | undefined} clientId
|
|
319
|
+
* @param {{ json: boolean }} options
|
|
320
|
+
*/
|
|
321
|
+
async function rotateSecretCmd(name, clientId, { json }) {
|
|
322
|
+
if (!name || !clientId) {
|
|
323
|
+
const msg = 'Usage: lamaste-server plugins rotate-secret <name> <clientId>';
|
|
324
|
+
if (json) emitError(msg);
|
|
325
|
+
else console.error(`\n ${msg}\n`);
|
|
326
|
+
process.exit(1);
|
|
327
|
+
}
|
|
328
|
+
const result = await panelRequest(
|
|
329
|
+
'POST',
|
|
330
|
+
`/api/plugins/${encodeURIComponent(name)}/oidc-clients/${encodeURIComponent(clientId)}/rotate-secret`,
|
|
331
|
+
);
|
|
332
|
+
if (json) {
|
|
333
|
+
emit(result);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
console.log(
|
|
337
|
+
`\n ${chalk.green('✓')} Secret of ${chalk.cyan(clientId)} rotated at ${result.rotatedAt}`,
|
|
338
|
+
);
|
|
339
|
+
console.log(` ${chalk.bold('New secret (shown once):')} ${result.secret}\n`);
|
|
340
|
+
}
|
|
341
|
+
|
|
255
342
|
const MUTATING = new Set(['install', 'enable', 'disable', 'uninstall']);
|
|
256
343
|
|
|
257
344
|
/** Set on the child run as lamaste: the root parent restarts the panel. */
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lamaste-server registry-credential — the panel's service credential on the
|
|
3
|
+
* Lamalibre npm registry (Feria), used to broker short-lived install tokens
|
|
4
|
+
* for agents.
|
|
5
|
+
*
|
|
6
|
+
* registry-credential show
|
|
7
|
+
* registry-credential set --url <registry> --oidc
|
|
8
|
+
* The panel authenticates as its own `lamaste-server` OIDC client on
|
|
9
|
+
* the relay's Authelia (client-credentials grant, Feria login). Nothing
|
|
10
|
+
* to paste; Feria's auth.json must trust the relay as a rules issuer.
|
|
11
|
+
* registry-credential set --url <registry> [--token-stdin]
|
|
12
|
+
* A long-lived Feria token, read from LAMASTE_REGISTRY_TOKEN or from
|
|
13
|
+
* stdin with --token-stdin. Never accepted as a command-line argument.
|
|
14
|
+
* registry-credential clear
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import chalk from 'chalk';
|
|
18
|
+
import { panelRequest } from '../panel-api.js';
|
|
19
|
+
import { emit } from '../ndjson.js';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {string[]} args
|
|
23
|
+
* @param {{ json: boolean }} options
|
|
24
|
+
*/
|
|
25
|
+
export async function runRegistryCredential(args, { json }) {
|
|
26
|
+
const sub = args[0];
|
|
27
|
+
switch (sub) {
|
|
28
|
+
case 'show':
|
|
29
|
+
return show({ json });
|
|
30
|
+
case 'set':
|
|
31
|
+
return set(args.slice(1), { json });
|
|
32
|
+
case 'clear':
|
|
33
|
+
return clear({ json });
|
|
34
|
+
default:
|
|
35
|
+
process.stderr.write(
|
|
36
|
+
`Usage: lamaste-server registry-credential <show | set --url <registry> (--oidc | --token-stdin) | clear>\n`,
|
|
37
|
+
);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function show({ json }) {
|
|
43
|
+
const result = await panelRequest('GET', '/api/registry-credential');
|
|
44
|
+
if (json) {
|
|
45
|
+
emit(result);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (!result.configured) {
|
|
49
|
+
console.log(chalk.dim('No registry credential configured.'));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
console.log(`${chalk.bold('Registry:')} ${result.registryUrl}`);
|
|
53
|
+
console.log(`${chalk.bold('Scope:')} ${result.scope}`);
|
|
54
|
+
console.log(
|
|
55
|
+
`${chalk.bold('Auth:')} ${
|
|
56
|
+
result.auth === 'oidc'
|
|
57
|
+
? `Authelia OIDC client ${result.clientId} (client credentials)`
|
|
58
|
+
: 'long-lived registry token'
|
|
59
|
+
}`,
|
|
60
|
+
);
|
|
61
|
+
console.log(`${chalk.bold('Owner:')} ${result.owner ?? chalk.dim('(unknown)')}`);
|
|
62
|
+
console.log(`${chalk.bold('Configured:')} ${result.configuredAt}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function set(args, { json }) {
|
|
66
|
+
let url;
|
|
67
|
+
let tokenFromStdin = false;
|
|
68
|
+
let oidc = false;
|
|
69
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
70
|
+
const arg = args[i];
|
|
71
|
+
if (arg === '--url') {
|
|
72
|
+
url = args[++i];
|
|
73
|
+
} else if (arg.startsWith('--url=')) {
|
|
74
|
+
url = arg.slice('--url='.length);
|
|
75
|
+
} else if (arg === '--token-stdin') {
|
|
76
|
+
tokenFromStdin = true;
|
|
77
|
+
} else if (arg === '--oidc') {
|
|
78
|
+
oidc = true;
|
|
79
|
+
} else if (arg === '--token' || arg.startsWith('--token=')) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
'The token is never accepted on the command line — set LAMASTE_REGISTRY_TOKEN or use --token-stdin',
|
|
82
|
+
);
|
|
83
|
+
} else {
|
|
84
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (!url) throw new Error('registry-credential set: --url <registry> is required');
|
|
88
|
+
if (oidc && tokenFromStdin) {
|
|
89
|
+
throw new Error('registry-credential set: choose --oidc or --token-stdin, not both');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let auth;
|
|
93
|
+
if (oidc) {
|
|
94
|
+
auth = { kind: 'oidc' };
|
|
95
|
+
} else {
|
|
96
|
+
let token = tokenFromStdin ? (await readStdin()).trim() : process.env.LAMASTE_REGISTRY_TOKEN;
|
|
97
|
+
if (!token) {
|
|
98
|
+
throw new Error(
|
|
99
|
+
'No token: use --oidc, set LAMASTE_REGISTRY_TOKEN, or pipe a token in with --token-stdin (e.g. from `feria-server token bootstrap`)',
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
token = token.trim();
|
|
103
|
+
auth = { kind: 'token', token };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const result = await panelRequest('PUT', '/api/registry-credential', {
|
|
107
|
+
registryUrl: url,
|
|
108
|
+
auth,
|
|
109
|
+
});
|
|
110
|
+
if (result.error) throw new Error(result.error);
|
|
111
|
+
if (json) {
|
|
112
|
+
emit(result);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
console.log(
|
|
116
|
+
`${chalk.green('✓')} Registry credential configured for ${result.registryUrl} as ${result.owner ?? '(unknown)'}` +
|
|
117
|
+
(result.auth === 'oidc' ? ` (Authelia client ${result.clientId})` : ''),
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function clear({ json }) {
|
|
122
|
+
const result = await panelRequest('DELETE', '/api/registry-credential');
|
|
123
|
+
if (json) {
|
|
124
|
+
emit(result);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
console.log(
|
|
128
|
+
result.removed
|
|
129
|
+
? `${chalk.green('✓')} Registry credential removed`
|
|
130
|
+
: chalk.dim('Nothing to remove.'),
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function readStdin() {
|
|
135
|
+
return new Promise((resolve, reject) => {
|
|
136
|
+
let data = '';
|
|
137
|
+
process.stdin.setEncoding('utf-8');
|
|
138
|
+
process.stdin.on('data', (chunk) => (data += chunk));
|
|
139
|
+
process.stdin.on('end', () => resolve(data));
|
|
140
|
+
process.stdin.on('error', reject);
|
|
141
|
+
});
|
|
142
|
+
}
|
|
@@ -13,15 +13,90 @@ import { PKI_DIR } from './config.js';
|
|
|
13
13
|
|
|
14
14
|
const PANEL_URL = 'https://127.0.0.1:9292';
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* The curl argument vector for one panel request. Pure, exported for tests.
|
|
18
|
+
*
|
|
19
|
+
* The JSON content type and the body travel together: a `Content-Type:
|
|
20
|
+
* application/json` header on a body-less DELETE made Fastify answer 400
|
|
21
|
+
* ("Body cannot be empty when content-type is set"), so `registry-credential
|
|
22
|
+
* clear`, `tunnels delete` and `sites delete` never reached their routes.
|
|
23
|
+
*
|
|
24
|
+
* @param {{ method: string, path: string, body?: Record<string, unknown> | null, p12Path: string, configPath: string }} request
|
|
25
|
+
* @returns {string[]}
|
|
26
|
+
*/
|
|
27
|
+
export function buildPanelCurlArgs({ method, path, body = null, p12Path, configPath }) {
|
|
28
|
+
const args = [
|
|
29
|
+
'-K',
|
|
30
|
+
configPath,
|
|
31
|
+
'--cert-type',
|
|
32
|
+
'P12',
|
|
33
|
+
'--cert',
|
|
34
|
+
p12Path,
|
|
35
|
+
'-k', // Server uses self-signed cert on localhost
|
|
36
|
+
'-s', // Silent
|
|
37
|
+
'-S', // Show errors
|
|
38
|
+
'-X',
|
|
39
|
+
method,
|
|
40
|
+
'-H',
|
|
41
|
+
'Accept: application/json',
|
|
42
|
+
];
|
|
43
|
+
if (body !== null && body !== undefined) {
|
|
44
|
+
args.push('-H', 'Content-Type: application/json', '-d', JSON.stringify(body));
|
|
45
|
+
}
|
|
46
|
+
// Append the HTTP status on its own line so an error response is not
|
|
47
|
+
// mistaken for a result — the panel answers errors with a JSON body.
|
|
48
|
+
args.push('-w', '\n%{http_code}', `${PANEL_URL}${path}`);
|
|
49
|
+
return args;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Split curl's output (`<payload>\n<status>`) and turn a non-2xx status
|
|
54
|
+
* into an error carrying the panel's `error` (and `details`). Pure,
|
|
55
|
+
* exported for tests.
|
|
56
|
+
*
|
|
57
|
+
* @param {string} stdout
|
|
58
|
+
* @param {string} method
|
|
59
|
+
* @param {string} path
|
|
60
|
+
* @returns {Record<string, unknown>}
|
|
61
|
+
*/
|
|
62
|
+
export function parsePanelResponse(stdout, method, path) {
|
|
63
|
+
const split = stdout.lastIndexOf('\n');
|
|
64
|
+
const payload = stdout.slice(0, split).trim();
|
|
65
|
+
const status = Number(stdout.slice(split + 1).trim());
|
|
66
|
+
let parsed = {};
|
|
67
|
+
if (payload) {
|
|
68
|
+
try {
|
|
69
|
+
parsed = JSON.parse(payload);
|
|
70
|
+
} catch {
|
|
71
|
+
parsed = { error: payload };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Anything but a 2xx is an error: 4xx/5xx from the panel, a 3xx curl did
|
|
76
|
+
// not follow, or 0 when curl reported no status at all.
|
|
77
|
+
if (!Number.isInteger(status) || status < 200 || status >= 300) {
|
|
78
|
+
const reason = [parsed.error, parsed.details].filter(Boolean).join(': ');
|
|
79
|
+
const err = new Error(
|
|
80
|
+
`${method} ${path} failed (HTTP ${status})${reason ? `: ${reason}` : ''}`,
|
|
81
|
+
);
|
|
82
|
+
err.statusCode = status;
|
|
83
|
+
throw err;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return parsed;
|
|
87
|
+
}
|
|
88
|
+
|
|
16
89
|
/**
|
|
17
90
|
* Make an authenticated API request to the local panel server.
|
|
18
91
|
*
|
|
19
92
|
* Uses curl with the admin client certificate (mTLS) and a temporary
|
|
20
|
-
* config file for the P12 password (never in process args).
|
|
93
|
+
* config file for the P12 password (never in process args). Resolves with
|
|
94
|
+
* the JSON body; rejects with an Error (`statusCode`, the panel's `error`
|
|
95
|
+
* in the message) for any non-2xx answer.
|
|
21
96
|
*
|
|
22
97
|
* @param {string} method
|
|
23
98
|
* @param {string} path e.g. '/api/tunnels'
|
|
24
|
-
* @param {Record<string, unknown> | null} [body]
|
|
99
|
+
* @param {Record<string, unknown> | null} [body] sent as JSON when given
|
|
25
100
|
* @returns {Promise<Record<string, unknown>>}
|
|
26
101
|
*/
|
|
27
102
|
export async function panelRequest(method, path, body = null) {
|
|
@@ -42,43 +117,13 @@ export async function panelRequest(method, path, body = null) {
|
|
|
42
117
|
try {
|
|
43
118
|
await writeFile(tmpConfigPath, `--pass "${p12Password}"\n`, { mode: 0o600 });
|
|
44
119
|
|
|
45
|
-
const
|
|
46
|
-
'
|
|
47
|
-
tmpConfigPath,
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
'--cert',
|
|
51
|
-
p12Path,
|
|
52
|
-
'-k', // Server uses self-signed cert on localhost
|
|
53
|
-
'-s', // Silent
|
|
54
|
-
'-S', // Show errors
|
|
55
|
-
'-X',
|
|
56
|
-
method,
|
|
57
|
-
'-H',
|
|
58
|
-
'Content-Type: application/json',
|
|
59
|
-
];
|
|
60
|
-
|
|
61
|
-
if (body) {
|
|
62
|
-
curlArgs.push('-d', JSON.stringify(body));
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Append the HTTP status on its own line so an error response is not
|
|
66
|
-
// mistaken for a result — the panel answers errors with a JSON body.
|
|
67
|
-
curlArgs.push('-w', '\n%{http_code}', `${PANEL_URL}${path}`);
|
|
68
|
-
|
|
69
|
-
const { stdout } = await execa('curl', curlArgs, { timeout: 30000 });
|
|
70
|
-
|
|
71
|
-
const split = stdout.lastIndexOf('\n');
|
|
72
|
-
const payload = stdout.slice(0, split).trim();
|
|
73
|
-
const status = Number(stdout.slice(split + 1).trim());
|
|
74
|
-
const parsed = payload ? JSON.parse(payload) : {};
|
|
75
|
-
|
|
76
|
-
if (!Number.isInteger(status) || status >= 400) {
|
|
77
|
-
const reason = [parsed.error, parsed.details].filter(Boolean).join(': ');
|
|
78
|
-
throw new Error(`${method} ${path} failed (HTTP ${status})${reason ? `: ${reason}` : ''}`);
|
|
79
|
-
}
|
|
120
|
+
const { stdout } = await execa(
|
|
121
|
+
'curl',
|
|
122
|
+
buildPanelCurlArgs({ method, path, body, p12Path, configPath: tmpConfigPath }),
|
|
123
|
+
{ timeout: 30000 },
|
|
124
|
+
);
|
|
80
125
|
|
|
81
|
-
return
|
|
126
|
+
return parsePanelResponse(stdout, method, path);
|
|
82
127
|
} finally {
|
|
83
128
|
await unlink(tmpConfigPath).catch(() => {});
|
|
84
129
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lamalibre/lamaste-serverd",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.1.0",
|
|
4
4
|
"description": "Lamaste server daemon — REST API and plugin host",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
6
6
|
"publishConfig": {
|
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
},
|
|
9
9
|
"type": "module",
|
|
10
10
|
"main": "src/index.js",
|
|
11
|
+
"files": [
|
|
12
|
+
"src"
|
|
13
|
+
],
|
|
11
14
|
"scripts": {
|
|
12
15
|
"dev": "node --watch src/index.js | pino-pretty",
|
|
13
16
|
"build": "echo 'No build step for lamaste-serverd'",
|
|
@@ -26,7 +29,7 @@
|
|
|
26
29
|
"@fastify/rate-limit": "^10.3.0",
|
|
27
30
|
"@fastify/static": "^9.1.1",
|
|
28
31
|
"@fastify/websocket": "^11.0.0",
|
|
29
|
-
"@lamalibre/lamaste": "^4.
|
|
32
|
+
"@lamalibre/lamaste": "^4.1.0",
|
|
30
33
|
"bcryptjs": "^3.0.3",
|
|
31
34
|
"execa": "^9.6.1",
|
|
32
35
|
"fastify": "^5.8.4",
|
|
@@ -28,6 +28,7 @@ import userAccessSessionMiddleware from './middleware/user-access-session.js';
|
|
|
28
28
|
import { getPluginCapabilities } from './lib/plugins.js';
|
|
29
29
|
import { setPluginCapabilities } from './lib/mtls.js';
|
|
30
30
|
import { startChiselReconciler } from './lib/chisel-reconcile.js';
|
|
31
|
+
import { syncOidcClients } from './lib/plugin-oidc.js';
|
|
31
32
|
import {
|
|
32
33
|
loadTicketScopeCapabilities,
|
|
33
34
|
checkInstanceLiveness,
|
|
@@ -76,6 +77,16 @@ async function start() {
|
|
|
76
77
|
// it can.
|
|
77
78
|
startChiselReconciler(server.log);
|
|
78
79
|
|
|
80
|
+
// Bring Authelia's OIDC clients (plugin manifests, the panel's own
|
|
81
|
+
// registry client) and the client-secret store in line with persisted
|
|
82
|
+
// state. Best effort: a failure is logged and the next lifecycle change
|
|
83
|
+
// retries. Only once onboarding has written Authelia's configuration.
|
|
84
|
+
if (config.onboarding.status === 'COMPLETED') {
|
|
85
|
+
syncOidcClients(server.log).catch((err) => {
|
|
86
|
+
server.log.warn({ err: err.message }, 'Startup OIDC client sync failed');
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
79
90
|
// --- Plugins ---
|
|
80
91
|
const ipOrigin = `https://${config.ip}:9292`;
|
|
81
92
|
await server.register(cors, {
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
reloadAuthelia as reloadAutheliaCore,
|
|
19
19
|
isAutheliaRunning as isAutheliaRunningCore,
|
|
20
20
|
updateAutheliaAccessControl,
|
|
21
|
+
updateAutheliaOidcProvider,
|
|
21
22
|
createUserFromInvitation as createUserFromInvitationCore,
|
|
22
23
|
base32Encode,
|
|
23
24
|
base32Decode,
|
|
@@ -73,6 +74,16 @@ export function updateAccessControl(sites) {
|
|
|
73
74
|
return updateAutheliaAccessControl(sites, execa);
|
|
74
75
|
}
|
|
75
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Make Authelia's OIDC provider serve exactly these clients.
|
|
79
|
+
* @param {string} domain
|
|
80
|
+
* @param {import('@lamalibre/lamaste/server').AutheliaOidcClient[]} clients
|
|
81
|
+
* @param {import('@lamalibre/lamaste/server').OidcProviderOptions} [options]
|
|
82
|
+
*/
|
|
83
|
+
export function updateOidcProvider(domain, clients, options = {}) {
|
|
84
|
+
return updateAutheliaOidcProvider(domain, clients, execa, options);
|
|
85
|
+
}
|
|
86
|
+
|
|
76
87
|
export function createUserFromInvitation(username, email, groups, hashedPassword) {
|
|
77
88
|
return createUserFromInvitationCore(username, email, groups, hashedPassword, execa);
|
|
78
89
|
}
|