@lamalibre/create-lamaste 4.0.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/LICENSE.md +118 -0
- package/README.md +114 -0
- package/bin/create-lamaste.js +15 -0
- package/package.json +54 -0
- package/scripts/bundle-vendor.js +110 -0
- package/scripts/lamaste-cert-info +62 -0
- package/scripts/lamaste-certbot +96 -0
- package/scripts/lamaste-priv +693 -0
- package/src/index.js +646 -0
- package/src/lib/accounts.js +86 -0
- package/src/lib/binaries.js +137 -0
- package/src/lib/cert-help-page.js +160 -0
- package/src/lib/env.js +132 -0
- package/src/lib/http-redirect.js +38 -0
- package/src/lib/ownership.js +231 -0
- package/src/lib/relay-services.js +103 -0
- package/src/lib/secrets.js +19 -0
- package/src/lib/service-config.js +311 -0
- package/src/lib/summary.js +131 -0
- package/src/lib/wrappers.js +63 -0
- package/src/tasks/gatekeeper.js +233 -0
- package/src/tasks/harden.js +293 -0
- package/src/tasks/mtls.js +193 -0
- package/src/tasks/nginx.js +265 -0
- package/src/tasks/node.js +110 -0
- package/src/tasks/panel.js +408 -0
- package/src/tasks/redeploy.js +438 -0
- package/vendor/docs/00-introduction/how-it-works.md +447 -0
- package/vendor/docs/00-introduction/quickstart.md +472 -0
- package/vendor/docs/00-introduction/what-is-lamaste.md +112 -0
- package/vendor/docs/01-concepts/authentication.md +464 -0
- package/vendor/docs/01-concepts/authorization.md +242 -0
- package/vendor/docs/01-concepts/certificates.md +452 -0
- package/vendor/docs/01-concepts/dns-and-domains.md +343 -0
- package/vendor/docs/01-concepts/mtls.md +490 -0
- package/vendor/docs/01-concepts/nginx-reverse-proxy.md +540 -0
- package/vendor/docs/01-concepts/security-model.md +600 -0
- package/vendor/docs/01-concepts/tickets.md +414 -0
- package/vendor/docs/01-concepts/tunneling.md +367 -0
- package/vendor/docs/02-guides/agent-setup.md +194 -0
- package/vendor/docs/02-guides/certificate-management.md +341 -0
- package/vendor/docs/02-guides/cloud-provisioning.md +406 -0
- package/vendor/docs/02-guides/desktop-app-setup.md +226 -0
- package/vendor/docs/02-guides/disaster-recovery.md +464 -0
- package/vendor/docs/02-guides/first-tunnel.md +230 -0
- package/vendor/docs/02-guides/installation.md +393 -0
- package/vendor/docs/02-guides/managing-users.md +255 -0
- package/vendor/docs/02-guides/onboarding.md +244 -0
- package/vendor/docs/02-guides/standalone-plugin-with-tickets.md +375 -0
- package/vendor/docs/02-guides/static-sites.md +364 -0
- package/vendor/docs/03-architecture/e2e-three-vm-sequences.md +457 -0
- package/vendor/docs/03-architecture/installer-flow.md +151 -0
- package/vendor/docs/03-architecture/installer.md +445 -0
- package/vendor/docs/03-architecture/management-flow.md +288 -0
- package/vendor/docs/03-architecture/nginx-configuration.md +830 -0
- package/vendor/docs/03-architecture/onboarding-flow.md +164 -0
- package/vendor/docs/03-architecture/overview.md +541 -0
- package/vendor/docs/03-architecture/panel-client.md +416 -0
- package/vendor/docs/03-architecture/panel-server.md +695 -0
- package/vendor/docs/03-architecture/state-management.md +529 -0
- package/vendor/docs/03-architecture/system-overview.md +167 -0
- package/vendor/docs/04-api-reference/certificates.md +1141 -0
- package/vendor/docs/04-api-reference/gatekeeper.md +592 -0
- package/vendor/docs/04-api-reference/onboarding.md +473 -0
- package/vendor/docs/04-api-reference/overview.md +404 -0
- package/vendor/docs/04-api-reference/services.md +267 -0
- package/vendor/docs/04-api-reference/settings.md +343 -0
- package/vendor/docs/04-api-reference/sites.md +716 -0
- package/vendor/docs/04-api-reference/system.md +268 -0
- package/vendor/docs/04-api-reference/tickets.md +618 -0
- package/vendor/docs/04-api-reference/tunnels.md +417 -0
- package/vendor/docs/04-api-reference/users.md +333 -0
- package/vendor/docs/05-operations/backup-and-restore.md +342 -0
- package/vendor/docs/05-operations/monitoring.md +285 -0
- package/vendor/docs/05-operations/uninstalling.md +344 -0
- package/vendor/docs/05-operations/upgrades.md +270 -0
- package/vendor/docs/06-reference/config-files.md +1080 -0
- package/vendor/docs/06-reference/glossary.md +528 -0
- package/vendor/docs/06-reference/installer-flags.md +260 -0
- package/vendor/docs/06-reference/ports-and-services.md +244 -0
- package/vendor/docs/06-reference/troubleshooting.md +665 -0
- package/vendor/docs/_index.json +233 -0
- package/vendor/docs/gatekeeper-design.md +953 -0
- package/vendor/docs/package.json +15 -0
- package/vendor/docs/todo/lib-cli-desktop-refactor.md +641 -0
- package/vendor/gatekeeper/dist/boot.d.ts +1 -0
- package/vendor/gatekeeper/dist/boot.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/boot.js +17 -0
- package/vendor/gatekeeper/dist/boot.js.map +1 -0
- package/vendor/gatekeeper/dist/cli/commands/access.d.ts +2 -0
- package/vendor/gatekeeper/dist/cli/commands/access.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/cli/commands/access.js +33 -0
- package/vendor/gatekeeper/dist/cli/commands/access.js.map +1 -0
- package/vendor/gatekeeper/dist/cli/commands/grants.d.ts +2 -0
- package/vendor/gatekeeper/dist/cli/commands/grants.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/cli/commands/grants.js +110 -0
- package/vendor/gatekeeper/dist/cli/commands/grants.js.map +1 -0
- package/vendor/gatekeeper/dist/cli/commands/groups.d.ts +2 -0
- package/vendor/gatekeeper/dist/cli/commands/groups.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/cli/commands/groups.js +98 -0
- package/vendor/gatekeeper/dist/cli/commands/groups.js.map +1 -0
- package/vendor/gatekeeper/dist/cli/index.d.ts +3 -0
- package/vendor/gatekeeper/dist/cli/index.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/cli/index.js +45 -0
- package/vendor/gatekeeper/dist/cli/index.js.map +1 -0
- package/vendor/gatekeeper/dist/index.d.ts +9 -0
- package/vendor/gatekeeper/dist/index.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/index.js +13 -0
- package/vendor/gatekeeper/dist/index.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/authz.d.ts +16 -0
- package/vendor/gatekeeper/dist/lib/authz.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/authz.js +42 -0
- package/vendor/gatekeeper/dist/lib/authz.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/cache-bust.d.ts +15 -0
- package/vendor/gatekeeper/dist/lib/cache-bust.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/cache-bust.js +19 -0
- package/vendor/gatekeeper/dist/lib/cache-bust.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/constants.d.ts +82 -0
- package/vendor/gatekeeper/dist/lib/constants.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/constants.js +79 -0
- package/vendor/gatekeeper/dist/lib/constants.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/grants.d.ts +32 -0
- package/vendor/gatekeeper/dist/lib/grants.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/grants.js +314 -0
- package/vendor/gatekeeper/dist/lib/grants.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/groups.d.ts +10 -0
- package/vendor/gatekeeper/dist/lib/groups.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/groups.js +247 -0
- package/vendor/gatekeeper/dist/lib/groups.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/migration.d.ts +17 -0
- package/vendor/gatekeeper/dist/lib/migration.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/migration.js +79 -0
- package/vendor/gatekeeper/dist/lib/migration.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/migrations/gatekeeper/0001_groups.sql +7 -0
- package/vendor/gatekeeper/dist/lib/migrations/gatekeeper/0002_access_grants.sql +14 -0
- package/vendor/gatekeeper/dist/lib/migrations/gatekeeper/0003_access_request_log.sql +9 -0
- package/vendor/gatekeeper/dist/lib/migrations/migrations/gatekeeper/0001_groups.sql +7 -0
- package/vendor/gatekeeper/dist/lib/migrations/migrations/gatekeeper/0002_access_grants.sql +14 -0
- package/vendor/gatekeeper/dist/lib/migrations/migrations/gatekeeper/0003_access_request_log.sql +9 -0
- package/vendor/gatekeeper/dist/lib/state-db.d.ts +16 -0
- package/vendor/gatekeeper/dist/lib/state-db.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/state-db.js +154 -0
- package/vendor/gatekeeper/dist/lib/state-db.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/templates.d.ts +21 -0
- package/vendor/gatekeeper/dist/lib/templates.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/templates.js +151 -0
- package/vendor/gatekeeper/dist/lib/templates.js.map +1 -0
- package/vendor/gatekeeper/dist/lib/types.d.ts +178 -0
- package/vendor/gatekeeper/dist/lib/types.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/lib/types.js +2 -0
- package/vendor/gatekeeper/dist/lib/types.js.map +1 -0
- package/vendor/gatekeeper/dist/server/index.d.ts +15 -0
- package/vendor/gatekeeper/dist/server/index.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/server/index.js +251 -0
- package/vendor/gatekeeper/dist/server/index.js.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/access-request.d.ts +7 -0
- package/vendor/gatekeeper/dist/server/routes/access-request.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/access-request.js +17 -0
- package/vendor/gatekeeper/dist/server/routes/access-request.js.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/authz.d.ts +3 -0
- package/vendor/gatekeeper/dist/server/routes/authz.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/authz.js +279 -0
- package/vendor/gatekeeper/dist/server/routes/authz.js.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/diagnostic.d.ts +13 -0
- package/vendor/gatekeeper/dist/server/routes/diagnostic.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/diagnostic.js +249 -0
- package/vendor/gatekeeper/dist/server/routes/diagnostic.js.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/grants.d.ts +3 -0
- package/vendor/gatekeeper/dist/server/routes/grants.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/grants.js +112 -0
- package/vendor/gatekeeper/dist/server/routes/grants.js.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/groups.d.ts +3 -0
- package/vendor/gatekeeper/dist/server/routes/groups.d.ts.map +1 -0
- package/vendor/gatekeeper/dist/server/routes/groups.js +139 -0
- package/vendor/gatekeeper/dist/server/routes/groups.js.map +1 -0
- package/vendor/gatekeeper/package.json +49 -0
- package/vendor/server/bin/lamaste-server.js +204 -0
- package/vendor/server/package.json +31 -0
- package/vendor/server/src/commands/certs.js +224 -0
- package/vendor/server/src/commands/chisel.js +132 -0
- package/vendor/server/src/commands/logs.js +49 -0
- package/vendor/server/src/commands/plugins.js +321 -0
- package/vendor/server/src/commands/reset-admin.js +478 -0
- package/vendor/server/src/commands/restart.js +94 -0
- package/vendor/server/src/commands/sites.js +257 -0
- package/vendor/server/src/commands/status.js +158 -0
- package/vendor/server/src/commands/tunnels.js +365 -0
- package/vendor/server/src/commands/uninstall.js +326 -0
- package/vendor/server/src/config.js +38 -0
- package/vendor/server/src/exec.js +26 -0
- package/vendor/server/src/logger.js +39 -0
- package/vendor/server/src/ndjson.js +56 -0
- package/vendor/server/src/panel-api.js +85 -0
- package/vendor/server/src/state.js +45 -0
- package/vendor/server-ui/dist/assets/index-B0nWuUyL.js +257 -0
- package/vendor/server-ui/dist/assets/index-BqWWFlTU.css +1 -0
- package/vendor/server-ui/dist/index.html +13 -0
- package/vendor/serverd/package.json +44 -0
- package/vendor/serverd/src/boot.js +13 -0
- package/vendor/serverd/src/index.js +191 -0
- package/vendor/serverd/src/lib/app-error.js +14 -0
- package/vendor/serverd/src/lib/authelia.js +85 -0
- package/vendor/serverd/src/lib/certbot.js +74 -0
- package/vendor/serverd/src/lib/chisel-args.js +6 -0
- package/vendor/serverd/src/lib/chisel-reconcile.js +232 -0
- package/vendor/serverd/src/lib/chisel-runtime.js +46 -0
- package/vendor/serverd/src/lib/chisel-users.js +109 -0
- package/vendor/serverd/src/lib/chisel.js +81 -0
- package/vendor/serverd/src/lib/config.js +190 -0
- package/vendor/serverd/src/lib/constants.js +8 -0
- package/vendor/serverd/src/lib/csr-signing.js +331 -0
- package/vendor/serverd/src/lib/enrollment.js +302 -0
- package/vendor/serverd/src/lib/files.js +19 -0
- package/vendor/serverd/src/lib/gatekeeper-client.js +135 -0
- package/vendor/serverd/src/lib/invite-page.js +281 -0
- package/vendor/serverd/src/lib/ip-utils.js +79 -0
- package/vendor/serverd/src/lib/migrations/state/0001_enrollment_tokens.sql +17 -0
- package/vendor/serverd/src/lib/migrations/state/0002_tickets.sql +70 -0
- package/vendor/serverd/src/lib/migrations/state/0003_plugins.sql +15 -0
- package/vendor/serverd/src/lib/migrations/state/0004_agents.sql +19 -0
- package/vendor/serverd/src/lib/mtls.js +795 -0
- package/vendor/serverd/src/lib/nginx.js +1090 -0
- package/vendor/serverd/src/lib/pki-paths.js +41 -0
- package/vendor/serverd/src/lib/pki-sign.js +103 -0
- package/vendor/serverd/src/lib/plugins.js +538 -0
- package/vendor/serverd/src/lib/push-install.js +390 -0
- package/vendor/serverd/src/lib/revocation.js +112 -0
- package/vendor/serverd/src/lib/services.js +131 -0
- package/vendor/serverd/src/lib/session.js +151 -0
- package/vendor/serverd/src/lib/state-db.js +130 -0
- package/vendor/serverd/src/lib/state.js +140 -0
- package/vendor/serverd/src/lib/storage.js +63 -0
- package/vendor/serverd/src/lib/system-stats.js +58 -0
- package/vendor/serverd/src/lib/tickets.js +1556 -0
- package/vendor/serverd/src/lib/totp.js +76 -0
- package/vendor/serverd/src/lib/tunnel-deps.js +41 -0
- package/vendor/serverd/src/lib/user-access-session.js +142 -0
- package/vendor/serverd/src/lib/user-access.js +52 -0
- package/vendor/serverd/src/middleware/errors.js +63 -0
- package/vendor/serverd/src/middleware/mtls.js +120 -0
- package/vendor/serverd/src/middleware/onboarding-guard.js +30 -0
- package/vendor/serverd/src/middleware/role-guard.js +58 -0
- package/vendor/serverd/src/middleware/twofa-session.js +65 -0
- package/vendor/serverd/src/middleware/user-access-session.js +52 -0
- package/vendor/serverd/src/routes/enrollment.js +120 -0
- package/vendor/serverd/src/routes/health.js +22 -0
- package/vendor/serverd/src/routes/invite.js +163 -0
- package/vendor/serverd/src/routes/management/agents.js +182 -0
- package/vendor/serverd/src/routes/management/certs.js +911 -0
- package/vendor/serverd/src/routes/management/gatekeeper-proxy.js +160 -0
- package/vendor/serverd/src/routes/management/identity.js +107 -0
- package/vendor/serverd/src/routes/management/invitations.js +166 -0
- package/vendor/serverd/src/routes/management/logs.js +218 -0
- package/vendor/serverd/src/routes/management/plugins.js +560 -0
- package/vendor/serverd/src/routes/management/services.js +63 -0
- package/vendor/serverd/src/routes/management/settings.js +325 -0
- package/vendor/serverd/src/routes/management/sites.js +522 -0
- package/vendor/serverd/src/routes/management/storage.js +192 -0
- package/vendor/serverd/src/routes/management/system.js +201 -0
- package/vendor/serverd/src/routes/management/tickets.js +457 -0
- package/vendor/serverd/src/routes/management/tunnels.js +709 -0
- package/vendor/serverd/src/routes/management/users.js +377 -0
- package/vendor/serverd/src/routes/management.js +36 -0
- package/vendor/serverd/src/routes/onboarding/dns.js +101 -0
- package/vendor/serverd/src/routes/onboarding/domain.js +42 -0
- package/vendor/serverd/src/routes/onboarding/index.js +18 -0
- package/vendor/serverd/src/routes/onboarding/provision.js +327 -0
- package/vendor/serverd/src/routes/onboarding/status.js +12 -0
- package/vendor/serverd/src/routes/plugin-router.js +79 -0
- package/vendor/serverd/src/routes/user-access.js +389 -0
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { getConfig } from '../lib/config.js';
|
|
3
|
+
import { ensureSessionSecret } from '../lib/session.js';
|
|
4
|
+
import { createUserSession } from '../lib/user-access-session.js';
|
|
5
|
+
import {
|
|
6
|
+
createGrant,
|
|
7
|
+
listGrants,
|
|
8
|
+
revokeGrant,
|
|
9
|
+
listGrantsForUser,
|
|
10
|
+
consumeGrant,
|
|
11
|
+
createOTP,
|
|
12
|
+
validateAndConsumeOTP,
|
|
13
|
+
} from '../lib/user-access.js';
|
|
14
|
+
import { createEnrollmentToken } from '../lib/enrollment.js';
|
|
15
|
+
import { readPlugins } from '../lib/plugins.js';
|
|
16
|
+
import { readTunnels } from '../lib/state.js';
|
|
17
|
+
|
|
18
|
+
// --- Zod schemas ---
|
|
19
|
+
|
|
20
|
+
const CreateGrantSchema = z.object({
|
|
21
|
+
username: z
|
|
22
|
+
.string()
|
|
23
|
+
.min(2)
|
|
24
|
+
.max(32)
|
|
25
|
+
.regex(/^[a-z0-9][a-z0-9_-]*$/, 'Invalid username format'),
|
|
26
|
+
pluginName: z
|
|
27
|
+
.string()
|
|
28
|
+
.min(1)
|
|
29
|
+
.max(200)
|
|
30
|
+
.regex(/^@lamalibre\//, 'Plugin must be @lamalibre/ scoped'),
|
|
31
|
+
target: z
|
|
32
|
+
.string()
|
|
33
|
+
.regex(/^(local|agent:[a-z0-9][a-z0-9-]*)$/, 'Target must be "local" or "agent:<label>"')
|
|
34
|
+
.optional()
|
|
35
|
+
.default('local'),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const GrantIdParamSchema = z.object({
|
|
39
|
+
grantId: z.string().uuid('Invalid grant ID format'),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const ExchangeBodySchema = z.object({
|
|
43
|
+
token: z.string().regex(/^[a-f0-9]{64}$/, 'Invalid token format'),
|
|
44
|
+
// PKCE verifier: 32–64 base64url chars (256–384 bits of entropy). The
|
|
45
|
+
// panel hashes this and timing-safe-compares against the stored S256
|
|
46
|
+
// challenge submitted at /authorize.
|
|
47
|
+
verifier: z.string().regex(/^[A-Za-z0-9_-]{32,64}$/, 'Invalid verifier format'),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// PKCE handshake parameters submitted by the desktop at /authorize.
|
|
51
|
+
const AuthorizeQuerySchema = z.object({
|
|
52
|
+
// base64url SHA-256 → 43 chars, no padding (PKCE S256, RFC 7636).
|
|
53
|
+
challenge: z.string().regex(/^[A-Za-z0-9_-]{43}$/, 'Invalid PKCE challenge'),
|
|
54
|
+
// 16 random bytes hex-encoded → 32 chars.
|
|
55
|
+
nonce: z.string().regex(/^[a-f0-9]{32}$/, 'Invalid nonce'),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const EnrollBodySchema = z.object({
|
|
59
|
+
grantId: z.string().uuid('Invalid grant ID format'),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// In-memory failed-exchange counter (per-process, resets on restart).
|
|
63
|
+
// Defense-in-depth: log a warning every N failures so an operator notices a
|
|
64
|
+
// scripted attacker exhausting the OTP keyspace. Cap is intentionally tiny —
|
|
65
|
+
// the OTP itself is 256 bits of entropy + bound to a verifier.
|
|
66
|
+
const FAILURE_LOG_INTERVAL = 10;
|
|
67
|
+
let exchangeFailureCount = 0;
|
|
68
|
+
|
|
69
|
+
// --- Admin routes (mTLS + roleGuard) ---
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Admin grant management routes.
|
|
73
|
+
* Registered inside the protectedContext (mTLS + roleGuard).
|
|
74
|
+
*/
|
|
75
|
+
export async function userAccessAdminRoutes(fastify, _opts) {
|
|
76
|
+
// GET /api/user-access/grants — list all grants
|
|
77
|
+
fastify.get(
|
|
78
|
+
'/user-access/grants',
|
|
79
|
+
{
|
|
80
|
+
preHandler: fastify.requireRole(['admin']),
|
|
81
|
+
},
|
|
82
|
+
async (request, reply) => {
|
|
83
|
+
try {
|
|
84
|
+
const grants = await listGrants();
|
|
85
|
+
return { grants };
|
|
86
|
+
} catch (err) {
|
|
87
|
+
request.log.error(err, 'Failed to list user access grants');
|
|
88
|
+
return reply.code(500).send({ error: 'Failed to list grants' });
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
// POST /api/user-access/grants — create a grant
|
|
94
|
+
fastify.post(
|
|
95
|
+
'/user-access/grants',
|
|
96
|
+
{
|
|
97
|
+
preHandler: fastify.requireRole(['admin']),
|
|
98
|
+
},
|
|
99
|
+
async (request, reply) => {
|
|
100
|
+
let body;
|
|
101
|
+
try {
|
|
102
|
+
body = CreateGrantSchema.parse(request.body);
|
|
103
|
+
} catch (err) {
|
|
104
|
+
return reply.code(400).send({ error: err.errors?.[0]?.message || 'Invalid request body' });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const grant = await createGrant(body.username, body.pluginName, request.log, {
|
|
109
|
+
target: body.target,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
return { ok: true, grant };
|
|
113
|
+
} catch (err) {
|
|
114
|
+
request.log.error(err, 'Failed to create user access grant');
|
|
115
|
+
const statusCode = err.statusCode || 500;
|
|
116
|
+
return reply.code(statusCode).send({ error: err.message || 'Failed to create grant' });
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
// DELETE /api/user-access/grants/:grantId — revoke a grant
|
|
122
|
+
fastify.delete(
|
|
123
|
+
'/user-access/grants/:grantId',
|
|
124
|
+
{
|
|
125
|
+
preHandler: fastify.requireRole(['admin']),
|
|
126
|
+
},
|
|
127
|
+
async (request, reply) => {
|
|
128
|
+
let params;
|
|
129
|
+
try {
|
|
130
|
+
params = GrantIdParamSchema.parse(request.params);
|
|
131
|
+
} catch (err) {
|
|
132
|
+
return reply.code(400).send({ error: err.errors?.[0]?.message || 'Invalid grant ID' });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
await revokeGrant(params.grantId, request.log);
|
|
137
|
+
|
|
138
|
+
return { ok: true };
|
|
139
|
+
} catch (err) {
|
|
140
|
+
const statusCode = err.statusCode || 500;
|
|
141
|
+
return reply.code(statusCode).send({ error: err.message || 'Failed to revoke grant' });
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// --- Public routes (no mTLS) ---
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Public user-access routes for the OAuth-like auth flow.
|
|
151
|
+
* Registered in the publicContext (no mTLS required).
|
|
152
|
+
*/
|
|
153
|
+
export async function userAccessPublicRoutes(fastify, _opts) {
|
|
154
|
+
// GET /authorize — Authelia-protected, generates OTP bound to a PKCE
|
|
155
|
+
// challenge, redirects to deep link with the OTP and nonce in the URL
|
|
156
|
+
// FRAGMENT (#). Fragments are not sent to servers, not stored in most OS
|
|
157
|
+
// URL handler logs, and stripped from third-party referrer chains — they
|
|
158
|
+
// are the right transport for short-lived secrets in a redirect URL.
|
|
159
|
+
fastify.get('/authorize', async (request, reply) => {
|
|
160
|
+
const config = getConfig();
|
|
161
|
+
|
|
162
|
+
// Remote-User header is set by nginx after Authelia forward auth succeeds
|
|
163
|
+
const username = request.headers['remote-user'];
|
|
164
|
+
if (!username) {
|
|
165
|
+
return reply.code(401).send({ error: 'Authentication required' });
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
let query;
|
|
169
|
+
try {
|
|
170
|
+
query = AuthorizeQuerySchema.parse(request.query);
|
|
171
|
+
} catch (err) {
|
|
172
|
+
return reply
|
|
173
|
+
.code(400)
|
|
174
|
+
.send({ error: err.errors?.[0]?.message || 'Invalid authorize parameters' });
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
const domain = config.domain;
|
|
179
|
+
if (!domain) {
|
|
180
|
+
return reply.code(503).send({ error: 'Server domain not configured' });
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const { token, nonce } = await createOTP(
|
|
184
|
+
username,
|
|
185
|
+
{ challenge: query.challenge, nonce: query.nonce },
|
|
186
|
+
request.log,
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
// Deep-link callback. The OS-registered scheme is `lamalibre://`
|
|
190
|
+
// (ecosystem-level — shared across all Lamalibre products); the
|
|
191
|
+
// `product=lamaste` query param tells the desktop's scheme dispatcher
|
|
192
|
+
// which product the callback is for. Future products (herd, shell,
|
|
193
|
+
// etc.) will reuse the single scheme with their own `product=` value.
|
|
194
|
+
//
|
|
195
|
+
// The OTP token, domain, and nonce stay in the URL fragment (after #)
|
|
196
|
+
// so the auth proxy never sees them. Authelia and any HTTP server in
|
|
197
|
+
// the chain only see path/query; fragments are kept by the user-agent.
|
|
198
|
+
// Putting the token in the query string would leak it to nginx access
|
|
199
|
+
// logs.
|
|
200
|
+
const callbackUrl =
|
|
201
|
+
`lamalibre://callback?product=lamaste` +
|
|
202
|
+
`#token=${encodeURIComponent(token)}` +
|
|
203
|
+
`&domain=${encodeURIComponent(domain)}` +
|
|
204
|
+
`&nonce=${encodeURIComponent(nonce)}`;
|
|
205
|
+
return reply.redirect(callbackUrl, 302);
|
|
206
|
+
} catch (err) {
|
|
207
|
+
const statusCode = err.statusCode || 500;
|
|
208
|
+
const message = statusCode < 500 ? err.message : 'Authorization failed';
|
|
209
|
+
request.log.error(err, 'Failed to create user access OTP');
|
|
210
|
+
return reply.code(statusCode).send({ error: message });
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// POST /exchange — exchange OTP + PKCE verifier for a user session token.
|
|
215
|
+
// The verifier proves possession of the desktop process that initiated
|
|
216
|
+
// /authorize. A malicious local app that intercepts the deep link has the
|
|
217
|
+
// OTP but NOT the verifier, so this call fails with the same generic error
|
|
218
|
+
// as any other invalid-OTP path (no information leakage).
|
|
219
|
+
fastify.post('/exchange', async (request, reply) => {
|
|
220
|
+
let body;
|
|
221
|
+
try {
|
|
222
|
+
body = ExchangeBodySchema.parse(request.body);
|
|
223
|
+
} catch {
|
|
224
|
+
// Treat shape errors the same as auth failures — no leak about which
|
|
225
|
+
// field was missing or malformed.
|
|
226
|
+
exchangeFailureCount += 1;
|
|
227
|
+
if (exchangeFailureCount % FAILURE_LOG_INTERVAL === 0) {
|
|
228
|
+
request.log.warn(
|
|
229
|
+
{ count: exchangeFailureCount },
|
|
230
|
+
'user-access /exchange repeated failures — possible brute force',
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
return reply.code(401).send({ error: 'Invalid or expired token' });
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
const { username } = await validateAndConsumeOTP(body.token, body.verifier);
|
|
238
|
+
const sessionSecret = await ensureSessionSecret();
|
|
239
|
+
const session = createUserSession(sessionSecret, username);
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
ok: true,
|
|
243
|
+
sessionToken: session.value,
|
|
244
|
+
username,
|
|
245
|
+
expiresAt: session.expiresAt,
|
|
246
|
+
};
|
|
247
|
+
} catch (err) {
|
|
248
|
+
exchangeFailureCount += 1;
|
|
249
|
+
if (exchangeFailureCount % FAILURE_LOG_INTERVAL === 0) {
|
|
250
|
+
request.log.warn(
|
|
251
|
+
{ count: exchangeFailureCount },
|
|
252
|
+
'user-access /exchange repeated failures — possible brute force',
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
// Use generic error message (no information leakage)
|
|
256
|
+
const statusCode = err.statusCode || 401;
|
|
257
|
+
return reply.code(statusCode).send({ error: 'Invalid or expired token' });
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// --- User-session-protected routes ---
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Routes accessible to authenticated Authelia users via Bearer session token.
|
|
266
|
+
* Registered with user-access-session middleware.
|
|
267
|
+
*/
|
|
268
|
+
export async function userAccessProtectedRoutes(fastify, _opts) {
|
|
269
|
+
// GET /plugins — list granted plugins for the authenticated user
|
|
270
|
+
fastify.get('/plugins', async (request, reply) => {
|
|
271
|
+
const username = request.userAccessUsername;
|
|
272
|
+
|
|
273
|
+
try {
|
|
274
|
+
const grants = await listGrantsForUser(username);
|
|
275
|
+
|
|
276
|
+
// Enrich with plugin metadata where available
|
|
277
|
+
let plugins = [];
|
|
278
|
+
try {
|
|
279
|
+
const registry = await readPlugins();
|
|
280
|
+
plugins = registry.plugins || [];
|
|
281
|
+
} catch {
|
|
282
|
+
// Plugin registry may not exist — proceed with grants only
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const pluginMap = new Map();
|
|
286
|
+
for (const p of plugins) {
|
|
287
|
+
pluginMap.set(p.packageName, p);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Load tunnels for agent-side grant enrichment
|
|
291
|
+
let tunnels = [];
|
|
292
|
+
try {
|
|
293
|
+
tunnels = await readTunnels();
|
|
294
|
+
} catch {
|
|
295
|
+
// Tunnel state may not exist — proceed without tunnel info
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const enrichedGrants = grants.map((g) => {
|
|
299
|
+
const plugin = pluginMap.get(g.pluginName);
|
|
300
|
+
const pluginMeta = plugin
|
|
301
|
+
? {
|
|
302
|
+
name: plugin.name,
|
|
303
|
+
displayName: plugin.displayName || plugin.name,
|
|
304
|
+
description: plugin.description,
|
|
305
|
+
version: plugin.version,
|
|
306
|
+
}
|
|
307
|
+
: null;
|
|
308
|
+
|
|
309
|
+
const target = g.target || 'local';
|
|
310
|
+
|
|
311
|
+
if (target.startsWith('agent:')) {
|
|
312
|
+
const agentLabel = target.slice('agent:'.length);
|
|
313
|
+
const tunnel = tunnels.find(
|
|
314
|
+
(t) =>
|
|
315
|
+
t.type === 'plugin' && t.agentLabel === agentLabel && t.pluginName === g.pluginName,
|
|
316
|
+
);
|
|
317
|
+
return {
|
|
318
|
+
...g,
|
|
319
|
+
target,
|
|
320
|
+
agentLabel,
|
|
321
|
+
tunnelUrl: tunnel ? `https://${tunnel.fqdn}` : null,
|
|
322
|
+
tunnelEnabled: tunnel?.enabled ?? false,
|
|
323
|
+
plugin: pluginMeta,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return {
|
|
328
|
+
...g,
|
|
329
|
+
target,
|
|
330
|
+
plugin: pluginMeta,
|
|
331
|
+
};
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
return { grants: enrichedGrants };
|
|
335
|
+
} catch (err) {
|
|
336
|
+
request.log.error(err, 'Failed to list user plugins');
|
|
337
|
+
return reply.code(500).send({ error: 'Failed to list plugins' });
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
// POST /enroll — consume a grant and generate an enrollment token
|
|
342
|
+
fastify.post('/enroll', async (request, reply) => {
|
|
343
|
+
const username = request.userAccessUsername;
|
|
344
|
+
|
|
345
|
+
let body;
|
|
346
|
+
try {
|
|
347
|
+
body = EnrollBodySchema.parse(request.body);
|
|
348
|
+
} catch {
|
|
349
|
+
return reply.code(400).send({ error: 'Invalid request' });
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
try {
|
|
353
|
+
// Agent-side grants do not require enrollment — they provide browser access
|
|
354
|
+
const allGrants = await listGrantsForUser(username);
|
|
355
|
+
const targetGrant = allGrants.find((g) => g.grantId === body.grantId);
|
|
356
|
+
if (targetGrant && (targetGrant.target || 'local').startsWith('agent:')) {
|
|
357
|
+
return reply.code(400).send({ error: 'Agent-side grants do not require enrollment' });
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Consume the grant (validates ownership and single-use)
|
|
361
|
+
const grant = await consumeGrant(body.grantId, username, request.log);
|
|
362
|
+
|
|
363
|
+
// Generate an enrollment token for a new agent
|
|
364
|
+
// Label is auto-generated from username + plugin short name.
|
|
365
|
+
// Underscores in usernames are replaced with hyphens to match agent label format
|
|
366
|
+
// (signCSR validates labels against /^[a-z0-9][a-z0-9-]*$/).
|
|
367
|
+
const pluginShortName = grant.pluginName.replace(/^@lamalibre\//, '').replace(/-server$/, '');
|
|
368
|
+
const sanitizedUsername = username.replace(/_/g, '-');
|
|
369
|
+
const label = `${sanitizedUsername}-${pluginShortName}`;
|
|
370
|
+
|
|
371
|
+
// Default capabilities for user-enrolled agents
|
|
372
|
+
const capabilities = ['tunnels:read', 'services:read', 'system:read'];
|
|
373
|
+
|
|
374
|
+
const tokenData = await createEnrollmentToken(label, capabilities, [], request.log);
|
|
375
|
+
|
|
376
|
+
return {
|
|
377
|
+
ok: true,
|
|
378
|
+
enrollmentToken: tokenData.token,
|
|
379
|
+
label: tokenData.label,
|
|
380
|
+
expiresAt: tokenData.expiresAt,
|
|
381
|
+
pluginName: grant.pluginName,
|
|
382
|
+
};
|
|
383
|
+
} catch (err) {
|
|
384
|
+
const statusCode = err.statusCode || 500;
|
|
385
|
+
const message = statusCode < 500 ? err.message : 'Enrollment failed';
|
|
386
|
+
return reply.code(statusCode).send({ error: message });
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
}
|