mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-10 22:02:55 +00:00
fix: tunnels and host settings missing from shared hosts on desktop
This commit is contained in:
1 parent
5a373c3963
commit
08e1eea2f3
4 files changed
+81
-5
No files matched your search
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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" },
|
||||
|
||||
Reference in new issue
Block a user