mirror of
https://github.com/fosrl/pangolin.git
synced 2026-05-27 11:12:55 +00:00
Support not push ssh method
This commit is contained in:
@@ -352,8 +352,11 @@ export const siteResources = pgTable("siteResources", {
|
|||||||
udpPortRangeString: varchar("udpPortRangeString").notNull().default("*"),
|
udpPortRangeString: varchar("udpPortRangeString").notNull().default("*"),
|
||||||
disableIcmp: boolean("disableIcmp").notNull().default(false),
|
disableIcmp: boolean("disableIcmp").notNull().default(false),
|
||||||
authDaemonPort: integer("authDaemonPort").default(22123),
|
authDaemonPort: integer("authDaemonPort").default(22123),
|
||||||
|
pamMode: varchar("pamMode", { length: 32 })
|
||||||
|
.$type<"passthrough" | "push">()
|
||||||
|
.default("passthrough"),
|
||||||
authDaemonMode: varchar("authDaemonMode", { length: 32 })
|
authDaemonMode: varchar("authDaemonMode", { length: 32 })
|
||||||
.$type<"site" | "remote">()
|
.$type<"site" | "remote" | "native">()
|
||||||
.default("site"),
|
.default("site"),
|
||||||
domainId: varchar("domainId").references(() => domains.domainId, {
|
domainId: varchar("domainId").references(() => domains.domainId, {
|
||||||
onDelete: "set null"
|
onDelete: "set null"
|
||||||
|
|||||||
@@ -387,8 +387,11 @@ export const siteResources = sqliteTable("siteResources", {
|
|||||||
.notNull()
|
.notNull()
|
||||||
.default(false),
|
.default(false),
|
||||||
authDaemonPort: integer("authDaemonPort").default(22123),
|
authDaemonPort: integer("authDaemonPort").default(22123),
|
||||||
|
pamMode: text("pamMode")
|
||||||
|
.$type<"passthrough" | "push">()
|
||||||
|
.default("passthrough"),
|
||||||
authDaemonMode: text("authDaemonMode")
|
authDaemonMode: text("authDaemonMode")
|
||||||
.$type<"site" | "remote">()
|
.$type<"site" | "remote" | "native">()
|
||||||
.default("site"),
|
.default("site"),
|
||||||
domainId: text("domainId").references(() => domains.domainId, {
|
domainId: text("domainId").references(() => domains.domainId, {
|
||||||
onDelete: "set null"
|
onDelete: "set null"
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ import {
|
|||||||
roundTripMessageTracker,
|
roundTripMessageTracker,
|
||||||
siteResources,
|
siteResources,
|
||||||
siteNetworks,
|
siteNetworks,
|
||||||
userOrgs
|
userOrgs,
|
||||||
|
sites
|
||||||
} 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";
|
||||||
@@ -48,7 +49,8 @@ const bodySchema = z
|
|||||||
.strictObject({
|
.strictObject({
|
||||||
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()
|
||||||
})
|
})
|
||||||
.refine(
|
.refine(
|
||||||
(data) => {
|
(data) => {
|
||||||
@@ -63,19 +65,19 @@ const bodySchema = z
|
|||||||
);
|
);
|
||||||
|
|
||||||
export type SignSshKeyResponse = {
|
export type SignSshKeyResponse = {
|
||||||
certificate: string;
|
certificate?: string;
|
||||||
messageIds: number[];
|
messageIds: number[];
|
||||||
messageId: number;
|
messageId?: number;
|
||||||
sshUsername: string;
|
sshUsername: string;
|
||||||
sshHost: string;
|
sshHost: string;
|
||||||
resourceId: number;
|
resourceId: number;
|
||||||
siteIds: number[];
|
siteIds: number[];
|
||||||
siteId: number;
|
siteId: number;
|
||||||
keyId: string;
|
keyId?: string;
|
||||||
validPrincipals: string[];
|
validPrincipals?: string[];
|
||||||
validAfter: string;
|
validAfter?: string;
|
||||||
validBefore: string;
|
validBefore?: string;
|
||||||
expiresIn: number;
|
expiresIn?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
// registry.registerPath({
|
// registry.registerPath({
|
||||||
@@ -126,7 +128,8 @@ export async function signSshKey(
|
|||||||
const {
|
const {
|
||||||
publicKey,
|
publicKey,
|
||||||
resourceId,
|
resourceId,
|
||||||
resource: resourceQueryString
|
resource: resourceQueryString,
|
||||||
|
username
|
||||||
} = parsedBody.data;
|
} = parsedBody.data;
|
||||||
const userId = req.user?.userId;
|
const userId = req.user?.userId;
|
||||||
const roleIds = req.userOrgRoleIds ?? [];
|
const roleIds = req.userOrgRoleIds ?? [];
|
||||||
@@ -174,101 +177,6 @@ export async function signSshKey(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let usernameToUse;
|
|
||||||
if (!userOrg.pamUsername) {
|
|
||||||
if (req.user?.email) {
|
|
||||||
// Extract username from email (first part before @)
|
|
||||||
usernameToUse = req.user?.email
|
|
||||||
.split("@")[0]
|
|
||||||
.replace(/[^a-zA-Z0-9_-]/g, "");
|
|
||||||
if (!usernameToUse) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.BAD_REQUEST,
|
|
||||||
"Unable to extract username from email"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else if (req.user?.username) {
|
|
||||||
usernameToUse = req.user.username;
|
|
||||||
// We need to clean out any spaces or special characters from the username to ensure it's valid for SSH certificates
|
|
||||||
usernameToUse = usernameToUse.replace(/[^a-zA-Z0-9_-]/g, "-");
|
|
||||||
if (!usernameToUse) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.BAD_REQUEST,
|
|
||||||
"Username is not valid for SSH certificate"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.BAD_REQUEST,
|
|
||||||
"User does not have a valid email or username for SSH certificate"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// prefix with p-
|
|
||||||
usernameToUse = `p-${usernameToUse}`;
|
|
||||||
|
|
||||||
// check if we have a existing user in this org with the same
|
|
||||||
const [existingUserWithSameName] = await db
|
|
||||||
.select()
|
|
||||||
.from(userOrgs)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(userOrgs.orgId, orgId),
|
|
||||||
eq(userOrgs.pamUsername, usernameToUse)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (existingUserWithSameName) {
|
|
||||||
let foundUniqueUsername = false;
|
|
||||||
for (let attempt = 0; attempt < 20; attempt++) {
|
|
||||||
const randomNum = Math.floor(Math.random() * 101); // 0 to 100
|
|
||||||
const candidateUsername = `${usernameToUse}${randomNum}`;
|
|
||||||
|
|
||||||
const [existingUser] = await db
|
|
||||||
.select()
|
|
||||||
.from(userOrgs)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(userOrgs.orgId, orgId),
|
|
||||||
eq(userOrgs.pamUsername, candidateUsername)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (!existingUser) {
|
|
||||||
usernameToUse = candidateUsername;
|
|
||||||
foundUniqueUsername = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!foundUniqueUsername) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.CONFLICT,
|
|
||||||
"Unable to generate a unique username for SSH certificate"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await db
|
|
||||||
.update(userOrgs)
|
|
||||||
.set({ pamUsername: usernameToUse })
|
|
||||||
.where(
|
|
||||||
and(eq(userOrgs.orgId, orgId), eq(userOrgs.userId, userId))
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
usernameToUse = userOrg.pamUsername;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get and decrypt the org's CA keys
|
// Get and decrypt the org's CA keys
|
||||||
const caKeys = await getOrgCAKeys(
|
const caKeys = await getOrgCAKeys(
|
||||||
orgId,
|
orgId,
|
||||||
@@ -361,90 +269,303 @@ export async function signSshKey(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const roleRows = await db
|
const sitesFromNetworks = await db
|
||||||
.select({
|
|
||||||
sshSudoCommands: roles.sshSudoCommands,
|
|
||||||
sshUnixGroups: roles.sshUnixGroups,
|
|
||||||
sshCreateHomeDir: roles.sshCreateHomeDir,
|
|
||||||
sshSudoMode: roles.sshSudoMode
|
|
||||||
})
|
|
||||||
.from(roles)
|
|
||||||
.innerJoin(
|
|
||||||
roleSiteResources,
|
|
||||||
eq(roleSiteResources.roleId, roles.roleId)
|
|
||||||
)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
inArray(roles.roleId, roleIds),
|
|
||||||
eq(
|
|
||||||
roleSiteResources.siteResourceId,
|
|
||||||
resource.siteResourceId
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const parsedSudoCommands: string[] = [];
|
|
||||||
const parsedGroupsSet = new Set<string>();
|
|
||||||
let homedir: boolean | null = null;
|
|
||||||
const sudoModeOrder = { none: 0, commands: 1, full: 2 };
|
|
||||||
let sudoMode: "none" | "commands" | "full" = "none";
|
|
||||||
for (const roleRow of roleRows) {
|
|
||||||
try {
|
|
||||||
const cmds = JSON.parse(roleRow?.sshSudoCommands ?? "[]");
|
|
||||||
if (Array.isArray(cmds)) parsedSudoCommands.push(...cmds);
|
|
||||||
} catch {
|
|
||||||
// skip
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const grps = JSON.parse(roleRow?.sshUnixGroups ?? "[]");
|
|
||||||
if (Array.isArray(grps))
|
|
||||||
grps.forEach((g: string) => parsedGroupsSet.add(g));
|
|
||||||
} catch {
|
|
||||||
// skip
|
|
||||||
}
|
|
||||||
if (roleRow?.sshCreateHomeDir === true) homedir = true;
|
|
||||||
const m = roleRow?.sshSudoMode ?? "none";
|
|
||||||
if (
|
|
||||||
sudoModeOrder[m as keyof typeof sudoModeOrder] >
|
|
||||||
sudoModeOrder[sudoMode]
|
|
||||||
) {
|
|
||||||
sudoMode = m as "none" | "commands" | "full";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const parsedGroups = Array.from(parsedGroupsSet);
|
|
||||||
if (homedir === null && roleRows.length > 0) {
|
|
||||||
homedir = roleRows[0].sshCreateHomeDir ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const sites = await db
|
|
||||||
.select({ siteId: siteNetworks.siteId })
|
.select({ siteId: siteNetworks.siteId })
|
||||||
.from(siteNetworks)
|
.from(siteNetworks)
|
||||||
.where(eq(siteNetworks.networkId, resource.networkId!));
|
.where(eq(siteNetworks.networkId, resource.networkId!));
|
||||||
|
|
||||||
const siteIds = sites.map((site) => site.siteId);
|
const siteIds = sitesFromNetworks.map((site) => site.siteId);
|
||||||
|
|
||||||
// Sign the public key
|
let expiresIn: number | undefined;
|
||||||
const now = BigInt(Math.floor(Date.now() / 1000));
|
let messageIds: number[] = [];
|
||||||
// only valid for 5 minutes
|
let cert:
|
||||||
const validFor = 300n;
|
| {
|
||||||
|
certificate: string;
|
||||||
|
keyId: string;
|
||||||
|
validPrincipals: string[];
|
||||||
|
validAfter: Date;
|
||||||
|
validBefore: Date;
|
||||||
|
}
|
||||||
|
| undefined;
|
||||||
|
// if the pam mode is push then we generate the user's pam username and use that or pull it from the userOrgs table
|
||||||
|
// if the mode is passthrough then just use what was provided because the user will log in themselves
|
||||||
|
let usernameToUse;
|
||||||
|
if (resource.pamMode === "push") {
|
||||||
|
if (!userOrg.pamUsername) {
|
||||||
|
if (req.user?.email) {
|
||||||
|
// Extract username from email (first part before @)
|
||||||
|
usernameToUse = req.user?.email
|
||||||
|
.split("@")[0]
|
||||||
|
.replace(/[^a-zA-Z0-9_-]/g, "");
|
||||||
|
if (!usernameToUse) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
"Unable to extract username from email"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (req.user?.username) {
|
||||||
|
usernameToUse = req.user.username;
|
||||||
|
// We need to clean out any spaces or special characters from the username to ensure it's valid for SSH certificates
|
||||||
|
usernameToUse = usernameToUse.replace(
|
||||||
|
/[^a-zA-Z0-9_-]/g,
|
||||||
|
"-"
|
||||||
|
);
|
||||||
|
if (!usernameToUse) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
"Username is not valid for SSH certificate"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
"User does not have a valid email or username for SSH certificate"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const cert = signPublicKey(caKeys.privateKeyPem, publicKey, {
|
// prefix with p-
|
||||||
keyId: `${usernameToUse}@${resource.niceId}`,
|
usernameToUse = `p-${usernameToUse}`;
|
||||||
validPrincipals: [usernameToUse, resource.niceId],
|
|
||||||
validAfter: now - 60n, // Start 1 min ago for clock skew
|
// check if we have a existing user in this org with the same
|
||||||
validBefore: now + validFor
|
const [existingUserWithSameName] = await db
|
||||||
});
|
.select()
|
||||||
|
.from(userOrgs)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(userOrgs.orgId, orgId),
|
||||||
|
eq(userOrgs.pamUsername, usernameToUse)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (existingUserWithSameName) {
|
||||||
|
let foundUniqueUsername = false;
|
||||||
|
for (let attempt = 0; attempt < 20; attempt++) {
|
||||||
|
const randomNum = Math.floor(Math.random() * 101); // 0 to 100
|
||||||
|
const candidateUsername = `${usernameToUse}${randomNum}`;
|
||||||
|
|
||||||
|
const [existingUser] = await db
|
||||||
|
.select()
|
||||||
|
.from(userOrgs)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(userOrgs.orgId, orgId),
|
||||||
|
eq(userOrgs.pamUsername, candidateUsername)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!existingUser) {
|
||||||
|
usernameToUse = candidateUsername;
|
||||||
|
foundUniqueUsername = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!foundUniqueUsername) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.CONFLICT,
|
||||||
|
"Unable to generate a unique username for SSH certificate"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await db
|
||||||
|
.update(userOrgs)
|
||||||
|
.set({ pamUsername: usernameToUse })
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(userOrgs.orgId, orgId),
|
||||||
|
eq(userOrgs.userId, userId)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
usernameToUse = userOrg.pamUsername;
|
||||||
|
}
|
||||||
|
|
||||||
|
const roleRows = await db
|
||||||
|
.select({
|
||||||
|
sshSudoCommands: roles.sshSudoCommands,
|
||||||
|
sshUnixGroups: roles.sshUnixGroups,
|
||||||
|
sshCreateHomeDir: roles.sshCreateHomeDir,
|
||||||
|
sshSudoMode: roles.sshSudoMode
|
||||||
|
})
|
||||||
|
.from(roles)
|
||||||
|
.innerJoin(
|
||||||
|
roleSiteResources,
|
||||||
|
eq(roleSiteResources.roleId, roles.roleId)
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
inArray(roles.roleId, roleIds),
|
||||||
|
eq(
|
||||||
|
roleSiteResources.siteResourceId,
|
||||||
|
resource.siteResourceId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const parsedSudoCommands: string[] = [];
|
||||||
|
const parsedGroupsSet = new Set<string>();
|
||||||
|
let homedir: boolean | null = null;
|
||||||
|
const sudoModeOrder = { none: 0, commands: 1, full: 2 };
|
||||||
|
let sudoMode: "none" | "commands" | "full" = "none";
|
||||||
|
for (const roleRow of roleRows) {
|
||||||
|
try {
|
||||||
|
const cmds = JSON.parse(roleRow?.sshSudoCommands ?? "[]");
|
||||||
|
if (Array.isArray(cmds)) parsedSudoCommands.push(...cmds);
|
||||||
|
} catch {
|
||||||
|
// skip
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const grps = JSON.parse(roleRow?.sshUnixGroups ?? "[]");
|
||||||
|
if (Array.isArray(grps))
|
||||||
|
grps.forEach((g: string) => parsedGroupsSet.add(g));
|
||||||
|
} catch {
|
||||||
|
// skip
|
||||||
|
}
|
||||||
|
if (roleRow?.sshCreateHomeDir === true) homedir = true;
|
||||||
|
const m = roleRow?.sshSudoMode ?? "none";
|
||||||
|
if (
|
||||||
|
sudoModeOrder[m as keyof typeof sudoModeOrder] >
|
||||||
|
sudoModeOrder[sudoMode]
|
||||||
|
) {
|
||||||
|
sudoMode = m as "none" | "commands" | "full";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const parsedGroups = Array.from(parsedGroupsSet);
|
||||||
|
if (homedir === null && roleRows.length > 0) {
|
||||||
|
homedir = roleRows[0].sshCreateHomeDir ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sign the public key
|
||||||
|
const now = BigInt(Math.floor(Date.now() / 1000));
|
||||||
|
// only valid for 5 minutes
|
||||||
|
const validFor = 300n;
|
||||||
|
expiresIn = Number(validFor); // seconds
|
||||||
|
|
||||||
|
const cert = signPublicKey(caKeys.privateKeyPem, publicKey, {
|
||||||
|
keyId: `${usernameToUse}@${resource.niceId}`,
|
||||||
|
validPrincipals: [usernameToUse, resource.niceId],
|
||||||
|
validAfter: now - 60n, // Start 1 min ago for clock skew
|
||||||
|
validBefore: now + validFor
|
||||||
|
});
|
||||||
|
|
||||||
|
const messageIds: number[] = [];
|
||||||
|
for (const siteId of siteIds) {
|
||||||
|
// get the site
|
||||||
|
const [newt] = await db
|
||||||
|
.select()
|
||||||
|
.from(newts)
|
||||||
|
.where(eq(newts.siteId, siteId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!newt) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
"Site associated with resource not found"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [message] = await db
|
||||||
|
.insert(roundTripMessageTracker)
|
||||||
|
.values({
|
||||||
|
wsClientId: newt.newtId,
|
||||||
|
messageType: `newt/pam/connection`,
|
||||||
|
sentAt: Math.floor(Date.now() / 1000)
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
if (!message) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
"Failed to create message tracker entry"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
messageIds.push(message.messageId);
|
||||||
|
|
||||||
|
await sendToClient(newt.newtId, {
|
||||||
|
type: `newt/pam/connection`,
|
||||||
|
data: {
|
||||||
|
messageId: message.messageId,
|
||||||
|
orgId: orgId,
|
||||||
|
agentPort: resource.authDaemonPort ?? 22123,
|
||||||
|
authDaemonMode: resource.authDaemonMode, // site, remote, native where native is the pty mode
|
||||||
|
externalAuthDaemon:
|
||||||
|
resource.authDaemonMode === "remote", // keep this for backward compatibility but new newts are using the authDaemonMode field
|
||||||
|
agentHost: resource.destination,
|
||||||
|
caCert: caKeys.publicKeyOpenSSH,
|
||||||
|
username: usernameToUse,
|
||||||
|
niceId: resource.niceId,
|
||||||
|
metadata: {
|
||||||
|
sudoMode: sudoMode,
|
||||||
|
sudoCommands: parsedSudoCommands,
|
||||||
|
homedir: homedir,
|
||||||
|
groups: parsedGroups
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (resource.pamMode === "passthrough") {
|
||||||
|
usernameToUse = username;
|
||||||
|
if (!usernameToUse) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
"Username must be provided when PAM mode is passthrough"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
"Invalid PAM mode configured for resource"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let sshHost: string | undefined;
|
||||||
|
if (
|
||||||
|
resource.authDaemonMode === "site" ||
|
||||||
|
resource.authDaemonMode === "remote"
|
||||||
|
) {
|
||||||
|
if (resource.alias && resource.alias != "") {
|
||||||
|
sshHost = resource.alias;
|
||||||
|
} else {
|
||||||
|
sshHost = resource.destination;
|
||||||
|
}
|
||||||
|
} else if (resource.authDaemonMode === "native") {
|
||||||
|
if (siteIds.length > 1) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
"Multiple sites associated with resource, unable to determine SSH host when in native mode"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const messageIds: number[] = [];
|
|
||||||
for (const siteId of siteIds) {
|
|
||||||
// get the site
|
// get the site
|
||||||
const [newt] = await db
|
const [site] = await db
|
||||||
.select()
|
.select()
|
||||||
.from(newts)
|
.from(sites)
|
||||||
.where(eq(newts.siteId, siteId))
|
.where(eq(sites.siteId, siteIds[0]))
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (!newt) {
|
if (!site) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.INTERNAL_SERVER_ERROR,
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
@@ -453,54 +574,26 @@ export async function signSshKey(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [message] = await db
|
if (!site.address) {
|
||||||
.insert(roundTripMessageTracker)
|
|
||||||
.values({
|
|
||||||
wsClientId: newt.newtId,
|
|
||||||
messageType: `newt/pam/connection`,
|
|
||||||
sentAt: Math.floor(Date.now() / 1000)
|
|
||||||
})
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
if (!message) {
|
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.INTERNAL_SERVER_ERROR,
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
"Failed to create message tracker entry"
|
"Site address not configured, unable to determine SSH host when in native mode"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
messageIds.push(message.messageId);
|
// its the address but split off the cidr if there is one
|
||||||
|
sshHost = site.address.split("/")[0];
|
||||||
await sendToClient(newt.newtId, {
|
|
||||||
type: `newt/pam/connection`,
|
|
||||||
data: {
|
|
||||||
messageId: message.messageId,
|
|
||||||
orgId: orgId,
|
|
||||||
agentPort: resource.authDaemonPort ?? 22123,
|
|
||||||
externalAuthDaemon: resource.authDaemonMode === "remote",
|
|
||||||
agentHost: resource.destination,
|
|
||||||
caCert: caKeys.publicKeyOpenSSH,
|
|
||||||
username: usernameToUse,
|
|
||||||
niceId: resource.niceId,
|
|
||||||
metadata: {
|
|
||||||
sudoMode: sudoMode,
|
|
||||||
sudoCommands: parsedSudoCommands,
|
|
||||||
homedir: homedir,
|
|
||||||
groups: parsedGroups
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const expiresIn = Number(validFor); // seconds
|
if (!sshHost) {
|
||||||
|
return next(
|
||||||
let sshHost;
|
createHttpError(
|
||||||
if (resource.alias && resource.alias != "") {
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
sshHost = resource.alias;
|
"Unable to determine SSH host for the resource"
|
||||||
} else {
|
)
|
||||||
sshHost = resource.destination;
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await logsDb.insert(actionAuditLog).values({
|
await logsDb.insert(actionAuditLog).values({
|
||||||
@@ -527,7 +620,7 @@ export async function signSshKey(
|
|||||||
: undefined,
|
: undefined,
|
||||||
metadata: {
|
metadata: {
|
||||||
resourceName: resource.name,
|
resourceName: resource.name,
|
||||||
siteId: siteIds[0],
|
siteIds: siteIds,
|
||||||
sshUsername: usernameToUse,
|
sshUsername: usernameToUse,
|
||||||
sshHost: sshHost
|
sshHost: sshHost
|
||||||
},
|
},
|
||||||
@@ -537,18 +630,18 @@ export async function signSshKey(
|
|||||||
|
|
||||||
return response<SignSshKeyResponse>(res, {
|
return response<SignSshKeyResponse>(res, {
|
||||||
data: {
|
data: {
|
||||||
certificate: cert.certificate,
|
certificate: cert?.certificate,
|
||||||
messageIds: messageIds,
|
messageIds: messageIds,
|
||||||
messageId: messageIds[0], // just pick the first one for backward compatibility
|
messageId: messageIds[0], // just pick the first one for backward compatibility with older olms
|
||||||
sshUsername: usernameToUse,
|
sshUsername: usernameToUse,
|
||||||
sshHost: sshHost,
|
sshHost: sshHost, // just pick the first one for backward compatibility with older olms
|
||||||
resourceId: resource.siteResourceId,
|
resourceId: resource.siteResourceId,
|
||||||
siteIds: siteIds,
|
siteIds: siteIds,
|
||||||
siteId: siteIds[0], // just pick the first one for backward compatibility
|
siteId: siteIds[0], // just pick the first one for backward compatibility with older olms
|
||||||
keyId: cert.keyId,
|
keyId: cert?.keyId,
|
||||||
validPrincipals: cert.validPrincipals,
|
validPrincipals: cert?.validPrincipals,
|
||||||
validAfter: cert.validAfter.toISOString(),
|
validAfter: cert?.validAfter.toISOString(),
|
||||||
validBefore: cert.validBefore.toISOString(),
|
validBefore: cert?.validBefore.toISOString(),
|
||||||
expiresIn
|
expiresIn
|
||||||
},
|
},
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user