Skip to content

Commit 35298ac

Browse files
authored
Impersonating run and clearing fix (#3144)
- Automatically impersonate a run when visiting /runs/<run_id> if an admin is logged in - Clear existing impersonation when switching
1 parent 1cfc296 commit 35298ac

File tree

5 files changed

+102
-16
lines changed

5 files changed

+102
-16
lines changed

apps/webapp/app/models/admin.server.ts

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,13 @@ import {
88
getImpersonationId,
99
setImpersonationId,
1010
} from "~/services/impersonation.server";
11+
import { authenticator } from "~/services/auth.server";
1112
import { requireUser } from "~/services/session.server";
1213
import { extractClientIp } from "~/utils/extractClientIp.server";
1314

1415
const pageSize = 20;
1516

16-
export async function adminGetUsers(
17-
userId: string,
18-
{ page, search }: SearchParams,
19-
) {
17+
export async function adminGetUsers(userId: string, { page, search }: SearchParams) {
2018
page = page || 1;
2119

2220
search = search ? decodeURIComponent(search) : undefined;
@@ -231,7 +229,11 @@ export async function redirectWithImpersonation(request: Request, userId: string
231229
},
232230
});
233231
} catch (error) {
234-
logger.error("Failed to create impersonation audit log", { error, adminId: user.id, targetId: userId });
232+
logger.error("Failed to create impersonation audit log", {
233+
error,
234+
adminId: user.id,
235+
targetId: userId,
236+
});
235237
}
236238

237239
const session = await setImpersonationId(userId, request);
@@ -242,24 +244,28 @@ export async function redirectWithImpersonation(request: Request, userId: string
242244
}
243245

244246
export async function clearImpersonation(request: Request, path: string) {
245-
const user = await requireUser(request);
247+
const authUser = await authenticator.isAuthenticated(request);
246248
const targetId = await getImpersonationId(request);
247249

248-
if (targetId) {
250+
if (targetId && authUser?.userId) {
249251
const xff = request.headers.get("x-forwarded-for");
250252
const ipAddress = extractClientIp(xff);
251253

252254
try {
253255
await prisma.impersonationAuditLog.create({
254256
data: {
255257
action: "STOP",
256-
adminId: user.id,
258+
adminId: authUser.userId,
257259
targetId,
258260
ipAddress,
259261
},
260262
});
261263
} catch (error) {
262-
logger.error("Failed to create impersonation audit log", { error, adminId: user.id, targetId });
264+
logger.error("Failed to create impersonation audit log", {
265+
error,
266+
adminId: authUser.userId,
267+
targetId,
268+
});
263269
}
264270
}
265271

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { redirect, type LoaderFunctionArgs } from "@remix-run/server-runtime";
2+
import { z } from "zod";
3+
import { prisma } from "~/db.server";
4+
import { redirectWithErrorMessage } from "~/models/message.server";
5+
import { requireUser } from "~/services/session.server";
6+
import { impersonate, rootPath, v3RunPath } from "~/utils/pathBuilder";
7+
8+
const ParamsSchema = z.object({
9+
runParam: z.string(),
10+
});
11+
12+
export async function loader({ params, request }: LoaderFunctionArgs) {
13+
const user = await requireUser(request);
14+
15+
const { runParam } = ParamsSchema.parse(params);
16+
17+
const isAdmin = user.admin || user.isImpersonating;
18+
19+
if (!isAdmin) {
20+
return redirectWithErrorMessage(
21+
rootPath(),
22+
request,
23+
"You're not an admin and cannot impersonate",
24+
{
25+
ephemeral: false,
26+
}
27+
);
28+
}
29+
30+
const run = await prisma.taskRun.findFirst({
31+
where: {
32+
friendlyId: runParam,
33+
},
34+
select: {
35+
runtimeEnvironment: {
36+
select: {
37+
slug: true,
38+
},
39+
},
40+
project: {
41+
select: {
42+
slug: true,
43+
organization: {
44+
select: {
45+
slug: true,
46+
},
47+
},
48+
},
49+
},
50+
},
51+
});
52+
53+
if (!run) {
54+
return redirectWithErrorMessage(rootPath(), request, "Run doesn't exist", {
55+
ephemeral: false,
56+
});
57+
}
58+
59+
const path = v3RunPath(
60+
{ slug: run.project.organization.slug },
61+
{ slug: run.project.slug },
62+
{ slug: run.runtimeEnvironment.slug },
63+
{ friendlyId: runParam }
64+
);
65+
66+
return redirect(impersonate(path));
67+
}

apps/webapp/app/routes/_app.@.orgs.$organizationSlug.$.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ import { requireUser } from "~/services/session.server";
77

88
export async function loader({ request, params }: LoaderFunctionArgs) {
99
const user = await requireUser(request);
10+
11+
// If already impersonating, we need to clear the impersonation
12+
if (user.isImpersonating) {
13+
const url = new URL(request.url);
14+
return clearImpersonation(request, url.pathname);
15+
}
16+
17+
// Only admins can impersonate
1018
if (!user.admin) {
1119
return redirect("/");
1220
}

apps/webapp/app/routes/runs.$runParam.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -57,12 +57,12 @@ export async function loader({ params, request }: LoaderFunctionArgs) {
5757
);
5858
}
5959

60-
return redirect(
61-
v3RunPath(
62-
{ slug: run.project.organization.slug },
63-
{ slug: run.project.slug },
64-
{ slug: run.runtimeEnvironment.slug },
65-
{ friendlyId: runParam }
66-
)
60+
const path = v3RunPath(
61+
{ slug: run.project.organization.slug },
62+
{ slug: run.project.slug },
63+
{ slug: run.runtimeEnvironment.slug },
64+
{ friendlyId: runParam }
6765
);
66+
67+
return redirect(path);
6868
}

apps/webapp/app/utils/pathBuilder.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ export function rootPath() {
5656
return `/`;
5757
}
5858

59+
/** Given a path, it makes it an impersonation path */
60+
export function impersonate(path: string) {
61+
return `/@${path}`;
62+
}
63+
5964
export function accountPath() {
6065
return `/account`;
6166
}

0 commit comments

Comments
 (0)