feat: finish the remote-desktop plugin (B14)

This commit is contained in:
LukeGus committed 2026-09-24 01:02:45 -05:00
1 parent 0af2c94983
commit 14eb558b85
215 files changed
+30029 -3921

No files matched your search

@@ -0,0 +1,228 @@
/**
* ctx.credentials.resolveHostProtocol: credentials:read is checked and every
* call audited, the owner gets their stored or credential-mode login, and a
* shared recipient only ever sees what core's sharing rules hand them.
*/
import { beforeEach, describe, expect, it, vi } from "vitest";
const grants = new Map<string, string[]>();
const auditEntries: Array<Record<string, unknown>> = [];
const state = vi.hoisted(() => ({
hosts: new Map<number, Record<string, unknown>>(),
credentials: new Map<number, { username: string; password: string }>(),
canConnect: true,
resolution: null as Record<string, unknown> | null,
resolverCalls: [] as Array<Record<string, unknown>>,
}));
vi.mock("../../database/repositories/factory.js", () => ({
createCurrentPluginPermissionGrantRepository: () => ({
listByPlugin: async (pluginId: string) =>
(grants.get(pluginId) ?? []).map((capability) => ({
pluginId,
capability,
})),
}),
createCurrentHostResolutionRepository: () => ({
findHostOwnerId: async (hostId: number) =>
(state.hosts.get(hostId)?.userId as string) ?? null,
findHostById: async (hostId: number) => state.hosts.get(hostId) ?? null,
findCredentialByIdForUser: async (id: number) =>
state.credentials.get(id) ?? null,
}),
}));
vi.mock("../../utils/audit-logger.js", () => ({
logAudit: async (entry: Record<string, unknown>) => {
auditEntries.push(entry);
},
}));
vi.mock("../../utils/permission-manager.js", () => ({
PermissionManager: {
getInstance: () => ({
canAccessHost: async () => ({ hasAccess: state.canConnect }),
}),
},
}));
vi.mock("../../utils/shared-host-auth-resolver.js", () => ({
resolveRecipientSharedHostAuthentication: async (
host: Record<string, unknown>,
) => {
state.resolverCalls.push(host);
return state.resolution;
},
}));
import { createPluginContext, createPluginHandle } from "../../plugins/ctx.js";
import { invalidatePluginPermissionCache } from "../../plugins/permissions.js";
import { runAsActor } from "../../plugins/actor.js";
import type { PluginManifest } from "@termix/plugin-sdk/manifest";
function contextFor(capabilities: string[]) {
const manifest = {
id: "demo",
name: "demo",
version: "1.0.0",
description: "",
author: { name: "test" },
license: "MIT",
category: "Productivity",
engine: { termix: ">=2.9.0", api: "1" },
capabilities,
} as PluginManifest;
const handle = createPluginHandle("demo", { activate: () => {} });
return createPluginContext(manifest, handle);
}
const baseHost = {
id: 7,
userId: "owner",
name: "Win box",
ip: "10.0.0.7",
port: 22,
username: "sshuser",
password: "ssh-secret",
jumpHosts: JSON.stringify([{ hostId: 3 }]),
rdpUser: "admin",
rdpPassword: "rdp-secret",
rdpDomain: "CORP",
rdpAuthType: null,
rdpCredentialId: null,
};
beforeEach(() => {
grants.clear();
auditEntries.length = 0;
invalidatePluginPermissionCache();
state.hosts.clear();
state.credentials.clear();
state.canConnect = true;
state.resolution = null;
state.resolverCalls.length = 0;
state.hosts.set(7, { ...baseHost });
});
describe("ctx.credentials.resolveHostProtocol", () => {
it("refuses without credentials:read and audits the refusal", async () => {
grants.set("demo", []);
const ctx = contextFor([]);
await expect(
runAsActor("owner", "request", () =>
ctx.credentials.resolveHostProtocol(7, "rdp"),
),
).rejects.toThrow(/credentials:read/);
expect(auditEntries.at(-1)).toMatchObject({
action: "plugin_credentials_read",
success: false,
});
});
it("returns the owner's stored login and audits it", async () => {
grants.set("demo", ["credentials:read"]);
const ctx = contextFor(["credentials:read"]);
const target = await runAsActor("owner", "request", () =>
ctx.credentials.resolveHostProtocol(7, "rdp"),
);
expect(target).toEqual({
host: {
id: 7,
name: "Win box",
ip: "10.0.0.7",
port: 22,
ownerUserId: "owner",
jumpHosts: [{ hostId: 3 }],
},
shared: false,
auth: {
authType: "direct",
username: "admin",
password: "rdp-secret",
domain: "CORP",
},
});
expect(auditEntries.at(-1)).toMatchObject({
action: "plugin_credentials_read",
success: true,
});
});
it("uses the stored credential in credential mode", async () => {
grants.set("demo", ["credentials:read"]);
state.hosts.set(7, {
...baseHost,
vncCredentialId: 11,
vncAuthType: "credential",
});
state.credentials.set(11, { username: "viewer", password: "vnc-pass" });
const ctx = contextFor(["credentials:read"]);
const target = await runAsActor("owner", "request", () =>
ctx.credentials.resolveHostProtocol(7, "vnc"),
);
expect(target?.auth).toMatchObject({
authType: "credential",
username: "viewer",
password: "vnc-pass",
});
});
it("never hands a shared recipient the owner's raw secrets", async () => {
grants.set("demo", ["credentials:read"]);
state.resolution = {
source: "owner-shared",
authType: "direct",
secret: { username: "shared", password: "snap", domain: null },
};
const ctx = contextFor(["credentials:read"]);
const target = await runAsActor("guest", "request", () =>
ctx.credentials.resolveHostProtocol(7, "rdp"),
);
expect(target?.shared).toBe(true);
expect(target?.auth).toMatchObject({
username: "shared",
password: "snap",
});
expect(state.resolverCalls[0]).toMatchObject({
password: null,
rdpUser: null,
rdpPassword: null,
});
});
it("gives a recipient with nothing shared an empty login", async () => {
grants.set("demo", ["credentials:read"]);
state.resolution = { source: "required" };
const ctx = contextFor(["credentials:read"]);
const target = await runAsActor("guest", "request", () =>
ctx.credentials.resolveHostProtocol(7, "telnet"),
);
expect(target?.auth.username).toBe("");
expect(target?.auth.password).toBe("");
});
it("returns null without connect access or for a missing host", async () => {
grants.set("demo", ["credentials:read"]);
state.canConnect = false;
const ctx = contextFor(["credentials:read"]);
await expect(
runAsActor("guest", "request", () =>
ctx.credentials.resolveHostProtocol(7, "rdp"),
),
).resolves.toBeNull();
await expect(
runAsActor("owner", "request", () =>
ctx.credentials.resolveHostProtocol(99, "rdp"),
),
).resolves.toBeNull();
expect(auditEntries.at(-1)).toMatchObject({ success: false });
});
});
@@ -121,3 +121,50 @@ describe("ctx.desktop.openIsolatedWindow", () => {
});
});
});
describe("ctx.desktop.launchNativeRdp", () => {
const request = { host: "win.example.test", port: 3389, username: "bob" };
it("refuses without desktop:window", async () => {
grants.set("demo", []);
const { ctx } = contextFor("demo", []);
await expect(ctx.desktop.launchNativeRdp(request)).rejects.toThrow(
/desktop:window/,
);
expect(bridge.requestFromElectronMain).not.toHaveBeenCalled();
expect(auditEntries.at(-1)).toMatchObject({
action: "plugin_desktop_launch_native_rdp",
success: false,
});
});
it("refuses outside the desktop app", async () => {
grants.set("demo", ["desktop:window"]);
const { ctx } = contextFor("demo", ["desktop:window"]);
expect(ctx.desktop.available()).toBe(false);
await expect(ctx.desktop.launchNativeRdp(request)).rejects.toThrow(
/desktop app/,
);
});
it("relays to Electron main and audits success", async () => {
grants.set("demo", ["desktop:window"]);
bridge.available = true;
const { ctx } = contextFor("demo", ["desktop:window"]);
expect(ctx.desktop.available()).toBe(true);
await expect(ctx.desktop.launchNativeRdp(request)).resolves.toEqual({
success: true,
});
expect(bridge.requestFromElectronMain).toHaveBeenCalledWith(
"launch-native-rdp",
request,
);
expect(auditEntries.at(-1)).toMatchObject({
action: "plugin_desktop_launch_native_rdp",
success: true,
});
});
});
@@ -21,6 +21,7 @@ const h = vi.hoisted(() => ({
activities: [] as Array<Record<string, unknown>>,
sessions: [] as number[],
released: [] as number[],
importRows: [] as Array<{ hostId: number; row: Record<string, unknown> }>,
roles: [] as Array<{
id: number;
name: string;
@@ -110,6 +111,14 @@ vi.mock("../../hosts/host-session-status.js", () => ({
},
},
}));
vi.mock("../../database/routes/host-plugin-settings.js", () => ({
applyPluginHostImportSettings: async (
hostId: number,
row: Record<string, unknown>,
) => {
h.importRows.push({ hostId, row });
},
}));
vi.mock("../../utils/shared-host-secrets-manager.js", () => ({
SharedHostSecretsManager: {
getInstance: () => ({
@@ -325,6 +334,7 @@ describe("ctx.hosts", () => {
port: 22,
username: "root",
authType: "password",
enableRdp: true,
});
expect(created).toMatchObject({
id: 99,
@@ -332,6 +342,11 @@ describe("ctx.hosts", () => {
userId: "user-1",
});
expect(h.created).toHaveLength(1);
// Another plugin's fields go through its import normalizer.
expect(h.importRows.at(-1)).toMatchObject({
hostId: 99,
row: { enableRdp: true },
});
});
it("updates a host once granted hosts:write", async () => {
@@ -83,6 +83,24 @@ describe("contributes.settings", () => {
expect(errors.join(" ")).toContain('Unknown field "wat"');
});
it("accepts a hidden field and rejects a non-boolean hidden", () => {
expect(
validateManifest(
withSettings({
host: {
fields: [{ key: "a", type: "number", labelKey: "k", hidden: true }],
},
}),
),
).toEqual([]);
const errors = validateManifest(
withSettings({
admin: [{ key: "a", type: "string", labelKey: "k", hidden: "yes" }],
}),
);
expect(errors.join(" ")).toContain("hidden must be a boolean");
});
it("rejects an unknown scope", () => {
const errors = validateManifest(
withSettings({ global: [{ key: "a", type: "string", labelKey: "k" }] }),