mirror of
https://github.com/fosrl/pangolin.git
synced 2026-08-28 23:15:10 +02:00
Compare commits
5 Commits
71756812b6
...
6df4bba3b6
| Author | SHA1 | Date | |
|---|---|---|---|
| 6df4bba3b6 | |||
| 9f83c0a0e8 | |||
| 0ab1854125 | |||
| b071fa2c9f | |||
| 8e2a79a0f5 |
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
+14
-11
@@ -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";
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,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";
|
||||
|
||||
|
||||
@@ -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
@@ -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";
|
||||
}
|
||||
|
||||
|
||||
@@ -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,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";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user