@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
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
normalizeIdentityBlock,
|
|
23
23
|
pluginHostCapability,
|
|
24
24
|
validateManifest as coreValidateManifest,
|
|
25
|
+
validateInstalledManifest,
|
|
25
26
|
} from '@lamalibre/lamaste';
|
|
26
27
|
import { listRegisteredSubScopeNames } from './tickets.js';
|
|
27
28
|
import { getStateDb } from './state-db.js';
|
|
@@ -31,6 +32,7 @@ import { RegistryCredentialError } from '@lamalibre/lamaste/server';
|
|
|
31
32
|
import {
|
|
32
33
|
bareLamalibrePackageName,
|
|
33
34
|
installedHookEntrySha256,
|
|
35
|
+
installedWebTreeSha256,
|
|
34
36
|
pinAgentHalf,
|
|
35
37
|
} from './plugin-packages.js';
|
|
36
38
|
|
|
@@ -72,8 +74,9 @@ async function getStmts() {
|
|
|
72
74
|
INSERT INTO plugins
|
|
73
75
|
(name, route, display_name, package_name, version, description, capabilities,
|
|
74
76
|
packages, panel, config, modes, hosting, agent, hooks, hooks_entry_sha256,
|
|
75
|
-
agent_half_pin, settings, identity,
|
|
76
|
-
|
|
77
|
+
agent_half_pin, settings, identity, user_surface, web_tree_sha256, status,
|
|
78
|
+
installed_at, enabled_at, domain)
|
|
79
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
77
80
|
`),
|
|
78
81
|
deleteByName: db.prepare('DELETE FROM plugins WHERE name = ?'),
|
|
79
82
|
// The plugin's audience and the state of its identity feed
|
|
@@ -141,7 +144,11 @@ function rowToPlugin(row) {
|
|
|
141
144
|
if (row.agent_half_pin != null) entry.agentHalfPin = JSON.parse(row.agent_half_pin);
|
|
142
145
|
if (row.settings != null) entry.settings = JSON.parse(row.settings);
|
|
143
146
|
if (row.identity != null) entry.identity = JSON.parse(row.identity);
|
|
147
|
+
if (row.user_surface != null) entry.userSurface = JSON.parse(row.user_surface);
|
|
148
|
+
if (row.web_tree_sha256 != null) entry.webTreeSha256 = row.web_tree_sha256;
|
|
144
149
|
if (row.enabled_at != null) entry.enabledAt = row.enabled_at;
|
|
150
|
+
// The domain the plugin belongs to; null: the primary (lib/domains.js).
|
|
151
|
+
entry.domain = row.domain ?? null;
|
|
145
152
|
return entry;
|
|
146
153
|
}
|
|
147
154
|
|
|
@@ -224,9 +231,12 @@ export async function writePlugins(data) {
|
|
|
224
231
|
nullableJson(p.agentHalfPin),
|
|
225
232
|
nullableJson(p.settings),
|
|
226
233
|
nullableJson(p.identity),
|
|
234
|
+
nullableJson(p.userSurface),
|
|
235
|
+
nullableString(p.webTreeSha256),
|
|
227
236
|
p.status,
|
|
228
237
|
p.installedAt,
|
|
229
238
|
nullableString(p.enabledAt),
|
|
239
|
+
nullableString(p.domain),
|
|
230
240
|
);
|
|
231
241
|
}
|
|
232
242
|
s.commit.run();
|
|
@@ -457,6 +467,17 @@ export function installPlugin(packageName, logger) {
|
|
|
457
467
|
throw Object.assign(new Error(err.message), { statusCode: err.statusCode || 400 });
|
|
458
468
|
}
|
|
459
469
|
|
|
470
|
+
// A web surface: its tree must be in the package and keep every rule of
|
|
471
|
+
// a web tree; its digest goes into the approval digest, and the relay
|
|
472
|
+
// serves only a copy that matches it.
|
|
473
|
+
let webTreeSha256 = null;
|
|
474
|
+
try {
|
|
475
|
+
webTreeSha256 = await installedWebTreeSha256(packageName, manifest);
|
|
476
|
+
} catch (err) {
|
|
477
|
+
await discardPackage();
|
|
478
|
+
throw Object.assign(new Error(err.message), { statusCode: err.statusCode || 400 });
|
|
479
|
+
}
|
|
480
|
+
|
|
460
481
|
// An agent half Lamaste installs: its tarball's integrity is pinned now,
|
|
461
482
|
// from the registry this install used — the approval covers it and the
|
|
462
483
|
// agent installs only a tarball that matches it.
|
|
@@ -508,6 +529,8 @@ export function installPlugin(packageName, logger) {
|
|
|
508
529
|
agentHalfPin,
|
|
509
530
|
settings: manifest.settings,
|
|
510
531
|
identity: identityOf(manifest),
|
|
532
|
+
userSurface: manifest.userSurface,
|
|
533
|
+
webTreeSha256,
|
|
511
534
|
status: 'disabled',
|
|
512
535
|
installedAt: new Date().toISOString(),
|
|
513
536
|
};
|
|
@@ -536,9 +559,13 @@ export function installPlugin(packageName, logger) {
|
|
|
536
559
|
nullableJson(entry.agentHalfPin),
|
|
537
560
|
nullableJson(entry.settings),
|
|
538
561
|
nullableJson(entry.identity),
|
|
562
|
+
nullableJson(entry.userSurface),
|
|
563
|
+
nullableString(entry.webTreeSha256),
|
|
539
564
|
entry.status,
|
|
540
565
|
entry.installedAt,
|
|
541
566
|
null,
|
|
567
|
+
// A plugin installed now belongs to the primary domain (lib/plugin-domain.js moves it).
|
|
568
|
+
null,
|
|
542
569
|
);
|
|
543
570
|
s.commit.run();
|
|
544
571
|
} catch (err) {
|
|
@@ -601,6 +628,13 @@ export function uninstallPlugin(name, logger) {
|
|
|
601
628
|
const { removeRelayHooks } = await import('./plugin-hooks.js');
|
|
602
629
|
await removeRelayHooks(name, logger);
|
|
603
630
|
|
|
631
|
+
// The OIDC clients it declares, read while its manifest is installed:
|
|
632
|
+
// their grants go with it below.
|
|
633
|
+
const { collectPluginOidcClients } = await import('./plugin-oidc.js');
|
|
634
|
+
const clientIds = (await collectPluginOidcClients().catch(() => []))
|
|
635
|
+
.filter((c) => c.plugin === name)
|
|
636
|
+
.map((c) => c.id);
|
|
637
|
+
|
|
604
638
|
// Uninstall the npm package
|
|
605
639
|
logger.info({ name, packageName: plugin.packageName }, 'Uninstalling plugin package');
|
|
606
640
|
await execa('npm', ['uninstall', plugin.packageName], {
|
|
@@ -674,6 +708,24 @@ export function uninstallPlugin(name, logger) {
|
|
|
674
708
|
|
|
675
709
|
logger.info({ name }, 'Plugin uninstalled');
|
|
676
710
|
await syncOidcClientsBestEffort(logger);
|
|
711
|
+
|
|
712
|
+
// Grants on its OIDC clients end with it: client ids are chosen, not
|
|
713
|
+
// random, so a later install of the plugin (or another declaring the
|
|
714
|
+
// same id) would otherwise admit whoever was granted before.
|
|
715
|
+
const { revokeOidcClientGrants } = await import('./owned-groups.js');
|
|
716
|
+
const revoked = await revokeOidcClientGrants(clientIds);
|
|
717
|
+
if (!revoked.ok) {
|
|
718
|
+
logger.warn(
|
|
719
|
+
{ name, err: revoked.error },
|
|
720
|
+
"Grants on the uninstalled plugin's OIDC clients stayed — revoke them in Gatekeeper",
|
|
721
|
+
);
|
|
722
|
+
} else if (revoked.revoked > 0) {
|
|
723
|
+
logger.info({ name, revoked: revoked.revoked }, 'Grants on its OIDC clients revoked');
|
|
724
|
+
const { syncDomainAccess } = await import('./domains.js');
|
|
725
|
+
await syncDomainAccess({ logger }).catch((err) => {
|
|
726
|
+
logger.warn({ err: err.message }, 'Rendering domain access after an uninstall failed');
|
|
727
|
+
});
|
|
728
|
+
}
|
|
677
729
|
return { ok: true, name };
|
|
678
730
|
});
|
|
679
731
|
}
|
|
@@ -684,7 +736,7 @@ const updateRowSql = `
|
|
|
684
736
|
UPDATE plugins SET
|
|
685
737
|
display_name = ?, version = ?, description = ?, capabilities = ?, packages = ?, panel = ?,
|
|
686
738
|
config = ?, modes = ?, hosting = ?, agent = ?, hooks = ?, hooks_entry_sha256 = ?,
|
|
687
|
-
agent_half_pin = ?, settings = ?, identity = ?
|
|
739
|
+
agent_half_pin = ?, settings = ?, identity = ?, user_surface = ?, web_tree_sha256 = ?
|
|
688
740
|
WHERE name = ?`;
|
|
689
741
|
|
|
690
742
|
/**
|
|
@@ -698,8 +750,16 @@ const updateRowSql = `
|
|
|
698
750
|
* beside it, and a secret whose field became a plain value is no longer
|
|
699
751
|
* reported set. The identity block is stored normalized; the update's
|
|
700
752
|
* caller recomputes the audiences once the version is in place.
|
|
753
|
+
* `webTreeSha256` is the digest of the version's web tree, read from the
|
|
754
|
+
* installed package (null when it declares no web surface).
|
|
701
755
|
*/
|
|
702
|
-
export async function writePluginRow(
|
|
756
|
+
export async function writePluginRow(
|
|
757
|
+
name,
|
|
758
|
+
manifest,
|
|
759
|
+
hooksEntrySha256,
|
|
760
|
+
agentHalfPin,
|
|
761
|
+
webTreeSha256 = null,
|
|
762
|
+
) {
|
|
703
763
|
const s = await getStmts();
|
|
704
764
|
const { prunedSettingsCache } = await import('./plugin-settings.js');
|
|
705
765
|
s.begin.run();
|
|
@@ -723,6 +783,8 @@ export async function writePluginRow(name, manifest, hooksEntrySha256, agentHalf
|
|
|
723
783
|
nullableJson(agentHalfPin),
|
|
724
784
|
nullableJson(manifest.settings),
|
|
725
785
|
nullableJson(identityOf(manifest)),
|
|
786
|
+
nullableJson(manifest.userSurface),
|
|
787
|
+
nullableString(webTreeSha256),
|
|
726
788
|
name,
|
|
727
789
|
);
|
|
728
790
|
const pruned = prunedSettingsCache(
|
|
@@ -750,12 +812,21 @@ export async function writePluginRow(name, manifest, hooksEntrySha256, agentHalf
|
|
|
750
812
|
|
|
751
813
|
/**
|
|
752
814
|
* Install `<packageName>@<version>` over the installed copy and read it
|
|
753
|
-
* back: the validated manifest
|
|
815
|
+
* back: the validated manifest, its relay entry module's SHA-256 and its
|
|
816
|
+
* web tree's digest. The
|
|
754
817
|
* row is not touched — the caller compares the result with what was
|
|
755
818
|
* reviewed and then writes it. Refuses a package whose manifest names
|
|
756
|
-
* another plugin.
|
|
819
|
+
* another plugin. `restoring` puts back the version that was installed (an
|
|
820
|
+
* update's revert): its manifest is read as an installed one, so a rule
|
|
821
|
+
* tightened since it was installed does not leave the revert half done.
|
|
757
822
|
*/
|
|
758
|
-
export function replacePluginPackage(
|
|
823
|
+
export function replacePluginPackage(
|
|
824
|
+
name,
|
|
825
|
+
packageName,
|
|
826
|
+
version,
|
|
827
|
+
logger,
|
|
828
|
+
{ restoring = false } = {},
|
|
829
|
+
) {
|
|
759
830
|
return withPluginLock(async () => {
|
|
760
831
|
const bare = bareLamalibrePackageName(packageName);
|
|
761
832
|
const spec = `${bare}@${version}`;
|
|
@@ -777,7 +848,8 @@ export function replacePluginPackage(name, packageName, version, logger) {
|
|
|
777
848
|
}
|
|
778
849
|
const require = createRequire(`${STATE_DIR}/`);
|
|
779
850
|
const manifestPath = require.resolve(`${bare}/lamaste-plugin.json`);
|
|
780
|
-
const
|
|
851
|
+
const raw = JSON.parse(await readFile(manifestPath, 'utf-8'));
|
|
852
|
+
const manifest = restoring ? validateInstalledManifest(raw) : validateManifest(raw);
|
|
781
853
|
if (manifest.name !== name) {
|
|
782
854
|
throw Object.assign(new Error(`${spec} is plugin "${manifest.name}", not "${name}"`), {
|
|
783
855
|
statusCode: 400,
|
|
@@ -790,7 +862,8 @@ export function replacePluginPackage(name, packageName, version, logger) {
|
|
|
790
862
|
);
|
|
791
863
|
}
|
|
792
864
|
const hooksEntrySha256 = await installedHookEntrySha256(packageName, manifest);
|
|
793
|
-
|
|
865
|
+
const webTreeSha256 = await installedWebTreeSha256(packageName, manifest);
|
|
866
|
+
return { manifest, hooksEntrySha256, webTreeSha256 };
|
|
794
867
|
});
|
|
795
868
|
}
|
|
796
869
|
|
|
@@ -57,6 +57,33 @@ export async function writeRegistryCredentialConfig(config) {
|
|
|
57
57
|
resetRegistryTokenState();
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Store `config` as the registry credential: written while its issuer
|
|
62
|
+
* domain is still an active domain (a domain's removal checks the
|
|
63
|
+
* credential under the same lock — 400 `unknown_domain` otherwise). When the
|
|
64
|
+
* registry's domains change (its issuer domain, the domain its host is
|
|
65
|
+
* under), the tokens of every user who is a member of none of the new ones
|
|
66
|
+
* are revoked: who may hold a token follows them. Returns the warnings.
|
|
67
|
+
*
|
|
68
|
+
* @param {object} config
|
|
69
|
+
* @param {{ logger?: import('pino').Logger }} [opts]
|
|
70
|
+
* @returns {Promise<Array<{ step: string, message: string }>>}
|
|
71
|
+
*/
|
|
72
|
+
export async function setRegistryCredential(config, { logger } = {}) {
|
|
73
|
+
const { registryDomainsOf, revokeRegistryTokensOutside, withActiveDomain } =
|
|
74
|
+
await import('./domains.js');
|
|
75
|
+
const issuerDomain = config.auth?.kind === 'oidc' ? config.auth.issuerDomain : undefined;
|
|
76
|
+
const change = await withActiveDomain(issuerDomain, async () => {
|
|
77
|
+
const before = await registryDomainsOf(await readRegistryCredentialConfig().catch(() => null));
|
|
78
|
+
await writeRegistryCredentialConfig(config);
|
|
79
|
+
const after = await registryDomainsOf(config);
|
|
80
|
+
const same = after.length === before.length && after.every((d) => before.includes(d));
|
|
81
|
+
return { changed: !same, after };
|
|
82
|
+
});
|
|
83
|
+
if (!change.changed) return [];
|
|
84
|
+
return revokeRegistryTokensOutside(change.after, config, logger);
|
|
85
|
+
}
|
|
86
|
+
|
|
60
87
|
export async function clearRegistryCredentialConfig() {
|
|
61
88
|
const removed = await clearCore(STATE_DIR);
|
|
62
89
|
resetRegistryTokenState();
|
|
@@ -70,9 +97,9 @@ export function resetRegistryTokenState() {
|
|
|
70
97
|
}
|
|
71
98
|
|
|
72
99
|
/**
|
|
73
|
-
* What the core needs to run the OIDC chain for a credential: the
|
|
74
|
-
* domain
|
|
75
|
-
* need nothing.
|
|
100
|
+
* What the core needs to run the OIDC chain for a credential: the primary
|
|
101
|
+
* domain (the issuer when the credential names no `issuerDomain`) and the
|
|
102
|
+
* `lamaste-server` client secret. Pasted-token credentials need nothing.
|
|
76
103
|
* @param {{ auth: { kind: string } }} config
|
|
77
104
|
*/
|
|
78
105
|
async function oidcDeps(config) {
|
|
@@ -166,10 +193,16 @@ export async function acquireAgentToken(agentLabel) {
|
|
|
166
193
|
* @param {string} username
|
|
167
194
|
* @param {{ before: string[], after: string[] } | { deleted: true }} change
|
|
168
195
|
* @param {import('pino').Logger | undefined} log
|
|
169
|
-
* @param {{ userId?: string | null }} [who]
|
|
196
|
+
* @param {{ userId?: string | null, config?: object }} [who] `config`: the credential
|
|
197
|
+
* already read (a batch of revocations reads it once)
|
|
170
198
|
*/
|
|
171
|
-
export async function revokeUserRegistryTokens(
|
|
172
|
-
|
|
199
|
+
export async function revokeUserRegistryTokens(
|
|
200
|
+
username,
|
|
201
|
+
change,
|
|
202
|
+
log,
|
|
203
|
+
{ userId = null, config: given } = {},
|
|
204
|
+
) {
|
|
205
|
+
const config = given ?? (await readRegistryCredentialConfig());
|
|
173
206
|
if (!config) return null;
|
|
174
207
|
|
|
175
208
|
const lost =
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { execa } from 'execa';
|
|
2
|
+
import { autheliaReady } from '@lamalibre/lamaste/server';
|
|
2
3
|
|
|
3
4
|
export const ALLOWED_SERVICES = ['nginx', 'chisel', 'authelia', 'lamalibre-lamaste-serverd'];
|
|
4
5
|
|
|
@@ -120,7 +121,6 @@ export async function executeServiceAction(name, action) {
|
|
|
120
121
|
|
|
121
122
|
try {
|
|
122
123
|
await execa('sudo', ['systemctl', action, name], { timeout: 30_000 });
|
|
123
|
-
return { ok: true, name, action };
|
|
124
124
|
} catch (err) {
|
|
125
125
|
const details = err.stderr || err.message;
|
|
126
126
|
const error = new Error(`Failed to ${action} ${name}`);
|
|
@@ -128,4 +128,12 @@ export async function executeServiceAction(name, action) {
|
|
|
128
128
|
error.details = details;
|
|
129
129
|
throw error;
|
|
130
130
|
}
|
|
131
|
+
// systemctl returns when Authelia's process is spawned, before it serves
|
|
132
|
+
// requests: the action is done once it does.
|
|
133
|
+
if (name === 'authelia' && action !== 'stop' && !(await autheliaReady(execa))) {
|
|
134
|
+
const error = new Error(`Authelia is not serving requests after ${action}`);
|
|
135
|
+
error.statusCode = 500;
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
return { ok: true, name, action };
|
|
131
139
|
}
|
|
@@ -48,6 +48,16 @@ export async function writeTunnels(tunnels) {
|
|
|
48
48
|
await fd.close();
|
|
49
49
|
|
|
50
50
|
await rename(tmpPath, filePath);
|
|
51
|
+
await hostsChanged();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* A tunnel or a site changed: the hosts that name external members are
|
|
56
|
+
* rendered again shortly (lib/domains.js `scheduleHostRender`).
|
|
57
|
+
*/
|
|
58
|
+
async function hostsChanged() {
|
|
59
|
+
const { scheduleHostRender } = await import('./domains.js');
|
|
60
|
+
scheduleHostRender();
|
|
51
61
|
}
|
|
52
62
|
|
|
53
63
|
function sitesPath() {
|
|
@@ -93,6 +103,25 @@ export async function writeSites(sites) {
|
|
|
93
103
|
await fd.close();
|
|
94
104
|
|
|
95
105
|
await rename(tmpPath, filePath);
|
|
106
|
+
await hostsChanged();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Serializes every read-modify-write of sites.json in this process. */
|
|
110
|
+
const sitesMutex = new PromiseChainMutex();
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Run `fn` holding the sites lock: a read of sites.json, the decisions made
|
|
114
|
+
* on it and the write that follows must not interleave with another. A
|
|
115
|
+
* workflow that gives a site a hostname (creating it, adding an alias,
|
|
116
|
+
* taking a tunnel's host) takes core's tunnel lock first, then this one —
|
|
117
|
+
* always in that order — so a site and a tunnel never take one name at once.
|
|
118
|
+
*
|
|
119
|
+
* @template T
|
|
120
|
+
* @param {() => Promise<T>} fn
|
|
121
|
+
* @returns {Promise<T>}
|
|
122
|
+
*/
|
|
123
|
+
export function withSitesLock(fn) {
|
|
124
|
+
return sitesMutex.run(fn);
|
|
96
125
|
}
|
|
97
126
|
|
|
98
127
|
function invitationsPath() {
|
|
@@ -7,8 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
9
|
writePublicVhost,
|
|
10
|
-
|
|
11
|
-
writeRestrictedVhost,
|
|
10
|
+
writeGatekeeperVhost,
|
|
12
11
|
removeAppVhost,
|
|
13
12
|
enableAppVhost,
|
|
14
13
|
disableAppVhost,
|
|
@@ -23,8 +22,7 @@ import { readTunnels, writeTunnels } from './state.js';
|
|
|
23
22
|
|
|
24
23
|
export const tunnelNginxDeps = Object.freeze({
|
|
25
24
|
writePublicVhost,
|
|
26
|
-
|
|
27
|
-
writeRestrictedVhost,
|
|
25
|
+
writeGatekeeperVhost,
|
|
28
26
|
writeAgentPanelVhost,
|
|
29
27
|
removeAppVhost,
|
|
30
28
|
removeAgentPanelVhost,
|
|
@@ -45,8 +45,11 @@ import {
|
|
|
45
45
|
RELAY_PANEL_AUDIENCE,
|
|
46
46
|
RELAY_PANEL_PORT,
|
|
47
47
|
isAgentHostedPlugin,
|
|
48
|
+
siteUserSurfaceRoute,
|
|
48
49
|
tunnelUserSurfaceRoutes,
|
|
50
|
+
userSurfaceHostServing,
|
|
49
51
|
userSurfaceHostingAgent,
|
|
52
|
+
userSurfaceHosts,
|
|
50
53
|
verifyRelayAssertion,
|
|
51
54
|
} from '@lamalibre/lamaste';
|
|
52
55
|
import {
|
|
@@ -54,15 +57,17 @@ import {
|
|
|
54
57
|
TunnelError,
|
|
55
58
|
bindUserSurface,
|
|
56
59
|
releaseUserSurfaceRoute,
|
|
60
|
+
tunnelHost,
|
|
57
61
|
unbindUserSurface,
|
|
58
62
|
} from '@lamalibre/lamaste/server';
|
|
59
63
|
import { httpError } from './app-error.js';
|
|
60
64
|
import { recordAuditBestEffort } from './audit.js';
|
|
61
65
|
import { getConfig } from './config.js';
|
|
66
|
+
import { pluginDomainName } from './domains.js';
|
|
62
67
|
import { installedVersionApproved } from './plugin-hooks.js';
|
|
63
68
|
import { findPluginByRoute } from './plugins.js';
|
|
64
69
|
import { relaySigner } from './relay-signing.js';
|
|
65
|
-
import { readTunnels } from './state.js';
|
|
70
|
+
import { readSites, readTunnels } from './state.js';
|
|
66
71
|
import { tunnelCertbotDeps, tunnelNginxDeps, tunnelStateDeps } from './tunnel-deps.js';
|
|
67
72
|
|
|
68
73
|
/**
|
|
@@ -102,7 +107,7 @@ export const USER_SURFACE_ROUTE_CONFIG = Object.freeze({
|
|
|
102
107
|
* @param {readonly object[]} tunnels
|
|
103
108
|
* @returns {Promise<string | null>}
|
|
104
109
|
*/
|
|
105
|
-
async function hostingAgentOf(plugin, tunnels) {
|
|
110
|
+
export async function hostingAgentOf(plugin, tunnels) {
|
|
106
111
|
if (!plugin.route) return null;
|
|
107
112
|
const { ownHosting } = await import('./plugin-hosting.js');
|
|
108
113
|
const record = await ownHosting(plugin);
|
|
@@ -160,7 +165,7 @@ async function routeServing(route, agentLabel, tunnels) {
|
|
|
160
165
|
* @param {object} plugin
|
|
161
166
|
* @param {readonly object[]} tunnels the tunnel state
|
|
162
167
|
*/
|
|
163
|
-
async function internalTunnelBodyLimit(plugin, tunnels) {
|
|
168
|
+
export async function internalTunnelBodyLimit(plugin, tunnels) {
|
|
164
169
|
const { ownHosting } = await import('./plugin-hosting.js');
|
|
165
170
|
const record = await ownHosting(plugin);
|
|
166
171
|
if (!record) return TUNNEL_MAX_BODY_MB.default;
|
|
@@ -222,8 +227,8 @@ export async function withUserSurfaceStatus(tunnel) {
|
|
|
222
227
|
// ---------------------------------------------------------------------------
|
|
223
228
|
|
|
224
229
|
/**
|
|
225
|
-
* What core's binding workflows need from the panel — among them its
|
|
226
|
-
*
|
|
230
|
+
* What core's binding workflows need from the panel — among them its Let's
|
|
231
|
+
* Encrypt email, which a vhost rewrite needs.
|
|
227
232
|
*/
|
|
228
233
|
function bindingOptions(id, route, logger) {
|
|
229
234
|
const config = getConfig();
|
|
@@ -233,7 +238,6 @@ function bindingOptions(id, route, logger) {
|
|
|
233
238
|
return {
|
|
234
239
|
id,
|
|
235
240
|
route,
|
|
236
|
-
domain: config.domain,
|
|
237
241
|
email: config.email,
|
|
238
242
|
nginx: tunnelNginxDeps,
|
|
239
243
|
certbot: tunnelCertbotDeps,
|
|
@@ -287,6 +291,34 @@ async function assertBindableRoute(route) {
|
|
|
287
291
|
'INVALID_SETTING',
|
|
288
292
|
);
|
|
289
293
|
}
|
|
294
|
+
// A route is served on one host at most: the plugin's own site, when it
|
|
295
|
+
// has one, is that host.
|
|
296
|
+
if ((await readSites()).some((site) => siteUserSurfaceRoute(site) === route)) {
|
|
297
|
+
throw new TunnelError(
|
|
298
|
+
`Plugin "${plugin.name}" has a web surface — its user surface is served there`,
|
|
299
|
+
'INVALID_SETTING',
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Refuse binding tunnel `id` as the user surface of the plugin at `route`
|
|
306
|
+
* when the tunnel's hostname is not on the plugin's domain (400
|
|
307
|
+
* `not_plugin_domain`): a plugin's users are its domain's members.
|
|
308
|
+
*/
|
|
309
|
+
async function assertOnPluginDomain(id, route) {
|
|
310
|
+
const tunnel = (await readTunnels()).find((t) => t.id === id);
|
|
311
|
+
const host = tunnel ? tunnelHost(tunnel) : null;
|
|
312
|
+
if (!host) return; // core refuses a tunnel without a hostname
|
|
313
|
+
const plugin = await findPluginByRoute(route);
|
|
314
|
+
const domain = await pluginDomainName(plugin);
|
|
315
|
+
if (host.domain !== domain) {
|
|
316
|
+
throw httpError(
|
|
317
|
+
`The tunnel is on ${host.domain}; the plugin holding '${route}' belongs to ${domain}`,
|
|
318
|
+
400,
|
|
319
|
+
{ code: 'not_plugin_domain' },
|
|
320
|
+
);
|
|
321
|
+
}
|
|
290
322
|
}
|
|
291
323
|
|
|
292
324
|
/** Whether an installed agent-hosted plugin holds `route`. */
|
|
@@ -318,7 +350,11 @@ async function isBindableRoute(route) {
|
|
|
318
350
|
export async function bindTunnelUserSurface(id, route, { actor, logger }) {
|
|
319
351
|
const result = await bindUserSurface({
|
|
320
352
|
...bindingOptions(id, route, logger),
|
|
321
|
-
|
|
353
|
+
// Under the tunnel lock, as the plugin's domain changes (lib/domains.js).
|
|
354
|
+
assertRoute: async (r) => {
|
|
355
|
+
await assertBindableRoute(r);
|
|
356
|
+
await assertOnPluginDomain(id, r);
|
|
357
|
+
},
|
|
322
358
|
});
|
|
323
359
|
if (result.changed && !(await isBindableRoute(route))) {
|
|
324
360
|
await pruneUserSurfaceBindings(route, { plugin: null, logger });
|
|
@@ -413,7 +449,8 @@ export async function refreshTunnelUserSurfaceVhosts(ids, logger) {
|
|
|
413
449
|
* Whether plugin `route`'s user surface is served may have changed — its
|
|
414
450
|
* version was approved, updated or reverted, it was hosted or unhosted (the
|
|
415
451
|
* body limit is its internal tunnel's): bring the vhosts of the tunnels it
|
|
416
|
-
* is bound to in line
|
|
452
|
+
* is bound to in line, and its web surface — the approved tree served, the
|
|
453
|
+
* vhost of the site it owns (`lib/web-surface.js`). Best effort — logged, never thrown; the panel's
|
|
417
454
|
* start reconciles every vhost again.
|
|
418
455
|
*
|
|
419
456
|
* @param {string | null | undefined} route
|
|
@@ -429,6 +466,9 @@ export async function refreshUserSurfaceVhosts(route, logger) {
|
|
|
429
466
|
'Could not bring the user-surface vhosts of a plugin in line',
|
|
430
467
|
);
|
|
431
468
|
}
|
|
469
|
+
// Its web surface too: the approved tree served, the site's vhost in line.
|
|
470
|
+
const { refreshWebSurface } = await import('./web-surface.js');
|
|
471
|
+
await refreshWebSurface(route, logger);
|
|
432
472
|
}
|
|
433
473
|
|
|
434
474
|
/**
|
|
@@ -491,6 +531,10 @@ export async function releasePluginUserSurface(route, { plugin, logger }) {
|
|
|
491
531
|
'Could not release the user surface of an uninstalled plugin',
|
|
492
532
|
);
|
|
493
533
|
}
|
|
534
|
+
// The site it owns stays, answering 503 until the plugin is back (or an
|
|
535
|
+
// administrator deletes it): its vhost goes in line.
|
|
536
|
+
const { refreshWebSurface } = await import('./web-surface.js');
|
|
537
|
+
await refreshWebSurface(route, logger);
|
|
494
538
|
}
|
|
495
539
|
|
|
496
540
|
// ---------------------------------------------------------------------------
|
|
@@ -560,33 +604,35 @@ export async function verifyUserSurfaceRelay(request) {
|
|
|
560
604
|
}
|
|
561
605
|
|
|
562
606
|
/**
|
|
563
|
-
* The enabled
|
|
564
|
-
*
|
|
565
|
-
*
|
|
566
|
-
*
|
|
607
|
+
* The enabled host that serves `route`'s user surface, or null — the check
|
|
608
|
+
* of every request, whatever a vhost not yet re-rendered still routes here:
|
|
609
|
+
* a tunnel bound to the route and carried by the agent hosting its plugin,
|
|
610
|
+
* or the site the plugin owns while its web surface is served
|
|
611
|
+
* (`lib/web-surface.js`). Read with core `userSurfaceHosts`, as Gatekeeper
|
|
612
|
+
* reads it, so a route two hosts would serve is served by neither.
|
|
567
613
|
*
|
|
568
614
|
* @param {string} route
|
|
615
|
+
* @returns {Promise<import('@lamalibre/lamaste').UserSurfaceHost | null>}
|
|
569
616
|
*/
|
|
570
|
-
async function
|
|
571
|
-
const tunnels = await readTunnels();
|
|
572
|
-
const
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
) ?? null;
|
|
579
|
-
if (!tunnel) return null;
|
|
617
|
+
async function servedUserSurfaceHost(route) {
|
|
618
|
+
const [tunnels, sites] = await Promise.all([readTunnels(), readSites()]);
|
|
619
|
+
const host = userSurfaceHostServing(userSurfaceHosts(tunnels, sites), route);
|
|
620
|
+
if (!host) return null;
|
|
621
|
+
if (host.kind === 'site') {
|
|
622
|
+
const { webSurfaceServing } = await import('./web-surface.js');
|
|
623
|
+
return (await webSurfaceServing(route, tunnels)).served ? host : null;
|
|
624
|
+
}
|
|
580
625
|
const plugin = await findPluginByRoute(route);
|
|
581
626
|
if (!plugin) return null;
|
|
627
|
+
const tunnel = tunnels.find((t) => t.id === host.id);
|
|
582
628
|
const hosting = await hostingAgentOf(plugin, tunnels);
|
|
583
|
-
return hosting !== null && tunnel
|
|
629
|
+
return hosting !== null && tunnel?.agentLabel === hosting ? host : null;
|
|
584
630
|
}
|
|
585
631
|
|
|
586
632
|
/**
|
|
587
633
|
* The forward target of `route`'s user surface (core `userSurfacePlugin`'s
|
|
588
|
-
* `resolveTarget`): null when no
|
|
589
|
-
*
|
|
634
|
+
* `resolveTarget`): null when no host serves it (see
|
|
635
|
+
* {@link servedUserSurfaceHost}), else what the admin forwarder resolves — the hosting's
|
|
590
636
|
* internal tunnel, or why not (not found, disabled, not hosted) — and
|
|
591
637
|
* `down` while the hosting's health is down: nothing of a user's request
|
|
592
638
|
* reaches the relay-loopback port then (503).
|
|
@@ -594,7 +640,7 @@ async function servedUserSurfaceTunnel(route) {
|
|
|
594
640
|
* @param {string} route
|
|
595
641
|
*/
|
|
596
642
|
export async function resolveUserSurfaceTarget(route) {
|
|
597
|
-
if (!(await
|
|
643
|
+
if (!(await servedUserSurfaceHost(route))) return null;
|
|
598
644
|
const { resolveForwardTarget } = await import('./plugin-hosting.js');
|
|
599
645
|
return resolveForwardTarget(route);
|
|
600
646
|
}
|
|
@@ -602,15 +648,15 @@ export async function resolveUserSurfaceTarget(route) {
|
|
|
602
648
|
/**
|
|
603
649
|
* The origin a request that may change something on `route`'s user surface
|
|
604
650
|
* must come from (core `userSurfacePlugin`'s `originFor`): the bound host,
|
|
605
|
-
* as the
|
|
606
|
-
*
|
|
651
|
+
* as the state names it — never the request's `Host`. Null when no host
|
|
652
|
+
* serves the route.
|
|
607
653
|
*
|
|
608
654
|
* @param {string} route
|
|
609
655
|
* @returns {Promise<string | null>}
|
|
610
656
|
*/
|
|
611
657
|
export async function userSurfaceOrigin(route) {
|
|
612
|
-
const
|
|
613
|
-
return
|
|
658
|
+
const host = await servedUserSurfaceHost(route);
|
|
659
|
+
return host ? `https://${host.fqdn}` : null;
|
|
614
660
|
}
|
|
615
661
|
|
|
616
662
|
// ---------------------------------------------------------------------------
|
|
@@ -702,7 +748,7 @@ export async function sweepOpenUserSurfaceResponses(logger) {
|
|
|
702
748
|
const { route, caller } = response;
|
|
703
749
|
let verdict;
|
|
704
750
|
try {
|
|
705
|
-
if (!served.has(route)) served.set(route, (await
|
|
751
|
+
if (!served.has(route)) served.set(route, (await servedUserSurfaceHost(route)) !== null);
|
|
706
752
|
const key = `${route}\n${caller.username}`;
|
|
707
753
|
if (!callers.has(key)) callers.set(key, await resolveUserCaller(route, caller.username));
|
|
708
754
|
const resolution = callers.get(key);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The users lock — the one lock around every read-modify-write of users.yml
|
|
3
|
+
* and every change of the user directory in this process (core's users
|
|
4
|
+
* functions take none), and around the changes that must agree with
|
|
5
|
+
* users.yml's groups: a membership's kind (an external member holds no
|
|
6
|
+
* Authelia group — lib/domains.js). Taken before the domains lock, never
|
|
7
|
+
* after it.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { PromiseChainMutex } from '@lamalibre/lamaste';
|
|
11
|
+
|
|
12
|
+
const usersMutex = new PromiseChainMutex();
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Run `fn` holding the users lock.
|
|
16
|
+
*
|
|
17
|
+
* @template T
|
|
18
|
+
* @param {() => Promise<T>} fn
|
|
19
|
+
* @returns {Promise<T>}
|
|
20
|
+
*/
|
|
21
|
+
export function withUsersLock(fn) {
|
|
22
|
+
return usersMutex.run(fn);
|
|
23
|
+
}
|