fix: prompt shared RDP users for credentials (#1345)

This commit is contained in:
ZacharyZcR
2026-08-27 06:38:56 +08:00
committed by GitHub
parent f848dee343
commit 6406c3a923
8 changed files with 129 additions and 8 deletions
@@ -346,6 +346,7 @@ const CONNECT_LEVEL_FIELDS = new Set([
"enableTelnet",
"sshPort",
"rdpPort",
"rdpAuthType",
"vncPort",
"telnetPort",
"defaultPath",
@@ -34,3 +34,31 @@ export function resolveRdpDomain(
? promptedDomain
: 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";
}
+16 -6
View File
@@ -6,7 +6,10 @@ import { withDriveSettings } from "./drive-settings.js";
import { guacLogger } from "../../utils/logger.js";
import { AuthManager } from "../../utils/auth-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 net from "net";
import crypto from "crypto";
@@ -25,7 +28,11 @@ import {
getRequestMeta,
} from "../../utils/audit-logger.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";
const router = express.Router();
@@ -322,6 +329,7 @@ router.post(
const hostRecord = host as Record<string, unknown>;
const hostRepository = hostResolutionRepository;
const isSharedConnection = host.userId !== userId;
let sharedAuthResolution: RecipientSharedHostAuthResolution | null = null;
if (isSharedConnection) {
// Recipients never read the owner's raw secrets; wipe them and use
@@ -341,6 +349,7 @@ router.post(
userId,
connectionType as AuthOverrideProtocol,
);
sharedAuthResolution = resolution;
const auth =
resolution.source === "personal-override"
? { ...resolution.credential, domain: null }
@@ -448,10 +457,11 @@ router.post(
let username: string;
let password: string;
const rdpAuthTypeForConnect = isSharedConnection
? null
: (host.rdpAuthType as string) ||
(host.rdpCredentialId ? "credential" : "direct");
const rdpAuthTypeForConnect = resolveRdpAuthTypeForConnect({
storedAuthType: host.rdpAuthType as string | null,
credentialId: host.rdpCredentialId as number | null,
sharedResolution: isSharedConnection ? sharedAuthResolution : undefined,
});
switch (connectionType) {
case "rdp":
@@ -394,6 +394,7 @@ describe("sanitizeHostForRecipient", () => {
{
...sharedHost,
permissionLevel: "connect",
rdpAuthType: "none",
authOverrides: {
ssh: {
credentialId: 9,
@@ -408,6 +409,7 @@ describe("sanitizeHostForRecipient", () => {
expect(result.ip).toBe("10.0.0.42");
expect(result.enableRdp).toBe(true);
expect(result.rdpPort).toBe(3389);
expect(result.rdpAuthType).toBe("none");
expect(result.permissionLevel).toBe("connect");
expect(result.shareSshAuth).toBe(true);
expect(result.authOverrides).toEqual({
@@ -1,6 +1,7 @@
import { describe, expect, it } from "vitest";
import {
buildRdpSettings,
resolveRdpAuthTypeForConnect,
resolveRdpDomain,
} from "../../../hosts/guacamole/rdp-settings.js";
@@ -52,3 +53,32 @@ describe("buildRdpSettings", () => {
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");
});
});
+7 -2
View File
@@ -46,6 +46,7 @@ import { ShareSessionModal } from "@/features/session-sharing/ShareSessionModal.
import type { SSHHost } from "@/types";
import { useConnectionDefaults } from "@/contexts/ConnectionDefaultsContext";
import { resolveConnectionDefaults } from "@/lib/connection-defaults";
import { needsRdpCredentialPrompt } from "@/features/guacamole/rdp-credential-prompt";
interface GuacamoleAppProps {
hostId?: string;
@@ -162,6 +163,7 @@ interface GuacamoleAppInnerProps {
| "domain"
| "guacamoleConfig"
| "rdpAuthType"
| "authOverrides"
| "syncId"
| "ip"
| "rdpPort"
@@ -230,8 +232,11 @@ const GuacamoleAppInner = React.forwardRef<
const resolvedProtocolForConnect = (protocol ??
hostConfig.connectionType ??
"rdp") as "rdp" | "vnc" | "telnet";
const needsCredentialPrompt =
resolvedProtocolForConnect === "rdp" && hostConfig.rdpAuthType === "none";
const needsCredentialPrompt = needsRdpCredentialPrompt({
protocol: resolvedProtocolForConnect,
rdpAuthType: hostConfig.rdpAuthType,
authOverrides: hostConfig.authOverrides,
});
const [promptedCredentials, setPromptedCredentials] = useState<{
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);
});
});