mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-31 15:45:23 +00:00
fix(sync): send shared hosts with text columns in their stored form (#1631)
resolveHostById turns forceKeyboardInteractive into a boolean, and the shared-host export passed it on as is. A desktop could not bind the boolean into the text column, so every shared host failed to apply.
This commit is contained in:
1 parent
1396b3a70c
commit
c044dbde87
2 files changed
+71
-7
No files matched your search
@@ -5,7 +5,7 @@
|
|||||||
* an entity in without renumbering; anything referenced syncs first.
|
* an entity in without renumbering; anything referenced syncs first.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { and, eq } from "drizzle-orm";
|
import { and, eq, getTableColumns } from "drizzle-orm";
|
||||||
import type { SyncRow } from "@termix-ssh/plugin-sdk/backend";
|
import type { SyncRow } from "@termix-ssh/plugin-sdk/backend";
|
||||||
import type { PluginManifest } from "@termix-ssh/plugin-sdk/manifest";
|
import type { PluginManifest } from "@termix-ssh/plugin-sdk/manifest";
|
||||||
import {
|
import {
|
||||||
@@ -493,6 +493,25 @@ const SHARED_HOST_DROP = [
|
|||||||
...HOST_LOCAL_FIELDS,
|
...HOST_LOCAL_FIELDS,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const HOST_TEXT_COLUMNS = new Set(
|
||||||
|
Object.entries(getTableColumns(hosts))
|
||||||
|
.filter(([, column]) => column.dataType === "string")
|
||||||
|
.map(([field]) => field),
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A resolved host field back in the form its column stores, as an owned host
|
||||||
|
* sends it: the resolver parses JSON columns and turns the "true"/"false" of
|
||||||
|
* forceKeyboardInteractive into a boolean, which a text column cannot bind.
|
||||||
|
*/
|
||||||
|
function storedForm(field: string, value: unknown): unknown {
|
||||||
|
if (value !== null && typeof value === "object") return JSON.stringify(value);
|
||||||
|
if (typeof value === "boolean" && HOST_TEXT_COLUMNS.has(field)) {
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
async function loadSharedHosts(userId: string): Promise<SyncRow[]> {
|
async function loadSharedHosts(userId: string): Promise<SyncRow[]> {
|
||||||
const {
|
const {
|
||||||
createCurrentRbacAccessRepository,
|
createCurrentRbacAccessRepository,
|
||||||
@@ -540,10 +559,7 @@ async function loadSharedHosts(userId: string): Promise<SyncRow[]> {
|
|||||||
const wire: SyncRow = {};
|
const wire: SyncRow = {};
|
||||||
for (const [field, value] of Object.entries(host)) {
|
for (const [field, value] of Object.entries(host)) {
|
||||||
if (SHARED_HOST_DROP.includes(field)) continue;
|
if (SHARED_HOST_DROP.includes(field)) continue;
|
||||||
wire[field] =
|
wire[field] = storedForm(field, value);
|
||||||
value !== null && typeof value === "object"
|
|
||||||
? JSON.stringify(value)
|
|
||||||
: value;
|
|
||||||
}
|
}
|
||||||
wire.jumpHosts = await jumpHostsToSyncIds(host.jumpHosts);
|
wire.jumpHosts = await jumpHostsToSyncIds(host.jumpHosts);
|
||||||
wire.pluginSettings = await exportHostPluginSettings(hostId, {
|
wire.pluginSettings = await exportHostPluginSettings(hostId, {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ const state = vi.hoisted(() => ({
|
|||||||
current: null as null | { drizzle: unknown },
|
current: null as null | { drizzle: unknown },
|
||||||
server: null as null | { drizzle: unknown },
|
server: null as null | { drizzle: unknown },
|
||||||
allow: true,
|
allow: true,
|
||||||
|
sharedHostIds: new Set<number>(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const SERVER_USER = "srv-user";
|
const SERVER_USER = "srv-user";
|
||||||
@@ -38,13 +39,18 @@ vi.mock("../../utils/system-crypto.js", () => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
vi.mock("../../utils/data-crypto.js", () => ({
|
vi.mock("../../utils/data-crypto.js", () => ({
|
||||||
DataCrypto: { getUserDataKey: () => Buffer.alloc(32, 3) },
|
DataCrypto: {
|
||||||
|
getUserDataKey: () => Buffer.alloc(32, 3),
|
||||||
|
decryptRecord: (_table: string, record: unknown) => record,
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
vi.mock("../../utils/permission-manager.js", () => ({
|
vi.mock("../../utils/permission-manager.js", () => ({
|
||||||
PermissionManager: {
|
PermissionManager: {
|
||||||
getInstance: () => ({
|
getInstance: () => ({
|
||||||
hasPermission: async () => state.allow,
|
hasPermission: async () => state.allow,
|
||||||
canAccessHost: async () => ({ hasAccess: false }),
|
canAccessHost: async (_userId: string, hostId: number) => ({
|
||||||
|
hasAccess: state.sharedHostIds.has(hostId),
|
||||||
|
}),
|
||||||
getUserPermissions: async () => [],
|
getUserPermissions: async () => [],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
@@ -239,6 +245,7 @@ beforeEach(async () => {
|
|||||||
resetFeed();
|
resetFeed();
|
||||||
resetSeqAllocator();
|
resetSeqAllocator();
|
||||||
state.allow = true;
|
state.allow = true;
|
||||||
|
state.sharedHostIds.clear();
|
||||||
server = await side(SERVER_USER);
|
server = await side(SERVER_USER);
|
||||||
state.server = server.ctx;
|
state.server = server.ctx;
|
||||||
desktops = [await side("desk-a"), await side("desk-b")];
|
desktops = [await side("desk-a"), await side("desk-b")];
|
||||||
@@ -480,6 +487,47 @@ describe("after linking", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("hosts shared with the account", () => {
|
||||||
|
it("arrive on the desktop with their text columns as stored", async () => {
|
||||||
|
const [desk] = desktops;
|
||||||
|
await on(server, async () => {
|
||||||
|
await server.db.run(
|
||||||
|
sql`INSERT INTO users (id, username, password_hash, is_admin) VALUES ('owner', 'owner', '', 0)`,
|
||||||
|
);
|
||||||
|
await db(server).insert(schema.hosts).values({
|
||||||
|
userId: "owner",
|
||||||
|
name: "theirs",
|
||||||
|
ip: "10.0.0.5",
|
||||||
|
port: 22,
|
||||||
|
username: "root",
|
||||||
|
authType: "none",
|
||||||
|
forceKeyboardInteractive: "true",
|
||||||
|
syncId: "host-theirs",
|
||||||
|
});
|
||||||
|
const [host] = await db(server).select().from(schema.hosts);
|
||||||
|
await db(server).insert(schema.hostAccess).values({
|
||||||
|
hostId: host.id,
|
||||||
|
userId: SERVER_USER,
|
||||||
|
grantedBy: "owner",
|
||||||
|
permissionLevel: "view",
|
||||||
|
});
|
||||||
|
state.sharedHostIds.add(host.id);
|
||||||
|
markChanged();
|
||||||
|
});
|
||||||
|
|
||||||
|
const engine = await linkDesktop(desk);
|
||||||
|
await pass(desk, engine);
|
||||||
|
|
||||||
|
const [copy] = await on(desk, () => hostsOf(desk));
|
||||||
|
expect(copy?.name).toBe("theirs");
|
||||||
|
expect(copy.forceKeyboardInteractive).toBe("true");
|
||||||
|
expect(JSON.parse(copy.sharedSource!)).toMatchObject({
|
||||||
|
owner: "owner",
|
||||||
|
permissionLevel: "view",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("two desktops on one account", () => {
|
describe("two desktops on one account", () => {
|
||||||
it("converge on edits, creates and deletes made offline on each", async () => {
|
it("converge on edits, creates and deletes made offline on each", async () => {
|
||||||
const [a, b] = desktops;
|
const [a, b] = desktops;
|
||||||
|
|||||||
Reference in new issue
Block a user