fix: tunnels and host settings missing from shared hosts on desktop

This commit is contained in:
LukeGus committed 2026-10-04 13:54:09 -05:00
1 parent 5a373c3963
commit 08e1eea2f3
4 files changed
+81 -5

No files matched your search

+1
View File
@@ -44,6 +44,7 @@ https://youtu.be/lngaePO96tM
- Host editor tabs being hidden when they did not fit
- SSH host keys being accepted without a check when the host was missing from the database
- The "last login failed" host status showing in English or mistranslated in several languages
- Tunnels and other host settings missing from shared hosts in the desktop app
- SSO and LDAP provider dialogs overflowing the screen and using mismatched toggles
- Plugin audit log entries failing to save when no user was signed in
+4
View File
@@ -546,6 +546,10 @@ async function loadSharedHosts(userId: string): Promise<SyncRow[]> {
: value;
}
wire.jumpHosts = await jumpHostsToSyncIds(host.jumpHosts);
wire.pluginSettings = await exportHostPluginSettings(hostId, {
userId,
permissionLevel,
});
wire.syncId = host.syncId;
wire.shared = {
hostId,
+32 -5
View File
@@ -15,6 +15,7 @@ import { databaseLogger } from "../utils/logger.js";
import type { DefaultOverrides } from "../../types/host-defaults.js";
import {
hostSettingsPlugins,
withHostPluginSettings,
type HostPluginSettings,
} from "../database/routes/host-plugin-settings.js";
@@ -23,19 +24,45 @@ export interface PluginHostSettingsSync {
importValue?: (key: string, value: unknown) => unknown | Promise<unknown>;
}
/** What a host carries to the other side of a sync pair. */
/** Who a shared host's copy is for, and at what share level. */
export interface SharedHostViewer {
userId: string;
permissionLevel: string;
}
/**
* What a host carries to the other side of a sync pair. With a viewer, only
* what that user sees of a host shared with them, as the host routes show it.
*/
export async function exportHostPluginSettings(
hostId: number,
viewer?: SharedHostViewer,
): Promise<HostPluginSettings> {
const visible = viewer
? (((
await withHostPluginSettings(
{
id: hostId,
isShared: true,
permissionLevel: viewer.permissionLevel,
},
viewer.userId,
)
).pluginSettings as HostPluginSettings | undefined) ?? {})
: null;
const result: HostPluginSettings = {};
for (const manifest of hostSettingsPlugins()) {
const fields = declaredFields(manifest, "host").filter(
(field) => field.type !== "secret",
(field) =>
field.type !== "secret" &&
(!visible || field.key in (visible[manifest.id] ?? {})),
);
if (fields.length === 0) continue;
const values = await getAllSettings(manifest, "host", hostId, {
redactSecrets: true,
});
const values = visible
? visible[manifest.id]
: await getAllSettings(manifest, "host", hostId, {
redactSecrets: true,
});
const hook = consume<PluginHostSettingsSync>(
`${manifest.id}.hostSettingsSync`,
);
@@ -11,10 +11,19 @@ const h = vi.hoisted(() => ({
writes: [] as Array<[string, number, string, unknown]>,
hooks: new Map<string, unknown>(),
manifests: [] as PluginManifest[],
visible: null as Record<string, Record<string, unknown>> | null,
viewerCalls: [] as Array<[Record<string, unknown>, string | undefined]>,
}));
vi.mock("../../database/routes/host-plugin-settings.js", () => ({
hostSettingsPlugins: () => h.manifests,
withHostPluginSettings: async (
host: Record<string, unknown>,
viewerId?: string,
) => {
h.viewerCalls.push([host, viewerId]);
return h.visible ? { ...host, pluginSettings: h.visible } : host;
},
}));
vi.mock("../../plugins/registry.js", () => ({
consume: (key: string) => h.hooks.get(key),
@@ -65,11 +74,27 @@ const VAULT = {
},
} as unknown as PluginManifest;
const TUNNELS = {
id: "tunnels",
contributes: {
settings: {
host: {
fields: [
{ key: "enableTunnel", type: "boolean", labelKey: "k" },
{ key: "tunnelConnections", type: "custom", labelKey: "k" },
],
},
},
},
} as unknown as PluginManifest;
beforeEach(() => {
h.stored.clear();
h.writes.length = 0;
h.hooks.clear();
h.manifests = [VAULT];
h.visible = null;
h.viewerCalls.length = 0;
h.hooks.set("vault.hostSettingsSync", {
exportValue: (key: string, value: unknown) =>
key === "profileId" && value === 4 ? "profile-sync-4" : value,
@@ -87,6 +112,25 @@ describe("host plugin settings over sync", () => {
});
});
it("exports only what a shared host's viewer sees", async () => {
h.manifests = [VAULT, TUNNELS];
h.visible = {
vault: { token: { set: true } },
tunnels: { enableTunnel: true, tunnelConnections: [{ sourcePort: 22 }] },
};
expect(
await exportHostPluginSettings(1, {
userId: "viewer",
permissionLevel: "connect",
}),
).toEqual({
tunnels: { enableTunnel: true, tunnelConnections: [{ sourcePort: 22 }] },
});
expect(h.viewerCalls).toEqual([
[{ id: 1, isShared: true, permissionLevel: "connect" }, "viewer"],
]);
});
it("imports them back to local ids", async () => {
await importHostPluginSettings(9, {
vault: { profileId: "profile-sync-4", token: "ignored" },