@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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fp from 'fastify-plugin';
|
|
2
2
|
import { getConfig } from '../lib/config.js';
|
|
3
3
|
import { findDirectoryUser } from '../lib/directory.js';
|
|
4
|
+
import { isPrimaryMember } from '../lib/domains.js';
|
|
4
5
|
import {
|
|
5
6
|
REFRESH_THRESHOLD,
|
|
6
7
|
validateUserSession,
|
|
@@ -38,11 +39,17 @@ async function userAccessSessionPlugin(fastify, _opts) {
|
|
|
38
39
|
return reply.code(401).send({ error: 'user_session_required' });
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
// The session's user must still hold the username, and be
|
|
42
|
-
// deleted user's username given to
|
|
43
|
-
// a disabled user
|
|
42
|
+
// The session's user must still hold the username, be enabled and be a
|
|
43
|
+
// member of the primary domain: a deleted user's username given to
|
|
44
|
+
// someone else never inherits it, and a disabled user, one whose
|
|
45
|
+
// membership ended or one made an external member is out at once.
|
|
44
46
|
const holder = await findDirectoryUser(result.payload.username);
|
|
45
|
-
if (
|
|
47
|
+
if (
|
|
48
|
+
!holder ||
|
|
49
|
+
holder.userId !== result.payload.userId ||
|
|
50
|
+
holder.status !== 'active' ||
|
|
51
|
+
!(await isPrimaryMember(holder.userId))
|
|
52
|
+
) {
|
|
46
53
|
return reply.code(401).send({ error: 'user_session_required' });
|
|
47
54
|
}
|
|
48
55
|
|
|
@@ -8,9 +8,9 @@ import {
|
|
|
8
8
|
generateTotpSecret,
|
|
9
9
|
writeTotpToDatabase,
|
|
10
10
|
} from '../lib/authelia.js';
|
|
11
|
-
import { getConfig } from '../lib/config.js';
|
|
12
11
|
import { httpError } from '../lib/app-error.js';
|
|
13
12
|
import { changeUsers, usersFileError } from '../lib/directory.js';
|
|
13
|
+
import { invitationDomainsNow, invitationSignInDomain } from '../lib/domains.js';
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Find an invitation by token using a constant-time comparison.
|
|
@@ -119,8 +119,11 @@ export default async function inviteRoutes(fastify, _opts) {
|
|
|
119
119
|
return reply.code(500).send({ error: 'Failed to process password' });
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
|
|
123
|
-
|
|
122
|
+
const grantedDomains = await invitationDomainsNow(invitation);
|
|
123
|
+
|
|
124
|
+
// Through the users lock: the directory gives the user their id, and
|
|
125
|
+
// the domains are rendered (their memberships, with the kinds the
|
|
126
|
+
// invitation named), before they can sign in.
|
|
124
127
|
try {
|
|
125
128
|
await changeUsers(
|
|
126
129
|
async () => {
|
|
@@ -149,6 +152,12 @@ export default async function inviteRoutes(fastify, _opts) {
|
|
|
149
152
|
// A removed user's grants, groups and second factors under the
|
|
150
153
|
// username go first; a username with an OIDC identity is refused.
|
|
151
154
|
creates: [invitation.username],
|
|
155
|
+
// The domains the invitation names that still are the ones it
|
|
156
|
+
// named (an invitation from before the multi-domain relay names
|
|
157
|
+
// none: the relay's one domain).
|
|
158
|
+
...(Array.isArray(invitation.domains)
|
|
159
|
+
? { domains: { [invitation.username]: grantedDomains } }
|
|
160
|
+
: {}),
|
|
152
161
|
expect: (users) => users[invitation.username]?.password === hashedPassword,
|
|
153
162
|
},
|
|
154
163
|
);
|
|
@@ -191,8 +200,9 @@ export default async function inviteRoutes(fastify, _opts) {
|
|
|
191
200
|
request.log.warn(err, 'Failed to update invitation status (user was created)');
|
|
192
201
|
}
|
|
193
202
|
|
|
194
|
-
|
|
195
|
-
const
|
|
203
|
+
// The sign-in of the first domain the user is a member of.
|
|
204
|
+
const loginDomain = await invitationSignInDomain(invitation);
|
|
205
|
+
const loginUrl = loginDomain ? `https://auth.${loginDomain}/` : null;
|
|
196
206
|
|
|
197
207
|
return {
|
|
198
208
|
ok: true,
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The relay's base domains and their members (the multi-domain relay,
|
|
3
|
+
* lib/domains.js). Admin only (mTLS).
|
|
4
|
+
*
|
|
5
|
+
* GET /api/domains domains, member counts (members, external), what uses each
|
|
6
|
+
* POST /api/domains add one (`pending`)
|
|
7
|
+
* POST /api/domains/:name/verify-dns does auth.<domain> resolve here?
|
|
8
|
+
* POST /api/domains/:name/activate issue auth.<domain>, serve its sign-in
|
|
9
|
+
* DELETE /api/domains/:name remove an unused, non-primary domain
|
|
10
|
+
* GET /api/domains/:name/members its members, each with the membership's kind
|
|
11
|
+
* PUT /api/domains/:name/members/:user make a user (id or username) a member —
|
|
12
|
+
* `{ kind: 'member' | 'external' }`, member
|
|
13
|
+
* unless named; another kind changes it
|
|
14
|
+
* DELETE /api/domains/:name/members/:user end a membership
|
|
15
|
+
* PUT /api/plugins/:name/domain move a plugin to another domain
|
|
16
|
+
*
|
|
17
|
+
* Routes validate and answer; the work is in lib/domains.js and
|
|
18
|
+
* lib/plugin-domain.js. Refusals answer `{ error, code }`.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { z } from 'zod';
|
|
22
|
+
import { DomainNameSchema, MEMBERSHIP_KINDS } from '@lamalibre/lamaste';
|
|
23
|
+
import { actorFromRequest } from '../../lib/actor.js';
|
|
24
|
+
import { sendFailure } from '../../lib/app-error.js';
|
|
25
|
+
import {
|
|
26
|
+
activateDomain,
|
|
27
|
+
addDomain,
|
|
28
|
+
addDomainMember,
|
|
29
|
+
domainsOverview,
|
|
30
|
+
getDomain,
|
|
31
|
+
listDomainMembers,
|
|
32
|
+
newUsersNeedDomains,
|
|
33
|
+
removeDomain,
|
|
34
|
+
removeDomainMember,
|
|
35
|
+
verifyDomainDns,
|
|
36
|
+
} from '../../lib/domains.js';
|
|
37
|
+
import { setPluginDomain } from '../../lib/plugin-domain.js';
|
|
38
|
+
|
|
39
|
+
const NameParamSchema = z.object({ name: DomainNameSchema });
|
|
40
|
+
// `user`: a directory id or a username (lib/domains.js decides which).
|
|
41
|
+
const MemberParamsSchema = z.object({ name: DomainNameSchema, user: z.string().min(1).max(64) });
|
|
42
|
+
const AddDomainSchema = z.object({ name: DomainNameSchema }).strict();
|
|
43
|
+
const MemberBodySchema = z.object({ kind: z.enum(MEMBERSHIP_KINDS).optional() }).strict();
|
|
44
|
+
const PluginParamSchema = z.object({ name: z.string().min(1).max(200) });
|
|
45
|
+
const PluginDomainSchema = z.object({ domain: DomainNameSchema }).strict();
|
|
46
|
+
|
|
47
|
+
export default async function domainRoutes(fastify) {
|
|
48
|
+
const admin = { preHandler: fastify.requireRole(['admin']) };
|
|
49
|
+
|
|
50
|
+
fastify.get('/domains', admin, async () => {
|
|
51
|
+
const domains = await domainsOverview();
|
|
52
|
+
// Whether users and invitations must name their domains (`domains_required`).
|
|
53
|
+
return { domains, newUsersNeedDomains: newUsersNeedDomains(domains) };
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
fastify.post('/domains', admin, async (request, reply) => {
|
|
57
|
+
const { name } = AddDomainSchema.parse(request.body);
|
|
58
|
+
try {
|
|
59
|
+
const domain = await addDomain(name, {
|
|
60
|
+
actor: actorFromRequest(request),
|
|
61
|
+
logger: request.log,
|
|
62
|
+
});
|
|
63
|
+
return reply.code(201).send({ domain });
|
|
64
|
+
} catch (err) {
|
|
65
|
+
return sendFailure(request, reply, err, 'The domain change failed');
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
fastify.post(
|
|
70
|
+
'/domains/:name/verify-dns',
|
|
71
|
+
{ ...admin, config: { rateLimit: { max: 30, timeWindow: '1 minute' } } },
|
|
72
|
+
async (request, reply) => {
|
|
73
|
+
const { name } = NameParamSchema.parse(request.params);
|
|
74
|
+
try {
|
|
75
|
+
return await verifyDomainDns(name);
|
|
76
|
+
} catch (err) {
|
|
77
|
+
return sendFailure(request, reply, err, 'The domain change failed');
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
fastify.post(
|
|
83
|
+
'/domains/:name/activate',
|
|
84
|
+
{ ...admin, config: { rateLimit: { max: 10, timeWindow: '1 minute' } } },
|
|
85
|
+
async (request, reply) => {
|
|
86
|
+
const { name } = NameParamSchema.parse(request.params);
|
|
87
|
+
try {
|
|
88
|
+
const domain = await activateDomain(name, {
|
|
89
|
+
actor: actorFromRequest(request),
|
|
90
|
+
logger: request.log,
|
|
91
|
+
});
|
|
92
|
+
return { domain };
|
|
93
|
+
} catch (err) {
|
|
94
|
+
return sendFailure(request, reply, err, 'The domain change failed');
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
fastify.delete('/domains/:name', admin, async (request, reply) => {
|
|
100
|
+
const { name } = NameParamSchema.parse(request.params);
|
|
101
|
+
try {
|
|
102
|
+
return await removeDomain(name, { actor: actorFromRequest(request), logger: request.log });
|
|
103
|
+
} catch (err) {
|
|
104
|
+
return sendFailure(request, reply, err, 'The domain change failed');
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
fastify.get('/domains/:name/members', admin, async (request, reply) => {
|
|
109
|
+
const { name } = NameParamSchema.parse(request.params);
|
|
110
|
+
if (!(await getDomain(name))) {
|
|
111
|
+
return reply.code(404).send({ error: 'Domain not found', code: 'domain_not_found' });
|
|
112
|
+
}
|
|
113
|
+
return { members: await listDomainMembers(name) };
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
fastify.put('/domains/:name/members/:user', admin, async (request, reply) => {
|
|
117
|
+
const { name, user } = MemberParamsSchema.parse(request.params);
|
|
118
|
+
const { kind } = MemberBodySchema.parse(request.body ?? {});
|
|
119
|
+
try {
|
|
120
|
+
return await addDomainMember(name, user, {
|
|
121
|
+
...(kind ? { kind } : {}),
|
|
122
|
+
actor: actorFromRequest(request),
|
|
123
|
+
logger: request.log,
|
|
124
|
+
});
|
|
125
|
+
} catch (err) {
|
|
126
|
+
return sendFailure(request, reply, err, 'The domain change failed');
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
fastify.delete('/domains/:name/members/:user', admin, async (request, reply) => {
|
|
131
|
+
const { name, user } = MemberParamsSchema.parse(request.params);
|
|
132
|
+
try {
|
|
133
|
+
return await removeDomainMember(name, user, {
|
|
134
|
+
actor: actorFromRequest(request),
|
|
135
|
+
logger: request.log,
|
|
136
|
+
});
|
|
137
|
+
} catch (err) {
|
|
138
|
+
return sendFailure(request, reply, err, 'The domain change failed');
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
fastify.put('/plugins/:name/domain', admin, async (request, reply) => {
|
|
143
|
+
const { name } = PluginParamSchema.parse(request.params);
|
|
144
|
+
const { domain } = PluginDomainSchema.parse(request.body);
|
|
145
|
+
try {
|
|
146
|
+
return await setPluginDomain(name, domain, {
|
|
147
|
+
actor: actorFromRequest(request),
|
|
148
|
+
logger: request.log,
|
|
149
|
+
});
|
|
150
|
+
} catch (err) {
|
|
151
|
+
return sendFailure(request, reply, err, 'The domain change failed');
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
}
|
|
@@ -21,13 +21,24 @@
|
|
|
21
21
|
* cannot be) — a reused username inherits nothing. The access log is purged
|
|
22
22
|
* for one username by POST, its name in the body (never in a URL, which
|
|
23
23
|
* logs keep).
|
|
24
|
+
*
|
|
25
|
+
* Grants and groups also decide what external members of a domain reach
|
|
26
|
+
* (packages/core/docs/external-members-design.md): after every change that
|
|
27
|
+
* went through, the domains are rendered (users.yml's owned groups — the
|
|
28
|
+
* restricted hosts and OIDC clients that name each external member), which
|
|
29
|
+
* follows whoever it narrowed (registry tokens, a device-code session). A
|
|
30
|
+
* rendering that fails is answered with `warnings` and tried again within
|
|
31
|
+
* seconds. A grant on an OIDC client (`resourceType: 'oidc-client'`) must
|
|
32
|
+
* name a user-facing client an installed plugin declares.
|
|
24
33
|
*/
|
|
25
34
|
|
|
26
35
|
import { recomputeAudiences } from '../../lib/audience.js';
|
|
27
36
|
import { withUsernamesForGrants } from '../../lib/directory.js';
|
|
37
|
+
import { scheduleRenderRetry, syncDomainAccess } from '../../lib/domains.js';
|
|
28
38
|
import { checkProxiedGrant } from '../../lib/entitlements.js';
|
|
29
39
|
import { actorFromRequest } from '../../lib/actor.js';
|
|
30
40
|
import { gatekeeperRequest } from '../../lib/gatekeeper-client.js';
|
|
41
|
+
import { checkOidcClientGrant } from '../../lib/owned-groups.js';
|
|
31
42
|
|
|
32
43
|
/** Gatekeeper's `path`, with the request's query string when it has one. */
|
|
33
44
|
function withQuery(path, request) {
|
|
@@ -55,18 +66,43 @@ export default async function gatekeeperProxyRoutes(fastify, _opts) {
|
|
|
55
66
|
};
|
|
56
67
|
};
|
|
57
68
|
|
|
58
|
-
/**
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
69
|
+
/**
|
|
70
|
+
* When Gatekeeper took a grant or group change: the plugin audiences and
|
|
71
|
+
* the domains' rendering follow it — which follows whoever it narrowed —
|
|
72
|
+
* all before the answer goes back. When the rendering fails, the answer
|
|
73
|
+
* carries `warnings` (Gatekeeper took the change, and enforces it on its
|
|
74
|
+
* own hosts; an OIDC client it no longer grants waits for a rendering)
|
|
75
|
+
* and one is tried again within seconds.
|
|
76
|
+
*/
|
|
77
|
+
const followChange = async (request, answer) => {
|
|
78
|
+
if (answer.statusCode >= 400) return answer;
|
|
79
|
+
await recomputeAudiences({ logger: request.log });
|
|
80
|
+
let warnings;
|
|
81
|
+
try {
|
|
82
|
+
warnings = (await syncDomainAccess({ logger: request.log })).warnings ?? [];
|
|
83
|
+
} catch (err) {
|
|
84
|
+
request.log.warn({ err: err.message }, 'Rendering domain access after a grant change failed');
|
|
85
|
+
scheduleRenderRetry(request.log);
|
|
86
|
+
warnings = [
|
|
87
|
+
...(err.warnings ?? []),
|
|
88
|
+
{
|
|
89
|
+
step: 'render',
|
|
90
|
+
message: `Gatekeeper took the change, but who may reach what was not fully rendered (${err.message}); the panel tries again within seconds`,
|
|
91
|
+
},
|
|
92
|
+
];
|
|
93
|
+
}
|
|
94
|
+
if (warnings.length === 0 || typeof answer.data !== 'object' || answer.data === null) {
|
|
95
|
+
return answer;
|
|
96
|
+
}
|
|
97
|
+
return { ...answer, data: { ...answer.data, warnings } };
|
|
62
98
|
};
|
|
63
99
|
|
|
64
100
|
/**
|
|
65
101
|
* A grant or group change: forwarded, and when Gatekeeper took it, the
|
|
66
|
-
* plugin audiences follow it before the answer goes back.
|
|
102
|
+
* plugin audiences and the domains follow it before the answer goes back.
|
|
67
103
|
*/
|
|
68
104
|
const proxyChange = async (request, method, gatekeeperPath, body) =>
|
|
69
|
-
|
|
105
|
+
followChange(request, await proxy(method, gatekeeperPath, body));
|
|
70
106
|
|
|
71
107
|
/**
|
|
72
108
|
* Give something to `usernames` in Gatekeeper: forwarded holding the
|
|
@@ -85,7 +121,7 @@ export default async function gatekeeperProxyRoutes(fastify, _opts) {
|
|
|
85
121
|
if (!Number.isInteger(err.statusCode)) throw err;
|
|
86
122
|
return { statusCode: err.statusCode, data: { error: err.message, code: err.code } };
|
|
87
123
|
}
|
|
88
|
-
return
|
|
124
|
+
return followChange(request, answer);
|
|
89
125
|
};
|
|
90
126
|
|
|
91
127
|
// --- Groups ---
|
|
@@ -164,7 +200,7 @@ export default async function gatekeeperProxyRoutes(fastify, _opts) {
|
|
|
164
200
|
fastify.post('/gatekeeper/grants', async (request, reply) => {
|
|
165
201
|
let body;
|
|
166
202
|
try {
|
|
167
|
-
body = await checkProxiedGrant(request.body);
|
|
203
|
+
body = await checkOidcClientGrant(await checkProxiedGrant(request.body));
|
|
168
204
|
} catch (err) {
|
|
169
205
|
if (!Number.isInteger(err.statusCode)) throw err;
|
|
170
206
|
return reply.code(err.statusCode).send({
|
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { readUsers } from '../../lib/authelia.js';
|
|
3
|
+
import { adminGroups, domainNames } from '../../lib/domains.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Authelia's users with their groups as anything outside Authelia sees
|
|
7
|
+
* them: the groups Lamaste owns (domain memberships, host groups, OIDC
|
|
8
|
+
* client grants) left out.
|
|
9
|
+
*/
|
|
10
|
+
async function shownUsers() {
|
|
11
|
+
const [users, domains] = await Promise.all([readUsers(), domainNames()]);
|
|
12
|
+
return users.map((u) => ({ ...u, groups: adminGroups(u.groups, domains) }));
|
|
13
|
+
}
|
|
3
14
|
|
|
4
15
|
const UsernameParamSchema = z.object({
|
|
5
16
|
username: z
|
|
@@ -37,10 +48,13 @@ export default async function identityRoutes(fastify, _opts) {
|
|
|
37
48
|
}
|
|
38
49
|
|
|
39
50
|
const groupsRaw = headerValue(request.headers['remote-groups']) || '';
|
|
40
|
-
const groups =
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
51
|
+
const groups = adminGroups(
|
|
52
|
+
groupsRaw
|
|
53
|
+
.split(',')
|
|
54
|
+
.map((g) => g.trim())
|
|
55
|
+
.filter(Boolean),
|
|
56
|
+
await domainNames(),
|
|
57
|
+
);
|
|
44
58
|
const name = headerValue(request.headers['remote-name']) || '';
|
|
45
59
|
const email = headerValue(request.headers['remote-email']) || '';
|
|
46
60
|
|
|
@@ -54,7 +68,7 @@ export default async function identityRoutes(fastify, _opts) {
|
|
|
54
68
|
{ preHandler: fastify.requireRole(['admin', 'agent'], { capability: 'identity:query' }) },
|
|
55
69
|
async (request, reply) => {
|
|
56
70
|
try {
|
|
57
|
-
const users = await
|
|
71
|
+
const users = await shownUsers();
|
|
58
72
|
return { users };
|
|
59
73
|
} catch (err) {
|
|
60
74
|
request.log.error({ err }, 'Failed to read Authelia users');
|
|
@@ -70,7 +84,7 @@ export default async function identityRoutes(fastify, _opts) {
|
|
|
70
84
|
async (request, reply) => {
|
|
71
85
|
const { username } = UsernameParamSchema.parse(request.params);
|
|
72
86
|
try {
|
|
73
|
-
const users = await
|
|
87
|
+
const users = await shownUsers();
|
|
74
88
|
const user = users.find((u) => u.username === username);
|
|
75
89
|
if (!user) {
|
|
76
90
|
return reply.code(404).send({ error: 'User not found' });
|
|
@@ -89,7 +103,7 @@ export default async function identityRoutes(fastify, _opts) {
|
|
|
89
103
|
{ preHandler: fastify.requireRole(['admin', 'agent'], { capability: 'identity:query' }) },
|
|
90
104
|
async (request, reply) => {
|
|
91
105
|
try {
|
|
92
|
-
const users = await
|
|
106
|
+
const users = await shownUsers();
|
|
93
107
|
/** @type {Set<string>} */
|
|
94
108
|
const groupSet = new Set();
|
|
95
109
|
for (const u of users) {
|
|
@@ -3,7 +3,15 @@ import { z } from 'zod';
|
|
|
3
3
|
import { USERNAME_MAX_LENGTH, USERNAME_MIN_LENGTH, USERNAME_REGEX } from '@lamalibre/lamaste';
|
|
4
4
|
import { readInvitations, withInvitationsLock, writeInvitations } from '../../lib/state.js';
|
|
5
5
|
import { readUsersRaw } from '../../lib/authelia.js';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
MembershipInputSchema,
|
|
8
|
+
assertAssignableGroups,
|
|
9
|
+
domainsForNewUser,
|
|
10
|
+
externalMemberGroupsError,
|
|
11
|
+
invitationMemberships,
|
|
12
|
+
invitationSignInDomain,
|
|
13
|
+
} from '../../lib/domains.js';
|
|
14
|
+
import { sendFailure } from '../../lib/app-error.js';
|
|
7
15
|
|
|
8
16
|
const IdParamSchema = z.object({ id: z.string().uuid() });
|
|
9
17
|
|
|
@@ -19,8 +27,15 @@ const CreateInvitationSchema = z.object({
|
|
|
19
27
|
'Username must start with a lowercase letter or digit and may contain underscores or hyphens',
|
|
20
28
|
),
|
|
21
29
|
email: z.string().email(),
|
|
22
|
-
groups
|
|
30
|
+
// Authelia groups (`admins`, `feria-*`): letters, digits, "-" and "_" —
|
|
31
|
+
// never for an external member.
|
|
32
|
+
groups: z.array(z.string()).max(100).optional().default([]),
|
|
23
33
|
expiresInDays: z.number().int().min(1).max(30).optional().default(7),
|
|
34
|
+
// The domains the user becomes a member of on accepting — a name (a
|
|
35
|
+
// member) or `{ name, kind }` (an external member reaches only what names
|
|
36
|
+
// them). Required while the relay has two or more; with one, the user
|
|
37
|
+
// becomes its member.
|
|
38
|
+
domains: z.array(MembershipInputSchema).max(50).optional(),
|
|
24
39
|
});
|
|
25
40
|
|
|
26
41
|
export default async function invitationRoutes(fastify, _opts) {
|
|
@@ -37,6 +52,7 @@ export default async function invitationRoutes(fastify, _opts) {
|
|
|
37
52
|
const enriched = invitations.map((inv) => ({
|
|
38
53
|
...inv,
|
|
39
54
|
token: undefined,
|
|
55
|
+
domains: invitationMemberships(inv),
|
|
40
56
|
status: inv.used ? 'accepted' : new Date(inv.expiresAt) < now ? 'expired' : 'pending',
|
|
41
57
|
}));
|
|
42
58
|
return { invitations: enriched };
|
|
@@ -55,6 +71,16 @@ export default async function invitationRoutes(fastify, _opts) {
|
|
|
55
71
|
},
|
|
56
72
|
async (request, reply) => {
|
|
57
73
|
const body = CreateInvitationSchema.parse(request.body);
|
|
74
|
+
let domains;
|
|
75
|
+
try {
|
|
76
|
+
assertAssignableGroups(body.groups);
|
|
77
|
+
domains = await domainsForNewUser(body.domains, { required: true });
|
|
78
|
+
if (body.groups.length > 0 && domains.some((m) => m.kind === 'external')) {
|
|
79
|
+
throw externalMemberGroupsError(400);
|
|
80
|
+
}
|
|
81
|
+
} catch (err) {
|
|
82
|
+
return sendFailure(request, reply, err, 'Failed to read the domains');
|
|
83
|
+
}
|
|
58
84
|
|
|
59
85
|
// Check username doesn't already exist in Authelia
|
|
60
86
|
let usersData;
|
|
@@ -97,6 +123,7 @@ export default async function invitationRoutes(fastify, _opts) {
|
|
|
97
123
|
username: body.username,
|
|
98
124
|
email: body.email,
|
|
99
125
|
groups: body.groups,
|
|
126
|
+
domains,
|
|
100
127
|
createdAt: now.toISOString(),
|
|
101
128
|
expiresAt: expiresAt.toISOString(),
|
|
102
129
|
used: false,
|
|
@@ -116,8 +143,10 @@ export default async function invitationRoutes(fastify, _opts) {
|
|
|
116
143
|
|
|
117
144
|
const { invitation } = outcome;
|
|
118
145
|
const { token } = invitation;
|
|
119
|
-
|
|
120
|
-
|
|
146
|
+
// On the sign-in page of the first domain it names (the invitation
|
|
147
|
+
// page is served on every domain's auth portal).
|
|
148
|
+
const inviteDomain = await invitationSignInDomain(invitation);
|
|
149
|
+
const inviteUrl = inviteDomain ? `https://auth.${inviteDomain}/invite/${token}` : null;
|
|
121
150
|
|
|
122
151
|
return reply.code(201).send({
|
|
123
152
|
ok: true,
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A plugin's web surface (slice 7): the site it owns, where the relay
|
|
3
|
+
* serves the shell its server package ships beside its user surface's API.
|
|
4
|
+
* Admin only. Business logic lives in `lib/web-surface.js`.
|
|
5
|
+
*
|
|
6
|
+
* GET /plugins/:name/web-surface what the installed version declares, the
|
|
7
|
+
* site, whether it is served, the trees, the offer
|
|
8
|
+
* POST /plugins/:name/web-surface { subdomain?, accessMode? } — create the site
|
|
9
|
+
* POST /plugins/:name/web-surface/move { tunnelId } — take over the bound tunnel's host
|
|
10
|
+
* DELETE /plugins/:name/web-surface delete the site and its trees
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { z } from 'zod';
|
|
14
|
+
import { WEB_SURFACE_ACCESS_MODES, derivePluginRoute } from '@lamalibre/lamaste';
|
|
15
|
+
import { actorFromRequest } from '../../lib/actor.js';
|
|
16
|
+
import { findPlugin } from '../../lib/plugins.js';
|
|
17
|
+
import {
|
|
18
|
+
createWebSurface,
|
|
19
|
+
deleteWebSurface,
|
|
20
|
+
moveWebSurfaceFromTunnel,
|
|
21
|
+
webSurfaceStatus,
|
|
22
|
+
} from '../../lib/web-surface.js';
|
|
23
|
+
|
|
24
|
+
const PluginNameParamSchema = z.object({
|
|
25
|
+
name: z
|
|
26
|
+
.string()
|
|
27
|
+
.min(1)
|
|
28
|
+
.max(100)
|
|
29
|
+
.regex(/^[a-z0-9-]+$/, 'Plugin name must contain only lowercase letters, numbers, and hyphens'),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const CreateBodySchema = z
|
|
33
|
+
.object({
|
|
34
|
+
subdomain: z
|
|
35
|
+
.string()
|
|
36
|
+
.min(1)
|
|
37
|
+
.max(63)
|
|
38
|
+
.regex(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/, 'subdomain is a DNS label')
|
|
39
|
+
.optional(),
|
|
40
|
+
accessMode: z.enum(WEB_SURFACE_ACCESS_MODES).optional(),
|
|
41
|
+
})
|
|
42
|
+
.strict();
|
|
43
|
+
|
|
44
|
+
const MoveBodySchema = z.object({ tunnelId: z.string().min(1).max(64) }).strict();
|
|
45
|
+
|
|
46
|
+
/** The refusal a library threw (its `statusCode`, message and `code`), or a 500. */
|
|
47
|
+
function sendFailure(request, reply, err, fallback) {
|
|
48
|
+
if (Number.isInteger(err.statusCode) && err.statusCode < 500) {
|
|
49
|
+
return reply
|
|
50
|
+
.code(err.statusCode)
|
|
51
|
+
.send({ error: err.message, ...(err.code ? { code: err.code } : {}) });
|
|
52
|
+
}
|
|
53
|
+
request.log.error(err, fallback);
|
|
54
|
+
return reply
|
|
55
|
+
.code(Number.isInteger(err.statusCode) ? err.statusCode : 500)
|
|
56
|
+
.send({ error: fallback, details: err.message });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The route plugin `:name` holds: the installed plugin's, else the one its
|
|
61
|
+
* name derives — a site outlives its plugin, and its status and deletion
|
|
62
|
+
* stay reachable (creating or moving refuses without an installed plugin).
|
|
63
|
+
*/
|
|
64
|
+
async function routeOf(request) {
|
|
65
|
+
const { name } = PluginNameParamSchema.parse(request.params);
|
|
66
|
+
const plugin = await findPlugin(name);
|
|
67
|
+
return plugin?.route ?? derivePluginRoute(name);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export default async function pluginWebSurfaceRoutes(fastify, _opts) {
|
|
71
|
+
fastify.get(
|
|
72
|
+
'/plugins/:name/web-surface',
|
|
73
|
+
{ preHandler: fastify.requireRole(['admin']) },
|
|
74
|
+
async (request, reply) => {
|
|
75
|
+
const route = await routeOf(request);
|
|
76
|
+
try {
|
|
77
|
+
return { webSurface: await webSurfaceStatus(route) };
|
|
78
|
+
} catch (err) {
|
|
79
|
+
return sendFailure(request, reply, err, 'Failed to read the web surface');
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
fastify.post(
|
|
85
|
+
'/plugins/:name/web-surface',
|
|
86
|
+
{
|
|
87
|
+
preHandler: fastify.requireRole(['admin']),
|
|
88
|
+
// Issues a certificate and reloads nginx.
|
|
89
|
+
config: { rateLimit: { max: 10, timeWindow: '1 minute' } },
|
|
90
|
+
},
|
|
91
|
+
async (request, reply) => {
|
|
92
|
+
const route = await routeOf(request);
|
|
93
|
+
const body = CreateBodySchema.parse(request.body ?? {});
|
|
94
|
+
try {
|
|
95
|
+
const webSurface = await createWebSurface(route, body, {
|
|
96
|
+
actor: actorFromRequest(request),
|
|
97
|
+
logger: request.log,
|
|
98
|
+
});
|
|
99
|
+
return reply.code(201).send({ webSurface });
|
|
100
|
+
} catch (err) {
|
|
101
|
+
return sendFailure(request, reply, err, 'Failed to create the web surface');
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
fastify.post(
|
|
107
|
+
'/plugins/:name/web-surface/move',
|
|
108
|
+
{
|
|
109
|
+
preHandler: fastify.requireRole(['admin']),
|
|
110
|
+
config: { rateLimit: { max: 10, timeWindow: '1 minute' } },
|
|
111
|
+
},
|
|
112
|
+
async (request, reply) => {
|
|
113
|
+
const route = await routeOf(request);
|
|
114
|
+
const { tunnelId } = MoveBodySchema.parse(request.body);
|
|
115
|
+
try {
|
|
116
|
+
return {
|
|
117
|
+
webSurface: await moveWebSurfaceFromTunnel(route, tunnelId, {
|
|
118
|
+
actor: actorFromRequest(request),
|
|
119
|
+
logger: request.log,
|
|
120
|
+
}),
|
|
121
|
+
};
|
|
122
|
+
} catch (err) {
|
|
123
|
+
return sendFailure(
|
|
124
|
+
request,
|
|
125
|
+
reply,
|
|
126
|
+
err,
|
|
127
|
+
'Failed to move the user surface to the web surface',
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
fastify.delete(
|
|
134
|
+
'/plugins/:name/web-surface',
|
|
135
|
+
{
|
|
136
|
+
preHandler: fastify.requireRole(['admin']),
|
|
137
|
+
config: { rateLimit: { max: 10, timeWindow: '1 minute' } },
|
|
138
|
+
},
|
|
139
|
+
async (request, reply) => {
|
|
140
|
+
const route = await routeOf(request);
|
|
141
|
+
try {
|
|
142
|
+
return await deleteWebSurface(route, {
|
|
143
|
+
actor: actorFromRequest(request),
|
|
144
|
+
logger: request.log,
|
|
145
|
+
});
|
|
146
|
+
} catch (err) {
|
|
147
|
+
return sendFailure(request, reply, err, 'Failed to delete the web surface');
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
);
|
|
151
|
+
}
|
|
@@ -42,6 +42,7 @@ import {
|
|
|
42
42
|
} from '../../lib/plugin-settings.js';
|
|
43
43
|
import { actorFromRequest, certSerialFromRequest } from '../../lib/actor.js';
|
|
44
44
|
import { recordAudit } from '../../lib/audit.js';
|
|
45
|
+
import { withPluginDomains } from '../../lib/domains.js';
|
|
45
46
|
import {
|
|
46
47
|
readPushInstallConfig,
|
|
47
48
|
readPushInstallSessions,
|
|
@@ -301,6 +302,8 @@ async function withHooks(plugin) {
|
|
|
301
302
|
agent: status.agent !== null,
|
|
302
303
|
// Lamaste installs the agent half (agent.package); false: none, or installed by hand.
|
|
303
304
|
agentHalf: status.agentHalf !== null,
|
|
305
|
+
// The installed version declares a web surface (its tree is in the approval).
|
|
306
|
+
web: status.web.declared,
|
|
304
307
|
approval: status.approval.state,
|
|
305
308
|
autoApprove: status.autoApprove,
|
|
306
309
|
failOpenCount: status.relay?.failOpenCount ?? 0,
|
|
@@ -335,7 +338,7 @@ export default async function pluginRoutes(fastify, _opts) {
|
|
|
335
338
|
// (see GET /plugins/:name/settings).
|
|
336
339
|
const plugins = await attachSettingsSummaries(await attachHostings(registry.plugins));
|
|
337
340
|
return {
|
|
338
|
-
plugins: await Promise.all(plugins.map(withHooks)),
|
|
341
|
+
plugins: await withPluginDomains(await Promise.all(plugins.map(withHooks))),
|
|
339
342
|
// Every agent's retained data — of uninstalled plugins too, which
|
|
340
343
|
// no entry of `plugins` lists any more.
|
|
341
344
|
retainedData: await listRetainedData(),
|
|
@@ -361,7 +364,8 @@ export default async function pluginRoutes(fastify, _opts) {
|
|
|
361
364
|
}
|
|
362
365
|
|
|
363
366
|
const [withHosting] = await attachSettingsSummaries(await attachHostings([plugin]));
|
|
364
|
-
|
|
367
|
+
const [shown] = await withPluginDomains([await withHooks(withHosting)]);
|
|
368
|
+
return { plugin: shown };
|
|
365
369
|
},
|
|
366
370
|
);
|
|
367
371
|
|