mirror of
https://github.com/fosrl/pangolin.git
synced 2026-06-11 18:09:05 +00:00
Pull the session from badger
This commit is contained in:
102
server/middlewares/verifyUserFromResourceSession.ts
Normal file
102
server/middlewares/verifyUserFromResourceSession.ts
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import { Response, NextFunction } from "express";
|
||||||
|
import { db } from "@server/db";
|
||||||
|
import { resourceSessions, users } from "@server/db";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import { getUserOrgRoleIds } from "@server/lib/userOrgRoles";
|
||||||
|
import { getUserSessionWithUser } from "@server/db/queries/verifySessionQueries";
|
||||||
|
import { encodeHexLowerCase } from "@oslojs/encoding";
|
||||||
|
import { sha256 } from "@oslojs/crypto/sha2";
|
||||||
|
import config from "@server/lib/config";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
|
||||||
|
export const verifyUserFromResourceSessionMiddleware = async (
|
||||||
|
req: any,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) => {
|
||||||
|
if (!req.user) {
|
||||||
|
const sessionCookieName =
|
||||||
|
config.getRawConfig().server.session_cookie_name;
|
||||||
|
|
||||||
|
// Collect all resource session cookies (format: {name}[_s].{timestamp}=token)
|
||||||
|
const cookieHeader: string | undefined = req.headers.cookie;
|
||||||
|
const candidates: { timestamp: number; token: string }[] = [];
|
||||||
|
|
||||||
|
if (cookieHeader) {
|
||||||
|
for (const part of cookieHeader.split(";")) {
|
||||||
|
const trimmed = part.trim();
|
||||||
|
const eqIdx = trimmed.indexOf("=");
|
||||||
|
if (eqIdx === -1) continue;
|
||||||
|
|
||||||
|
const cookieName = trimmed.slice(0, eqIdx).trim();
|
||||||
|
const cookieValue = trimmed.slice(eqIdx + 1).trim();
|
||||||
|
|
||||||
|
// Match both secure (_s.timestamp) and non-secure (.timestamp) variants
|
||||||
|
const securePrefix = `${sessionCookieName}_s.`;
|
||||||
|
const httpPrefix = `${sessionCookieName}.`;
|
||||||
|
|
||||||
|
let timestampStr: string | null = null;
|
||||||
|
if (cookieName.startsWith(securePrefix)) {
|
||||||
|
timestampStr = cookieName.slice(securePrefix.length);
|
||||||
|
} else if (cookieName.startsWith(httpPrefix)) {
|
||||||
|
timestampStr = cookieName.slice(httpPrefix.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timestampStr !== null && /^\d+$/.test(timestampStr)) {
|
||||||
|
candidates.push({
|
||||||
|
timestamp: parseInt(timestampStr, 10),
|
||||||
|
token: cookieValue
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pick the most recently issued session (highest timestamp)
|
||||||
|
candidates.sort((a, b) => b.timestamp - a.timestamp);
|
||||||
|
const best = candidates[0];
|
||||||
|
|
||||||
|
if (best) {
|
||||||
|
try {
|
||||||
|
const sessionId = encodeHexLowerCase(
|
||||||
|
sha256(new TextEncoder().encode(best.token))
|
||||||
|
);
|
||||||
|
|
||||||
|
const [resourceSession] = await db
|
||||||
|
.select()
|
||||||
|
.from(resourceSessions)
|
||||||
|
.where(eq(resourceSessions.sessionId, sessionId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (resourceSession && Date.now() < resourceSession.expiresAt) {
|
||||||
|
if (resourceSession.userSessionId) {
|
||||||
|
const result = await getUserSessionWithUser(
|
||||||
|
resourceSession.userSessionId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result?.user && result?.session) {
|
||||||
|
req.user = result.user;
|
||||||
|
req.session = result.session;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.error(
|
||||||
|
"verifyUserFromResourceSessionMiddleware: failed to validate resource session",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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,104 +0,0 @@
|
|||||||
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();
|
|
||||||
};
|
|
||||||
@@ -22,7 +22,7 @@ import * as ssh from "#private/routers/ssh";
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
verifySessionUserMiddleware,
|
verifySessionUserMiddleware,
|
||||||
verifyUserFromSessionOrHeadersMiddleware
|
verifyUserFromResourceSessionMiddleware
|
||||||
} from "@server/middlewares";
|
} from "@server/middlewares";
|
||||||
|
|
||||||
import { internalRouter as ir } from "@server/routers/internal";
|
import { internalRouter as ir } from "@server/routers/internal";
|
||||||
@@ -48,7 +48,7 @@ internalRouter.get("/maintenance/info", resource.getMaintenanceInfo);
|
|||||||
|
|
||||||
internalRouter.post(
|
internalRouter.post(
|
||||||
"/org/:orgId/ssh/sign-key",
|
"/org/:orgId/ssh/sign-key",
|
||||||
verifyUserFromSessionOrHeadersMiddleware,
|
verifyUserFromResourceSessionMiddleware,
|
||||||
ssh.signSshKey
|
ssh.signSshKey
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user