mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 06:05:24 +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.
|
||||
*/
|
||||
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { and, eq, getTableColumns } from "drizzle-orm";
|
||||
import type { SyncRow } from "@termix-ssh/plugin-sdk/backend";
|
||||
import type { PluginManifest } from "@termix-ssh/plugin-sdk/manifest";
|
||||
import {
|
||||
@@ -493,6 +493,25 @@ const SHARED_HOST_DROP = [
|
||||
...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[]> {
|
||||
const {
|
||||
createCurrentRbacAccessRepository,
|
||||
@@ -540,10 +559,7 @@ async function loadSharedHosts(userId: string): Promise<SyncRow[]> {
|
||||
const wire: SyncRow = {};
|
||||
for (const [field, value] of Object.entries(host)) {
|
||||
if (SHARED_HOST_DROP.includes(field)) continue;
|
||||
wire[field] =
|
||||
value !== null && typeof value === "object"
|
||||
? JSON.stringify(value)
|
||||
: value;
|
||||
wire[field] = storedForm(field, value);
|
||||
}
|
||||
wire.jumpHosts = await jumpHostsToSyncIds(host.jumpHosts);
|
||||
wire.pluginSettings = await exportHostPluginSettings(hostId, {
|
||||
|
||||
@@ -14,6 +14,7 @@ const state = vi.hoisted(() => ({
|
||||
current: null as null | { drizzle: unknown },
|
||||
server: null as null | { drizzle: unknown },
|
||||
allow: true,
|
||||
sharedHostIds: new Set<number>(),
|
||||
}));
|
||||
|
||||
const SERVER_USER = "srv-user";
|
||||
@@ -38,13 +39,18 @@ vi.mock("../../utils/system-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", () => ({
|
||||
PermissionManager: {
|
||||
getInstance: () => ({
|
||||
hasPermission: async () => state.allow,
|
||||
canAccessHost: async () => ({ hasAccess: false }),
|
||||
canAccessHost: async (_userId: string, hostId: number) => ({
|
||||
hasAccess: state.sharedHostIds.has(hostId),
|
||||
}),
|
||||
getUserPermissions: async () => [],
|
||||
}),
|
||||
},
|
||||
@@ -239,6 +245,7 @@ beforeEach(async () => {
|
||||
resetFeed();
|
||||
resetSeqAllocator();
|
||||
state.allow = true;
|
||||
state.sharedHostIds.clear();
|
||||
server = await side(SERVER_USER);
|
||||
state.server = server.ctx;
|
||||
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", () => {
|
||||
it("converge on edits, creates and deletes made offline on each", async () => {
|
||||
const [a, b] = desktops;
|
||||
|
||||
Reference in new issue
Block a user