Compare commits

...

5 Commits

Author SHA1 Message Date
Owen 6df4bba3b6 Bump version 2026-05-29 17:12:26 -07:00
Owen 9f83c0a0e8 Show error right 2026-05-29 17:11:44 -07:00
Owen 0ab1854125 Fix import 2026-05-29 15:38:37 -07:00
Owen b071fa2c9f Be able to pull users from the proxy 2026-05-29 15:34:34 -07:00
Owen 8e2a79a0f5 Move to private 2026-05-29 15:23:40 -07:00
18 changed files with 174 additions and 39 deletions
+1 -1
View File
@@ -2,7 +2,7 @@ import path from "path";
import { fileURLToPath } from "url";
// This is a placeholder value replaced by the build process
export const APP_VERSION = "1.18.4";
export const APP_VERSION = "1.19.0";
export const __FILENAME = fileURLToPath(import.meta.url);
export const __DIRNAME = path.dirname(__FILENAME);
+1
View File
@@ -28,6 +28,7 @@ export * from "./verifyApiKeyAccess";
export * from "./verifySiteProvisioningKeyAccess";
export * from "./verifyDomainAccess";
export * from "./verifyUserIsOrgOwner";
export * from "./verifyUserFromSessionOrHeaders";
export * from "./verifySiteResourceAccess";
export * from "./logActionAudit";
export * from "./verifyOlmAccess";
@@ -0,0 +1,104 @@
import { Response, NextFunction } from "express";
import { db } from "@server/db";
import { users } from "@server/db";
import { eq, or } from "drizzle-orm";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import { verifySession } from "@server/auth/sessions/verifySession";
import { getUserOrgRoleIds } from "@server/lib/userOrgRoles";
/**
* Middleware that populates req.user from either:
* 1. A valid session cookie (normal authenticated flow), or
* 2. Badger-injected headers: Remote-User-Id, Remote-User (username), Remote-Email
*
* If an orgId is present in req.params, req.userOrgRoleIds is also populated.
*
* If neither source yields a user, returns 401.
* If header-based lookup matches more than one user, returns 400.
*/
export const verifyUserFromSessionOrHeadersMiddleware = async (
req: any,
res: Response,
next: NextFunction
) => {
// 1. Try session-based auth first
if (!req.user) {
try {
const { session, user } = await verifySession(req);
if (session && user) {
const rows = await db
.select()
.from(users)
.where(eq(users.userId, user.userId));
if (rows[0]) {
req.user = rows[0];
req.session = session;
}
}
} catch {
// session lookup failure is not fatal; fall through to header auth
}
}
// 2. Fall back to Badger-injected headers
if (!req.user) {
const userId = req.headers["remote-user-id"] as string | undefined;
const username = req.headers["remote-user"] as string | undefined;
const email = req.headers["remote-email"] as string | undefined;
if (!userId && !username && !email) {
return next(
createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated")
);
}
let foundUsers;
if (userId) {
// Most reliable: look up directly by ID
foundUsers = await db
.select()
.from(users)
.where(eq(users.userId, userId));
} else {
// Fall back to username / email (may be absent depending on badger version)
const conditions = [];
if (username) conditions.push(eq(users.username, username));
if (email) conditions.push(eq(users.email, email));
foundUsers = await db
.select()
.from(users)
.where(or(...conditions));
}
if (!foundUsers || foundUsers.length === 0) {
return next(
createHttpError(HttpCode.UNAUTHORIZED, "User not found")
);
}
if (foundUsers.length > 1) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Multiple users found matching the provided credentials"
)
);
}
req.user = foundUsers[0];
}
// 3. Populate userOrgRoleIds if an orgId is available in route params
if (req.user && req.params?.orgId && !req.userOrgRoleIds) {
req.userOrgRoleIds = await getUserOrgRoleIds(
req.user.userId,
req.params.orgId
);
}
next();
};
@@ -1,3 +1,16 @@
/*
* This file is part of a proprietary work.
*
* Copyright (c) 2025-2026 Fossorial, Inc.
* All rights reserved.
*
* This file is licensed under the Fossorial Commercial License.
* You may not use this file except in compliance with the License.
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
*
* This file is not licensed under the AGPLv3.
*/
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { browserGatewayTarget, db } from "@server/db";
@@ -10,6 +23,7 @@ import { fromError } from "zod-validation-error";
import logger from "@server/logger";
import { decrypt } from "@server/lib/crypto";
import config from "@server/lib/config";
import { GetBrowserTargetResponse } from "@server/routers/browserGatewayTarget";
const getBrowserTargetSchema = z
.object({
@@ -17,17 +31,6 @@ const getBrowserTargetSchema = z
})
.strict();
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(
req: Request,
res: Response,
@@ -16,3 +16,4 @@ export * from "./updateBrowserGatewayTarget";
export * from "./deleteBrowserGatewayTarget";
export * from "./getBrowserGatewayTarget";
export * from "./listBrowserGatewayTargets";
export * from "./getBrowserTarget";
+14 -1
View File
@@ -17,8 +17,13 @@ import * as orgIdp from "#private/routers/orgIdp";
import * as billing from "#private/routers/billing";
import * as license from "#private/routers/license";
import * as resource from "#private/routers/resource";
import * as browserTarget from "#private/routers/browserGatewayTarget";
import * as ssh from "#private/routers/ssh";
import { verifySessionUserMiddleware } from "@server/middlewares";
import {
verifySessionUserMiddleware,
verifyUserFromSessionOrHeadersMiddleware
} from "@server/middlewares";
import { internalRouter as ir } from "@server/routers/internal";
@@ -40,3 +45,11 @@ internalRouter.post(
internalRouter.get(`/license/status`, license.getLicenseStatus);
internalRouter.get("/maintenance/info", resource.getMaintenanceInfo);
internalRouter.post(
"/org/:orgId/ssh/sign-key",
verifyUserFromSessionOrHeadersMiddleware,
ssh.signSshKey
);
internalRouter.get("/resource/browser-target", browserTarget.getBrowserTarget);
+1 -1
View File
@@ -546,7 +546,7 @@ export async function signSshKey(
if (resource.alias && resource.alias != "") {
sshHost = resource.alias;
} else {
sshHost = resource.destination || ""; // TODO: IF WE HAVE THE NATIVE SSH MODE WHAT SHOULD WE DO HERE?
sshHost = resource.destination || "";
}
} else if (resource.authDaemonMode === "native") {
if (siteIds.length > 1) {
@@ -0,0 +1 @@
export * from "./types";
@@ -0,0 +1,10 @@
export type GetBrowserTargetResponse = {
ip: string;
port: number;
authToken: string;
orgId: string;
resourceId: number;
niceId: string;
pamMode: "passthrough" | "push" | null;
authDaemonMode: "site" | "remote" | "native" | null;
};
-2
View File
@@ -42,8 +42,6 @@ internalRouter.get("/idp", idp.listIdps);
internalRouter.get("/idp/:idpId", idp.getIdp);
internalRouter.get("/resource/browser-target", resource.getBrowserTarget);
// Gerbil routes
const gerbilRouter = Router();
internalRouter.use("/gerbil", gerbilRouter);
-1
View File
@@ -33,5 +33,4 @@ export * from "./removeUserFromResource";
export * from "./listAllResourceNames";
export * from "./removeEmailFromResourceWhitelist";
export * from "./getStatusHistory";
export * from "./getBrowserTarget";
export * from "./getResourcePolicies";
@@ -419,7 +419,7 @@ export default function Page() {
pamMode === "push" &&
portVal
? Number(portVal)
: null;
: undefined;
Object.assign(payload, {
subdomain: sanitizedSubdomain
@@ -430,7 +430,7 @@ export default function Page() {
mode: resourceType,
pamMode,
authDaemonMode: effectiveMode,
authDaemonPort: effectivePort
authDaemonPort: effectivePort || undefined
});
} else {
const tcpUdpData = tcpUdpForm.getValues();
+1 -1
View File
@@ -14,7 +14,7 @@ import type {
RdpFileTransferProvider,
FileInfo
} from "@devolutions/iron-remote-desktop-rdp/dist";
import { GetBrowserTargetResponse } from "@server/routers/resource";
import { GetBrowserTargetResponse } from "@server/routers/browserGatewayTarget";
import {
Card,
CardContent,
+1 -1
View File
@@ -1,7 +1,7 @@
import { headers } from "next/headers";
import { priv } from "@app/lib/api";
import { AxiosResponse } from "axios";
import { GetBrowserTargetResponse } from "@server/routers/resource";
import { GetBrowserTargetResponse } from "@server/routers/browserGatewayTarget";
import RdpClient from "./RdpClient";
import AuthFooter from "@app/components/AuthFooter";
+1 -1
View File
@@ -7,7 +7,7 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import type { SignSshKeyResponse } from "@server/private/routers/ssh";
import { GetBrowserTargetResponse } from "@server/routers/resource";
import { GetBrowserTargetResponse } from "@server/routers/browserGatewayTarget";
import {
Card,
CardContent,
+20 -15
View File
@@ -1,7 +1,7 @@
import { headers } from "next/headers";
import { priv } from "@app/lib/api";
import { AxiosResponse } from "axios";
import { GetBrowserTargetResponse } from "@server/routers/resource";
import { GetBrowserTargetResponse } from "@server/routers/browserGatewayTarget";
import SshClient from "./SshClient";
import { SignSshKeyResponse } from "@server/private/routers/ssh";
import crypto from "crypto";
@@ -64,21 +64,26 @@ export default async function SshPage() {
target = res.data.data;
if (target.pamMode === "push") {
const { privateKeyPem, publicKeyOpenSSH } =
generateEphemeralKeyPair();
privateKey = privateKeyPem;
const res = await priv.post<AxiosResponse<SignSshKeyResponse>>(
`/org/${target.orgId}/ssh/sign-key`,
{
publicKey: publicKeyOpenSSH,
resource: target.niceId
}
);
signedKeyData = res.data.data;
console.log("Received signed SSH key:", signedKeyData);
try {
const { privateKeyPem, publicKeyOpenSSH } =
generateEphemeralKeyPair();
privateKey = privateKeyPem;
const res = await priv.post<AxiosResponse<SignSshKeyResponse>>(
`/org/${target.orgId}/ssh/sign-key`,
{
publicKey: publicKeyOpenSSH,
resource: target.niceId
}
);
signedKeyData = res.data.data;
console.log("Received signed SSH key:", signedKeyData);
} catch (err) {
console.error("Error signing SSH key:", err);
error = "Failed to sign SSH key for PAM push authentication.";
}
}
} catch (error) {
console.error("Error fetching browser target:", error);
} catch (err) {
console.error("Error fetching browser target:", err);
error = "No resource found for this domain";
}
+1 -1
View File
@@ -5,7 +5,7 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { toast } from "@app/hooks/useToast";
import { GetBrowserTargetResponse } from "@server/routers/resource";
import { GetBrowserTargetResponse } from "@server/routers/browserGatewayTarget";
import {
Card,
CardContent,
+1 -1
View File
@@ -1,7 +1,7 @@
import { headers } from "next/headers";
import { priv } from "@app/lib/api";
import { AxiosResponse } from "axios";
import { GetBrowserTargetResponse } from "@server/routers/resource";
import { GetBrowserTargetResponse } from "@server/routers/browserGatewayTarget";
import VncClient from "./VncClient";
import AuthFooter from "@app/components/AuthFooter";