From 5b0423c8e0339c3613d1f65bb9808936cd97418d Mon Sep 17 00:00:00 2001 From: ZacharyZcR Date: Wed, 30 Sep 2026 22:50:32 +0800 Subject: [PATCH] fix(database): preserve RDP domain during startup backfill (#1497) --- .../database/db/rdp-domain-backfill.test.ts | 67 +++++++++++++++++++ .../upgrade/protocol-auth-migration.test.ts | 41 ++++++++++++ .../upgrade/protocol-auth-migration.ts | 3 +- 3 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 src/backend/tests/database/db/rdp-domain-backfill.test.ts diff --git a/src/backend/tests/database/db/rdp-domain-backfill.test.ts b/src/backend/tests/database/db/rdp-domain-backfill.test.ts new file mode 100644 index 000000000..c1f8637d4 --- /dev/null +++ b/src/backend/tests/database/db/rdp-domain-backfill.test.ts @@ -0,0 +1,67 @@ +import fs from "fs"; +import os from "os"; +import path from "path"; +import Database from "better-sqlite3"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +describe("RDP domain across database boots", () => { + let dataDir: string; + + beforeEach(() => { + dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "termix-rdp-domain-")); + vi.resetModules(); + vi.stubEnv("DATA_DIR", dataDir); + vi.stubEnv("DB_FILE_ENCRYPTION", "false"); + vi.stubEnv("ALLOW_EMPTY_DATA_DIR", "true"); + }); + + afterEach(() => { + vi.unstubAllEnvs(); + fs.rmSync(dataDir, { recursive: true, force: true }); + }); + + it.each([ + { saved: "CORP", legacy: null, expected: "CORP" }, + { saved: "CORP", legacy: "OLD", expected: "CORP" }, + { saved: "", legacy: "OLD", expected: "" }, + { saved: null, legacy: "LEGACY", expected: null }, + ])("preserves $saved with legacy domain $legacy across boots", async ({ + saved, + legacy, + expected, + }) => { + const seed = new Database(":memory:"); + seed.exec( + fs.readFileSync( + new URL("../../fixtures/sqlite-2.8-schema.sql", import.meta.url), + "utf8", + ), + ); + seed.exec(` + INSERT INTO users (id, username, password_hash) + VALUES ('owner', 'alice', 'hash'); + `); + seed.prepare(` + INSERT INTO ssh_data + (id, user_id, ip, port, username, auth_type, connection_type, + rdp_auth_type, rdp_user, rdp_password, rdp_domain, domain) + VALUES (1, 'owner', '192.0.2.1', 3389, '', 'password', 'rdp', + 'credential', NULL, NULL, ?, ?) + `).run(saved, legacy); + fs.writeFileSync(path.join(dataDir, "db.sqlite"), seed.serialize()); + seed.close(); + + for (let boot = 0; boot < 2; boot++) { + vi.resetModules(); + const db = await import("../../../database/db/index.js"); + await db.initializeDatabase(); + expect( + db.getSqlite() + .prepare("SELECT rdp_domain FROM ssh_data WHERE id = 1") + .pluck() + .get(), + ).toBe(expected); + await db.saveMemoryDatabaseToFile(); + } + }); +}); diff --git a/src/backend/tests/upgrade/protocol-auth-migration.test.ts b/src/backend/tests/upgrade/protocol-auth-migration.test.ts index 84856d700..b33e0737d 100644 --- a/src/backend/tests/upgrade/protocol-auth-migration.test.ts +++ b/src/backend/tests/upgrade/protocol-auth-migration.test.ts @@ -35,6 +35,7 @@ vi.mock("../../utils/data-crypto.js", () => ({ import { copiedMarker, + legacyLogin, runProtocolAuthMigration, } from "../../upgrade/protocol-auth-migration.js"; import { createCurrentHostProtocolAuthRepository } from "../../database/repositories/factory.js"; @@ -221,3 +222,43 @@ describe("runProtocolAuthMigration", () => { expect(await logins()).toHaveProperty("3:telnet"); }); }); + +describe("legacyLogin rdp domain", () => { + const rdp = { + id: "rdp", + user: "rdp_user", + password: "rdp_password", + passwordField: "rdpPassword", + credential: "rdp_credential_id", + authType: "rdp_auth_type", + domain: "rdp_domain", + }; + + it.each([ + { saved: "CORP", legacy: null, expected: { domain: "CORP" } }, + { saved: "CORP", legacy: "OLD", expected: { domain: "CORP" } }, + { saved: "", legacy: "OLD", expected: {} }, + { saved: null, legacy: "LEGACY", expected: { domain: "LEGACY" } }, + ])( + "keeps saved domain $saved over legacy $legacy for a credential host", + ({ saved, legacy, expected }) => { + const login = legacyLogin( + { + id: 1, + user_id: "owner", + connection_type: "rdp", + rdp_auth_type: "credential", + rdp_credential_id: 5, + rdp_user: null, + rdp_password: null, + rdp_domain: saved, + domain: legacy, + }, + rdp, + ownerKey, + () => true, + ); + expect(login?.fields).toEqual(expected); + }, + ); +}); diff --git a/src/backend/upgrade/protocol-auth-migration.ts b/src/backend/upgrade/protocol-auth-migration.ts index 8e73ef288..bad66b03e 100644 --- a/src/backend/upgrade/protocol-auth-migration.ts +++ b/src/backend/upgrade/protocol-auth-migration.ts @@ -139,7 +139,8 @@ export function legacyLogin( password = openLegacySecret(row.password, dek, recordId, ["password"]); domain = protocol.domain ? text(row.domain) : ""; if (!username && !password && !domain) return null; - } else if (protocol.domain && !domain) { + } else if (protocol.domain && row[protocol.domain] == null) { + // A saved empty domain was cleared on purpose, so only fill a missing one. domain = text(row.domain); }