@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
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Authelia groups Lamaste owns, as the relay's state says they should
|
|
3
|
+
* be (packages/core/docs/external-members-design.md; core owned-groups.ts) —
|
|
4
|
+
* what lib/domains.js `syncDomainAccess` writes into users.yml:
|
|
5
|
+
*
|
|
6
|
+
* - each domain's members and external members (`lamaste:m:<key>`,
|
|
7
|
+
* `lamaste:x:<key>`, the key a random one of the domain's record);
|
|
8
|
+
* - a protected static site's listed users (`lamaste:s:<site id>`);
|
|
9
|
+
* - the external members granted a Gatekeeper-gated `restricted` host — a
|
|
10
|
+
* restricted tunnel (a user surface's too) or a restricted web surface —
|
|
11
|
+
* by a grant in Gatekeeper, direct or through a Lamaste group (the host's
|
|
12
|
+
* group, named by its hostname);
|
|
13
|
+
* - the external members granted a user-facing OIDC client
|
|
14
|
+
* (`lamaste:oidc@<id>`, a Gatekeeper grant `resourceType: 'oidc-client'`).
|
|
15
|
+
*
|
|
16
|
+
* Grants are read from Gatekeeper only while some domain has external
|
|
17
|
+
* members (members reach those hosts and clients through their domain's
|
|
18
|
+
* group). When they cannot be read — Gatekeeper, the tunnels or the plugin
|
|
19
|
+
* manifests — external members hold no host or client group until they
|
|
20
|
+
* can (fail closed): the memberships, from state.db, are rendered all the
|
|
21
|
+
* same.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { OWNED_GROUP_PREFIX, renderOwnedGroups } from '@lamalibre/lamaste';
|
|
25
|
+
import { httpError } from './app-error.js';
|
|
26
|
+
import {
|
|
27
|
+
listGatekeeperGroups,
|
|
28
|
+
listGrantsOfType,
|
|
29
|
+
revokeGatekeeperGrant,
|
|
30
|
+
} from './gatekeeper-client.js';
|
|
31
|
+
import { readTunnels } from './state.js';
|
|
32
|
+
|
|
33
|
+
/** The Gatekeeper resource type of a grant on an OIDC client (`resourceId`: the client id). */
|
|
34
|
+
export const OIDC_CLIENT_RESOURCE = 'oidc-client';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Whether an OIDC client signs users in — a grant can name it — rather than
|
|
38
|
+
* a service (`client_credentials`, never mixed with a user grant).
|
|
39
|
+
*
|
|
40
|
+
* @param {{ grantTypes: readonly string[] }} client
|
|
41
|
+
*/
|
|
42
|
+
export function isUserFacingOidcClient(client) {
|
|
43
|
+
return !client.grantTypes.includes('client_credentials');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** An access mode as Gatekeeper reads it: restricted when absent or unknown. */
|
|
47
|
+
function accessModeOf(value) {
|
|
48
|
+
return value === 'public' || value === 'authenticated' ? value : 'restricted';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The Gatekeeper-gated restricted hosts: enabled restricted tunnels with a
|
|
53
|
+
* hostname (an internal plugin tunnel has none; an agent panel is mTLS,
|
|
54
|
+
* not Gatekeeper's; a tunnel without an access mode is restricted, as
|
|
55
|
+
* Gatekeeper reads it) and restricted web surfaces (plugin-owned sites),
|
|
56
|
+
* each with the resource its grants name. A disabled tunnel serves nobody —
|
|
57
|
+
* a web surface moved off a tunnel leaves it disabled on the same hostname,
|
|
58
|
+
* and its grants must not name anyone there.
|
|
59
|
+
*
|
|
60
|
+
* @param {ReadonlyArray<Record<string, unknown>>} tunnels
|
|
61
|
+
* @param {ReadonlyArray<Record<string, unknown>>} sites
|
|
62
|
+
* @returns {Array<{ fqdn: string, resourceType: 'tunnel' | 'site', resourceId: string }>}
|
|
63
|
+
*/
|
|
64
|
+
export function restrictedHosts(tunnels, sites) {
|
|
65
|
+
const hosts = [];
|
|
66
|
+
for (const t of tunnels) {
|
|
67
|
+
if (
|
|
68
|
+
typeof t?.fqdn === 'string' &&
|
|
69
|
+
typeof t.id === 'string' &&
|
|
70
|
+
t.enabled !== false &&
|
|
71
|
+
t.type !== 'panel' &&
|
|
72
|
+
t.exposure !== 'internal' &&
|
|
73
|
+
accessModeOf(t.accessMode) === 'restricted'
|
|
74
|
+
) {
|
|
75
|
+
hosts.push({ fqdn: t.fqdn.toLowerCase(), resourceType: 'tunnel', resourceId: t.id });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
for (const s of sites) {
|
|
79
|
+
if (s?.owner && accessModeOf(s.accessMode) === 'restricted' && typeof s.fqdn === 'string') {
|
|
80
|
+
hosts.push({ fqdn: s.fqdn.toLowerCase(), resourceType: 'site', resourceId: s.id });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return hosts;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The usernames each grant of `grants` reaches — a user grant its user, a
|
|
88
|
+
* group grant the members of the Lamaste group — keyed by resource id.
|
|
89
|
+
*
|
|
90
|
+
* @param {ReadonlyArray<{ principalType: 'user' | 'group', principalId: string, resourceId: string }>} grants
|
|
91
|
+
* @param {ReadonlyMap<string, { members: readonly string[] }>} groups by name
|
|
92
|
+
* @returns {Map<string, Set<string>>}
|
|
93
|
+
*/
|
|
94
|
+
export function grantedUsernames(grants, groups) {
|
|
95
|
+
const out = new Map();
|
|
96
|
+
for (const grant of grants) {
|
|
97
|
+
let set = out.get(grant.resourceId);
|
|
98
|
+
if (!set) out.set(grant.resourceId, (set = new Set()));
|
|
99
|
+
if (grant.principalType === 'user') set.add(grant.principalId);
|
|
100
|
+
else for (const member of groups.get(grant.principalId)?.members ?? []) set.add(member);
|
|
101
|
+
}
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Every grant on tunnels, sites and OIDC clients, with the Lamaste groups
|
|
107
|
+
* expanded — the usernames granted each resource, by type and id — or the
|
|
108
|
+
* reason Gatekeeper could not say.
|
|
109
|
+
*
|
|
110
|
+
* @returns {Promise<{ ok: true, granted: Record<'tunnel' | 'site' | 'oidc-client', Map<string, Set<string>>> } | { ok: false, error: string }>}
|
|
111
|
+
*/
|
|
112
|
+
export async function readGrantedResources() {
|
|
113
|
+
const [tunnel, site, client, groups] = await Promise.all([
|
|
114
|
+
listGrantsOfType('tunnel'),
|
|
115
|
+
listGrantsOfType('site'),
|
|
116
|
+
listGrantsOfType(OIDC_CLIENT_RESOURCE),
|
|
117
|
+
listGatekeeperGroups(),
|
|
118
|
+
]);
|
|
119
|
+
const failed = [tunnel, site, client, groups].find((r) => !r.ok);
|
|
120
|
+
if (failed) return { ok: false, error: failed.error };
|
|
121
|
+
const byName = new Map(groups.groups.map((g) => [g.name, g]));
|
|
122
|
+
return {
|
|
123
|
+
ok: true,
|
|
124
|
+
granted: {
|
|
125
|
+
tunnel: grantedUsernames(tunnel.grants, byName),
|
|
126
|
+
site: grantedUsernames(site.grants, byName),
|
|
127
|
+
[OIDC_CLIENT_RESOURCE]: grantedUsernames(client.grants, byName),
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* The user-facing OIDC clients Authelia serves, each with its domain — the
|
|
134
|
+
* clients a grant can name (a `client_credentials` client signs in no one).
|
|
135
|
+
*
|
|
136
|
+
* @returns {Promise<Array<{ id: string, domain: string }>>}
|
|
137
|
+
*/
|
|
138
|
+
export async function userFacingOidcClients() {
|
|
139
|
+
const { planOidcClients } = await import('./plugin-oidc.js');
|
|
140
|
+
const { served } = await planOidcClients();
|
|
141
|
+
return served.filter(isUserFacingOidcClient).map((c) => ({ id: c.id, domain: c.domain }));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* The owned groups every user should hold (core `renderOwnedGroups`):
|
|
146
|
+
* `complete` false — with a `warning` — when the grants could not be read,
|
|
147
|
+
* and the hosts and OIDC clients external members are granted were then
|
|
148
|
+
* left out (fail closed).
|
|
149
|
+
*
|
|
150
|
+
* @param {{
|
|
151
|
+
* domains: ReadonlyArray<{ name: string, groupKey: string, members: readonly string[], externals: readonly string[] }>,
|
|
152
|
+
* sites: ReadonlyArray<Record<string, unknown>>,
|
|
153
|
+
* }} input `domains` the active ones with their people, `sites` sites.json
|
|
154
|
+
* (or the sites being saved)
|
|
155
|
+
* @returns {Promise<{ desired: Map<string, string[]>, complete: boolean, warning?: string }>}
|
|
156
|
+
*/
|
|
157
|
+
export async function desiredOwnedGroups({ domains, sites }) {
|
|
158
|
+
const siteGroups = sites
|
|
159
|
+
.filter(
|
|
160
|
+
(s) =>
|
|
161
|
+
s?.autheliaProtected === true &&
|
|
162
|
+
typeof s.id === 'string' &&
|
|
163
|
+
typeof s.fqdn === 'string' &&
|
|
164
|
+
Array.isArray(s.allowedUsers) &&
|
|
165
|
+
s.allowedUsers.length > 0,
|
|
166
|
+
)
|
|
167
|
+
.map((s) => ({ id: s.id, fqdn: s.fqdn.toLowerCase(), users: s.allowedUsers }));
|
|
168
|
+
|
|
169
|
+
let hostGrants = [];
|
|
170
|
+
let clientGrants = [];
|
|
171
|
+
let warning;
|
|
172
|
+
if (domains.some((d) => d.externals.length > 0)) {
|
|
173
|
+
try {
|
|
174
|
+
const [hosts, clients, read] = await Promise.all([
|
|
175
|
+
readTunnels().then((tunnels) => restrictedHosts(tunnels, sites)),
|
|
176
|
+
userFacingOidcClients(),
|
|
177
|
+
readGrantedResources(),
|
|
178
|
+
]);
|
|
179
|
+
if (!read.ok) throw new Error(`Gatekeeper: ${read.error}`);
|
|
180
|
+
hostGrants = hosts.map((h) => ({
|
|
181
|
+
fqdn: h.fqdn,
|
|
182
|
+
users: [...(read.granted[h.resourceType].get(h.resourceId) ?? [])],
|
|
183
|
+
}));
|
|
184
|
+
clientGrants = clients.map((c) => ({
|
|
185
|
+
clientId: c.id,
|
|
186
|
+
domain: c.domain,
|
|
187
|
+
users: [...(read.granted[OIDC_CLIENT_RESOURCE].get(c.id) ?? [])],
|
|
188
|
+
}));
|
|
189
|
+
} catch (err) {
|
|
190
|
+
hostGrants = [];
|
|
191
|
+
clientGrants = [];
|
|
192
|
+
warning = `What external members are granted could not be read (${err.message}): they hold no granted host or OIDC client until it can — the panel renders again within a minute`;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
desired: renderOwnedGroups({ domains, sites: siteGroups, hostGrants, clientGrants }),
|
|
198
|
+
complete: warning === undefined,
|
|
199
|
+
...(warning ? { warning } : {}),
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Whether an owned group names a grant (a host group, an OIDC client group) rather than a membership or a site's list. */
|
|
204
|
+
function isGrantGroup(group) {
|
|
205
|
+
return !group.startsWith(OWNED_GROUP_PREFIX) || group.startsWith(`${OWNED_GROUP_PREFIX}oidc@`);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* The owned groups each user held as the last rendering from complete
|
|
210
|
+
* inputs decided (membership groups as the last rendering of any kind
|
|
211
|
+
* did) — what a narrowing is measured against. Null until the first
|
|
212
|
+
* rendering, which takes what users.yml holds then.
|
|
213
|
+
*
|
|
214
|
+
* @type {Map<string, readonly string[]> | null}
|
|
215
|
+
*/
|
|
216
|
+
let baseline = null;
|
|
217
|
+
|
|
218
|
+
/** Forget the baseline (tests). */
|
|
219
|
+
export function resetOwnedGroupsBaselineForTests() {
|
|
220
|
+
baseline = null;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Who lost owned groups since the baseline, as `desired` decides — each
|
|
225
|
+
* enabled user of `held` (users.yml now) with the groups they lose — and
|
|
226
|
+
* the baseline moved on. When `complete` is false the grants were not read
|
|
227
|
+
* and `desired` holds none: grant groups are then neither counted as lost
|
|
228
|
+
* nor forgotten — a grant revoked meanwhile is found lost by the next
|
|
229
|
+
* complete rendering. A user who is gone or disabled is no narrowing to
|
|
230
|
+
* follow (Authelia refuses them; offboarding revokes their tokens).
|
|
231
|
+
*
|
|
232
|
+
* @param {ReadonlyMap<string, readonly string[]>} desired
|
|
233
|
+
* @param {boolean} complete
|
|
234
|
+
* @param {ReadonlyMap<string, { groups: readonly string[], active: boolean }>} held
|
|
235
|
+
* @returns {Array<{ username: string, lost: string[], before: readonly string[], after: string[] }>}
|
|
236
|
+
*/
|
|
237
|
+
export function narrowingSince(desired, complete, held) {
|
|
238
|
+
if (baseline === null) {
|
|
239
|
+
baseline = new Map([...held].map(([u, h]) => [u, [...h.groups]]));
|
|
240
|
+
}
|
|
241
|
+
const previous = baseline;
|
|
242
|
+
const next = new Map();
|
|
243
|
+
const narrowed = [];
|
|
244
|
+
for (const username of new Set([...previous.keys(), ...desired.keys()])) {
|
|
245
|
+
const before = previous.get(username) ?? [];
|
|
246
|
+
const kept = complete ? [] : before.filter(isGrantGroup);
|
|
247
|
+
const after = [...new Set([...(desired.get(username) ?? []), ...kept])];
|
|
248
|
+
if (after.length > 0) next.set(username, after);
|
|
249
|
+
const lost = before.filter((g) => !after.includes(g));
|
|
250
|
+
if (lost.length > 0 && held.get(username)?.active) {
|
|
251
|
+
narrowed.push({ username, lost, before, after });
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
baseline = next;
|
|
255
|
+
return narrowed;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* The user-facing OIDC clients that sign users in with the device-code
|
|
260
|
+
* flow, each with its domain's group key (core `reachedOidcClients`) —
|
|
261
|
+
* those Authelia checks against the groups a session last refreshed.
|
|
262
|
+
*
|
|
263
|
+
* @param {ReadonlyArray<{ name: string, groupKey: string }>} domains the active domains
|
|
264
|
+
* @returns {Promise<Array<{ id: string, groupKey: string }>>}
|
|
265
|
+
*/
|
|
266
|
+
export async function deviceCodeClients(domains) {
|
|
267
|
+
const keys = new Map(domains.map((d) => [d.name, d.groupKey]));
|
|
268
|
+
const { planOidcClients } = await import('./plugin-oidc.js');
|
|
269
|
+
const { served } = await planOidcClients();
|
|
270
|
+
return served
|
|
271
|
+
.filter((c) => isUserFacingOidcClient(c) && c.grantTypes.includes(DEVICE_CODE_GRANT))
|
|
272
|
+
.filter((c) => keys.has(c.domain))
|
|
273
|
+
.map((c) => ({ id: c.id, groupKey: keys.get(c.domain) }));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** The OAuth grant type of the device-code flow. */
|
|
277
|
+
export const DEVICE_CODE_GRANT = 'urn:ietf:params:oauth:grant-type:device_code';
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Revoke every Gatekeeper grant on the OIDC clients `clientIds` — a plugin
|
|
281
|
+
* uninstalled takes the grants on its clients with it.
|
|
282
|
+
*
|
|
283
|
+
* @param {readonly string[]} clientIds
|
|
284
|
+
* @returns {Promise<{ ok: true, revoked: number } | { ok: false, error: string }>}
|
|
285
|
+
*/
|
|
286
|
+
export async function revokeOidcClientGrants(clientIds) {
|
|
287
|
+
if (clientIds.length === 0) return { ok: true, revoked: 0 };
|
|
288
|
+
const list = await listGrantsOfType(OIDC_CLIENT_RESOURCE);
|
|
289
|
+
if (!list.ok) return { ok: false, error: list.error };
|
|
290
|
+
let revoked = 0;
|
|
291
|
+
for (const grant of list.grants.filter((g) => clientIds.includes(g.resourceId))) {
|
|
292
|
+
const result = await revokeGatekeeperGrant(grant.grantId);
|
|
293
|
+
if (!result.ok) return { ok: false, error: result.error };
|
|
294
|
+
revoked += 1;
|
|
295
|
+
}
|
|
296
|
+
return { ok: true, revoked };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* A grant the Gatekeeper proxy is about to create, checked when it names an
|
|
301
|
+
* OIDC client: the client must be one an installed plugin declares (or the
|
|
302
|
+
* panel's own) and sign users in — a `client_credentials` client has no
|
|
303
|
+
* user to grant (400 `unknown_oidc_client` / `service_oidc_client`). Any
|
|
304
|
+
* other body goes on as it came.
|
|
305
|
+
*
|
|
306
|
+
* @param {unknown} body
|
|
307
|
+
* @returns {Promise<unknown>}
|
|
308
|
+
*/
|
|
309
|
+
export async function checkOidcClientGrant(body) {
|
|
310
|
+
if (typeof body !== 'object' || body === null || body.resourceType !== OIDC_CLIENT_RESOURCE) {
|
|
311
|
+
return body;
|
|
312
|
+
}
|
|
313
|
+
const { collectPluginOidcClients } = await import('./plugin-oidc.js');
|
|
314
|
+
const client = (await collectPluginOidcClients()).find((c) => c.id === body.resourceId);
|
|
315
|
+
if (!client) {
|
|
316
|
+
throw httpError('No installed plugin declares this OIDC client', 400, {
|
|
317
|
+
code: 'unknown_oidc_client',
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
if (!isUserFacingOidcClient(client)) {
|
|
321
|
+
throw httpError('This OIDC client signs in no user: there is nothing to grant', 400, {
|
|
322
|
+
code: 'service_oidc_client',
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
return body;
|
|
326
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The domain a plugin belongs to (the multi-domain relay): its OIDC
|
|
3
|
+
* clients' `{domain}` and sign-in, the tunnels that may be bound as its user
|
|
4
|
+
* surface, its web surface's hostname and its audience — the members of the
|
|
5
|
+
* domain. `plugins.domain` (state.db 0016); NULL is the primary.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { withTunnelLock } from '@lamalibre/lamaste/server';
|
|
9
|
+
import { tunnelUserSurfaceRoutes } from '@lamalibre/lamaste';
|
|
10
|
+
import { httpError } from './app-error.js';
|
|
11
|
+
import { recordAuditBestEffort } from './audit.js';
|
|
12
|
+
import { pluginDomainName, resolveActiveDomain } from './domains.js';
|
|
13
|
+
import { findPlugin } from './plugins.js';
|
|
14
|
+
import { getStateDb } from './state-db.js';
|
|
15
|
+
import { readSites, readTunnels, withSitesLock } from './state.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Move plugin `name` to the active domain `domain`. Refused (409
|
|
19
|
+
* `surface_bound`) while a tunnel is bound as its user surface or it owns a
|
|
20
|
+
* web surface — both are hostnames of its present domain: unbind or delete
|
|
21
|
+
* them first. Under the tunnel and sites locks, which every binding and web
|
|
22
|
+
* surface change holds too. Then its OIDC clients are written again and the
|
|
23
|
+
* audiences recomputed.
|
|
24
|
+
*
|
|
25
|
+
* @param {string} name
|
|
26
|
+
* @param {string} domain
|
|
27
|
+
* @param {{ actor: string, logger: import('pino').Logger }} opts
|
|
28
|
+
* @returns {Promise<{ name: string, domain: string, changed: boolean }>}
|
|
29
|
+
*/
|
|
30
|
+
export async function setPluginDomain(name, domain, { actor, logger }) {
|
|
31
|
+
const result = await withTunnelLock(() =>
|
|
32
|
+
withSitesLock(async () => {
|
|
33
|
+
const plugin = await findPlugin(name);
|
|
34
|
+
if (!plugin) throw httpError(`Plugin "${name}" is not installed`, 404, { code: 'not_found' });
|
|
35
|
+
const target = await resolveActiveDomain(domain);
|
|
36
|
+
const current = await pluginDomainName(plugin);
|
|
37
|
+
if (current === target) return { name, domain: target, changed: false };
|
|
38
|
+
if (plugin.route) {
|
|
39
|
+
const [tunnels, sites] = await Promise.all([readTunnels(), readSites()]);
|
|
40
|
+
const bound = tunnels.find((t) => tunnelUserSurfaceRoutes(t).includes(plugin.route));
|
|
41
|
+
const owned = sites.find((s) => s.owner?.plugin === plugin.route);
|
|
42
|
+
if (bound || owned) {
|
|
43
|
+
throw httpError(
|
|
44
|
+
`Plugin "${name}" is served on ${(bound ?? owned).fqdn ?? 'a host'} of ${current} — unbind its user surface and delete its web surface first`,
|
|
45
|
+
409,
|
|
46
|
+
{ code: 'surface_bound' },
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const db = await getStateDb();
|
|
51
|
+
db.prepare('UPDATE plugins SET domain = ? WHERE name = ?').run(target, name);
|
|
52
|
+
await recordAuditBestEffort(
|
|
53
|
+
{
|
|
54
|
+
actor,
|
|
55
|
+
action: 'plugin.domain.changed',
|
|
56
|
+
plugin: name,
|
|
57
|
+
details: { from: current, to: target },
|
|
58
|
+
},
|
|
59
|
+
logger,
|
|
60
|
+
);
|
|
61
|
+
return { name, domain: target, changed: true };
|
|
62
|
+
}),
|
|
63
|
+
);
|
|
64
|
+
if (result.changed) {
|
|
65
|
+
const { syncOidcClients } = await import('./plugin-oidc.js');
|
|
66
|
+
try {
|
|
67
|
+
await syncOidcClients(logger);
|
|
68
|
+
} catch (err) {
|
|
69
|
+
logger.warn(
|
|
70
|
+
{ plugin: name, err: err.message },
|
|
71
|
+
'OIDC clients not rewritten for a new domain',
|
|
72
|
+
);
|
|
73
|
+
// The identity tick syncs them again within a minute; until then the
|
|
74
|
+
// old domain's members still sign in to its clients — say so.
|
|
75
|
+
result.warnings = [
|
|
76
|
+
{
|
|
77
|
+
step: 'oidc',
|
|
78
|
+
message: `The plugin's OIDC clients still admit the previous domain's members (${err.message}); the panel retries within a minute`,
|
|
79
|
+
},
|
|
80
|
+
];
|
|
81
|
+
}
|
|
82
|
+
const { recomputeAudiences } = await import('./audience.js');
|
|
83
|
+
await recomputeAudiences({ logger });
|
|
84
|
+
}
|
|
85
|
+
return result;
|
|
86
|
+
}
|