@lamalibre/create-lamaste 4.6.0 → 4.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/scripts/bundle-vendor.js +20 -3
- package/scripts/lamaste-priv +22 -3
- package/scripts/pin-release.js +98 -0
- package/src/index.js +8 -1
- package/src/lib/env.js +19 -0
- package/src/lib/service-config.js +7 -0
- package/src/tasks/redeploy.js +8 -0
- package/vendor/docs/00-introduction/how-it-works.md +2 -2
- package/vendor/docs/00-introduction/quickstart.md +5 -5
- package/vendor/docs/00-introduction/what-is-lamaste.md +5 -5
- package/vendor/docs/01-concepts/authentication.md +6 -2
- package/vendor/docs/01-concepts/authorization.md +4 -0
- package/vendor/docs/01-concepts/certificates.md +3 -3
- package/vendor/docs/01-concepts/dns-and-domains.md +9 -0
- package/vendor/docs/01-concepts/mtls.md +2 -2
- package/vendor/docs/01-concepts/security-model.md +28 -3
- package/vendor/docs/01-concepts/tickets.md +2 -2
- package/vendor/docs/01-concepts/tunneling.md +4 -4
- package/vendor/docs/02-guides/agent-setup.md +5 -5
- package/vendor/docs/02-guides/certificate-management.md +2 -2
- package/vendor/docs/02-guides/cloud-provisioning.md +3 -3
- package/vendor/docs/02-guides/desktop-app-setup.md +10 -10
- package/vendor/docs/02-guides/installation.md +3 -3
- package/vendor/docs/02-guides/managing-users.md +5 -11
- package/vendor/docs/02-guides/onboarding.md +2 -2
- package/vendor/docs/02-guides/second-domain.md +82 -0
- package/vendor/docs/02-guides/standalone-plugin-with-tickets.md +2 -2
- package/vendor/docs/03-architecture/installer-flow.md +2 -2
- package/vendor/docs/03-architecture/installer.md +4 -4
- package/vendor/docs/03-architecture/management-flow.md +1 -1
- package/vendor/docs/03-architecture/nginx-configuration.md +15 -5
- package/vendor/docs/03-architecture/overview.md +5 -5
- package/vendor/docs/03-architecture/panel-client.md +6 -6
- package/vendor/docs/03-architecture/panel-server.md +2 -2
- package/vendor/docs/03-architecture/state-management.md +3 -3
- package/vendor/docs/03-architecture/system-overview.md +1 -1
- package/vendor/docs/04-api-reference/agent-hosted-plugins.md +17 -13
- package/vendor/docs/04-api-reference/app-host.md +3 -3
- package/vendor/docs/04-api-reference/domains.md +192 -0
- package/vendor/docs/04-api-reference/gatekeeper.md +35 -7
- package/vendor/docs/04-api-reference/lifecycle-hooks.md +4 -4
- package/vendor/docs/04-api-reference/onboarding.md +1 -1
- package/vendor/docs/04-api-reference/overview.md +9 -0
- package/vendor/docs/04-api-reference/registry-and-oidc.md +26 -13
- package/vendor/docs/04-api-reference/sites.md +5 -0
- package/vendor/docs/04-api-reference/tunnels.md +15 -10
- package/vendor/docs/04-api-reference/users.md +41 -29
- package/vendor/docs/05-operations/backup-and-restore.md +1 -1
- package/vendor/docs/05-operations/uninstalling.md +1 -1
- package/vendor/docs/05-operations/upgrades.md +6 -6
- package/vendor/docs/06-reference/config-files.md +1 -1
- package/vendor/docs/06-reference/glossary.md +3 -3
- package/vendor/docs/06-reference/installer-flags.md +13 -13
- package/vendor/docs/06-reference/ports-and-services.md +1 -1
- package/vendor/docs/06-reference/troubleshooting.md +8 -8
- package/vendor/docs/_index.json +105 -19
- package/vendor/docs/external-members-design.md +816 -0
- package/vendor/docs/gatekeeper-design.md +8 -8
- package/vendor/docs/identity-directory-design.md +3 -3
- package/vendor/docs/multi-domain-relay-design.md +836 -0
- package/vendor/docs/package.json +1 -1
- package/vendor/docs/todo/lib-cli-desktop-refactor.md +27 -27
- package/vendor/gatekeeper/dist/cli/index.js +0 -0
- package/vendor/gatekeeper/dist/lib/grants.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/lib/grants.js +20 -13
- package/vendor/gatekeeper/dist/lib/grants.js.map +1 -1
- package/vendor/gatekeeper/dist/lib/migrations/gatekeeper/0004_unconsume_access_grants.sql +8 -0
- package/vendor/gatekeeper/dist/lib/secret.d.ts +6 -0
- package/vendor/gatekeeper/dist/lib/secret.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/secret.js +12 -0
- package/vendor/gatekeeper/dist/lib/secret.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/templates.d.ts +20 -0
- package/vendor/gatekeeper/dist/lib/templates.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/lib/templates.js +80 -14
- package/vendor/gatekeeper/dist/lib/templates.js.map +1 -1
- package/vendor/gatekeeper/dist/server/index.d.ts +5 -1
- package/vendor/gatekeeper/dist/server/index.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/index.js +28 -2
- package/vendor/gatekeeper/dist/server/index.js.map +1 -1
- package/vendor/gatekeeper/dist/server/routes/authz.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/routes/authz.js +225 -36
- package/vendor/gatekeeper/dist/server/routes/authz.js.map +1 -1
- package/vendor/gatekeeper/dist/server/routes/diagnostic.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/routes/diagnostic.js +5 -2
- package/vendor/gatekeeper/dist/server/routes/diagnostic.js.map +1 -1
- package/vendor/gatekeeper/package.json +2 -2
- package/vendor/server/bin/lamaste-server.js +7 -0
- package/vendor/server/package.json +2 -2
- package/vendor/server/src/commands/domains.js +214 -0
- package/vendor/server/src/commands/plugin-identity.js +2 -16
- package/vendor/server/src/commands/plugins.js +112 -8
- package/vendor/server/src/commands/registry-credential.js +10 -3
- package/vendor/server/src/commands/sites.js +3 -0
- package/vendor/server/src/commands/status.js +19 -0
- package/vendor/server/src/commands/tunnels.js +4 -1
- package/vendor/server/src/commands/users.js +8 -19
- package/vendor/server/src/domains.js +92 -0
- package/vendor/server/src/fail.js +21 -0
- package/vendor/server/src/oidc-client-grants.js +63 -0
- package/vendor/server/src/panel-api.js +15 -0
- package/vendor/server-ui/dist/assets/index-B8Tv_ocF.js +84 -0
- package/vendor/server-ui/dist/assets/{index-BZwQ_aQc.css → index-Cb-ADsbB.css} +1 -1
- package/vendor/server-ui/dist/index.html +2 -2
- package/vendor/serverd/package.json +2 -2
- package/vendor/serverd/src/index.js +42 -16
- package/vendor/serverd/src/lib/app-error.js +24 -0
- package/vendor/serverd/src/lib/audience.js +12 -3
- package/vendor/serverd/src/lib/authelia.js +341 -35
- package/vendor/serverd/src/lib/directory.js +67 -52
- package/vendor/serverd/src/lib/domains.js +1792 -0
- package/vendor/serverd/src/lib/gatekeeper-client.js +20 -4
- package/vendor/serverd/src/lib/identity-sync.js +35 -0
- package/vendor/serverd/src/lib/migrations/state/0016_domains.sql +53 -0
- package/vendor/serverd/src/lib/migrations/state/0017_membership_kind.sql +15 -0
- package/vendor/serverd/src/lib/migrations/state/0018_domain_group_keys.sql +23 -0
- package/vendor/serverd/src/lib/nginx.js +318 -140
- package/vendor/serverd/src/lib/owned-groups.js +326 -0
- package/vendor/serverd/src/lib/plugin-domain.js +86 -0
- package/vendor/serverd/src/lib/plugin-lifecycle.js +1 -0
- package/vendor/serverd/src/lib/plugin-oidc.js +144 -42
- package/vendor/serverd/src/lib/plugins.js +45 -5
- package/vendor/serverd/src/lib/registry-credential.js +39 -6
- package/vendor/serverd/src/lib/services.js +9 -1
- package/vendor/serverd/src/lib/state.js +11 -0
- package/vendor/serverd/src/lib/tunnel-deps.js +2 -4
- package/vendor/serverd/src/lib/user-surface.js +29 -4
- package/vendor/serverd/src/lib/users-lock.js +23 -0
- package/vendor/serverd/src/lib/vhost-reconcile.js +149 -41
- package/vendor/serverd/src/lib/web-surface.js +49 -32
- package/vendor/serverd/src/middleware/user-access-session.js +11 -4
- package/vendor/serverd/src/routes/invite.js +15 -5
- package/vendor/serverd/src/routes/management/domains.js +154 -0
- package/vendor/serverd/src/routes/management/gatekeeper-proxy.js +44 -8
- package/vendor/serverd/src/routes/management/identity.js +21 -7
- package/vendor/serverd/src/routes/management/invitations.js +33 -4
- package/vendor/serverd/src/routes/management/plugins.js +4 -2
- package/vendor/serverd/src/routes/management/registry-credential.js +86 -57
- package/vendor/serverd/src/routes/management/sites.js +50 -11
- package/vendor/serverd/src/routes/management/tunnels.js +29 -5
- package/vendor/serverd/src/routes/management/users.js +184 -64
- package/vendor/serverd/src/routes/management.js +2 -0
- package/vendor/serverd/src/routes/onboarding/dns.js +3 -17
- package/vendor/serverd/src/routes/onboarding/provision.js +22 -13
- package/vendor/serverd/src/routes/user-access.js +13 -1
- package/vendor/gatekeeper/dist/lib/migrations/migrations/gatekeeper/0001_groups.sql +0 -7
- package/vendor/gatekeeper/dist/lib/migrations/migrations/gatekeeper/0002_access_grants.sql +0 -14
- package/vendor/gatekeeper/dist/lib/migrations/migrations/gatekeeper/0003_access_request_log.sql +0 -9
- package/vendor/server-ui/dist/assets/index-BvEfCZYS.js +0 -84
|
@@ -5,18 +5,38 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Users go through here alone: every write of users.yml is a locked
|
|
7
7
|
* read-modify-write in `lib/directory.js` (`changeUsers`), which brings the
|
|
8
|
-
* user directory in line
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* user directory in line and renders the domains afterwards. Authelia
|
|
9
|
+
* watches the file (`authentication_backend.file.watch`) and applies a
|
|
10
|
+
* change at each user's next request — nothing restarts it.
|
|
11
|
+
*
|
|
12
|
+
* users.yml also carries the groups Lamaste owns (core owned-groups.ts:
|
|
13
|
+
* domain memberships with their kind, static sites' listed users, host
|
|
14
|
+
* groups, OIDC client grants), which the domains' rendering writes
|
|
15
|
+
* ({@link writeOwnedGroups}). Every write here passes through one file lock
|
|
16
|
+
* and keeps them as that rendering last decided — a read-modify-write that
|
|
17
|
+
* read the file before a rendering never writes an older set back; a user
|
|
18
|
+
* the rendering did not know (created since, perhaps under a deleted
|
|
19
|
+
* user's username) gets none until the next one; after a rendering failed,
|
|
20
|
+
* writes only narrow. A write never drops a user it was not told to delete.
|
|
21
|
+
* After every write the file is touched once Authelia's reload cooldown has
|
|
22
|
+
* passed ({@link USERS_FILE_TOUCH_DELAY_MS}) — read again, the owned groups
|
|
23
|
+
* applied again, written: Authelia skips a change that lands within half a
|
|
24
|
+
* second of its previous reload, and this guarantees one reload after the
|
|
25
|
+
* last write, with the groups Lamaste decided even if something else wrote
|
|
26
|
+
* the file meanwhile. The identity tick and the start touch it too.
|
|
27
|
+
*
|
|
28
|
+
* Tests point the users file, the TOTP writer and the renderers elsewhere
|
|
29
|
+
* ({@link setAutheliaForTests}).
|
|
11
30
|
*/
|
|
12
31
|
|
|
13
32
|
import { execa } from 'execa';
|
|
14
33
|
import bcrypt from 'bcryptjs';
|
|
34
|
+
import { readFile } from 'node:fs/promises';
|
|
35
|
+
import { PromiseChainMutex, isLamasteOwnedGroup, mergeOwnedGroups } from '@lamalibre/lamaste';
|
|
15
36
|
import {
|
|
16
37
|
AUTHELIA_USERS,
|
|
17
38
|
isAutheliaInstalled as isAutheliaInstalledCore,
|
|
18
39
|
writeAutheliaConfig as writeAutheliaConfigCore,
|
|
19
|
-
createAutheliaUser,
|
|
20
40
|
readAutheliaUsers,
|
|
21
41
|
writeAutheliaUsers,
|
|
22
42
|
readAutheliaUsersRaw,
|
|
@@ -24,9 +44,9 @@ import {
|
|
|
24
44
|
startAuthelia as startAutheliaCore,
|
|
25
45
|
reloadAuthelia as reloadAutheliaCore,
|
|
26
46
|
isAutheliaRunning as isAutheliaRunningCore,
|
|
27
|
-
|
|
47
|
+
updateAutheliaDomainAccess,
|
|
48
|
+
autheliaWatchesUsersFile,
|
|
28
49
|
updateAutheliaOidcProvider,
|
|
29
|
-
createUserFromInvitation as createUserFromInvitationCore,
|
|
30
50
|
base32Encode,
|
|
31
51
|
base32Decode,
|
|
32
52
|
generateTotpSecret,
|
|
@@ -35,6 +55,13 @@ import {
|
|
|
35
55
|
autheliaHoldsOpaqueIdentifier,
|
|
36
56
|
} from '@lamalibre/lamaste/server';
|
|
37
57
|
|
|
58
|
+
/**
|
|
59
|
+
* How long after a write of users.yml it is written again with the same
|
|
60
|
+
* content: past Authelia's half-second reload cooldown (its watcher skips a
|
|
61
|
+
* change within 0.5 s of its previous reload, it does not queue it).
|
|
62
|
+
*/
|
|
63
|
+
export const USERS_FILE_TOUCH_DELAY_MS = 600;
|
|
64
|
+
|
|
38
65
|
// bcryptjs hasher — signature matches the core lib's BcryptHashFn.
|
|
39
66
|
const bcryptHash = (password, cost) => bcrypt.hash(password, cost);
|
|
40
67
|
|
|
@@ -50,24 +77,54 @@ const realUserStorage = Object.freeze({
|
|
|
50
77
|
|
|
51
78
|
/** Authelia's users file — another one in tests. */
|
|
52
79
|
let usersFile = AUTHELIA_USERS;
|
|
53
|
-
/** Restart Authelia (it reads
|
|
80
|
+
/** Restart Authelia (it reads its configuration when it starts) — replaced in tests. */
|
|
54
81
|
let restartAuthelia = restartRealAuthelia;
|
|
82
|
+
const realWatches = () => autheliaWatchesUsersFile();
|
|
83
|
+
/** Whether Authelia's configuration has it watch users.yml — replaced in tests (true). */
|
|
84
|
+
let watchesUsersFile = realWatches;
|
|
85
|
+
/** Whether users.yml changed since Authelia last (re)read it for certain — see {@link takeUsersFileDirty}. */
|
|
86
|
+
let usersFileDirty = false;
|
|
87
|
+
/** The reload touch's delay; null: none (tests unless they ask). */
|
|
88
|
+
let touchDelayMs = USERS_FILE_TOUCH_DELAY_MS;
|
|
55
89
|
/** Store a TOTP secret in Authelia's database — replaced in tests. */
|
|
56
90
|
let totpWriter = writeRealTotp;
|
|
57
91
|
/** Authelia's per-user storage — replaced in tests. */
|
|
58
92
|
let userStorage = realUserStorage;
|
|
93
|
+
const updateRealDomainAccess = (access) => updateAutheliaDomainAccess(access, execa);
|
|
94
|
+
/** Render the domains into Authelia's configuration — replaced in tests. */
|
|
95
|
+
let domainAccessWriter = updateRealDomainAccess;
|
|
96
|
+
const updateRealOidcProvider = (domains, clients, options) =>
|
|
97
|
+
updateAutheliaOidcProvider(domains, clients, execa, options);
|
|
98
|
+
/** Write Authelia's OIDC provider — replaced in tests. */
|
|
99
|
+
let oidcProviderWriter = updateRealOidcProvider;
|
|
59
100
|
|
|
60
101
|
/**
|
|
61
|
-
* Point the users file, the Authelia restart, the TOTP writer
|
|
62
|
-
* per-user storage elsewhere (tests); `null`
|
|
102
|
+
* Point the users file, the Authelia restart, the TOTP writer, the
|
|
103
|
+
* per-user storage and the domain rendering elsewhere (tests); `null`
|
|
104
|
+
* restores Authelia's. The reload touch is off in tests unless
|
|
105
|
+
* `touchDelayMs` asks for it. The owned groups the last rendering decided
|
|
106
|
+
* are forgotten either way.
|
|
63
107
|
*
|
|
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
|
|
108
|
+
* @param {{ usersFile?: string, restart?: () => Promise<unknown>, watchesUsersFile?: () => Promise<boolean>, writeTotp?: (username: string, secret: string) => Promise<void>, userStorage?: { purgeSecondFactors: (username: string) => Promise<void>, holdsOidcIdentifier: (username: string) => Promise<boolean> }, domainAccess?: (access: object) => Promise<{ changed: boolean }>, oidcProvider?: (domains: object[], clients: object[], options: object) => Promise<{ changed: boolean, clients: number }>, touchDelayMs?: number | null } | null} overrides
|
|
65
109
|
*/
|
|
66
110
|
export function setAutheliaForTests(overrides) {
|
|
67
111
|
usersFile = overrides?.usersFile ?? AUTHELIA_USERS;
|
|
68
112
|
restartAuthelia = overrides?.restart ?? restartRealAuthelia;
|
|
113
|
+
watchesUsersFile = overrides?.watchesUsersFile ?? (overrides ? async () => true : realWatches);
|
|
114
|
+
usersFileDirty = false;
|
|
115
|
+
touchDelayMs =
|
|
116
|
+
overrides?.touchDelayMs !== undefined
|
|
117
|
+
? overrides.touchDelayMs
|
|
118
|
+
: overrides
|
|
119
|
+
? null
|
|
120
|
+
: USERS_FILE_TOUCH_DELAY_MS;
|
|
121
|
+
ownedGroups = null;
|
|
122
|
+
if (touchTimer) clearTimeout(touchTimer);
|
|
123
|
+
touchTimer = null;
|
|
69
124
|
totpWriter = overrides?.writeTotp ?? writeRealTotp;
|
|
70
125
|
userStorage = overrides?.userStorage ?? realUserStorage;
|
|
126
|
+
domainAccessWriter = overrides?.domainAccess ?? updateRealDomainAccess;
|
|
127
|
+
oidcProviderWriter = overrides?.oidcProvider ?? updateRealOidcProvider;
|
|
71
128
|
}
|
|
72
129
|
|
|
73
130
|
/**
|
|
@@ -95,21 +152,255 @@ export function isAutheliaInstalled() {
|
|
|
95
152
|
return isAutheliaInstalledCore();
|
|
96
153
|
}
|
|
97
154
|
|
|
98
|
-
|
|
99
|
-
|
|
155
|
+
/**
|
|
156
|
+
* Write Authelia's first configuration (onboarding): its rules name the
|
|
157
|
+
* primary domain's groups by the key of its record, which onboarding has
|
|
158
|
+
* made just before.
|
|
159
|
+
*/
|
|
160
|
+
export async function writeAutheliaConfig(domain, secrets) {
|
|
161
|
+
const { domainGroupKey } = await import('./domains.js');
|
|
162
|
+
const groupKey = (await domainGroupKey(domain)) ?? undefined;
|
|
163
|
+
return writeAutheliaConfigCore(domain, secrets, groupKey ? { groupKey } : {});
|
|
100
164
|
}
|
|
101
165
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
// users.yml: one writer, the owned groups kept, a reload guaranteed
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
|
|
170
|
+
/** One write of users.yml at a time (a rendering's and a change's alike). */
|
|
171
|
+
const usersFileMutex = new PromiseChainMutex();
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* The owned groups the domains' rendering last decided — `desired` by
|
|
175
|
+
* username, the domain records they were decided for (every domain record
|
|
176
|
+
* and every retired one), the usernames users.yml held then (`known`), and
|
|
177
|
+
* whether a rendering failed since (`narrowOnly`) — or null before the
|
|
178
|
+
* first rendering (a write then keeps the owned groups it was given).
|
|
179
|
+
*
|
|
180
|
+
* @type {{ desired: Map<string, readonly string[]>, domains: readonly string[], known: Set<string>, narrowOnly: boolean } | null}
|
|
181
|
+
*/
|
|
182
|
+
let ownedGroups = null;
|
|
183
|
+
|
|
184
|
+
/** The pending reload touch. */
|
|
185
|
+
let touchTimer = null;
|
|
186
|
+
|
|
187
|
+
/** `usersData` with the owned groups the last rendering decided, when there was one. */
|
|
188
|
+
function withOwnedGroups(usersData) {
|
|
189
|
+
if (!ownedGroups) return usersData;
|
|
190
|
+
const merged = mergeOwnedGroups(usersData.users ?? {}, ownedGroups.desired, ownedGroups.domains, {
|
|
191
|
+
known: ownedGroups.known,
|
|
192
|
+
narrowOnly: ownedGroups.narrowOnly,
|
|
193
|
+
});
|
|
194
|
+
return merged.changed ? { ...usersData, users: merged.users } : usersData;
|
|
105
195
|
}
|
|
106
196
|
|
|
107
|
-
|
|
108
|
-
|
|
197
|
+
/**
|
|
198
|
+
* users.yml for a write (holding the file lock): a missing file is empty;
|
|
199
|
+
* an empty file — read while another writer truncated it — is read again a
|
|
200
|
+
* few times before it is believed.
|
|
201
|
+
*/
|
|
202
|
+
async function readCurrentLocked() {
|
|
203
|
+
for (let attempt = 0; ; attempt++) {
|
|
204
|
+
let content;
|
|
205
|
+
try {
|
|
206
|
+
content = await readFile(usersFile, 'utf-8');
|
|
207
|
+
} catch (err) {
|
|
208
|
+
if (err.code === 'ENOENT') return { users: {} };
|
|
209
|
+
throw err;
|
|
210
|
+
}
|
|
211
|
+
if (content.trim().length > 0 || attempt >= EMPTY_READ_RETRIES) {
|
|
212
|
+
return readAutheliaUsersRaw(usersFile);
|
|
213
|
+
}
|
|
214
|
+
await new Promise((resolve) => setTimeout(resolve, EMPTY_READ_RETRY_MS));
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** How often an empty users.yml is read again before a write believes it, and how far apart. */
|
|
219
|
+
const EMPTY_READ_RETRIES = 3;
|
|
220
|
+
const EMPTY_READ_RETRY_MS = 50;
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Write users.yml (holding the file lock) and schedule the reload touch
|
|
224
|
+
* ({@link touchUsersFileLocked}) for {@link USERS_FILE_TOUCH_DELAY_MS}
|
|
225
|
+
* after the last write. A write that changes the file marks it dirty
|
|
226
|
+
* ({@link takeUsersFileDirty}).
|
|
227
|
+
*/
|
|
228
|
+
async function writeUsersLocked(usersData, current) {
|
|
229
|
+
if (JSON.stringify(current) !== JSON.stringify(usersData)) usersFileDirty = true;
|
|
230
|
+
await writeAutheliaUsers(usersData, usersFile);
|
|
231
|
+
scheduleTouch();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function scheduleTouch() {
|
|
235
|
+
if (touchDelayMs === null) return;
|
|
236
|
+
if (touchTimer) clearTimeout(touchTimer);
|
|
237
|
+
touchTimer = setTimeout(() => {
|
|
238
|
+
touchTimer = null;
|
|
239
|
+
void usersFileMutex.run(touchUsersFileLocked).catch(() => {
|
|
240
|
+
// The file went or cannot be read (tests): the next write, the tick or
|
|
241
|
+
// the next start touches again.
|
|
242
|
+
});
|
|
243
|
+
}, touchDelayMs);
|
|
244
|
+
touchTimer.unref();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Read users.yml, apply the owned groups the last rendering decided, and
|
|
249
|
+
* write it — whether or not anything changed: a write Authelia's watcher
|
|
250
|
+
* skipped in its cooldown is reloaded now, and a change something else
|
|
251
|
+
* made meanwhile (a hand edit) gets Lamaste's groups back.
|
|
252
|
+
*/
|
|
253
|
+
async function touchUsersFileLocked() {
|
|
254
|
+
const current = await readCurrentLocked();
|
|
255
|
+
const next = withOwnedGroups(current);
|
|
256
|
+
if (next !== current) usersFileDirty = true;
|
|
257
|
+
await writeAutheliaUsers(next, usersFile);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Touch users.yml now ({@link touchUsersFileLocked}) — at start and on the
|
|
262
|
+
* identity tick, so a reload a restart of the panel cut short (its touch
|
|
263
|
+
* pending) or skipped for any reason is never missing for long.
|
|
264
|
+
*/
|
|
265
|
+
export function touchUsersFile() {
|
|
266
|
+
return usersFileMutex.run(touchUsersFileLocked);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Whether users.yml changed since this was last asked — and forget it.
|
|
271
|
+
* When Authelia is not known to watch the file (its configuration could not
|
|
272
|
+
* be brought in line), the domains' rendering restarts it after such a
|
|
273
|
+
* change, so it reads the file as 4.7 did.
|
|
274
|
+
*/
|
|
275
|
+
export function takeUsersFileDirty() {
|
|
276
|
+
const dirty = usersFileDirty;
|
|
277
|
+
usersFileDirty = false;
|
|
278
|
+
return dirty;
|
|
109
279
|
}
|
|
110
280
|
|
|
111
|
-
|
|
112
|
-
|
|
281
|
+
/** Whether Authelia's configuration has it watch users.yml (core `autheliaWatchesUsersFile`). */
|
|
282
|
+
export function autheliaWatchesUsers() {
|
|
283
|
+
return watchesUsersFile();
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Write users.yml — every write of the file the panel makes. The owned
|
|
288
|
+
* groups are those the last rendering decided (see {@link withOwnedGroups}).
|
|
289
|
+
* Refused when it would drop a user `deleting` does not name — a write
|
|
290
|
+
* built from a read that missed users (a partial file) never removes them;
|
|
291
|
+
* a user it deletes is forgotten by the owned groups as well, so whoever
|
|
292
|
+
* takes the username next inherits none of them.
|
|
293
|
+
*
|
|
294
|
+
* @param {import('@lamalibre/lamaste/server').UsersYamlFile} usersData
|
|
295
|
+
* @param {{ deleting?: readonly string[] }} [opts]
|
|
296
|
+
*/
|
|
297
|
+
export function writeUsers(usersData, { deleting = [] } = {}) {
|
|
298
|
+
return usersFileMutex.run(async () => {
|
|
299
|
+
const current = await readCurrentLocked();
|
|
300
|
+
const next = usersData.users ?? {};
|
|
301
|
+
const dropped = Object.keys(current.users ?? {}).filter((u) => !Object.hasOwn(next, u));
|
|
302
|
+
if (dropped.some((u) => !deleting.includes(u))) {
|
|
303
|
+
throw new Error(
|
|
304
|
+
'users.yml was not written: it would remove users this change does not delete (read the file again and retry)',
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
if (ownedGroups) {
|
|
308
|
+
for (const u of dropped) {
|
|
309
|
+
ownedGroups.desired.delete(u);
|
|
310
|
+
ownedGroups.known.delete(u);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
await writeUsersLocked(withOwnedGroups(usersData), current);
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Give every user exactly the owned groups of `desired` (core
|
|
319
|
+
* `renderOwnedGroups`) — the domains' rendering; `domains` are every
|
|
320
|
+
* domain record and every retired one. Compares first: an unchanged file
|
|
321
|
+
* is not written. From now on every write keeps these groups (and adds
|
|
322
|
+
* them only to the users the file holds now). Returns whether the file
|
|
323
|
+
* changed.
|
|
324
|
+
*
|
|
325
|
+
* @param {ReadonlyMap<string, readonly string[]>} desired
|
|
326
|
+
* @param {readonly string[]} domains
|
|
327
|
+
* @returns {Promise<{ changed: boolean }>}
|
|
328
|
+
*/
|
|
329
|
+
export function writeOwnedGroups(desired, domains) {
|
|
330
|
+
return usersFileMutex.run(async () => {
|
|
331
|
+
const current = await readCurrentLocked();
|
|
332
|
+
ownedGroups = {
|
|
333
|
+
desired: new Map(desired),
|
|
334
|
+
domains,
|
|
335
|
+
known: new Set(Object.keys(current.users ?? {})),
|
|
336
|
+
narrowOnly: false,
|
|
337
|
+
};
|
|
338
|
+
const merged = mergeOwnedGroups(current.users ?? {}, desired, domains);
|
|
339
|
+
if (!merged.changed) return { changed: false };
|
|
340
|
+
await writeUsersLocked({ ...current, users: merged.users }, current);
|
|
341
|
+
return { changed: true };
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* After a rendering failed to compute or write the owned groups: until the
|
|
347
|
+
* next one succeeds, writes only narrow (keep the owned groups a user holds
|
|
348
|
+
* that the last rendering decided too, add none) — what it decided may be
|
|
349
|
+
* older than the relay's state.
|
|
350
|
+
*/
|
|
351
|
+
export function narrowOwnedGroupsOnly() {
|
|
352
|
+
if (ownedGroups) ownedGroups.narrowOnly = true;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* The owned groups each user of users.yml holds now, and whether each is
|
|
357
|
+
* enabled — what the domains' rendering weighs a narrowing against.
|
|
358
|
+
*
|
|
359
|
+
* @param {readonly string[]} domains every domain record and every retired one
|
|
360
|
+
* @returns {Promise<Map<string, { groups: string[], active: boolean }>>}
|
|
361
|
+
*/
|
|
362
|
+
export async function heldOwnedGroups(domains) {
|
|
363
|
+
const raw = await readAutheliaUsersRaw(usersFile);
|
|
364
|
+
const out = new Map();
|
|
365
|
+
for (const [username, entry] of Object.entries(raw.users ?? {})) {
|
|
366
|
+
const groups = Array.isArray(entry?.groups)
|
|
367
|
+
? entry.groups.filter((g) => typeof g === 'string' && isLamasteOwnedGroup(g, domains))
|
|
368
|
+
: [];
|
|
369
|
+
out.set(username, { groups, active: entry?.disabled !== true });
|
|
370
|
+
}
|
|
371
|
+
return out;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Add the default administrator (group `admins`) to users.yml at
|
|
376
|
+
* onboarding — inside `changeUsers`.
|
|
377
|
+
*/
|
|
378
|
+
export async function createUser(username, password) {
|
|
379
|
+
const hash = await hashPassword(password);
|
|
380
|
+
if (!hash || !hash.startsWith('$2')) throw new Error('Bcrypt hashing produced invalid output');
|
|
381
|
+
const usersData = await readUsersForWrite();
|
|
382
|
+
usersData.users[username] = {
|
|
383
|
+
displayname: username,
|
|
384
|
+
password: hash,
|
|
385
|
+
email: `${username}@lamaste.local`,
|
|
386
|
+
groups: ['admins'],
|
|
387
|
+
};
|
|
388
|
+
await writeUsers(usersData);
|
|
389
|
+
return { username, created: true };
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/** users.yml for a read-modify-write: a missing file starts empty, an unreadable one is refused. */
|
|
393
|
+
async function readUsersForWrite() {
|
|
394
|
+
try {
|
|
395
|
+
return await readAutheliaUsersRaw(usersFile);
|
|
396
|
+
} catch (err) {
|
|
397
|
+
if (err.code === 'ENOENT') return { users: {} };
|
|
398
|
+
throw err;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
export function readUsers() {
|
|
403
|
+
return readAutheliaUsers(usersFile);
|
|
113
404
|
}
|
|
114
405
|
|
|
115
406
|
/** users.yml as written, password hashes included; throws when it cannot be read or parsed. */
|
|
@@ -125,38 +416,53 @@ export function startAuthelia() {
|
|
|
125
416
|
return startAutheliaCore(execa);
|
|
126
417
|
}
|
|
127
418
|
|
|
128
|
-
/**
|
|
129
|
-
|
|
130
|
-
|
|
419
|
+
/**
|
|
420
|
+
* Restart Authelia — every session ends. A change of users.yml needs none
|
|
421
|
+
* (Authelia watches it); only configuration rewrites restart it, in core.
|
|
422
|
+
*/
|
|
423
|
+
export async function reloadAuthelia() {
|
|
424
|
+
const restarted = await restartAuthelia();
|
|
425
|
+
usersFileDirty = false;
|
|
426
|
+
return restarted;
|
|
131
427
|
}
|
|
132
428
|
|
|
133
429
|
export function isAutheliaRunning() {
|
|
134
430
|
return isAutheliaRunningCore(execa);
|
|
135
431
|
}
|
|
136
432
|
|
|
137
|
-
|
|
138
|
-
|
|
433
|
+
/**
|
|
434
|
+
* Bring Authelia's session cookies and access control in line with the
|
|
435
|
+
* relay's domains and protected sites (core `updateDomainAccess`) —
|
|
436
|
+
* lib/domains.js `syncDomainAccess` alone calls it.
|
|
437
|
+
*
|
|
438
|
+
* @param {import('@lamalibre/lamaste/server').DomainAccess} access
|
|
439
|
+
* @returns {Promise<{ changed: boolean }>}
|
|
440
|
+
*/
|
|
441
|
+
export function updateDomainAccess(access) {
|
|
442
|
+
return domainAccessWriter(access);
|
|
139
443
|
}
|
|
140
444
|
|
|
141
445
|
/**
|
|
142
|
-
* Make Authelia's OIDC provider serve exactly these clients
|
|
143
|
-
*
|
|
446
|
+
* Make Authelia's OIDC provider serve exactly these clients, each of one of
|
|
447
|
+
* `domains` (the active domains, each with its group key).
|
|
448
|
+
* @param {import('@lamalibre/lamaste/server').AutheliaDomain[]} domains
|
|
144
449
|
* @param {import('@lamalibre/lamaste/server').AutheliaOidcClient[]} clients
|
|
145
450
|
* @param {import('@lamalibre/lamaste/server').OidcProviderOptions} [options]
|
|
146
451
|
*/
|
|
147
|
-
export function updateOidcProvider(
|
|
148
|
-
return
|
|
452
|
+
export function updateOidcProvider(domains, clients, options = {}) {
|
|
453
|
+
return oidcProviderWriter(domains, clients, options);
|
|
149
454
|
}
|
|
150
455
|
|
|
151
456
|
/**
|
|
152
|
-
* Add an invited user to users.yml — inside `changeUsers`, which
|
|
153
|
-
*
|
|
457
|
+
* Add an invited user to users.yml — inside `changeUsers`, which brings the
|
|
458
|
+
* directory and the domains' rendering in line with it.
|
|
154
459
|
*/
|
|
155
|
-
export function createUserFromInvitation(username, email, groups, hashedPassword) {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
460
|
+
export async function createUserFromInvitation(username, email, groups, hashedPassword) {
|
|
461
|
+
const usersData = await readUsersForWrite();
|
|
462
|
+
if (usersData.users[username]) throw new Error(`User '${username}' already exists`);
|
|
463
|
+
usersData.users[username] = { displayname: username, email, password: hashedPassword, groups };
|
|
464
|
+
await writeUsers(usersData);
|
|
465
|
+
return { username, created: true };
|
|
160
466
|
}
|
|
161
467
|
|
|
162
468
|
export function writeTotpToDatabase(username, base32Secret) {
|