Merge branch 'main' into logs-database

This commit is contained in:
Owen
2026-02-23 16:39:39 -08:00
84 changed files with 4253 additions and 3672 deletions

View File

@@ -25,7 +25,8 @@ import {
loginPageOrg,
orgs,
resources,
roles
roles,
siteResources
} from "@server/db";
import { eq } from "drizzle-orm";
@@ -286,6 +287,10 @@ async function disableFeature(
await disableAutoProvisioning(orgId);
break;
case TierFeature.SshPam:
await disableSshPam(orgId);
break;
default:
logger.warn(
`Unknown feature ${feature} for org ${orgId}, skipping`
@@ -315,6 +320,12 @@ async function disableDeviceApprovals(orgId: string): Promise<void> {
logger.info(`Disabled device approvals on all roles for org ${orgId}`);
}
async function disableSshPam(orgId: string): Promise<void> {
logger.info(
`Disabled SSH PAM options on all roles and site resources for org ${orgId}`
);
}
async function disableLoginPageBranding(orgId: string): Promise<void> {
const [existingBranding] = await db
.select()

View File

@@ -514,7 +514,7 @@ authenticated.post(
verifyValidSubscription(tierMatrix.sshPam),
verifyOrgAccess,
verifyLimits,
// verifyUserHasAction(ActionsEnum.signSshKey),
verifyUserHasAction(ActionsEnum.signSshKey),
logActionAudit(ActionsEnum.signSshKey),
ssh.signSshKey
);

View File

@@ -13,7 +13,17 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { db, newts, orgs, roundTripMessageTracker, siteResources, sites, userOrgs } from "@server/db";
import {
db,
newts,
roles,
roundTripMessageTracker,
siteResources,
sites,
userOrgs
} from "@server/db";
import { isLicensedOrSubscribed } from "#private/lib/isLicencedOrSubscribed";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
@@ -135,11 +145,26 @@ export async function signSshKey(
);
}
const isLicensed = await isLicensedOrSubscribed(
orgId,
tierMatrix.sshPam
);
if (!isLicensed) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"SSH key signing requires a paid plan"
)
);
}
let usernameToUse;
if (!userOrg.pamUsername) {
if (req.user?.email) {
// Extract username from email (first part before @)
usernameToUse = req.user?.email.split("@")[0];
usernameToUse = req.user?.email
.split("@")[0]
.replace(/[^a-zA-Z0-9_-]/g, "");
if (!usernameToUse) {
return next(
createHttpError(
@@ -301,6 +326,29 @@ export async function signSshKey(
);
}
const [roleRow] = await db
.select()
.from(roles)
.where(eq(roles.roleId, roleId))
.limit(1);
let parsedSudoCommands: string[] = [];
let parsedGroups: string[] = [];
try {
parsedSudoCommands = JSON.parse(roleRow?.sshSudoCommands ?? "[]");
if (!Array.isArray(parsedSudoCommands)) parsedSudoCommands = [];
} catch {
parsedSudoCommands = [];
}
try {
parsedGroups = JSON.parse(roleRow?.sshUnixGroups ?? "[]");
if (!Array.isArray(parsedGroups)) parsedGroups = [];
} catch {
parsedGroups = [];
}
const homedir = roleRow?.sshCreateHomeDir ?? null;
const sudoMode = roleRow?.sshSudoMode ?? "none";
// get the site
const [newt] = await db
.select()
@@ -334,7 +382,7 @@ export async function signSshKey(
.values({
wsClientId: newt.newtId,
messageType: `newt/pam/connection`,
sentAt: Math.floor(Date.now() / 1000),
sentAt: Math.floor(Date.now() / 1000)
})
.returning();
@@ -352,14 +400,17 @@ export async function signSshKey(
data: {
messageId: message.messageId,
orgId: orgId,
agentPort: 22123,
agentPort: resource.authDaemonPort ?? 22123,
externalAuthDaemon: resource.authDaemonMode === "remote",
agentHost: resource.destination,
caCert: caKeys.publicKeyOpenSSH,
username: usernameToUse,
niceId: resource.niceId,
metadata: {
sudo: true, // we are hardcoding these for now but should make configurable from the role or something
homedir: true
sudoMode: sudoMode,
sudoCommands: parsedSudoCommands,
homedir: homedir,
groups: parsedGroups
}
}
});