mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-08-29 18:31:33 +00:00
fix: prompt shared RDP users for credentials (#1345)
This commit is contained in:
@@ -346,6 +346,7 @@ const CONNECT_LEVEL_FIELDS = new Set([
|
|||||||
"enableTelnet",
|
"enableTelnet",
|
||||||
"sshPort",
|
"sshPort",
|
||||||
"rdpPort",
|
"rdpPort",
|
||||||
|
"rdpAuthType",
|
||||||
"vncPort",
|
"vncPort",
|
||||||
"telnetPort",
|
"telnetPort",
|
||||||
"defaultPath",
|
"defaultPath",
|
||||||
|
|||||||
@@ -34,3 +34,31 @@ export function resolveRdpDomain(
|
|||||||
? promptedDomain
|
? promptedDomain
|
||||||
: storedDomain;
|
: storedDomain;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SharedRdpAuthResolution =
|
||||||
|
| { source: "personal-override" }
|
||||||
|
| { source: "owner-shared"; authType: string }
|
||||||
|
| { source: "secretless" }
|
||||||
|
| { source: "required" }
|
||||||
|
| null;
|
||||||
|
|
||||||
|
export function resolveRdpAuthTypeForConnect({
|
||||||
|
storedAuthType,
|
||||||
|
credentialId,
|
||||||
|
sharedResolution,
|
||||||
|
}: {
|
||||||
|
storedAuthType?: string | null;
|
||||||
|
credentialId?: number | null;
|
||||||
|
sharedResolution?: SharedRdpAuthResolution;
|
||||||
|
}): string {
|
||||||
|
if (sharedResolution === undefined) {
|
||||||
|
return storedAuthType || (credentialId ? "credential" : "direct");
|
||||||
|
}
|
||||||
|
if (sharedResolution?.source === "personal-override") return "credential";
|
||||||
|
if (sharedResolution?.source === "owner-shared") {
|
||||||
|
return sharedResolution.authType;
|
||||||
|
}
|
||||||
|
return sharedResolution?.source === "secretless" && storedAuthType === "none"
|
||||||
|
? "none"
|
||||||
|
: "direct";
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ import { withDriveSettings } from "./drive-settings.js";
|
|||||||
import { guacLogger } from "../../utils/logger.js";
|
import { guacLogger } from "../../utils/logger.js";
|
||||||
import { AuthManager } from "../../utils/auth-manager.js";
|
import { AuthManager } from "../../utils/auth-manager.js";
|
||||||
import { PermissionManager } from "../../utils/permission-manager.js";
|
import { PermissionManager } from "../../utils/permission-manager.js";
|
||||||
import { resolveRecipientSharedHostAuthentication } from "../../utils/shared-host-auth-resolver.js";
|
import {
|
||||||
|
resolveRecipientSharedHostAuthentication,
|
||||||
|
type RecipientSharedHostAuthResolution,
|
||||||
|
} from "../../utils/shared-host-auth-resolver.js";
|
||||||
import type { AuthOverrideProtocol } from "../../../types/auth-protocols.js";
|
import type { AuthOverrideProtocol } from "../../../types/auth-protocols.js";
|
||||||
import net from "net";
|
import net from "net";
|
||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
@@ -25,7 +28,11 @@ import {
|
|||||||
getRequestMeta,
|
getRequestMeta,
|
||||||
} from "../../utils/audit-logger.js";
|
} from "../../utils/audit-logger.js";
|
||||||
import { resolveJumpTunnelEndpoint } from "./jump-tunnel-endpoint.js";
|
import { resolveJumpTunnelEndpoint } from "./jump-tunnel-endpoint.js";
|
||||||
import { buildRdpSettings, resolveRdpDomain } from "./rdp-settings.js";
|
import {
|
||||||
|
buildRdpSettings,
|
||||||
|
resolveRdpAuthTypeForConnect,
|
||||||
|
resolveRdpDomain,
|
||||||
|
} from "./rdp-settings.js";
|
||||||
import { createMacosVncCompatibilityProxy } from "./macos-vnc-proxy.js";
|
import { createMacosVncCompatibilityProxy } from "./macos-vnc-proxy.js";
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
@@ -322,6 +329,7 @@ router.post(
|
|||||||
const hostRecord = host as Record<string, unknown>;
|
const hostRecord = host as Record<string, unknown>;
|
||||||
const hostRepository = hostResolutionRepository;
|
const hostRepository = hostResolutionRepository;
|
||||||
const isSharedConnection = host.userId !== userId;
|
const isSharedConnection = host.userId !== userId;
|
||||||
|
let sharedAuthResolution: RecipientSharedHostAuthResolution | null = null;
|
||||||
|
|
||||||
if (isSharedConnection) {
|
if (isSharedConnection) {
|
||||||
// Recipients never read the owner's raw secrets; wipe them and use
|
// Recipients never read the owner's raw secrets; wipe them and use
|
||||||
@@ -341,6 +349,7 @@ router.post(
|
|||||||
userId,
|
userId,
|
||||||
connectionType as AuthOverrideProtocol,
|
connectionType as AuthOverrideProtocol,
|
||||||
);
|
);
|
||||||
|
sharedAuthResolution = resolution;
|
||||||
const auth =
|
const auth =
|
||||||
resolution.source === "personal-override"
|
resolution.source === "personal-override"
|
||||||
? { ...resolution.credential, domain: null }
|
? { ...resolution.credential, domain: null }
|
||||||
@@ -448,10 +457,11 @@ router.post(
|
|||||||
let username: string;
|
let username: string;
|
||||||
let password: string;
|
let password: string;
|
||||||
|
|
||||||
const rdpAuthTypeForConnect = isSharedConnection
|
const rdpAuthTypeForConnect = resolveRdpAuthTypeForConnect({
|
||||||
? null
|
storedAuthType: host.rdpAuthType as string | null,
|
||||||
: (host.rdpAuthType as string) ||
|
credentialId: host.rdpCredentialId as number | null,
|
||||||
(host.rdpCredentialId ? "credential" : "direct");
|
sharedResolution: isSharedConnection ? sharedAuthResolution : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
switch (connectionType) {
|
switch (connectionType) {
|
||||||
case "rdp":
|
case "rdp":
|
||||||
|
|||||||
@@ -394,6 +394,7 @@ describe("sanitizeHostForRecipient", () => {
|
|||||||
{
|
{
|
||||||
...sharedHost,
|
...sharedHost,
|
||||||
permissionLevel: "connect",
|
permissionLevel: "connect",
|
||||||
|
rdpAuthType: "none",
|
||||||
authOverrides: {
|
authOverrides: {
|
||||||
ssh: {
|
ssh: {
|
||||||
credentialId: 9,
|
credentialId: 9,
|
||||||
@@ -408,6 +409,7 @@ describe("sanitizeHostForRecipient", () => {
|
|||||||
expect(result.ip).toBe("10.0.0.42");
|
expect(result.ip).toBe("10.0.0.42");
|
||||||
expect(result.enableRdp).toBe(true);
|
expect(result.enableRdp).toBe(true);
|
||||||
expect(result.rdpPort).toBe(3389);
|
expect(result.rdpPort).toBe(3389);
|
||||||
|
expect(result.rdpAuthType).toBe("none");
|
||||||
expect(result.permissionLevel).toBe("connect");
|
expect(result.permissionLevel).toBe("connect");
|
||||||
expect(result.shareSshAuth).toBe(true);
|
expect(result.shareSshAuth).toBe(true);
|
||||||
expect(result.authOverrides).toEqual({
|
expect(result.authOverrides).toEqual({
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import {
|
import {
|
||||||
buildRdpSettings,
|
buildRdpSettings,
|
||||||
|
resolveRdpAuthTypeForConnect,
|
||||||
resolveRdpDomain,
|
resolveRdpDomain,
|
||||||
} from "../../../hosts/guacamole/rdp-settings.js";
|
} from "../../../hosts/guacamole/rdp-settings.js";
|
||||||
|
|
||||||
@@ -52,3 +53,32 @@ describe("buildRdpSettings", () => {
|
|||||||
expect(resolveRdpDomain("none", undefined, "SAVED")).toBe("SAVED");
|
expect(resolveRdpDomain("none", undefined, "SAVED")).toBe("SAVED");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("resolveRdpAuthTypeForConnect", () => {
|
||||||
|
it("keeps prompt-on-connect authentication for a secretless recipient", () => {
|
||||||
|
expect(
|
||||||
|
resolveRdpAuthTypeForConnect({
|
||||||
|
storedAuthType: "none",
|
||||||
|
sharedResolution: { source: "secretless" },
|
||||||
|
}),
|
||||||
|
).toBe("none");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses a recipient override instead of prompting", () => {
|
||||||
|
expect(
|
||||||
|
resolveRdpAuthTypeForConnect({
|
||||||
|
storedAuthType: "none",
|
||||||
|
sharedResolution: { source: "personal-override" },
|
||||||
|
}),
|
||||||
|
).toBe("credential");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses authentication shared by the owner", () => {
|
||||||
|
expect(
|
||||||
|
resolveRdpAuthTypeForConnect({
|
||||||
|
storedAuthType: "credential",
|
||||||
|
sharedResolution: { source: "owner-shared", authType: "credential" },
|
||||||
|
}),
|
||||||
|
).toBe("credential");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ import { ShareSessionModal } from "@/features/session-sharing/ShareSessionModal.
|
|||||||
import type { SSHHost } from "@/types";
|
import type { SSHHost } from "@/types";
|
||||||
import { useConnectionDefaults } from "@/contexts/ConnectionDefaultsContext";
|
import { useConnectionDefaults } from "@/contexts/ConnectionDefaultsContext";
|
||||||
import { resolveConnectionDefaults } from "@/lib/connection-defaults";
|
import { resolveConnectionDefaults } from "@/lib/connection-defaults";
|
||||||
|
import { needsRdpCredentialPrompt } from "@/features/guacamole/rdp-credential-prompt";
|
||||||
|
|
||||||
interface GuacamoleAppProps {
|
interface GuacamoleAppProps {
|
||||||
hostId?: string;
|
hostId?: string;
|
||||||
@@ -162,6 +163,7 @@ interface GuacamoleAppInnerProps {
|
|||||||
| "domain"
|
| "domain"
|
||||||
| "guacamoleConfig"
|
| "guacamoleConfig"
|
||||||
| "rdpAuthType"
|
| "rdpAuthType"
|
||||||
|
| "authOverrides"
|
||||||
| "syncId"
|
| "syncId"
|
||||||
| "ip"
|
| "ip"
|
||||||
| "rdpPort"
|
| "rdpPort"
|
||||||
@@ -230,8 +232,11 @@ const GuacamoleAppInner = React.forwardRef<
|
|||||||
const resolvedProtocolForConnect = (protocol ??
|
const resolvedProtocolForConnect = (protocol ??
|
||||||
hostConfig.connectionType ??
|
hostConfig.connectionType ??
|
||||||
"rdp") as "rdp" | "vnc" | "telnet";
|
"rdp") as "rdp" | "vnc" | "telnet";
|
||||||
const needsCredentialPrompt =
|
const needsCredentialPrompt = needsRdpCredentialPrompt({
|
||||||
resolvedProtocolForConnect === "rdp" && hostConfig.rdpAuthType === "none";
|
protocol: resolvedProtocolForConnect,
|
||||||
|
rdpAuthType: hostConfig.rdpAuthType,
|
||||||
|
authOverrides: hostConfig.authOverrides,
|
||||||
|
});
|
||||||
|
|
||||||
const [promptedCredentials, setPromptedCredentials] = useState<{
|
const [promptedCredentials, setPromptedCredentials] = useState<{
|
||||||
username: string;
|
username: string;
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import type { HostAuthOverrides } from "@/types/auth-protocols";
|
||||||
|
|
||||||
|
export function needsRdpCredentialPrompt({
|
||||||
|
protocol,
|
||||||
|
rdpAuthType,
|
||||||
|
authOverrides,
|
||||||
|
}: {
|
||||||
|
protocol: "rdp" | "vnc" | "telnet";
|
||||||
|
rdpAuthType?: string;
|
||||||
|
authOverrides?: HostAuthOverrides;
|
||||||
|
}): boolean {
|
||||||
|
return (
|
||||||
|
protocol === "rdp" &&
|
||||||
|
rdpAuthType === "none" &&
|
||||||
|
!authOverrides?.rdp?.credentialId
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { needsRdpCredentialPrompt } from "@/features/guacamole/rdp-credential-prompt";
|
||||||
|
|
||||||
|
describe("needsRdpCredentialPrompt", () => {
|
||||||
|
it("prompts recipients when RDP has no saved authentication", () => {
|
||||||
|
expect(
|
||||||
|
needsRdpCredentialPrompt({ protocol: "rdp", rdpAuthType: "none" }),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not prompt when the recipient has a personal override", () => {
|
||||||
|
expect(
|
||||||
|
needsRdpCredentialPrompt({
|
||||||
|
protocol: "rdp",
|
||||||
|
rdpAuthType: "none",
|
||||||
|
authOverrides: {
|
||||||
|
rdp: { credentialId: 7, required: false, ownerAuthShared: true },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not prompt for other protocols", () => {
|
||||||
|
expect(
|
||||||
|
needsRdpCredentialPrompt({ protocol: "vnc", rdpAuthType: "none" }),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user