mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 22:55:24 +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
|
- 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
|
- 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
|
- 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
|
- 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
|
- 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;
|
: value;
|
||||||
}
|
}
|
||||||
wire.jumpHosts = await jumpHostsToSyncIds(host.jumpHosts);
|
wire.jumpHosts = await jumpHostsToSyncIds(host.jumpHosts);
|
||||||
|
wire.pluginSettings = await exportHostPluginSettings(hostId, {
|
||||||
|
userId,
|
||||||
|
permissionLevel,
|
||||||
|
});
|
||||||
wire.syncId = host.syncId;
|
wire.syncId = host.syncId;
|
||||||
wire.shared = {
|
wire.shared = {
|
||||||
hostId,
|
hostId,
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { databaseLogger } from "../utils/logger.js";
|
|||||||
import type { DefaultOverrides } from "../../types/host-defaults.js";
|
import type { DefaultOverrides } from "../../types/host-defaults.js";
|
||||||
import {
|
import {
|
||||||
hostSettingsPlugins,
|
hostSettingsPlugins,
|
||||||
|
withHostPluginSettings,
|
||||||
type HostPluginSettings,
|
type HostPluginSettings,
|
||||||
} from "../database/routes/host-plugin-settings.js";
|
} from "../database/routes/host-plugin-settings.js";
|
||||||
|
|
||||||
@@ -23,19 +24,45 @@ export interface PluginHostSettingsSync {
|
|||||||
importValue?: (key: string, value: unknown) => unknown | Promise<unknown>;
|
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(
|
export async function exportHostPluginSettings(
|
||||||
hostId: number,
|
hostId: number,
|
||||||
|
viewer?: SharedHostViewer,
|
||||||
): Promise<HostPluginSettings> {
|
): Promise<HostPluginSettings> {
|
||||||
|
const visible = viewer
|
||||||
|
? (((
|
||||||
|
await withHostPluginSettings(
|
||||||
|
{
|
||||||
|
id: hostId,
|
||||||
|
isShared: true,
|
||||||
|
permissionLevel: viewer.permissionLevel,
|
||||||
|
},
|
||||||
|
viewer.userId,
|
||||||
|
)
|
||||||
|
).pluginSettings as HostPluginSettings | undefined) ?? {})
|
||||||
|
: null;
|
||||||
const result: HostPluginSettings = {};
|
const result: HostPluginSettings = {};
|
||||||
for (const manifest of hostSettingsPlugins()) {
|
for (const manifest of hostSettingsPlugins()) {
|
||||||
const fields = declaredFields(manifest, "host").filter(
|
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;
|
if (fields.length === 0) continue;
|
||||||
const values = await getAllSettings(manifest, "host", hostId, {
|
const values = visible
|
||||||
redactSecrets: true,
|
? visible[manifest.id]
|
||||||
});
|
: await getAllSettings(manifest, "host", hostId, {
|
||||||
|
redactSecrets: true,
|
||||||
|
});
|
||||||
const hook = consume<PluginHostSettingsSync>(
|
const hook = consume<PluginHostSettingsSync>(
|
||||||
`${manifest.id}.hostSettingsSync`,
|
`${manifest.id}.hostSettingsSync`,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,10 +11,19 @@ const h = vi.hoisted(() => ({
|
|||||||
writes: [] as Array<[string, number, string, unknown]>,
|
writes: [] as Array<[string, number, string, unknown]>,
|
||||||
hooks: new Map<string, unknown>(),
|
hooks: new Map<string, unknown>(),
|
||||||
manifests: [] as PluginManifest[],
|
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", () => ({
|
vi.mock("../../database/routes/host-plugin-settings.js", () => ({
|
||||||
hostSettingsPlugins: () => h.manifests,
|
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", () => ({
|
vi.mock("../../plugins/registry.js", () => ({
|
||||||
consume: (key: string) => h.hooks.get(key),
|
consume: (key: string) => h.hooks.get(key),
|
||||||
@@ -65,11 +74,27 @@ const VAULT = {
|
|||||||
},
|
},
|
||||||
} as unknown as PluginManifest;
|
} 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(() => {
|
beforeEach(() => {
|
||||||
h.stored.clear();
|
h.stored.clear();
|
||||||
h.writes.length = 0;
|
h.writes.length = 0;
|
||||||
h.hooks.clear();
|
h.hooks.clear();
|
||||||
h.manifests = [VAULT];
|
h.manifests = [VAULT];
|
||||||
|
h.visible = null;
|
||||||
|
h.viewerCalls.length = 0;
|
||||||
h.hooks.set("vault.hostSettingsSync", {
|
h.hooks.set("vault.hostSettingsSync", {
|
||||||
exportValue: (key: string, value: unknown) =>
|
exportValue: (key: string, value: unknown) =>
|
||||||
key === "profileId" && value === 4 ? "profile-sync-4" : value,
|
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 () => {
|
it("imports them back to local ids", async () => {
|
||||||
await importHostPluginSettings(9, {
|
await importHostPluginSettings(9, {
|
||||||
vault: { profileId: "profile-sync-4", token: "ignored" },
|
vault: { profileId: "profile-sync-4", token: "ignored" },
|
||||||
|
|||||||
Reference in new issue
Block a user