@lamalibre/create-lamaste 4.0.0 → 4.1.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.
Files changed (61) hide show
  1. package/package.json +2 -2
  2. package/scripts/lamaste-priv +21 -2
  3. package/src/lib/oidc-issuer-key.js +124 -0
  4. package/src/lib/relay-services.js +4 -0
  5. package/src/lib/service-config.js +16 -0
  6. package/vendor/docs/00-introduction/how-it-works.md +2 -2
  7. package/vendor/docs/00-introduction/quickstart.md +5 -5
  8. package/vendor/docs/00-introduction/what-is-lamaste.md +5 -5
  9. package/vendor/docs/01-concepts/authentication.md +45 -1
  10. package/vendor/docs/01-concepts/certificates.md +3 -3
  11. package/vendor/docs/01-concepts/mtls.md +2 -2
  12. package/vendor/docs/01-concepts/tickets.md +2 -2
  13. package/vendor/docs/01-concepts/tunneling.md +4 -4
  14. package/vendor/docs/02-guides/agent-setup.md +3 -3
  15. package/vendor/docs/02-guides/certificate-management.md +2 -2
  16. package/vendor/docs/02-guides/cloud-provisioning.md +3 -3
  17. package/vendor/docs/02-guides/desktop-app-setup.md +10 -10
  18. package/vendor/docs/02-guides/installation.md +3 -3
  19. package/vendor/docs/02-guides/onboarding.md +2 -2
  20. package/vendor/docs/02-guides/standalone-plugin-with-tickets.md +2 -2
  21. package/vendor/docs/03-architecture/installer-flow.md +2 -2
  22. package/vendor/docs/03-architecture/installer.md +6 -5
  23. package/vendor/docs/03-architecture/management-flow.md +1 -1
  24. package/vendor/docs/03-architecture/overview.md +5 -5
  25. package/vendor/docs/03-architecture/panel-client.md +6 -6
  26. package/vendor/docs/03-architecture/panel-server.md +2 -2
  27. package/vendor/docs/03-architecture/state-management.md +2 -2
  28. package/vendor/docs/03-architecture/system-overview.md +1 -1
  29. package/vendor/docs/04-api-reference/onboarding.md +1 -1
  30. package/vendor/docs/04-api-reference/overview.md +111 -105
  31. package/vendor/docs/04-api-reference/registry-and-oidc.md +182 -0
  32. package/vendor/docs/05-operations/backup-and-restore.md +1 -1
  33. package/vendor/docs/05-operations/uninstalling.md +1 -1
  34. package/vendor/docs/05-operations/upgrades.md +6 -5
  35. package/vendor/docs/06-reference/config-files.md +103 -45
  36. package/vendor/docs/06-reference/glossary.md +3 -3
  37. package/vendor/docs/06-reference/installer-flags.md +13 -13
  38. package/vendor/docs/06-reference/ports-and-services.md +1 -1
  39. package/vendor/docs/06-reference/troubleshooting.md +8 -8
  40. package/vendor/docs/_index.json +5 -0
  41. package/vendor/docs/gatekeeper-design.md +8 -8
  42. package/vendor/docs/identity-directory-design.md +443 -0
  43. package/vendor/docs/package.json +1 -1
  44. package/vendor/docs/todo/lib-cli-desktop-refactor.md +27 -27
  45. package/vendor/gatekeeper/package.json +1 -1
  46. package/vendor/server/bin/lamaste-server.js +10 -1
  47. package/vendor/server/package.json +4 -3
  48. package/vendor/server/src/commands/plugins.js +87 -0
  49. package/vendor/server/src/commands/registry-credential.js +142 -0
  50. package/vendor/server/src/panel-api.js +83 -38
  51. package/vendor/serverd/package.json +5 -2
  52. package/vendor/serverd/src/index.js +11 -0
  53. package/vendor/serverd/src/lib/authelia.js +11 -0
  54. package/vendor/serverd/src/lib/plugin-oidc.js +216 -0
  55. package/vendor/serverd/src/lib/plugins.js +26 -2
  56. package/vendor/serverd/src/lib/registry-credential.js +145 -0
  57. package/vendor/serverd/src/routes/management/agents.js +81 -0
  58. package/vendor/serverd/src/routes/management/plugins.js +65 -2
  59. package/vendor/serverd/src/routes/management/registry-credential.js +170 -0
  60. package/vendor/serverd/src/routes/management/users.js +31 -0
  61. package/vendor/serverd/src/routes/management.js +2 -0
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Admin management of the panel's registry (Feria) service credential.
3
+ *
4
+ * GET /api/registry-credential what is configured — never a token
5
+ * PUT /api/registry-credential store a credential after verifying the
6
+ * whole chain against the registry
7
+ * (admin scope required)
8
+ * DELETE /api/registry-credential remove it
9
+ *
10
+ * `auth.kind` is `token` (a pasted long-lived Feria token) or `oidc` (the
11
+ * panel's own `lamaste-server` client on Authelia: client-credentials grant,
12
+ * Feria login). For `oidc` the client is written to Authelia before the
13
+ * chain is verified; if verification fails it is removed again, and the
14
+ * error says what Feria's `auth.json` needs.
15
+ *
16
+ * With a credential in place, agents obtain short-lived read tokens through
17
+ * `GET /api/agents/me/registry-credential` for plugin installs.
18
+ */
19
+
20
+ import { z } from 'zod';
21
+ import {
22
+ LAMASTE_SERVER_OIDC_CLIENT_ID,
23
+ RegistryCredentialError,
24
+ summariseRegistryCredential,
25
+ } from '@lamalibre/lamaste/server';
26
+
27
+ import {
28
+ clearRegistryCredentialConfig,
29
+ readRegistryCredentialConfig,
30
+ verifyRegistryCredential,
31
+ writeRegistryCredentialConfig,
32
+ } from '../../lib/registry-credential.js';
33
+ import { syncOidcClients } from '../../lib/plugin-oidc.js';
34
+
35
+ const FeriaTokenSchema = z
36
+ .string()
37
+ .min(20)
38
+ .max(512)
39
+ .regex(/^feria_[A-Za-z0-9_-]+$/, 'token must be a Feria registry token');
40
+
41
+ const AuthSchema = z.discriminatedUnion('kind', [
42
+ z.object({ kind: z.literal('token'), token: FeriaTokenSchema }),
43
+ z.object({ kind: z.literal('oidc') }),
44
+ ]);
45
+
46
+ const PutSchema = z
47
+ .object({
48
+ registryUrl: z.string().url(),
49
+ scope: z
50
+ .string()
51
+ .regex(/^@[a-z0-9][a-z0-9-]*$/)
52
+ .optional(),
53
+ auth: AuthSchema.optional(),
54
+ /** The first release's shape — a token at the top level. */
55
+ token: FeriaTokenSchema.optional(),
56
+ })
57
+ .superRefine((body, ctx) => {
58
+ if (!body.auth && !body.token) {
59
+ ctx.addIssue({
60
+ code: z.ZodIssueCode.custom,
61
+ path: ['auth'],
62
+ message: 'auth is required: { kind: "token", token } or { kind: "oidc" }',
63
+ });
64
+ }
65
+ });
66
+
67
+ /** The stored `auth` for a request body. */
68
+ function authOf(body) {
69
+ if (body.auth?.kind === 'oidc') return { kind: 'oidc', clientId: LAMASTE_SERVER_OIDC_CLIENT_ID };
70
+ if (body.auth?.kind === 'token') return { kind: 'token', token: body.auth.token };
71
+ return { kind: 'token', token: body.token };
72
+ }
73
+
74
+ export default async function registryCredentialRoutes(fastify) {
75
+ fastify.get(
76
+ '/registry-credential',
77
+ { preHandler: fastify.requireRole(['admin']), config: { rateLimit: {} } },
78
+ async () => {
79
+ const config = await readRegistryCredentialConfig();
80
+ if (!config) return { configured: false };
81
+ return { configured: true, ...summariseRegistryCredential(config) };
82
+ },
83
+ );
84
+
85
+ fastify.put(
86
+ '/registry-credential',
87
+ {
88
+ preHandler: fastify.requireRole(['admin']),
89
+ config: { rateLimit: { max: 10, timeWindow: '1 minute' } },
90
+ },
91
+ async (request, reply) => {
92
+ const body = PutSchema.parse(request.body);
93
+ const auth = authOf(body);
94
+ const candidate = { registryUrl: body.registryUrl, auth };
95
+
96
+ // The panel's own client must exist in Authelia (with a secret in the
97
+ // store) before the chain can be exercised.
98
+ if (auth.kind === 'oidc') {
99
+ try {
100
+ const synced = await syncOidcClients(request.log, {
101
+ registryAuth: { registryUrl: body.registryUrl },
102
+ });
103
+ if (!synced) {
104
+ return reply.code(503).send({ error: 'Complete onboarding (set a domain) first' });
105
+ }
106
+ } catch (err) {
107
+ return reply.code(500).send({
108
+ error: `Could not register the ${LAMASTE_SERVER_OIDC_CLIENT_ID} client in Authelia: ${err.message}`,
109
+ });
110
+ }
111
+ }
112
+
113
+ let verified;
114
+ try {
115
+ verified = await verifyRegistryCredential(candidate);
116
+ } catch (err) {
117
+ if (auth.kind === 'oidc') {
118
+ // Leave Authelia as the stored credential says — the candidate is
119
+ // not kept.
120
+ await syncOidcClients(request.log).catch((syncErr) =>
121
+ request.log.warn({ err: syncErr.message }, 'Failed to roll back the OIDC client'),
122
+ );
123
+ }
124
+ const status = err instanceof RegistryCredentialError ? err.statusCode : 502;
125
+ return reply.code(status).send({ error: err.message });
126
+ }
127
+
128
+ const config = {
129
+ provider: 'feria',
130
+ registryUrl: body.registryUrl,
131
+ scope: body.scope ?? '@lamalibre',
132
+ auth,
133
+ owner: verified.owner,
134
+ configuredAt: new Date().toISOString(),
135
+ };
136
+ await writeRegistryCredentialConfig(config);
137
+ // The stored credential now decides whether Authelia carries the client.
138
+ await syncOidcClients(request.log).catch((err) =>
139
+ request.log.warn(
140
+ { err: err.message },
141
+ 'Failed to sync OIDC clients after credential change',
142
+ ),
143
+ );
144
+ request.log.info(
145
+ { registryUrl: body.registryUrl, owner: verified.owner, auth: auth.kind },
146
+ 'Registry credential configured',
147
+ );
148
+ return { configured: true, ...summariseRegistryCredential(config) };
149
+ },
150
+ );
151
+
152
+ fastify.delete(
153
+ '/registry-credential',
154
+ { preHandler: fastify.requireRole(['admin']), config: { rateLimit: {} } },
155
+ async (request) => {
156
+ const removed = await clearRegistryCredentialConfig();
157
+ if (removed) {
158
+ request.log.info('Registry credential removed');
159
+ // Removes the lamaste-server client and its secret if it was in use.
160
+ await syncOidcClients(request.log).catch((err) =>
161
+ request.log.warn(
162
+ { err: err.message },
163
+ 'Failed to sync OIDC clients after credential removal',
164
+ ),
165
+ );
166
+ }
167
+ return { configured: false, removed };
168
+ },
169
+ );
170
+ }
@@ -9,6 +9,7 @@ import {
9
9
  writeTotpToDatabase,
10
10
  } from '../../lib/authelia.js';
11
11
  import { removeGrantsForUser } from '../../lib/user-access.js';
12
+ import { revokeUserRegistryTokens } from '../../lib/registry-credential.js';
12
13
  import { bumpUserEpoch } from '../../lib/config.js';
13
14
  import { gatekeeperRequest } from '../../lib/gatekeeper-client.js';
14
15
 
@@ -156,6 +157,7 @@ export default async function usersRoutes(fastify, _opts) {
156
157
  }
157
158
 
158
159
  const user = usersData.users[username];
160
+ const groupsBefore = Array.isArray(user.groups) ? [...user.groups] : [];
159
161
 
160
162
  if (body.displayname !== undefined) {
161
163
  user.displayname = body.displayname;
@@ -188,6 +190,23 @@ export default async function usersRoutes(fastify, _opts) {
188
190
  request.log.warn(err, 'Failed to reload Authelia after user update');
189
191
  }
190
192
 
193
+ // Registry scopes are granted at login from feria-* groups; losing a
194
+ // group must not leave a valid token behind. Best effort — the user
195
+ // update itself is already done.
196
+ const warnings = [];
197
+ if (body.groups !== undefined) {
198
+ try {
199
+ await revokeUserRegistryTokens(
200
+ username,
201
+ { before: groupsBefore, after: user.groups || [] },
202
+ request.log,
203
+ );
204
+ } catch (err) {
205
+ request.log.warn({ username, err: err.message }, 'Failed to revoke registry tokens');
206
+ warnings.push(`registry tokens were not revoked: ${err.message}`);
207
+ }
208
+ }
209
+
191
210
  return {
192
211
  ok: true,
193
212
  user: {
@@ -196,6 +215,7 @@ export default async function usersRoutes(fastify, _opts) {
196
215
  email: user.email,
197
216
  groups: user.groups || [],
198
217
  },
218
+ ...(warnings.length > 0 ? { warnings } : {}),
199
219
  };
200
220
  },
201
221
  );
@@ -252,6 +272,17 @@ export default async function usersRoutes(fastify, _opts) {
252
272
  // ---------------------------------------------------------------
253
273
  const warnings = [];
254
274
 
275
+ // 0. Registry (Feria) tokens the user holds — their groups are gone.
276
+ try {
277
+ await revokeUserRegistryTokens(username, { deleted: true }, request.log);
278
+ } catch (err) {
279
+ request.log.warn(
280
+ { username, err: err.message },
281
+ 'Cascade: registry token revocation failed',
282
+ );
283
+ warnings.push(`registry tokens were not revoked: ${err.message}`);
284
+ }
285
+
255
286
  // 1. Legacy user-plugin-access.json grants (pre-gatekeeper migration)
256
287
  try {
257
288
  const removed = await removeGrantsForUser(username, request.log);
@@ -14,6 +14,7 @@ import identityRoutes from './management/identity.js';
14
14
  import storageRoutes from './management/storage.js';
15
15
  import gatekeeperProxyRoutes from './management/gatekeeper-proxy.js';
16
16
  import agentsRoutes from './management/agents.js';
17
+ import registryCredentialRoutes from './management/registry-credential.js';
17
18
 
18
19
  export default async function managementRoutes(fastify, _opts) {
19
20
  fastify.addHook('onRequest', managementOnly());
@@ -33,4 +34,5 @@ export default async function managementRoutes(fastify, _opts) {
33
34
  await fastify.register(storageRoutes);
34
35
  await fastify.register(gatekeeperProxyRoutes);
35
36
  await fastify.register(agentsRoutes);
37
+ await fastify.register(registryCredentialRoutes);
36
38
  }