mirror of
https://github.com/fosrl/pangolin.git
synced 2026-06-05 23:28:44 +00:00
Native ssh push users is working
This commit is contained in:
@@ -19,6 +19,7 @@ import * as license from "#private/routers/license";
|
|||||||
import * as resource from "#private/routers/resource";
|
import * as resource from "#private/routers/resource";
|
||||||
import * as browserTarget from "#private/routers/browserGatewayTarget";
|
import * as browserTarget from "#private/routers/browserGatewayTarget";
|
||||||
import * as ssh from "#private/routers/ssh";
|
import * as ssh from "#private/routers/ssh";
|
||||||
|
import * as ws from "@server/routers/ws";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
verifySessionUserMiddleware,
|
verifySessionUserMiddleware,
|
||||||
@@ -52,4 +53,10 @@ internalRouter.post(
|
|||||||
ssh.signSshKey
|
ssh.signSshKey
|
||||||
);
|
);
|
||||||
|
|
||||||
|
internalRouter.get(
|
||||||
|
"/ws/round-trip-message/:messageId",
|
||||||
|
verifyUserFromResourceSessionMiddleware,
|
||||||
|
ws.checkRoundTripMessage
|
||||||
|
);
|
||||||
|
|
||||||
internalRouter.get("/resource/browser-target", browserTarget.getBrowserTarget);
|
internalRouter.get("/resource/browser-target", browserTarget.getBrowserTarget);
|
||||||
|
|||||||
@@ -19,12 +19,18 @@ import {
|
|||||||
logsDb,
|
logsDb,
|
||||||
newts,
|
newts,
|
||||||
roles,
|
roles,
|
||||||
|
roleResources,
|
||||||
roleSiteResources,
|
roleSiteResources,
|
||||||
|
resources,
|
||||||
roundTripMessageTracker,
|
roundTripMessageTracker,
|
||||||
siteResources,
|
siteResources,
|
||||||
siteNetworks,
|
siteNetworks,
|
||||||
|
targets,
|
||||||
userOrgs,
|
userOrgs,
|
||||||
sites
|
sites,
|
||||||
|
Resource,
|
||||||
|
SiteResource,
|
||||||
|
browserGatewayTarget
|
||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
import { logAccessAudit } from "#private/lib/logAccessAudit";
|
import { logAccessAudit } from "#private/lib/logAccessAudit";
|
||||||
import { isLicensedOrSubscribed } from "#private/lib/isLicencedOrSubscribed";
|
import { isLicensedOrSubscribed } from "#private/lib/isLicencedOrSubscribed";
|
||||||
@@ -35,6 +41,7 @@ import createHttpError from "http-errors";
|
|||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import { and, eq, inArray, or } from "drizzle-orm";
|
import { and, eq, inArray, or } from "drizzle-orm";
|
||||||
|
import { canUserAccessResource } from "@server/auth/canUserAccessResource";
|
||||||
import { canUserAccessSiteResource } from "@server/auth/canUserAccessSiteResource";
|
import { canUserAccessSiteResource } from "@server/auth/canUserAccessSiteResource";
|
||||||
import { signPublicKey, getOrgCAKeys } from "@server/lib/sshCA";
|
import { signPublicKey, getOrgCAKeys } from "@server/lib/sshCA";
|
||||||
import config from "@server/lib/config";
|
import config from "@server/lib/config";
|
||||||
@@ -50,7 +57,8 @@ const bodySchema = z
|
|||||||
publicKey: z.string().nonempty(),
|
publicKey: z.string().nonempty(),
|
||||||
resourceId: z.number().int().positive().optional(),
|
resourceId: z.number().int().positive().optional(),
|
||||||
resource: z.string().nonempty().optional(), // this is either the nice id or the alias
|
resource: z.string().nonempty().optional(), // this is either the nice id or the alias
|
||||||
username: z.string().nonempty().optional()
|
username: z.string().nonempty().optional(),
|
||||||
|
type: z.enum(["public", "private"]).default("private")
|
||||||
})
|
})
|
||||||
.refine(
|
.refine(
|
||||||
(data) => {
|
(data) => {
|
||||||
@@ -111,6 +119,7 @@ export async function signSshKey(
|
|||||||
const {
|
const {
|
||||||
publicKey,
|
publicKey,
|
||||||
resourceId,
|
resourceId,
|
||||||
|
type,
|
||||||
resource: resourceQueryString,
|
resource: resourceQueryString,
|
||||||
username
|
username
|
||||||
} = parsedBody.data;
|
} = parsedBody.data;
|
||||||
@@ -175,18 +184,25 @@ export async function signSshKey(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify the resource exists and belongs to the org
|
let matchingResources: SiteResource[] | Resource[] = [];
|
||||||
// Build the where clause dynamically based on which field is provided
|
// Verify the resource exists and belongs to the org.
|
||||||
|
// Build the where clause dynamically based on which field is provided.
|
||||||
let whereClause;
|
let whereClause;
|
||||||
if (resourceId !== undefined) {
|
if (resourceId !== undefined) {
|
||||||
whereClause = eq(siteResources.siteResourceId, resourceId);
|
whereClause =
|
||||||
|
type === "private"
|
||||||
|
? eq(siteResources.siteResourceId, resourceId)
|
||||||
|
: eq(resources.resourceId, resourceId);
|
||||||
} else if (resourceQueryString !== undefined) {
|
} else if (resourceQueryString !== undefined) {
|
||||||
whereClause = or(
|
whereClause =
|
||||||
eq(siteResources.niceId, resourceQueryString),
|
type === "private"
|
||||||
eq(siteResources.alias, resourceQueryString)
|
? or(
|
||||||
);
|
eq(siteResources.niceId, resourceQueryString),
|
||||||
|
eq(siteResources.alias, resourceQueryString)
|
||||||
|
)
|
||||||
|
: eq(resources.niceId, resourceQueryString);
|
||||||
} else {
|
} else {
|
||||||
// This should never happen due to the schema validation, but TypeScript doesn't know that
|
// This should never happen due to the schema validation, but TypeScript doesn't know that.
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.BAD_REQUEST,
|
HttpCode.BAD_REQUEST,
|
||||||
@@ -195,18 +211,25 @@ export async function signSshKey(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const resources = await db
|
if (type === "private") {
|
||||||
.select()
|
matchingResources = await db
|
||||||
.from(siteResources)
|
.select()
|
||||||
.where(and(whereClause, eq(siteResources.orgId, orgId)));
|
.from(siteResources)
|
||||||
|
.where(and(whereClause, eq(siteResources.orgId, orgId)));
|
||||||
|
} else {
|
||||||
|
matchingResources = await db
|
||||||
|
.select()
|
||||||
|
.from(resources)
|
||||||
|
.where(and(whereClause, eq(resources.orgId, orgId)));
|
||||||
|
}
|
||||||
|
|
||||||
if (!resources || resources.length === 0) {
|
if (!matchingResources || matchingResources.length === 0) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(HttpCode.NOT_FOUND, `Resource not found`)
|
createHttpError(HttpCode.NOT_FOUND, `Resource not found`)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resources.length > 1) {
|
if (matchingResources.length > 1) {
|
||||||
// error but this should not happen because the nice id cant contain a dot and the alias has to have a dot and both have to be unique within the org so there should never be multiple matches
|
// error but this should not happen because the nice id cant contain a dot and the alias has to have a dot and both have to be unique within the org so there should never be multiple matches
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
@@ -216,7 +239,11 @@ export async function signSshKey(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const resource = resources[0];
|
const resource = matchingResources[0];
|
||||||
|
const normalizedResourceId =
|
||||||
|
type === "private"
|
||||||
|
? (resource as SiteResource).siteResourceId
|
||||||
|
: (resource as Resource).resourceId;
|
||||||
|
|
||||||
if (resource.orgId !== orgId) {
|
if (resource.orgId !== orgId) {
|
||||||
return next(
|
return next(
|
||||||
@@ -237,11 +264,18 @@ export async function signSshKey(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if the user has access to the resource
|
// Check if the user has access to the resource
|
||||||
const hasAccess = await canUserAccessSiteResource({
|
const hasAccess =
|
||||||
userId: userId,
|
type === "private"
|
||||||
resourceId: resource.siteResourceId,
|
? await canUserAccessSiteResource({
|
||||||
roleIds
|
userId: userId,
|
||||||
});
|
resourceId: (resource as SiteResource).siteResourceId,
|
||||||
|
roleIds
|
||||||
|
})
|
||||||
|
: await canUserAccessResource({
|
||||||
|
userId: userId,
|
||||||
|
resourceId: (resource as Resource).resourceId,
|
||||||
|
roleIds
|
||||||
|
});
|
||||||
|
|
||||||
if (!hasAccess) {
|
if (!hasAccess) {
|
||||||
return next(
|
return next(
|
||||||
@@ -252,12 +286,56 @@ export async function signSshKey(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const sitesFromNetworks = await db
|
const siteAgentHostMap = new Map<number, string>();
|
||||||
.select({ siteId: siteNetworks.siteId })
|
let siteIds: number[] = [];
|
||||||
.from(siteNetworks)
|
|
||||||
.where(eq(siteNetworks.networkId, resource.networkId!));
|
|
||||||
|
|
||||||
const siteIds = sitesFromNetworks.map((site) => site.siteId);
|
if (type === "private") {
|
||||||
|
const privateResource = resource as SiteResource;
|
||||||
|
const sitesFromNetworks = await db
|
||||||
|
.select({ siteId: siteNetworks.siteId })
|
||||||
|
.from(siteNetworks)
|
||||||
|
.where(eq(siteNetworks.networkId, privateResource.networkId!));
|
||||||
|
|
||||||
|
siteIds = sitesFromNetworks.map((site) => site.siteId);
|
||||||
|
for (const siteId of siteIds) {
|
||||||
|
if (privateResource.destination) {
|
||||||
|
siteAgentHostMap.set(siteId, privateResource.destination);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const publicResource = resource as Resource;
|
||||||
|
const targetRows = await db
|
||||||
|
.select({
|
||||||
|
siteId: browserGatewayTarget.siteId,
|
||||||
|
ip: browserGatewayTarget.destination
|
||||||
|
})
|
||||||
|
.from(browserGatewayTarget)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(
|
||||||
|
browserGatewayTarget.resourceId,
|
||||||
|
publicResource.resourceId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (targetRows.length === 0) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.NOT_FOUND,
|
||||||
|
"No enabled targets found for the resource"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const targetRow of targetRows) {
|
||||||
|
if (!siteAgentHostMap.has(targetRow.siteId)) {
|
||||||
|
siteAgentHostMap.set(targetRow.siteId, targetRow.ip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
siteIds = Array.from(siteAgentHostMap.keys());
|
||||||
|
}
|
||||||
|
|
||||||
let expiresIn: number | undefined;
|
let expiresIn: number | undefined;
|
||||||
let messageIds: number[] = [];
|
let messageIds: number[] = [];
|
||||||
@@ -374,27 +452,50 @@ export async function signSshKey(
|
|||||||
usernameToUse = userOrg.pamUsername;
|
usernameToUse = userOrg.pamUsername;
|
||||||
}
|
}
|
||||||
|
|
||||||
const roleRows = await db
|
const roleRows =
|
||||||
.select({
|
type === "private"
|
||||||
sshSudoCommands: roles.sshSudoCommands,
|
? await db
|
||||||
sshUnixGroups: roles.sshUnixGroups,
|
.select({
|
||||||
sshCreateHomeDir: roles.sshCreateHomeDir,
|
sshSudoCommands: roles.sshSudoCommands,
|
||||||
sshSudoMode: roles.sshSudoMode
|
sshUnixGroups: roles.sshUnixGroups,
|
||||||
})
|
sshCreateHomeDir: roles.sshCreateHomeDir,
|
||||||
.from(roles)
|
sshSudoMode: roles.sshSudoMode
|
||||||
.innerJoin(
|
})
|
||||||
roleSiteResources,
|
.from(roles)
|
||||||
eq(roleSiteResources.roleId, roles.roleId)
|
.innerJoin(
|
||||||
)
|
roleSiteResources,
|
||||||
.where(
|
eq(roleSiteResources.roleId, roles.roleId)
|
||||||
and(
|
)
|
||||||
inArray(roles.roleId, roleIds),
|
.where(
|
||||||
eq(
|
and(
|
||||||
roleSiteResources.siteResourceId,
|
inArray(roles.roleId, roleIds),
|
||||||
resource.siteResourceId
|
eq(
|
||||||
)
|
roleSiteResources.siteResourceId,
|
||||||
)
|
(resource as SiteResource).siteResourceId
|
||||||
);
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
: await db
|
||||||
|
.select({
|
||||||
|
sshSudoCommands: roles.sshSudoCommands,
|
||||||
|
sshUnixGroups: roles.sshUnixGroups,
|
||||||
|
sshCreateHomeDir: roles.sshCreateHomeDir,
|
||||||
|
sshSudoMode: roles.sshSudoMode
|
||||||
|
})
|
||||||
|
.from(roles)
|
||||||
|
.innerJoin(
|
||||||
|
roleResources,
|
||||||
|
eq(roleResources.roleId, roles.roleId)
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
inArray(roles.roleId, roleIds),
|
||||||
|
eq(
|
||||||
|
roleResources.resourceId,
|
||||||
|
(resource as Resource).resourceId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
const parsedSudoCommands: string[] = [];
|
const parsedSudoCommands: string[] = [];
|
||||||
const parsedGroupsSet = new Set<string>();
|
const parsedGroupsSet = new Set<string>();
|
||||||
@@ -480,6 +581,16 @@ export async function signSshKey(
|
|||||||
|
|
||||||
messageIds.push(message.messageId);
|
messageIds.push(message.messageId);
|
||||||
|
|
||||||
|
const agentHost = siteAgentHostMap.get(siteId);
|
||||||
|
if (!agentHost) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
`Unable to determine agent host for site ${siteId}`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
await sendToClient(newt.newtId, {
|
await sendToClient(newt.newtId, {
|
||||||
type: `newt/pam/connection`,
|
type: `newt/pam/connection`,
|
||||||
data: {
|
data: {
|
||||||
@@ -489,7 +600,7 @@ export async function signSshKey(
|
|||||||
authDaemonMode: resource.authDaemonMode, // site, remote, native where native is the pty mode
|
authDaemonMode: resource.authDaemonMode, // site, remote, native where native is the pty mode
|
||||||
externalAuthDaemon:
|
externalAuthDaemon:
|
||||||
resource.authDaemonMode === "remote", // keep this for backward compatibility but new newts are using the authDaemonMode field
|
resource.authDaemonMode === "remote", // keep this for backward compatibility but new newts are using the authDaemonMode field
|
||||||
agentHost: resource.destination,
|
agentHost,
|
||||||
caCert: caKeys.publicKeyOpenSSH,
|
caCert: caKeys.publicKeyOpenSSH,
|
||||||
username: usernameToUse,
|
username: usernameToUse,
|
||||||
niceId: resource.niceId,
|
niceId: resource.niceId,
|
||||||
@@ -526,10 +637,19 @@ export async function signSshKey(
|
|||||||
resource.authDaemonMode === "site" ||
|
resource.authDaemonMode === "site" ||
|
||||||
resource.authDaemonMode === "remote"
|
resource.authDaemonMode === "remote"
|
||||||
) {
|
) {
|
||||||
if (resource.alias && resource.alias != "") {
|
if (type === "private") {
|
||||||
sshHost = resource.alias;
|
const privateResource = resource as SiteResource;
|
||||||
|
if (privateResource.alias && privateResource.alias !== "") {
|
||||||
|
sshHost = privateResource.alias;
|
||||||
|
} else {
|
||||||
|
sshHost = privateResource.destination || "";
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
sshHost = resource.destination || "";
|
const publicResource = resource as Resource;
|
||||||
|
sshHost =
|
||||||
|
publicResource.fullDomain ||
|
||||||
|
publicResource.subdomain ||
|
||||||
|
publicResource.niceId;
|
||||||
}
|
}
|
||||||
} else if (resource.authDaemonMode === "native") {
|
} else if (resource.authDaemonMode === "native") {
|
||||||
if (siteIds.length > 1) {
|
if (siteIds.length > 1) {
|
||||||
@@ -587,7 +707,8 @@ export async function signSshKey(
|
|||||||
actorId: req.user?.userId ?? "",
|
actorId: req.user?.userId ?? "",
|
||||||
action: ActionsEnum.signSshKey,
|
action: ActionsEnum.signSshKey,
|
||||||
metadata: JSON.stringify({
|
metadata: JSON.stringify({
|
||||||
resourceId: resource.siteResourceId,
|
resourceId: normalizedResourceId,
|
||||||
|
resourceType: type,
|
||||||
resource: resource.name,
|
resource: resource.name,
|
||||||
siteIds: siteIds
|
siteIds: siteIds
|
||||||
})
|
})
|
||||||
@@ -597,7 +718,14 @@ export async function signSshKey(
|
|||||||
action: true,
|
action: true,
|
||||||
type: "ssh",
|
type: "ssh",
|
||||||
orgId: orgId,
|
orgId: orgId,
|
||||||
siteResourceId: resource.siteResourceId,
|
resourceId:
|
||||||
|
type === "public"
|
||||||
|
? (resource as Resource).resourceId
|
||||||
|
: undefined,
|
||||||
|
siteResourceId:
|
||||||
|
type === "private"
|
||||||
|
? (resource as SiteResource).siteResourceId
|
||||||
|
: undefined,
|
||||||
user: req.user
|
user: req.user
|
||||||
? { username: req.user.username ?? "", userId: req.user.userId }
|
? { username: req.user.username ?? "", userId: req.user.userId }
|
||||||
: undefined,
|
: undefined,
|
||||||
@@ -618,7 +746,7 @@ export async function signSshKey(
|
|||||||
messageId: messageIds[0], // just pick the first one for backward compatibility with older olms
|
messageId: messageIds[0], // just pick the first one for backward compatibility with older olms
|
||||||
sshUsername: usernameToUse,
|
sshUsername: usernameToUse,
|
||||||
sshHost: sshHost, // just pick the first one for backward compatibility with older olms
|
sshHost: sshHost, // just pick the first one for backward compatibility with older olms
|
||||||
resourceId: resource.siteResourceId,
|
resourceId: normalizedResourceId,
|
||||||
siteIds: siteIds,
|
siteIds: siteIds,
|
||||||
siteId: siteIds[0], // just pick the first one for backward compatibility with older olms
|
siteId: siteIds[0], // just pick the first one for backward compatibility with older olms
|
||||||
keyId: cert?.keyId,
|
keyId: cert?.keyId,
|
||||||
|
|||||||
@@ -337,15 +337,6 @@ export default function SshClient({
|
|||||||
)}
|
)}
|
||||||
{connected && (
|
{connected && (
|
||||||
<div className="fixed inset-0 z-50 flex flex-col bg-neutral-900">
|
<div className="fixed inset-0 z-50 flex flex-col bg-neutral-900">
|
||||||
<div className="flex flex-wrap items-center gap-2 bg-black p-2 text-white">
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="destructive"
|
|
||||||
onClick={disconnect}
|
|
||||||
>
|
|
||||||
Terminate
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
ref={terminalRef}
|
ref={terminalRef}
|
||||||
className="flex-1 overflow-hidden"
|
className="flex-1 overflow-hidden"
|
||||||
|
|||||||
@@ -7,6 +7,62 @@ import { SignSshKeyResponse } from "@server/private/routers/ssh";
|
|||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
import AuthFooter from "@app/components/AuthFooter";
|
import AuthFooter from "@app/components/AuthFooter";
|
||||||
|
|
||||||
|
const pollInitialDelayMs = 250;
|
||||||
|
const pollStartIntervalMs = 250;
|
||||||
|
const pollBackoffSteps = 6;
|
||||||
|
|
||||||
|
type RoundTripMessageResponse = {
|
||||||
|
messageId: number;
|
||||||
|
complete: boolean;
|
||||||
|
sentAt: number | string;
|
||||||
|
receivedAt: number | string | null;
|
||||||
|
error: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
function sleep(ms: number): Promise<void> {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForRoundTripCompletion(
|
||||||
|
messageIds: number[],
|
||||||
|
cookieHeader: string
|
||||||
|
): Promise<void> {
|
||||||
|
if (messageIds.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await sleep(pollInitialDelayMs);
|
||||||
|
|
||||||
|
let interval = pollStartIntervalMs;
|
||||||
|
for (let i = 0; i <= pollBackoffSteps; i++) {
|
||||||
|
for (const messageId of messageIds) {
|
||||||
|
const res = await priv.get<AxiosResponse<RoundTripMessageResponse>>(
|
||||||
|
`/ws/round-trip-message/${messageId}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Cookie: cookieHeader
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const message = res.data.data;
|
||||||
|
if (message.complete) {
|
||||||
|
if (message.error) {
|
||||||
|
throw new Error(message.error);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i < pollBackoffSteps) {
|
||||||
|
await sleep(interval);
|
||||||
|
interval *= 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("Timed out waiting for round-trip message completion");
|
||||||
|
}
|
||||||
|
|
||||||
function generateEphemeralKeyPair(): {
|
function generateEphemeralKeyPair(): {
|
||||||
privateKeyPem: string;
|
privateKeyPem: string;
|
||||||
publicKeyOpenSSH: string;
|
publicKeyOpenSSH: string;
|
||||||
@@ -51,6 +107,7 @@ export default async function SshPage() {
|
|||||||
const headersList = await headers();
|
const headersList = await headers();
|
||||||
const host = headersList.get("host") || "";
|
const host = headersList.get("host") || "";
|
||||||
const hostname = host.split(":")[0];
|
const hostname = host.split(":")[0];
|
||||||
|
const cookieHeader = headersList.get("cookie") || "";
|
||||||
|
|
||||||
let target: GetBrowserTargetResponse | null = null;
|
let target: GetBrowserTargetResponse | null = null;
|
||||||
let signedKeyData: SignSshKeyResponse | null = null;
|
let signedKeyData: SignSshKeyResponse | null = null;
|
||||||
@@ -72,11 +129,25 @@ export default async function SshPage() {
|
|||||||
`/org/${target.orgId}/ssh/sign-key`,
|
`/org/${target.orgId}/ssh/sign-key`,
|
||||||
{
|
{
|
||||||
publicKey: publicKeyOpenSSH,
|
publicKey: publicKeyOpenSSH,
|
||||||
resource: target.niceId
|
resourceId: target.resourceId,
|
||||||
|
type: "public"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Cookie: cookieHeader
|
||||||
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
signedKeyData = res.data.data;
|
signedKeyData = res.data.data;
|
||||||
console.log("Received signed SSH key:", signedKeyData);
|
|
||||||
|
const messageIds =
|
||||||
|
signedKeyData.messageIds.length > 0
|
||||||
|
? signedKeyData.messageIds
|
||||||
|
: signedKeyData.messageId
|
||||||
|
? [signedKeyData.messageId]
|
||||||
|
: [];
|
||||||
|
|
||||||
|
await waitForRoundTripCompletion(messageIds, cookieHeader);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error signing SSH key:", err);
|
console.error("Error signing SSH key:", err);
|
||||||
error = "Failed to sign SSH key for PAM push authentication.";
|
error = "Failed to sign SSH key for PAM push authentication.";
|
||||||
|
|||||||
Reference in New Issue
Block a user