@lamalibre/create-lamaste 4.4.1 → 4.6.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 +234 -18
- package/src/index.js +1 -0
- package/src/lib/backend-cookie.js +65 -0
- package/src/lib/ip-panel-vhost.js +208 -0
- package/src/lib/service-config.js +5 -3
- package/src/tasks/nginx.js +18 -135
- package/src/tasks/redeploy.js +39 -1
- package/vendor/docs/00-introduction/how-it-works.md +2 -2
- package/vendor/docs/00-introduction/quickstart.md +5 -5
- package/vendor/docs/00-introduction/what-is-lamaste.md +5 -5
- package/vendor/docs/01-concepts/authentication.md +1 -1
- package/vendor/docs/01-concepts/authorization.md +66 -48
- package/vendor/docs/01-concepts/certificates.md +3 -3
- package/vendor/docs/01-concepts/mtls.md +2 -2
- package/vendor/docs/01-concepts/security-model.md +70 -33
- package/vendor/docs/01-concepts/tickets.md +2 -2
- package/vendor/docs/01-concepts/tunneling.md +4 -4
- package/vendor/docs/02-guides/agent-setup.md +5 -5
- package/vendor/docs/02-guides/certificate-management.md +2 -2
- package/vendor/docs/02-guides/cloud-provisioning.md +3 -3
- package/vendor/docs/02-guides/desktop-app-setup.md +10 -10
- package/vendor/docs/02-guides/installation.md +3 -3
- package/vendor/docs/02-guides/managing-users.md +113 -25
- package/vendor/docs/02-guides/onboarding.md +2 -2
- package/vendor/docs/02-guides/standalone-plugin-with-tickets.md +2 -2
- package/vendor/docs/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 +3 -1
- package/vendor/docs/03-architecture/nginx-configuration.md +229 -24
- package/vendor/docs/03-architecture/overview.md +13 -17
- package/vendor/docs/03-architecture/panel-client.md +6 -6
- package/vendor/docs/03-architecture/panel-server.md +3 -2
- package/vendor/docs/03-architecture/state-management.md +4 -8
- package/vendor/docs/03-architecture/system-overview.md +1 -1
- package/vendor/docs/04-api-reference/agent-hosted-plugins.md +378 -34
- package/vendor/docs/04-api-reference/app-host.md +3 -3
- package/vendor/docs/04-api-reference/gatekeeper.md +78 -5
- package/vendor/docs/04-api-reference/lifecycle-hooks.md +14 -12
- package/vendor/docs/04-api-reference/onboarding.md +1 -1
- package/vendor/docs/04-api-reference/registry-and-oidc.md +3 -3
- package/vendor/docs/04-api-reference/sites.md +4 -0
- package/vendor/docs/04-api-reference/tunnels.md +87 -47
- package/vendor/docs/04-api-reference/users.md +241 -45
- package/vendor/docs/05-operations/backup-and-restore.md +35 -26
- package/vendor/docs/05-operations/monitoring.md +39 -0
- package/vendor/docs/05-operations/uninstalling.md +2 -1
- package/vendor/docs/05-operations/upgrades.md +24 -6
- package/vendor/docs/06-reference/config-files.md +100 -202
- package/vendor/docs/06-reference/glossary.md +3 -3
- package/vendor/docs/06-reference/installer-flags.md +13 -13
- package/vendor/docs/06-reference/ports-and-services.md +25 -26
- package/vendor/docs/06-reference/troubleshooting.md +8 -8
- package/vendor/docs/gatekeeper-design.md +10 -8
- package/vendor/docs/identity-directory-design.md +48 -14
- package/vendor/docs/package.json +1 -1
- package/vendor/docs/todo/lib-cli-desktop-refactor.md +27 -27
- package/vendor/gatekeeper/dist/cli/index.js +0 -0
- package/vendor/gatekeeper/dist/index.d.ts +4 -3
- package/vendor/gatekeeper/dist/index.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/index.js +4 -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 +6 -0
- package/vendor/gatekeeper/dist/lib/grants.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/lib/grants.js +6 -1
- package/vendor/gatekeeper/dist/lib/grants.js.map +1 -1
- package/vendor/gatekeeper/dist/lib/groups.d.ts +5 -0
- package/vendor/gatekeeper/dist/lib/groups.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/lib/groups.js +5 -1
- package/vendor/gatekeeper/dist/lib/groups.js.map +1 -1
- package/vendor/gatekeeper/dist/lib/principals.d.ts +29 -0
- package/vendor/gatekeeper/dist/lib/principals.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/principals.js +67 -0
- package/vendor/gatekeeper/dist/lib/principals.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/tunnels.d.ts +10 -0
- package/vendor/gatekeeper/dist/lib/tunnels.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/tunnels.js +56 -0
- package/vendor/gatekeeper/dist/lib/tunnels.js.map +1 -0
- package/vendor/gatekeeper/dist/server/index.d.ts +2 -1
- package/vendor/gatekeeper/dist/server/index.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/index.js +49 -56
- package/vendor/gatekeeper/dist/server/index.js.map +1 -1
- package/vendor/gatekeeper/dist/server/routes/authz.d.ts +15 -1
- package/vendor/gatekeeper/dist/server/routes/authz.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/routes/authz.js +332 -104
- package/vendor/gatekeeper/dist/server/routes/authz.js.map +1 -1
- package/vendor/gatekeeper/dist/server/routes/diagnostic.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/routes/diagnostic.js +61 -4
- package/vendor/gatekeeper/dist/server/routes/diagnostic.js.map +1 -1
- package/vendor/gatekeeper/dist/server/routes/principals.d.ts +3 -0
- package/vendor/gatekeeper/dist/server/routes/principals.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/principals.js +27 -0
- package/vendor/gatekeeper/dist/server/routes/principals.js.map +1 -0
- package/vendor/gatekeeper/package.json +2 -2
- package/vendor/server/bin/lamaste-server.js +15 -1
- package/vendor/server/package.json +2 -2
- package/vendor/server/src/commands/plugin-identity.js +335 -0
- package/vendor/server/src/commands/plugin-web-surface.js +269 -0
- package/vendor/server/src/commands/plugins.js +156 -11
- package/vendor/server/src/commands/registry-credential.js +2 -1
- package/vendor/server/src/commands/sites.js +15 -1
- package/vendor/server/src/commands/tunnels.js +102 -2
- package/vendor/server/src/commands/uninstall.js +3 -1
- package/vendor/server/src/commands/users.js +358 -0
- package/vendor/server/src/plugin-approval.js +186 -5
- package/vendor/server/src/plugin-identity.js +382 -0
- package/vendor/server/src/plugin-web-surface.js +307 -0
- package/vendor/server-ui/dist/assets/index-BZwQ_aQc.css +1 -0
- package/vendor/server-ui/dist/assets/index-BvEfCZYS.js +84 -0
- package/vendor/server-ui/dist/index.html +2 -2
- package/vendor/serverd/package.json +2 -2
- package/vendor/serverd/src/index.js +34 -5
- package/vendor/serverd/src/lib/audience.js +576 -0
- package/vendor/serverd/src/lib/authelia.js +78 -7
- package/vendor/serverd/src/lib/chisel-runtime.js +18 -6
- package/vendor/serverd/src/lib/chisel.js +10 -2
- package/vendor/serverd/src/lib/config.js +68 -43
- package/vendor/serverd/src/lib/directory.js +1113 -0
- package/vendor/serverd/src/lib/entitlements.js +517 -0
- package/vendor/serverd/src/lib/gatekeeper-client.js +226 -10
- package/vendor/serverd/src/lib/hook-pipelines.js +21 -3
- package/vendor/serverd/src/lib/identity-sync.js +905 -0
- package/vendor/serverd/src/lib/migrations/state/0013_identity_directory.sql +158 -0
- package/vendor/serverd/src/lib/migrations/state/0014_identity_ledger.sql +20 -0
- package/vendor/serverd/src/lib/migrations/state/0015_plugin_web_surface.sql +14 -0
- package/vendor/serverd/src/lib/mtls.js +6 -2
- package/vendor/serverd/src/lib/nginx.js +546 -48
- package/vendor/serverd/src/lib/plugin-hooks.js +127 -45
- package/vendor/serverd/src/lib/plugin-hosting.js +33 -5
- package/vendor/serverd/src/lib/plugin-lifecycle.js +227 -13
- package/vendor/serverd/src/lib/plugin-packages.js +74 -2
- package/vendor/serverd/src/lib/plugins.js +95 -7
- package/vendor/serverd/src/lib/registry-credential.js +9 -6
- package/vendor/serverd/src/lib/request-logging.js +19 -0
- package/vendor/serverd/src/lib/retained-data.js +50 -4
- package/vendor/serverd/src/lib/state.js +36 -0
- package/vendor/serverd/src/lib/tunnel-hooks.js +4 -1
- package/vendor/serverd/src/lib/user-access-session.js +21 -11
- package/vendor/serverd/src/lib/user-caller.js +67 -0
- package/vendor/serverd/src/lib/user-surface.js +754 -0
- package/vendor/serverd/src/lib/vhost-reconcile.js +137 -33
- package/vendor/serverd/src/lib/web-surface.js +766 -0
- package/vendor/serverd/src/middleware/user-access-session.js +9 -0
- package/vendor/serverd/src/routes/agent-forwarder.js +4 -2
- package/vendor/serverd/src/routes/invite.js +53 -12
- package/vendor/serverd/src/routes/management/gatekeeper-proxy.js +107 -16
- package/vendor/serverd/src/routes/management/identity-directory.js +79 -0
- package/vendor/serverd/src/routes/management/invitations.js +78 -68
- package/vendor/serverd/src/routes/management/plugin-identity.js +221 -0
- package/vendor/serverd/src/routes/management/plugin-web-surface.js +151 -0
- package/vendor/serverd/src/routes/management/plugins.js +2 -0
- package/vendor/serverd/src/routes/management/registry-credential.js +1 -1
- package/vendor/serverd/src/routes/management/sites.js +143 -65
- package/vendor/serverd/src/routes/management/tunnels.js +109 -22
- package/vendor/serverd/src/routes/management/users.js +290 -208
- package/vendor/serverd/src/routes/management.js +6 -0
- package/vendor/serverd/src/routes/onboarding/provision.js +14 -1
- package/vendor/serverd/src/routes/user-access.js +17 -4
- package/vendor/serverd/src/routes/user-surface.js +69 -0
- package/vendor/server-ui/dist/assets/index-25wo_8av.css +0 -1
- package/vendor/server-ui/dist/assets/index-roDHz3dM.js +0 -84
|
@@ -2,11 +2,18 @@
|
|
|
2
2
|
* Shim — Authelia lifecycle + config management now lives in
|
|
3
3
|
* `@lamalibre/lamaste/server`. This file wires the daemon's execa instance
|
|
4
4
|
* and bcryptjs hasher to the parameterized core API.
|
|
5
|
+
*
|
|
6
|
+
* Users go through here alone: every write of users.yml is a locked
|
|
7
|
+
* read-modify-write in `lib/directory.js` (`changeUsers`), which brings the
|
|
8
|
+
* user directory in line before Authelia restarts. Tests point the users
|
|
9
|
+
* file, the restart and the TOTP writer elsewhere
|
|
10
|
+
* ({@link setAutheliaForTests}) — there is no Authelia to restart there.
|
|
5
11
|
*/
|
|
6
12
|
|
|
7
13
|
import { execa } from 'execa';
|
|
8
14
|
import bcrypt from 'bcryptjs';
|
|
9
15
|
import {
|
|
16
|
+
AUTHELIA_USERS,
|
|
10
17
|
isAutheliaInstalled as isAutheliaInstalledCore,
|
|
11
18
|
writeAutheliaConfig as writeAutheliaConfigCore,
|
|
12
19
|
createAutheliaUser,
|
|
@@ -24,11 +31,65 @@ import {
|
|
|
24
31
|
base32Decode,
|
|
25
32
|
generateTotpSecret,
|
|
26
33
|
writeTotpToDatabase as writeTotpToDatabaseCore,
|
|
34
|
+
purgeAutheliaSecondFactors,
|
|
35
|
+
autheliaHoldsOpaqueIdentifier,
|
|
27
36
|
} from '@lamalibre/lamaste/server';
|
|
28
37
|
|
|
29
38
|
// bcryptjs hasher — signature matches the core lib's BcryptHashFn.
|
|
30
39
|
const bcryptHash = (password, cost) => bcrypt.hash(password, cost);
|
|
31
40
|
|
|
41
|
+
const restartRealAuthelia = () => reloadAutheliaCore(execa);
|
|
42
|
+
const writeRealTotp = (username, base32Secret) =>
|
|
43
|
+
writeTotpToDatabaseCore(username, base32Secret, execa);
|
|
44
|
+
|
|
45
|
+
/** Authelia's per-user storage through the root helper — replaced in tests. */
|
|
46
|
+
const realUserStorage = Object.freeze({
|
|
47
|
+
purgeSecondFactors: (username) => purgeAutheliaSecondFactors(username, execa),
|
|
48
|
+
holdsOidcIdentifier: (username) => autheliaHoldsOpaqueIdentifier(username, execa),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
/** Authelia's users file — another one in tests. */
|
|
52
|
+
let usersFile = AUTHELIA_USERS;
|
|
53
|
+
/** Restart Authelia (it reads users.yml when it starts) — replaced in tests. */
|
|
54
|
+
let restartAuthelia = restartRealAuthelia;
|
|
55
|
+
/** Store a TOTP secret in Authelia's database — replaced in tests. */
|
|
56
|
+
let totpWriter = writeRealTotp;
|
|
57
|
+
/** Authelia's per-user storage — replaced in tests. */
|
|
58
|
+
let userStorage = realUserStorage;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Point the users file, the Authelia restart, the TOTP writer and the
|
|
62
|
+
* per-user storage elsewhere (tests); `null` restores Authelia's.
|
|
63
|
+
*
|
|
64
|
+
* @param {{ usersFile?: string, restart?: () => Promise<unknown>, writeTotp?: (username: string, secret: string) => Promise<void>, userStorage?: { purgeSecondFactors: (username: string) => Promise<void>, holdsOidcIdentifier: (username: string) => Promise<boolean> } } | null} overrides
|
|
65
|
+
*/
|
|
66
|
+
export function setAutheliaForTests(overrides) {
|
|
67
|
+
usersFile = overrides?.usersFile ?? AUTHELIA_USERS;
|
|
68
|
+
restartAuthelia = overrides?.restart ?? restartRealAuthelia;
|
|
69
|
+
totpWriter = overrides?.writeTotp ?? writeRealTotp;
|
|
70
|
+
userStorage = overrides?.userStorage ?? realUserStorage;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Delete `username`'s TOTP configuration and WebAuthn credentials from
|
|
75
|
+
* Authelia's storage (idempotent; throws when the helper fails).
|
|
76
|
+
*
|
|
77
|
+
* @param {string} username
|
|
78
|
+
*/
|
|
79
|
+
export function purgeAutheliaUserSecondFactors(username) {
|
|
80
|
+
return userStorage.purgeSecondFactors(username);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Whether Authelia's storage holds an OpenID Connect opaque identifier for
|
|
85
|
+
* `username` (throws when it cannot be told).
|
|
86
|
+
*
|
|
87
|
+
* @param {string} username
|
|
88
|
+
*/
|
|
89
|
+
export function autheliaHoldsOidcIdentity(username) {
|
|
90
|
+
return userStorage.holdsOidcIdentifier(username);
|
|
91
|
+
}
|
|
92
|
+
|
|
32
93
|
/** True once create-lamaste has installed Authelia. */
|
|
33
94
|
export function isAutheliaInstalled() {
|
|
34
95
|
return isAutheliaInstalledCore();
|
|
@@ -38,20 +99,22 @@ export function writeAutheliaConfig(domain, secrets) {
|
|
|
38
99
|
return writeAutheliaConfigCore(domain, secrets);
|
|
39
100
|
}
|
|
40
101
|
|
|
102
|
+
/** Add the default administrator to users.yml (onboarding) — inside `changeUsers`. */
|
|
41
103
|
export function createUser(username, password) {
|
|
42
|
-
return createAutheliaUser(username, password, bcryptHash);
|
|
104
|
+
return createAutheliaUser(username, password, bcryptHash, usersFile);
|
|
43
105
|
}
|
|
44
106
|
|
|
45
107
|
export function readUsers() {
|
|
46
|
-
return readAutheliaUsers();
|
|
108
|
+
return readAutheliaUsers(usersFile);
|
|
47
109
|
}
|
|
48
110
|
|
|
49
111
|
export function writeUsers(usersData) {
|
|
50
|
-
return writeAutheliaUsers(usersData);
|
|
112
|
+
return writeAutheliaUsers(usersData, usersFile);
|
|
51
113
|
}
|
|
52
114
|
|
|
115
|
+
/** users.yml as written, password hashes included; throws when it cannot be read or parsed. */
|
|
53
116
|
export function readUsersRaw() {
|
|
54
|
-
return readAutheliaUsersRaw();
|
|
117
|
+
return readAutheliaUsersRaw(usersFile);
|
|
55
118
|
}
|
|
56
119
|
|
|
57
120
|
export function hashPassword(password) {
|
|
@@ -62,8 +125,9 @@ export function startAuthelia() {
|
|
|
62
125
|
return startAutheliaCore(execa);
|
|
63
126
|
}
|
|
64
127
|
|
|
128
|
+
/** Restart Authelia: it reads users.yml when it starts, and every session ends. */
|
|
65
129
|
export function reloadAuthelia() {
|
|
66
|
-
return
|
|
130
|
+
return restartAuthelia();
|
|
67
131
|
}
|
|
68
132
|
|
|
69
133
|
export function isAutheliaRunning() {
|
|
@@ -84,12 +148,19 @@ export function updateOidcProvider(domain, clients, options = {}) {
|
|
|
84
148
|
return updateAutheliaOidcProvider(domain, clients, execa, options);
|
|
85
149
|
}
|
|
86
150
|
|
|
151
|
+
/**
|
|
152
|
+
* Add an invited user to users.yml — inside `changeUsers`, which restarts
|
|
153
|
+
* Authelia once the directory knows the user.
|
|
154
|
+
*/
|
|
87
155
|
export function createUserFromInvitation(username, email, groups, hashedPassword) {
|
|
88
|
-
return createUserFromInvitationCore(username, email, groups, hashedPassword, execa
|
|
156
|
+
return createUserFromInvitationCore(username, email, groups, hashedPassword, execa, {
|
|
157
|
+
restart: false,
|
|
158
|
+
usersFile,
|
|
159
|
+
});
|
|
89
160
|
}
|
|
90
161
|
|
|
91
162
|
export function writeTotpToDatabase(username, base32Secret) {
|
|
92
|
-
return
|
|
163
|
+
return totpWriter(username, base32Secret);
|
|
93
164
|
}
|
|
94
165
|
|
|
95
166
|
// Pure helpers — no dependency injection needed
|
|
@@ -30,17 +30,29 @@ export function chiselReloadsAuthfile() {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
33
|
+
* Whether an authfile change must restart chisel: a grant withdrawn (so
|
|
34
|
+
* live sessions drop the binding) — unless `keepLiveSessions` — or any
|
|
35
|
+
* change a chisel too old to reload its authfile would not see.
|
|
36
|
+
*
|
|
37
|
+
* @param {{ changed: boolean, revoked: boolean }} change
|
|
38
|
+
* @param {{ keepLiveSessions?: boolean }} [opts]
|
|
39
|
+
*/
|
|
40
|
+
export function authfileChangeNeedsRestart(change, { keepLiveSessions = false } = {}) {
|
|
41
|
+
return (change.revoked && !keepLiveSessions) || (change.changed && !runningPinnedRelease);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Restart chisel if the change requires it ({@link authfileChangeNeedsRestart}).
|
|
46
|
+
* `try-restart` leaves a stopped chisel stopped (see chisel-reconcile.js: a
|
|
47
|
+
* server that failed to reconcile keeps chisel down until it succeeds).
|
|
36
48
|
*
|
|
37
49
|
* @param {{ changed: boolean, revoked: boolean }} change
|
|
38
50
|
* @param {import('pino').Logger} [logger]
|
|
51
|
+
* @param {{ keepLiveSessions?: boolean }} [opts]
|
|
39
52
|
* @returns {Promise<{ restartOk: boolean, restartError?: string }>}
|
|
40
53
|
*/
|
|
41
|
-
export async function applyAuthfileChange(change, logger) {
|
|
42
|
-
|
|
43
|
-
if (!needsRestart) return { restartOk: true };
|
|
54
|
+
export async function applyAuthfileChange(change, logger, opts = {}) {
|
|
55
|
+
if (!authfileChangeNeedsRestart(change, opts)) return { restartOk: true };
|
|
44
56
|
const result = await reloadChiselAuth(execa, logger);
|
|
45
57
|
return result.ok ? { restartOk: true } : { restartOk: false, restartError: result.error };
|
|
46
58
|
}
|
|
@@ -71,10 +71,18 @@ export function getChiselStatus() {
|
|
|
71
71
|
* grant, chisel is restarted so live sessions drop the binding. Call only
|
|
72
72
|
* after the tunnel state has been written. Throws if the restart fails,
|
|
73
73
|
* since a withdrawn grant would otherwise stay live.
|
|
74
|
+
*
|
|
75
|
+
* `keepLiveSessions`: withdraw the grant for new sessions only, without the
|
|
76
|
+
* restart — which drops every session, the hosting agents' internal plugin
|
|
77
|
+
* tunnels with them. Only for a port nothing routes to any more, whose
|
|
78
|
+
* agent drops the binding at its next sync (a tunnel moved to a plugin's
|
|
79
|
+
* web surface).
|
|
80
|
+
*
|
|
81
|
+
* @param {{ keepLiveSessions?: boolean }} [opts]
|
|
74
82
|
*/
|
|
75
|
-
export async function syncChisel() {
|
|
83
|
+
export async function syncChisel(opts = {}) {
|
|
76
84
|
const change = await syncChiselAuthfile();
|
|
77
|
-
const { restartOk, restartError } = await applyAuthfileChange(change);
|
|
85
|
+
const { restartOk, restartError } = await applyAuthfileChange(change, undefined, opts);
|
|
78
86
|
if (!restartOk) {
|
|
79
87
|
throw new Error(`Chisel restart failed after withdrawing a port grant: ${restartError}`);
|
|
80
88
|
}
|
|
@@ -3,6 +3,8 @@ import { readFile, writeFile, rename, open } from 'node:fs/promises';
|
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { z } from 'zod';
|
|
6
|
+
import { isIdentityUuid } from '@lamalibre/lamaste';
|
|
7
|
+
import { SESSION_MAX_AGE } from './user-access-session.js';
|
|
6
8
|
|
|
7
9
|
const ConfigSchema = z.object({
|
|
8
10
|
ip: z.string().min(1),
|
|
@@ -25,11 +27,13 @@ const ConfigSchema = z.object({
|
|
|
25
27
|
.optional()
|
|
26
28
|
.default({ enabled: false, secret: null, setupComplete: false }),
|
|
27
29
|
sessionSecret: z.string().nullable().optional().default(null),
|
|
28
|
-
// Per-user invalidation epochs (Unix seconds)
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
30
|
+
// Per-user invalidation epochs (Unix seconds), keyed by the directory's
|
|
31
|
+
// user id — never by username, so panel.json names nobody. Offboarding a
|
|
32
|
+
// user (disable, delete) bumps theirs, and the session middleware rejects
|
|
33
|
+
// every user-access session issued before it. Sessions are bound to the
|
|
34
|
+
// user id too, so a username given to someone else later never revives
|
|
35
|
+
// one. An epoch older than a session's longest life can reject nothing and
|
|
36
|
+
// is dropped (`pruneUserEpochs`).
|
|
33
37
|
userEpochs: z.record(z.string(), z.number().int().nonnegative()).optional().default({}),
|
|
34
38
|
onboarding: z.object({
|
|
35
39
|
status: z.enum(['FRESH', 'DOMAIN_SET', 'DNS_READY', 'PROVISIONING', 'COMPLETED']),
|
|
@@ -45,8 +49,8 @@ let configPath = null;
|
|
|
45
49
|
|
|
46
50
|
/**
|
|
47
51
|
* Promise-chain mutex serializing all panel.json writes so concurrent
|
|
48
|
-
* updateConfig() calls cannot race (e.g. two simultaneous
|
|
49
|
-
* bumping userEpochs).
|
|
52
|
+
* updateConfig() / bumpUserEpoch() calls cannot race (e.g. two simultaneous
|
|
53
|
+
* user deletes both bumping userEpochs).
|
|
50
54
|
*/
|
|
51
55
|
let configWriteLock = Promise.resolve();
|
|
52
56
|
function withConfigLock(fn) {
|
|
@@ -58,6 +62,36 @@ function withConfigLock(fn) {
|
|
|
58
62
|
return prev.then(fn).finally(resolve);
|
|
59
63
|
}
|
|
60
64
|
|
|
65
|
+
/**
|
|
66
|
+
* The epochs worth keeping: keyed by a user id (keys of earlier versions
|
|
67
|
+
* were usernames — dropped, as the sessions they guarded no longer
|
|
68
|
+
* validate: a session now names its user id) and younger than a session's
|
|
69
|
+
* longest life.
|
|
70
|
+
*
|
|
71
|
+
* @param {Record<string, number>} epochs
|
|
72
|
+
* @param {number} nowSeconds
|
|
73
|
+
*/
|
|
74
|
+
export function pruneUserEpochs(epochs, nowSeconds) {
|
|
75
|
+
const out = {};
|
|
76
|
+
for (const [userId, epoch] of Object.entries(epochs ?? {})) {
|
|
77
|
+
if (isIdentityUuid(userId) && epoch > nowSeconds - SESSION_MAX_AGE) out[userId] = epoch;
|
|
78
|
+
}
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Write `validated` to panel.json atomically (temp → fsync → rename), 0600. */
|
|
83
|
+
async function persistConfig(validated) {
|
|
84
|
+
const tmpPath = `${configPath}.tmp`;
|
|
85
|
+
await writeFile(tmpPath, JSON.stringify(validated, null, 2) + '\n', {
|
|
86
|
+
encoding: 'utf-8',
|
|
87
|
+
mode: 0o600,
|
|
88
|
+
});
|
|
89
|
+
const fd = await open(tmpPath, 'r');
|
|
90
|
+
await fd.sync();
|
|
91
|
+
await fd.close();
|
|
92
|
+
await rename(tmpPath, configPath);
|
|
93
|
+
}
|
|
94
|
+
|
|
61
95
|
function resolveConfigPath() {
|
|
62
96
|
if (process.env.LAMALIBRE_LAMASTE_CONFIG) {
|
|
63
97
|
return process.env.LAMALIBRE_LAMASTE_CONFIG;
|
|
@@ -95,18 +129,18 @@ export async function loadConfig() {
|
|
|
95
129
|
const validated = ConfigSchema.parse(parsed);
|
|
96
130
|
|
|
97
131
|
// Auto-generate serverId if missing — used as bucket prefix for multi-server isolation
|
|
132
|
+
let rewrite = false;
|
|
98
133
|
if (!validated.serverId) {
|
|
99
134
|
validated.serverId = crypto.randomUUID();
|
|
100
|
-
|
|
101
|
-
await writeFile(tmpPath, JSON.stringify(validated, null, 2) + '\n', {
|
|
102
|
-
encoding: 'utf-8',
|
|
103
|
-
mode: 0o600,
|
|
104
|
-
});
|
|
105
|
-
const fd = await open(tmpPath, 'r');
|
|
106
|
-
await fd.sync();
|
|
107
|
-
await fd.close();
|
|
108
|
-
await rename(tmpPath, configPath);
|
|
135
|
+
rewrite = true;
|
|
109
136
|
}
|
|
137
|
+
// Epochs keyed by username (before 4.5) or too old to reject anything go.
|
|
138
|
+
const epochs = pruneUserEpochs(validated.userEpochs, Math.floor(Date.now() / 1000));
|
|
139
|
+
if (Object.keys(epochs).length !== Object.keys(validated.userEpochs).length) {
|
|
140
|
+
validated.userEpochs = epochs;
|
|
141
|
+
rewrite = true;
|
|
142
|
+
}
|
|
143
|
+
if (rewrite) await persistConfig(validated);
|
|
110
144
|
|
|
111
145
|
config = validated;
|
|
112
146
|
return structuredClone(config);
|
|
@@ -140,28 +174,13 @@ export function updateConfig(patch) {
|
|
|
140
174
|
patch.panel2fa !== null
|
|
141
175
|
) {
|
|
142
176
|
merged.panel2fa = { ...merged.panel2fa, ...patch.panel2fa };
|
|
143
|
-
} else if (
|
|
144
|
-
key === 'userEpochs' &&
|
|
145
|
-
typeof patch.userEpochs === 'object' &&
|
|
146
|
-
patch.userEpochs !== null
|
|
147
|
-
) {
|
|
148
|
-
merged.userEpochs = { ...(merged.userEpochs ?? {}), ...patch.userEpochs };
|
|
149
177
|
} else {
|
|
150
178
|
merged[key] = patch[key];
|
|
151
179
|
}
|
|
152
180
|
}
|
|
153
181
|
|
|
154
182
|
const validated = ConfigSchema.parse(merged);
|
|
155
|
-
|
|
156
|
-
const tmpPath = `${configPath}.tmp`;
|
|
157
|
-
await writeFile(tmpPath, JSON.stringify(validated, null, 2) + '\n', {
|
|
158
|
-
encoding: 'utf-8',
|
|
159
|
-
mode: 0o600,
|
|
160
|
-
});
|
|
161
|
-
const fd = await open(tmpPath, 'r');
|
|
162
|
-
await fd.sync();
|
|
163
|
-
await fd.close();
|
|
164
|
-
await rename(tmpPath, configPath);
|
|
183
|
+
await persistConfig(validated);
|
|
165
184
|
|
|
166
185
|
config = validated;
|
|
167
186
|
return structuredClone(config);
|
|
@@ -169,20 +188,26 @@ export function updateConfig(patch) {
|
|
|
169
188
|
}
|
|
170
189
|
|
|
171
190
|
/**
|
|
172
|
-
* Bump a user's session-invalidation epoch to "now" (Unix seconds)
|
|
173
|
-
* user-access session
|
|
174
|
-
*
|
|
191
|
+
* Bump a user's session-invalidation epoch to "now" (Unix seconds): every
|
|
192
|
+
* user-access session of `userId` issued before it is revoked (the session
|
|
193
|
+
* middleware). Epochs that can no longer reject anything are dropped in
|
|
194
|
+
* the same write ({@link pruneUserEpochs}). Idempotent.
|
|
175
195
|
*
|
|
176
|
-
*
|
|
177
|
-
* timestamp simply replaces the earlier one).
|
|
178
|
-
*
|
|
179
|
-
* @param {string} username
|
|
196
|
+
* @param {string} userId the directory's id of the user
|
|
180
197
|
* @returns {Promise<number>} The new epoch (Unix seconds)
|
|
181
198
|
*/
|
|
182
|
-
export
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
return
|
|
199
|
+
export function bumpUserEpoch(userId) {
|
|
200
|
+
if (!isIdentityUuid(userId)) throw new Error('A session epoch is keyed by a directory user id');
|
|
201
|
+
if (config === null) throw new Error('Config not loaded. Call loadConfig() first.');
|
|
202
|
+
return withConfigLock(async () => {
|
|
203
|
+
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
204
|
+
const next = structuredClone(config);
|
|
205
|
+
next.userEpochs = { ...pruneUserEpochs(next.userEpochs, nowSeconds), [userId]: nowSeconds };
|
|
206
|
+
const validated = ConfigSchema.parse(next);
|
|
207
|
+
await persistConfig(validated);
|
|
208
|
+
config = validated;
|
|
209
|
+
return nowSeconds;
|
|
210
|
+
});
|
|
186
211
|
}
|
|
187
212
|
|
|
188
213
|
export function getConfigPath() {
|