feat: finish the fleets plugin, add ctx.hosts and host-commands SDK (B4)

This commit is contained in:
LukeGus committed 2026-09-23 11:24:16 -05:00
1 parent d2ff5fc78f
commit 3b2c849514
59 files changed
+31807 -2977

No files matched your search

+244
View File
@@ -0,0 +1,244 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
const h = vi.hoisted(() => ({
granted: new Set<string>(),
actor: undefined as string | undefined,
access: {
hasAccess: true,
isOwner: true,
isShared: false,
} as Record<string, unknown>,
ownedHosts: [] as Array<Record<string, unknown>>,
sharedRows: [] as Array<Record<string, unknown>>,
visibleGrants: [] as Array<{ hostId: number }>,
ownerId: "user-1",
hostById: null as Record<string, unknown> | null,
upserts: [] as Array<Record<string, unknown>>,
users: [] as Array<{ id: string; username: string }>,
roles: [] as Array<{
id: number;
name: string;
displayName: string | null;
isSystem: boolean;
}>,
}));
vi.mock("../../plugins/permissions.js", async () => {
const { PluginCapabilityError } = await import("@termix/plugin-sdk/backend");
return {
assertCapability: async (
pluginId: string,
capability: string,
declared: readonly string[],
) => {
if (!declared.includes(capability) || !h.granted.has(capability)) {
throw new PluginCapabilityError(pluginId, capability);
}
},
};
});
vi.mock("../../plugins/actor.js", () => ({ getActor: () => h.actor }));
vi.mock("../../utils/permission-manager.js", () => ({
PermissionManager: {
getInstance: () => ({
canAccessHost: async () => h.access,
}),
},
}));
vi.mock("../../database/repositories/factory.js", () => ({
createCurrentHostResolutionRepository: () => ({
findHostsByUserId: async () => h.ownedHosts,
listHostRowsForAccessList: async () => h.sharedRows,
findHostOwnerId: async () => h.ownerId,
findHostById: async () => h.hostById,
}),
createCurrentRoleRepository: () => ({
listUserRoleIds: async () => [],
listRoles: async () => h.roles,
}),
createCurrentRbacAccessRepository: () => ({
listVisibleHostAccessEntries: async () => h.visibleGrants,
upsertHostAccess: async (input: Record<string, unknown>) => {
h.upserts.push(input);
return { id: h.upserts.length, created: true };
},
}),
createCurrentUserRepository: () => ({
listAll: async () => h.users,
}),
}));
vi.mock("../../utils/shared-host-secrets-manager.js", () => ({
SharedHostSecretsManager: {
getInstance: () => ({
snapshotForUser: async () => {},
snapshotForRole: async () => {},
}),
},
}));
const { createPluginHosts } = await import("../../plugins/ctx-hosts.js");
const { PluginCapabilityError } = await import("@termix/plugin-sdk/backend");
function manifest(capabilities: string[]) {
return {
id: "fixture",
name: "Fixture",
version: "1.0.0",
capabilities,
} as never;
}
beforeEach(() => {
h.granted = new Set();
h.actor = "user-1";
h.access = { hasAccess: true, isOwner: true, isShared: false };
h.ownedHosts = [];
h.sharedRows = [];
h.visibleGrants = [];
h.ownerId = "user-1";
h.hostById = null;
h.upserts = [];
h.users = [];
h.roles = [];
});
describe("ctx.hosts", () => {
it("refuses list/get/checkAccess without hosts:read", async () => {
const audit = vi.fn(async () => {});
const hosts = createPluginHosts({ manifest: manifest([]), audit });
await expect(hosts.list()).rejects.toBeInstanceOf(PluginCapabilityError);
await expect(hosts.get(1)).rejects.toBeInstanceOf(PluginCapabilityError);
await expect(hosts.checkAccess(1, "view")).rejects.toBeInstanceOf(
PluginCapabilityError,
);
});
it("lists owned and shared hosts once granted hosts:read", async () => {
h.granted = new Set(["hosts:read"]);
h.ownedHosts = [
{
id: 1,
userId: "user-1",
name: "own",
ip: "10.0.0.1",
port: 22,
username: "root",
tags: "prod",
folder: null,
authType: "password",
},
];
h.sharedRows = [
{
id: 2,
userId: "user-2",
name: "shared",
ip: "10.0.0.2",
port: 22,
username: "root",
tags: null,
folder: null,
authType: "key",
},
];
const hosts = createPluginHosts({
manifest: manifest(["hosts:read"]),
audit: vi.fn(async () => {}),
});
const list = await hosts.list();
expect(list.map((h2) => h2.id)).toEqual([1, 2]);
});
it("checkAccess reports the granted level", async () => {
h.granted = new Set(["hosts:read"]);
h.access = {
hasAccess: true,
isOwner: false,
isShared: true,
permissionLevel: "edit",
expiresAt: null,
};
const hosts = createPluginHosts({
manifest: manifest(["hosts:read"]),
audit: vi.fn(async () => {}),
});
const access = await hosts.checkAccess(1, "edit");
expect(access).toMatchObject({ hasAccess: true, permissionLevel: "edit" });
});
it("refuses share, listUsers, listRoles without hosts:write, auditing the refusal", async () => {
const audit = vi.fn(async () => {});
const hosts = createPluginHosts({ manifest: manifest([]), audit });
await expect(
hosts.share(1, [{ type: "user", id: "user-2" }], "view"),
).rejects.toBeInstanceOf(PluginCapabilityError);
await expect(hosts.listUsers()).rejects.toBeInstanceOf(
PluginCapabilityError,
);
await expect(hosts.listRoles()).rejects.toBeInstanceOf(
PluginCapabilityError,
);
expect(audit).toHaveBeenCalledWith(
"hosts_share",
expect.any(String),
expect.objectContaining({ success: false }),
);
});
it("shares a host it manages and snapshots secrets per target", async () => {
h.granted = new Set(["hosts:write"]);
h.access = { hasAccess: true, isOwner: true, isShared: false };
const hosts = createPluginHosts({
manifest: manifest(["hosts:write"]),
audit: vi.fn(async () => {}),
});
const result = await hosts.share(
5,
[{ type: "user", id: "user-2" }],
"view",
24,
);
expect(result).toEqual({ hostId: 5, shared: true });
expect(h.upserts).toHaveLength(1);
expect(h.upserts[0]).toMatchObject({
hostId: 5,
permissionLevel: "view",
targetType: "user",
targetUserId: "user-2",
});
});
it("refuses to share a host the caller does not manage", async () => {
h.granted = new Set(["hosts:write"]);
h.access = { hasAccess: false, isOwner: false, isShared: false };
const hosts = createPluginHosts({
manifest: manifest(["hosts:write"]),
audit: vi.fn(async () => {}),
});
const result = await hosts.share(
5,
[{ type: "user", id: "user-2" }],
"view",
);
expect(result).toEqual({ hostId: 5, shared: false, reason: "forbidden" });
});
it("lists non-system roles and users once granted hosts:write", async () => {
h.granted = new Set(["hosts:write"]);
h.users = [{ id: "user-1", username: "alice" }];
h.roles = [
{ id: 1, name: "admin", displayName: "Admin", isSystem: true },
{ id: 2, name: "auditor", displayName: "Auditor", isSystem: false },
];
const hosts = createPluginHosts({
manifest: manifest(["hosts:write"]),
audit: vi.fn(async () => {}),
});
expect(await hosts.listUsers()).toEqual([
{ id: "user-1", username: "alice" },
]);
expect(await hosts.listRoles()).toEqual([
{ id: 2, name: "auditor", displayName: "Auditor" },
]);
});
});
@@ -0,0 +1,116 @@
import { describe, it, expect } from "vitest";
import {
parsePlatformProbe,
buildPackageActionCommand,
buildPackageRemoveCommand,
isValidPackageName,
buildSudoCommand,
parseUpgradable,
} from "@termix/plugin-sdk/host-commands";
describe("parsePlatformProbe", () => {
it("prefers dnf over yum when both are present", () => {
const out = ["dnf=1", "yum=1", "apt=0", "pacman=0"].join("\n");
expect(parsePlatformProbe(out).pkg).toBe("dnf");
});
it("reads the pretty OS name and tool flags", () => {
const out = [
"systemd=1",
"apt=1",
"dnf=0",
"yum=0",
"pacman=0",
"certbot=1",
"acmesh=0",
"docker=1",
"os=Debian GNU/Linux 12 (bookworm)",
].join("\n");
const info = parsePlatformProbe(out);
expect(info).toMatchObject({
hasSystemd: true,
pkg: "apt",
hasCertbot: true,
hasAcmeSh: false,
hasDocker: true,
osPrettyName: "Debian GNU/Linux 12 (bookworm)",
});
});
it("nulls the OS name when the probe line is empty", () => {
expect(parsePlatformProbe("os=").osPrettyName).toBeNull();
});
});
describe("buildPackageActionCommand", () => {
it("builds install and upgrade-all commands per package manager", () => {
expect(buildPackageActionCommand("apt", "install", "curl")).toContain(
"apt-get -y install curl",
);
expect(buildPackageActionCommand("dnf", "upgrade-all")).toBe(
"dnf -y upgrade",
);
expect(buildPackageActionCommand("yum", "install", "curl")).toBe(
"yum -y install curl",
);
expect(buildPackageActionCommand("pacman", "upgrade-all")).toBe(
"pacman -Syu --noconfirm",
);
});
it("returns null when no package manager was detected", () => {
expect(buildPackageActionCommand(null, "install", "curl")).toBeNull();
});
});
describe("buildPackageRemoveCommand", () => {
it("builds the correct remove command per package manager", () => {
expect(buildPackageRemoveCommand("apt", "curl")).toContain(
"apt-get -y remove curl",
);
expect(buildPackageRemoveCommand("dnf", "curl")).toBe("dnf -y remove curl");
expect(buildPackageRemoveCommand("yum", "curl")).toBe("yum -y remove curl");
expect(buildPackageRemoveCommand("pacman", "curl")).toBe(
"pacman -R --noconfirm curl",
);
});
it("returns null when no package manager was detected", () => {
expect(buildPackageRemoveCommand(null, "curl")).toBeNull();
});
});
describe("isValidPackageName", () => {
it("accepts ordinary package names", () => {
expect(isValidPackageName("curl")).toBe(true);
expect(isValidPackageName("lib32-glibc")).toBe(true);
});
it("rejects shell metacharacters and empty strings", () => {
expect(isValidPackageName("curl; rm -rf /")).toBe(false);
expect(isValidPackageName("")).toBe(false);
expect(isValidPackageName(123)).toBe(false);
});
});
describe("buildSudoCommand", () => {
it("single-quotes the password and wraps the marker echo around the command", () => {
const cmd = buildSudoCommand("uptime", "p'w");
expect(cmd).toContain("sudo -S -p ''");
expect(cmd).toContain("__TX_SUDO_OK__");
expect(cmd).toContain("uptime");
});
});
describe("parseUpgradable", () => {
it("parses apt list --upgradable output", () => {
const out = "curl/stable 7.88.0 amd64 [upgradable from: 7.87.0]";
expect(parseUpgradable("apt", out)).toEqual([
{ name: "curl", newVersion: "7.88.0", currentVersion: "7.87.0" },
]);
});
it("returns an empty list for no package manager", () => {
expect(parseUpgradable(null, "anything")).toEqual([]);
});
});