diff --git a/server/db/pg/schema/schema.ts b/server/db/pg/schema/schema.ts index fd291c512..1df6871a3 100644 --- a/server/db/pg/schema/schema.ts +++ b/server/db/pg/schema/schema.ts @@ -160,7 +160,13 @@ export const resources = pgTable("resources", { postAuthPath: text("postAuthPath"), health: varchar("health").default("unknown"), // "healthy", "unhealthy", "unknown" wildcard: boolean("wildcard").notNull().default(false), - browserAccessType: text("browserAccessType").default("http") // rdp, ssh, http, vnc + browserAccessType: text("browserAccessType").default("http"), // rdp, ssh, http, vnc + pamMode: varchar("pamMode", { length: 32 }) + .$type<"passthrough" | "push">() + .default("passthrough"), + authDaemonMode: varchar("authDaemonMode", { length: 32 }) + .$type<"site" | "remote" | "native">() + .default("site") }); export const labels = pgTable("labels", { diff --git a/server/db/sqlite/schema/schema.ts b/server/db/sqlite/schema/schema.ts index 113f4c443..04eba1c21 100644 --- a/server/db/sqlite/schema/schema.ts +++ b/server/db/sqlite/schema/schema.ts @@ -181,7 +181,13 @@ export const resources = sqliteTable("resources", { postAuthPath: text("postAuthPath"), health: text("health").default("unknown"), // "healthy", "unhealthy", "unknown" wildcard: integer("wildcard", { mode: "boolean" }).notNull().default(false), - browserAccessType: text("browserAccessType").default("http") // rdp, ssh, http, vnc + browserAccessType: text("browserAccessType").default("http"), // rdp, ssh, http, vnc + pamMode: text("pamMode") + .$type<"passthrough" | "push">() + .default("passthrough"), + authDaemonMode: text("authDaemonMode") + .$type<"site" | "remote" | "native">() + .default("site") }); export const labels = sqliteTable("labels", { diff --git a/server/routers/resource/getBrowserTarget.ts b/server/routers/resource/getBrowserTarget.ts index 3ea1c4aa2..f18419a95 100644 --- a/server/routers/resource/getBrowserTarget.ts +++ b/server/routers/resource/getBrowserTarget.ts @@ -21,6 +21,11 @@ export type GetBrowserTargetResponse = { ip: string; port: number; authToken: string; + orgId: string; + resourceId: number; + niceId: string; + pamMode: "passthrough" | "push" | null; + authDaemonMode: "site" | "remote" | "native" | null; }; export async function getBrowserTarget( @@ -47,7 +52,12 @@ export async function getBrowserTarget( .select({ destination: browserGatewayTarget.destination, destinationPort: browserGatewayTarget.destinationPort, - authToken: browserGatewayTarget.authToken + authToken: browserGatewayTarget.authToken, + resourceId: resources.resourceId, + niceId: resources.niceId, + orgId: resources.orgId, + pamMode: resources.pamMode, + authDaemonMode: resources.authDaemonMode }) .from(browserGatewayTarget) .innerJoin( @@ -57,7 +67,7 @@ export async function getBrowserTarget( .where(eq(resources.fullDomain, fullDomain)) .limit(1); - const decryptAuthToken = decrypt( + const decryptedAuthToken = decrypt( browserTarget.authToken, config.getRawConfig().server.secret! ); @@ -75,7 +85,12 @@ export async function getBrowserTarget( data: { ip: browserTarget.destination, port: browserTarget.destinationPort, - authToken: decryptAuthToken + authToken: decryptedAuthToken, + pamMode: browserTarget.pamMode, + authDaemonMode: browserTarget.authDaemonMode, + orgId: browserTarget.orgId, + resourceId: browserTarget.resourceId, + niceId: browserTarget.niceId }, success: true, error: false, diff --git a/src/app/ssh/SshClient.tsx b/src/app/ssh/SshClient.tsx index d8a07770f..9472e9bee 100644 --- a/src/app/ssh/SshClient.tsx +++ b/src/app/ssh/SshClient.tsx @@ -6,12 +6,8 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; - -type Target = { - ip: string; - port: number; - authToken: string; -}; +import type { SignSshKeyResponse } from "@server/private/routers/ssh"; +import { GetBrowserTargetResponse } from "@server/routers/resource"; type FormState = { username: string; @@ -19,12 +15,23 @@ type FormState = { privateKey: string; }; +type ConnectCredentials = { + username: string; + password?: string; + privateKey?: string; + certificate?: string; +}; + export default function SshClient({ target, - error + error, + signedKeyData, + privateKey: signedPrivateKey }: { - target: Target | null; + target: GetBrowserTargetResponse | null; error: string | null; + signedKeyData?: SignSshKeyResponse | null; + privateKey?: string | null; }) { const STORAGE_KEY = "pangolin_ssh_credentials"; @@ -148,7 +155,19 @@ export default function SshClient({ }; }, []); - function connect() { + // Auto-connect when signed key data is provided (push PAM mode). + useEffect(() => { + if (signedKeyData && signedPrivateKey && target) { + connect({ + username: signedKeyData.sshUsername, + privateKey: signedPrivateKey, + certificate: signedKeyData.certificate + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + function connect(override?: ConnectCredentials) { setConnectError(null); setConnecting(true); @@ -158,11 +177,16 @@ export default function SshClient({ return; } + const username = override?.username ?? form.username; + const password = override?.password ?? form.password; + const privateKey = override?.privateKey ?? form.privateKey; + const certificate = override?.certificate; + const proxyAddress = `${window.location.protocol === "https:" ? "wss" : "ws"}://${window.location.host}/gateway/ssh`; const url = new URL(proxyAddress); url.searchParams.set("host", target.ip ?? ""); url.searchParams.set("port", String(target.port ?? 22)); - url.searchParams.set("username", form.username); + url.searchParams.set("username", username); url.searchParams.set("authToken", target.authToken ?? ""); const ws = new WebSocket(url.toString(), ["ssh"]); @@ -174,14 +198,17 @@ export default function SshClient({ ws.send( JSON.stringify({ type: "auth", - password: form.password, - privateKey: form.privateKey + password, + privateKey, + certificate }) ); - try { - localStorage.setItem(STORAGE_KEY, JSON.stringify(form)); - } catch { - // ignore + if (!override) { + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(form)); + } catch { + // ignore + } } setConnecting(false); setConnected(true); @@ -240,6 +267,43 @@ export default function SshClient({ ); } + // In push mode, show a connecting/connected state without the login form. + if (signedKeyData && signedPrivateKey) { + return ( +
+ {connectError + ? connectError + : connecting + ? "Connecting…" + : "Initializing…"} +
+