mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-11-01 16:15:24 +00:00
fix: remote desktop logins missing from shared hosts on desktop
This commit is contained in:
1 parent
08e1eea2f3
commit
2646b649d7
4 files changed
+114
-2
No files matched your search
+1
-1
@@ -44,7 +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
|
- Tunnels, remote desktop logins 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
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import {
|
|||||||
} from "./host-plugin-settings.js";
|
} from "./host-plugin-settings.js";
|
||||||
import {
|
import {
|
||||||
exportProtocolLogins,
|
exportProtocolLogins,
|
||||||
|
exportSharedProtocolLogins,
|
||||||
importProtocolLogins,
|
importProtocolLogins,
|
||||||
} from "./host-protocol-auth.js";
|
} from "./host-protocol-auth.js";
|
||||||
import {
|
import {
|
||||||
@@ -477,7 +478,7 @@ async function writeAccountProfile(userId: string, wire: SyncRow) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Host columns a shared copy never carries. */
|
/** Host columns a shared copy never carries as the owner has them. */
|
||||||
const SHARED_HOST_DROP = [
|
const SHARED_HOST_DROP = [
|
||||||
"id",
|
"id",
|
||||||
"userId",
|
"userId",
|
||||||
@@ -550,6 +551,7 @@ async function loadSharedHosts(userId: string): Promise<SyncRow[]> {
|
|||||||
userId,
|
userId,
|
||||||
permissionLevel,
|
permissionLevel,
|
||||||
});
|
});
|
||||||
|
wire.protocolAuth = await exportSharedProtocolLogins(host, userId);
|
||||||
wire.syncId = host.syncId;
|
wire.syncId = host.syncId;
|
||||||
wire.shared = {
|
wire.shared = {
|
||||||
hostId,
|
hostId,
|
||||||
|
|||||||
@@ -6,12 +6,15 @@
|
|||||||
|
|
||||||
import type { SyncRow } from "@termix/plugin-sdk/backend";
|
import type { SyncRow } from "@termix/plugin-sdk/backend";
|
||||||
import type { HostProtocolLogin } from "../database/repositories/host-protocol-auth-repository.js";
|
import type { HostProtocolLogin } from "../database/repositories/host-protocol-auth-repository.js";
|
||||||
|
import { createCurrentHostProtocolAuthRepository } from "../database/repositories/factory.js";
|
||||||
import {
|
import {
|
||||||
fromPortableLogin,
|
fromPortableLogin,
|
||||||
listProtocolLogins,
|
listProtocolLogins,
|
||||||
replaceProtocolLogins,
|
replaceProtocolLogins,
|
||||||
|
resolveRecipientProtocolLogin,
|
||||||
toPortableLogins,
|
toPortableLogins,
|
||||||
} from "../hosts/protocol-auth/protocol-auth.js";
|
} from "../hosts/protocol-auth/protocol-auth.js";
|
||||||
|
import { findHostProtocol } from "../hosts/protocol-auth/registry.js";
|
||||||
import { syncLogger } from "../utils/logger.js";
|
import { syncLogger } from "../utils/logger.js";
|
||||||
|
|
||||||
type ResolveSyncId = (entityType: string, id: number) => Promise<string | null>;
|
type ResolveSyncId = (entityType: string, id: number) => Promise<string | null>;
|
||||||
@@ -38,6 +41,45 @@ export async function exportProtocolLogins(
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The logins a user a host is shared with may use, for their desktop's copy:
|
||||||
|
* their own override or the owner's shared snapshot, never the owner's own
|
||||||
|
* secret. Each goes as a direct login since its credential is not theirs.
|
||||||
|
*/
|
||||||
|
export async function exportSharedProtocolLogins(
|
||||||
|
host: Record<string, unknown>,
|
||||||
|
userId: string,
|
||||||
|
): Promise<SyncRow> {
|
||||||
|
const out: SyncRow = {};
|
||||||
|
const hostId = Number(host.id);
|
||||||
|
const rows =
|
||||||
|
await createCurrentHostProtocolAuthRepository().listRowsForHost(hostId);
|
||||||
|
for (const row of rows) {
|
||||||
|
const declared = findHostProtocol(row.protocol);
|
||||||
|
if (!declared) continue;
|
||||||
|
try {
|
||||||
|
const login = await resolveRecipientProtocolLogin(host, userId, declared);
|
||||||
|
out[row.protocol] = {
|
||||||
|
authType: login.authType === "none" ? "none" : "direct",
|
||||||
|
username: login.username || null,
|
||||||
|
password: login.password || null,
|
||||||
|
fields: Object.fromEntries(
|
||||||
|
Object.entries(login.fields).filter(([, value]) => value),
|
||||||
|
),
|
||||||
|
credentialSyncId: null,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
syncLogger.warn("Could not include a shared host's protocol login", {
|
||||||
|
operation: "sync_shared_host_protocol_auth",
|
||||||
|
hostId,
|
||||||
|
protocol: row.protocol,
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Writes what a synced host carried. A row from a peer that sends no
|
* Writes what a synced host carried. A row from a peer that sends no
|
||||||
* protocolAuth leaves the stored logins alone.
|
* protocolAuth leaves the stored logins alone.
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
|||||||
const state = vi.hoisted(() => ({
|
const state = vi.hoisted(() => ({
|
||||||
logins: [] as Array<Record<string, unknown>>,
|
logins: [] as Array<Record<string, unknown>>,
|
||||||
replaced: null as null | { ownerId: string; hostId: number; logins: unknown },
|
replaced: null as null | { ownerId: string; hostId: number; logins: unknown },
|
||||||
|
rows: [] as Array<{ protocol: string }>,
|
||||||
|
recipient: new Map<string, Record<string, unknown> | Error>(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock(
|
vi.mock(
|
||||||
@@ -20,6 +22,15 @@ vi.mock(
|
|||||||
return {
|
return {
|
||||||
...actual,
|
...actual,
|
||||||
listProtocolLogins: async () => state.logins,
|
listProtocolLogins: async () => state.logins,
|
||||||
|
resolveRecipientProtocolLogin: async (
|
||||||
|
_host: unknown,
|
||||||
|
_userId: string,
|
||||||
|
declared: { id: string },
|
||||||
|
) => {
|
||||||
|
const login = state.recipient.get(declared.id);
|
||||||
|
if (login instanceof Error) throw login;
|
||||||
|
return login;
|
||||||
|
},
|
||||||
replaceProtocolLogins: async (
|
replaceProtocolLogins: async (
|
||||||
ownerId: string,
|
ownerId: string,
|
||||||
hostId: number,
|
hostId: number,
|
||||||
@@ -30,6 +41,11 @@ vi.mock(
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
vi.mock("../../database/repositories/factory.js", () => ({
|
||||||
|
createCurrentHostProtocolAuthRepository: () => ({
|
||||||
|
listRowsForHost: async () => state.rows,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
vi.mock("../../database/db/index.js", () => ({
|
vi.mock("../../database/db/index.js", () => ({
|
||||||
getDb: () => null,
|
getDb: () => null,
|
||||||
getSqlite: () => null,
|
getSqlite: () => null,
|
||||||
@@ -37,12 +53,15 @@ vi.mock("../../database/db/index.js", () => ({
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
exportProtocolLogins,
|
exportProtocolLogins,
|
||||||
|
exportSharedProtocolLogins,
|
||||||
importProtocolLogins,
|
importProtocolLogins,
|
||||||
} from "../../sync/host-protocol-auth.js";
|
} from "../../sync/host-protocol-auth.js";
|
||||||
import { setHostProtocolSource } from "../../hosts/protocol-auth/registry.js";
|
import { setHostProtocolSource } from "../../hosts/protocol-auth/registry.js";
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
state.replaced = null;
|
state.replaced = null;
|
||||||
|
state.rows = [];
|
||||||
|
state.recipient.clear();
|
||||||
state.logins = [
|
state.logins = [
|
||||||
{
|
{
|
||||||
protocol: "spice",
|
protocol: "spice",
|
||||||
@@ -129,3 +148,52 @@ describe("host protocol logins over sync", () => {
|
|||||||
expect(state.replaced).toBeNull();
|
expect(state.replaced).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("shared host protocol logins over sync", () => {
|
||||||
|
it("sends what the recipient may use as direct logins", async () => {
|
||||||
|
state.rows = [{ protocol: "spice" }, { protocol: "unknown" }];
|
||||||
|
state.recipient.set("spice", {
|
||||||
|
authType: "credential",
|
||||||
|
username: "alice",
|
||||||
|
password: "pw",
|
||||||
|
fields: { display: "2", ticket: "" },
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
await exportSharedProtocolLogins({ id: 1, userId: "owner" }, "viewer"),
|
||||||
|
).toEqual({
|
||||||
|
spice: {
|
||||||
|
authType: "direct",
|
||||||
|
username: "alice",
|
||||||
|
password: "pw",
|
||||||
|
fields: { display: "2" },
|
||||||
|
credentialSyncId: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps a none login and skips one that fails", async () => {
|
||||||
|
setHostProtocolSource(() => [
|
||||||
|
{ id: "spice", credentialFields: [], pluginId: "p", pluginName: "P" },
|
||||||
|
{ id: "vnc", credentialFields: [], pluginId: "p", pluginName: "P" },
|
||||||
|
]);
|
||||||
|
state.rows = [{ protocol: "spice" }, { protocol: "vnc" }];
|
||||||
|
state.recipient.set("spice", {
|
||||||
|
authType: "none",
|
||||||
|
username: "",
|
||||||
|
password: "",
|
||||||
|
fields: {},
|
||||||
|
});
|
||||||
|
state.recipient.set("vnc", new Error("locked"));
|
||||||
|
expect(
|
||||||
|
await exportSharedProtocolLogins({ id: 1, userId: "owner" }, "viewer"),
|
||||||
|
).toEqual({
|
||||||
|
spice: {
|
||||||
|
authType: "none",
|
||||||
|
username: null,
|
||||||
|
password: null,
|
||||||
|
fields: {},
|
||||||
|
credentialSyncId: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in new issue
Block a user