mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-30 23:25:24 +00:00
fix(sync): resolve shared hosts for connect-only recipients (#1568)
This commit is contained in:
1 parent
f73c4b5ad6
commit
f412a28bb8
4 files changed
+86
-3
No files matched your search
@@ -288,6 +288,7 @@ export function stripSensitiveFields(
|
|||||||
// Connection essentials a connect-level recipient is allowed to see.
|
// Connection essentials a connect-level recipient is allowed to see.
|
||||||
const CONNECT_LEVEL_FIELDS = new Set([
|
const CONNECT_LEVEL_FIELDS = new Set([
|
||||||
"id",
|
"id",
|
||||||
|
"syncId",
|
||||||
"userId",
|
"userId",
|
||||||
"ownerId",
|
"ownerId",
|
||||||
"ownerUsername",
|
"ownerUsername",
|
||||||
|
|||||||
@@ -360,7 +360,7 @@ router.get("/v2/events", authenticateJWT, (req: Request, res: Response) => {
|
|||||||
* /sync/v2/hosts/{syncId}:
|
* /sync/v2/hosts/{syncId}:
|
||||||
* get:
|
* get:
|
||||||
* summary: This server's id for a host, by its sync id
|
* summary: This server's id for a host, by its sync id
|
||||||
* description: For a linked desktop that asks the server to act on a host (a tunnel through it), which needs the server's own id. Only for hosts the caller can see.
|
* description: For a linked desktop that asks the server to act on a host (a tunnel through it), which needs the server's own id. Only for hosts the caller can connect to.
|
||||||
* tags:
|
* tags:
|
||||||
* - Sync
|
* - Sync
|
||||||
* parameters:
|
* parameters:
|
||||||
@@ -373,7 +373,7 @@ router.get("/v2/events", authenticateJWT, (req: Request, res: Response) => {
|
|||||||
* 200:
|
* 200:
|
||||||
* description: The host's id and name.
|
* description: The host's id and name.
|
||||||
* 404:
|
* 404:
|
||||||
* description: No such host the caller can see.
|
* description: No such host the caller can connect to.
|
||||||
*/
|
*/
|
||||||
router.get(
|
router.get(
|
||||||
"/v2/hosts/:syncId",
|
"/v2/hosts/:syncId",
|
||||||
@@ -393,7 +393,7 @@ router.get(
|
|||||||
const access = await PermissionManager.getInstance().canAccessHost(
|
const access = await PermissionManager.getInstance().canAccessHost(
|
||||||
userId,
|
userId,
|
||||||
hostId,
|
hostId,
|
||||||
"view",
|
"connect",
|
||||||
);
|
);
|
||||||
if (!access.hasAccess)
|
if (!access.hasAccess)
|
||||||
return res.status(404).json({ error: "Not found" });
|
return res.status(404).json({ error: "Not found" });
|
||||||
|
|||||||
@@ -497,3 +497,11 @@ describe("transformHostResponse terminal fields", () => {
|
|||||||
expect(shared.terminalConfig).toEqual({ keepaliveInterval: 9 });
|
expect(shared.terminalConfig).toEqual({ keepaliveInterval: 9 });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("preserves a shared host sync identity for connect-only recipients", () => {
|
||||||
|
const result = sanitizeHostForRecipient(
|
||||||
|
{ id: 9, syncId: "remote-host-41", password: "secret", notes: "private" },
|
||||||
|
"connect",
|
||||||
|
);
|
||||||
|
expect(result).toEqual({ id: 9, syncId: "remote-host-41" });
|
||||||
|
});
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import { beforeEach, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
const { findHostIdBySyncId, canAccessHost } = vi.hoisted(() => ({
|
||||||
|
findHostIdBySyncId: vi.fn(),
|
||||||
|
canAccessHost: vi.fn(),
|
||||||
|
}));
|
||||||
|
vi.mock("../../../utils/auth-manager.js", () => ({
|
||||||
|
AuthManager: { getInstance: () => ({ createAuthMiddleware: () => vi.fn() }) },
|
||||||
|
}));
|
||||||
|
vi.mock("../../../utils/logger.js", () => ({ syncLogger: { error: vi.fn() } }));
|
||||||
|
vi.mock("../../../utils/app-version.js", () => ({ getLocalVersion: vi.fn() }));
|
||||||
|
vi.mock("../../../plugins/index.js", () => ({ getPluginRuntime: vi.fn() }));
|
||||||
|
vi.mock("../../../database/repositories/factory.js", () => ({
|
||||||
|
createCurrentHostResolutionRepository: () => ({ findHostIdBySyncId }),
|
||||||
|
createCurrentHostRepository: () => ({
|
||||||
|
findById: async () => ({ name: "Shared host" }),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
vi.mock("../../../utils/permission-manager.js", () => ({
|
||||||
|
PermissionManager: { getInstance: () => ({ canAccessHost }) },
|
||||||
|
}));
|
||||||
|
vi.mock("../../../sync/entities.js", () => ({
|
||||||
|
registerCoreSyncEntities: vi.fn(),
|
||||||
|
}));
|
||||||
|
vi.mock("../../../sync/records.js", () => ({}));
|
||||||
|
vi.mock("../../../sync/server/feed.js", () => ({}));
|
||||||
|
vi.mock("../../../sync/server/push.js", () => ({}));
|
||||||
|
vi.mock("../../../database/routes/branding-settings.js", () => ({}));
|
||||||
|
|
||||||
|
import router from "../../../sync/server/routes.js";
|
||||||
|
|
||||||
|
const route = router.stack.find(
|
||||||
|
(layer) => layer.route?.path === "/v2/hosts/:syncId",
|
||||||
|
)!.route!;
|
||||||
|
const handler = route.stack[route.stack.length - 1].handle;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
findHostIdBySyncId.mockResolvedValue(41);
|
||||||
|
canAccessHost.mockImplementation(async (_user, _host, action) => ({
|
||||||
|
hasAccess: action === "connect",
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
async function lookup() {
|
||||||
|
const res = { status: vi.fn().mockReturnThis(), json: vi.fn() };
|
||||||
|
await handler(
|
||||||
|
{ userId: "recipient", params: { syncId: "host-sync-id" } } as never,
|
||||||
|
res as never,
|
||||||
|
vi.fn(),
|
||||||
|
);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
it("resolves the server ID for a connect-only recipient", async () => {
|
||||||
|
const res = await lookup();
|
||||||
|
expect(findHostIdBySyncId).toHaveBeenCalledWith("host-sync-id");
|
||||||
|
expect(canAccessHost).toHaveBeenCalledWith("recipient", 41, "connect");
|
||||||
|
expect(res.json).toHaveBeenCalledWith({ id: 41, name: "Shared host" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not disclose a host the caller cannot connect to", async () => {
|
||||||
|
canAccessHost.mockResolvedValue({ hasAccess: false });
|
||||||
|
const res = await lookup();
|
||||||
|
expect(res.status).toHaveBeenCalledWith(404);
|
||||||
|
expect(res.json).toHaveBeenCalledWith({ error: "Not found" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns 404 for a missing sync ID", async () => {
|
||||||
|
findHostIdBySyncId.mockResolvedValue(null);
|
||||||
|
const res = await lookup();
|
||||||
|
expect(res.status).toHaveBeenCalledWith(404);
|
||||||
|
expect(canAccessHost).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
Reference in new issue
Block a user