@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.
- package/package.json +2 -2
- package/scripts/lamaste-priv +21 -2
- package/src/lib/oidc-issuer-key.js +124 -0
- package/src/lib/relay-services.js +4 -0
- package/src/lib/service-config.js +16 -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 +45 -1
- package/vendor/docs/01-concepts/certificates.md +3 -3
- package/vendor/docs/01-concepts/mtls.md +2 -2
- 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 +3 -3
- 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/onboarding.md +2 -2
- 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 +6 -5
- package/vendor/docs/03-architecture/management-flow.md +1 -1
- 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 +2 -2
- package/vendor/docs/03-architecture/system-overview.md +1 -1
- package/vendor/docs/04-api-reference/onboarding.md +1 -1
- package/vendor/docs/04-api-reference/overview.md +111 -105
- package/vendor/docs/04-api-reference/registry-and-oidc.md +182 -0
- 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 -5
- package/vendor/docs/06-reference/config-files.md +103 -45
- 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 +5 -0
- package/vendor/docs/gatekeeper-design.md +8 -8
- package/vendor/docs/identity-directory-design.md +443 -0
- package/vendor/docs/package.json +1 -1
- package/vendor/docs/todo/lib-cli-desktop-refactor.md +27 -27
- package/vendor/gatekeeper/package.json +1 -1
- package/vendor/server/bin/lamaste-server.js +10 -1
- package/vendor/server/package.json +4 -3
- package/vendor/server/src/commands/plugins.js +87 -0
- package/vendor/server/src/commands/registry-credential.js +142 -0
- package/vendor/server/src/panel-api.js +83 -38
- package/vendor/serverd/package.json +5 -2
- package/vendor/serverd/src/index.js +11 -0
- package/vendor/serverd/src/lib/authelia.js +11 -0
- package/vendor/serverd/src/lib/plugin-oidc.js +216 -0
- package/vendor/serverd/src/lib/plugins.js +26 -2
- package/vendor/serverd/src/lib/registry-credential.js +145 -0
- package/vendor/serverd/src/routes/management/agents.js +81 -0
- package/vendor/serverd/src/routes/management/plugins.js +65 -2
- package/vendor/serverd/src/routes/management/registry-credential.js +170 -0
- package/vendor/serverd/src/routes/management/users.js +31 -0
- 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
|
}
|