From 5e3164237920aabcce23f14ab945bb52c634ee32 Mon Sep 17 00:00:00 2001 From: LukeGus Date: Sat, 3 Oct 2026 15:31:05 -0500 Subject: [PATCH] fix: fleets, proxmox and automations not getting the host sudo password --- plugins/automations/manifest.json | 1 + .../tests/backend/sudo-capability.test.ts | 17 +++++++++++++++++ plugins/fleets/manifest.json | 1 + plugins/fleets/src/backend/routes.ts | 8 ++++---- .../tests/backend/sudo-capability.test.ts | 17 +++++++++++++++++ plugins/proxmox/manifest.json | 1 + plugins/proxmox/src/backend/routes.ts | 8 +++++++- .../tests/backend/sudo-capability.test.ts | 17 +++++++++++++++++ 8 files changed, 65 insertions(+), 5 deletions(-) create mode 100644 plugins/automations/tests/backend/sudo-capability.test.ts create mode 100644 plugins/fleets/tests/backend/sudo-capability.test.ts create mode 100644 plugins/proxmox/tests/backend/sudo-capability.test.ts diff --git a/plugins/automations/manifest.json b/plugins/automations/manifest.json index ba149255f..0da548831 100644 --- a/plugins/automations/manifest.json +++ b/plugins/automations/manifest.json @@ -17,6 +17,7 @@ "capabilities": [ "hosts:read", "credentials:use", + "credentials:read", "ssh:connect", "db:own", "events:core", diff --git a/plugins/automations/tests/backend/sudo-capability.test.ts b/plugins/automations/tests/backend/sudo-capability.test.ts new file mode 100644 index 000000000..ab3f3d7a1 --- /dev/null +++ b/plugins/automations/tests/backend/sudo-capability.test.ts @@ -0,0 +1,17 @@ +import { describe, expect, it } from "vitest"; +import fs from "node:fs"; +import path from "node:path"; + +// Elevated commands read the host's sudo password, which core only hands to +// a plugin holding credentials:read. +describe("sudo password access", () => { + it("declares credentials:read", () => { + const manifest = JSON.parse( + fs.readFileSync( + path.resolve(import.meta.dirname, "../../manifest.json"), + "utf8", + ), + ); + expect(manifest.capabilities).toContain("credentials:read"); + }); +}); diff --git a/plugins/fleets/manifest.json b/plugins/fleets/manifest.json index 5d0ded88f..e11eab9d8 100644 --- a/plugins/fleets/manifest.json +++ b/plugins/fleets/manifest.json @@ -18,6 +18,7 @@ "hosts:read", "hosts:write", "credentials:use", + "credentials:read", "ssh:connect", "db:own", "network:serve", diff --git a/plugins/fleets/src/backend/routes.ts b/plugins/fleets/src/backend/routes.ts index 99d91f77c..857a291ae 100644 --- a/plugins/fleets/src/backend/routes.ts +++ b/plugins/fleets/src/backend/routes.ts @@ -1357,9 +1357,9 @@ export function registerFleetRoutes( fleetId, "manage", async (host) => { - // Package actions need the host's sudo password, an operational - // secret ctx.hosts.get() leaves out. ctx.ssh.connect resolves the - // full host the connect pipeline uses, which carries it. + // Package actions need the host's sudo password. connect() hands + // back a redacted host, so read it through resolveHost. + const full = await ctx.ssh.resolveHost(host.id); const connection = await ctx.ssh.connect(host.id, { purpose: "fleet", }); @@ -1388,7 +1388,7 @@ export function registerFleetRoutes( const result = await execElevated( sshClient, cmd, - connection.host.sudoPassword as string | undefined, + full?.sudoPassword as string | undefined, { forceSudo: true, timeoutMs: 600000 }, ); return { diff --git a/plugins/fleets/tests/backend/sudo-capability.test.ts b/plugins/fleets/tests/backend/sudo-capability.test.ts new file mode 100644 index 000000000..ab3f3d7a1 --- /dev/null +++ b/plugins/fleets/tests/backend/sudo-capability.test.ts @@ -0,0 +1,17 @@ +import { describe, expect, it } from "vitest"; +import fs from "node:fs"; +import path from "node:path"; + +// Elevated commands read the host's sudo password, which core only hands to +// a plugin holding credentials:read. +describe("sudo password access", () => { + it("declares credentials:read", () => { + const manifest = JSON.parse( + fs.readFileSync( + path.resolve(import.meta.dirname, "../../manifest.json"), + "utf8", + ), + ); + expect(manifest.capabilities).toContain("credentials:read"); + }); +}); diff --git a/plugins/proxmox/manifest.json b/plugins/proxmox/manifest.json index 44ed32036..7a1a6af5f 100644 --- a/plugins/proxmox/manifest.json +++ b/plugins/proxmox/manifest.json @@ -18,6 +18,7 @@ "hosts:read", "hosts:write", "credentials:use", + "credentials:read", "ssh:connect", "db:own", "network:serve", diff --git a/plugins/proxmox/src/backend/routes.ts b/plugins/proxmox/src/backend/routes.ts index 930e97828..af29f715b 100644 --- a/plugins/proxmox/src/backend/routes.ts +++ b/plugins/proxmox/src/backend/routes.ts @@ -275,7 +275,13 @@ async function discoverProxmoxGuestsForHost( timeoutMs: 35000, overrides: { tryKeyboard: false, readyTimeout: 30000 }, }); - const hostWithSudo: PluginSshHostWithSudo = host; + // connect() always hands back a redacted host; the sudo password comes + // from resolveHost, which needs credentials:read. + const full = await ctx.ssh.resolveHost(parsedHostId); + const hostWithSudo: PluginSshHostWithSudo = { + ...host, + sudoPassword: (full?.sudoPassword as string | undefined) ?? undefined, + }; const hostCredentialId = (hostWithSudo.credentialId as number | null) ?? null; try { diff --git a/plugins/proxmox/tests/backend/sudo-capability.test.ts b/plugins/proxmox/tests/backend/sudo-capability.test.ts new file mode 100644 index 000000000..ab3f3d7a1 --- /dev/null +++ b/plugins/proxmox/tests/backend/sudo-capability.test.ts @@ -0,0 +1,17 @@ +import { describe, expect, it } from "vitest"; +import fs from "node:fs"; +import path from "node:path"; + +// Elevated commands read the host's sudo password, which core only hands to +// a plugin holding credentials:read. +describe("sudo password access", () => { + it("declares credentials:read", () => { + const manifest = JSON.parse( + fs.readFileSync( + path.resolve(import.meta.dirname, "../../manifest.json"), + "utf8", + ), + ); + expect(manifest.capabilities).toContain("credentials:read"); + }); +});