diff --git a/apps/webapp/app/models/admin.server.ts b/apps/webapp/app/models/admin.server.ts index 35e68f8999..bd3adc8cbf 100644 --- a/apps/webapp/app/models/admin.server.ts +++ b/apps/webapp/app/models/admin.server.ts @@ -8,15 +8,13 @@ import { getImpersonationId, setImpersonationId, } from "~/services/impersonation.server"; +import { authenticator } from "~/services/auth.server"; import { requireUser } from "~/services/session.server"; import { extractClientIp } from "~/utils/extractClientIp.server"; const pageSize = 20; -export async function adminGetUsers( - userId: string, - { page, search }: SearchParams, -) { +export async function adminGetUsers(userId: string, { page, search }: SearchParams) { page = page || 1; search = search ? decodeURIComponent(search) : undefined; @@ -231,7 +229,11 @@ export async function redirectWithImpersonation(request: Request, userId: string }, }); } catch (error) { - logger.error("Failed to create impersonation audit log", { error, adminId: user.id, targetId: userId }); + logger.error("Failed to create impersonation audit log", { + error, + adminId: user.id, + targetId: userId, + }); } const session = await setImpersonationId(userId, request); @@ -242,10 +244,10 @@ export async function redirectWithImpersonation(request: Request, userId: string } export async function clearImpersonation(request: Request, path: string) { - const user = await requireUser(request); + const authUser = await authenticator.isAuthenticated(request); const targetId = await getImpersonationId(request); - if (targetId) { + if (targetId && authUser?.userId) { const xff = request.headers.get("x-forwarded-for"); const ipAddress = extractClientIp(xff); @@ -253,13 +255,17 @@ export async function clearImpersonation(request: Request, path: string) { await prisma.impersonationAuditLog.create({ data: { action: "STOP", - adminId: user.id, + adminId: authUser.userId, targetId, ipAddress, }, }); } catch (error) { - logger.error("Failed to create impersonation audit log", { error, adminId: user.id, targetId }); + logger.error("Failed to create impersonation audit log", { + error, + adminId: authUser.userId, + targetId, + }); } } diff --git a/apps/webapp/app/routes/@.runs.$runParam.ts b/apps/webapp/app/routes/@.runs.$runParam.ts new file mode 100644 index 0000000000..a52600628d --- /dev/null +++ b/apps/webapp/app/routes/@.runs.$runParam.ts @@ -0,0 +1,67 @@ +import { redirect, type LoaderFunctionArgs } from "@remix-run/server-runtime"; +import { z } from "zod"; +import { prisma } from "~/db.server"; +import { redirectWithErrorMessage } from "~/models/message.server"; +import { requireUser } from "~/services/session.server"; +import { impersonate, rootPath, v3RunPath } from "~/utils/pathBuilder"; + +const ParamsSchema = z.object({ + runParam: z.string(), +}); + +export async function loader({ params, request }: LoaderFunctionArgs) { + const user = await requireUser(request); + + const { runParam } = ParamsSchema.parse(params); + + const isAdmin = user.admin || user.isImpersonating; + + if (!isAdmin) { + return redirectWithErrorMessage( + rootPath(), + request, + "You're not an admin and cannot impersonate", + { + ephemeral: false, + } + ); + } + + const run = await prisma.taskRun.findFirst({ + where: { + friendlyId: runParam, + }, + select: { + runtimeEnvironment: { + select: { + slug: true, + }, + }, + project: { + select: { + slug: true, + organization: { + select: { + slug: true, + }, + }, + }, + }, + }, + }); + + if (!run) { + return redirectWithErrorMessage(rootPath(), request, "Run doesn't exist", { + ephemeral: false, + }); + } + + const path = v3RunPath( + { slug: run.project.organization.slug }, + { slug: run.project.slug }, + { slug: run.runtimeEnvironment.slug }, + { friendlyId: runParam } + ); + + return redirect(impersonate(path)); +} diff --git a/apps/webapp/app/routes/_app.@.orgs.$organizationSlug.$.ts b/apps/webapp/app/routes/_app.@.orgs.$organizationSlug.$.ts index 9115c1b21a..b430b4a219 100644 --- a/apps/webapp/app/routes/_app.@.orgs.$organizationSlug.$.ts +++ b/apps/webapp/app/routes/_app.@.orgs.$organizationSlug.$.ts @@ -7,6 +7,14 @@ import { requireUser } from "~/services/session.server"; export async function loader({ request, params }: LoaderFunctionArgs) { const user = await requireUser(request); + + // If already impersonating, we need to clear the impersonation + if (user.isImpersonating) { + const url = new URL(request.url); + return clearImpersonation(request, url.pathname); + } + + // Only admins can impersonate if (!user.admin) { return redirect("/"); } diff --git a/apps/webapp/app/routes/runs.$runParam.ts b/apps/webapp/app/routes/runs.$runParam.ts index 4e9ce3d7a3..4a8d7a12d3 100644 --- a/apps/webapp/app/routes/runs.$runParam.ts +++ b/apps/webapp/app/routes/runs.$runParam.ts @@ -57,12 +57,12 @@ export async function loader({ params, request }: LoaderFunctionArgs) { ); } - return redirect( - v3RunPath( - { slug: run.project.organization.slug }, - { slug: run.project.slug }, - { slug: run.runtimeEnvironment.slug }, - { friendlyId: runParam } - ) + const path = v3RunPath( + { slug: run.project.organization.slug }, + { slug: run.project.slug }, + { slug: run.runtimeEnvironment.slug }, + { friendlyId: runParam } ); + + return redirect(path); } diff --git a/apps/webapp/app/utils/pathBuilder.ts b/apps/webapp/app/utils/pathBuilder.ts index c39234a7bb..20f3fcd7c2 100644 --- a/apps/webapp/app/utils/pathBuilder.ts +++ b/apps/webapp/app/utils/pathBuilder.ts @@ -56,6 +56,11 @@ export function rootPath() { return `/`; } +/** Given a path, it makes it an impersonation path */ +export function impersonate(path: string) { + return `/@${path}`; +} + export function accountPath() { return `/account`; }