Skip to content

Commit 30ccb91

Browse files
d-csclaude
andcommitted
fix(run-ops split): resolve org-membership authz on primary + full split deps for waitpoint detail
Route the org-membership authorization gate through the primary client on the debug, run-inspector, and idempotency-key-reset routes so it matches the cancel and replay paths and never authorizes against lagging replica state. Restore the primary-DB org fallback in the replay action's RBAC scope resolver so the scope is never resolved without an org during replica lag. Pass the full split-read deps to WaitpointPresenter on the detail route so a waitpoint resident on the new run-ops DB resolves the same way it does on the list route. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent ec1aa6e commit 30ccb91

5 files changed

Lines changed: 31 additions & 8 deletions

File tree

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.waitpoints.tokens.$waitpointParam/route.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ import { useProject } from "~/hooks/useProject";
1111
import { findProjectBySlug } from "~/models/project.server";
1212
import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server";
1313
import { WaitpointPresenter } from "~/presenters/v3/WaitpointPresenter.server";
14+
import {
15+
runOpsNewReplicaClient,
16+
runOpsLegacyReplica,
17+
runOpsSplitReadEnabled,
18+
type PrismaClientOrTransaction,
19+
} from "~/db.server";
1420
import { requireUserId } from "~/services/session.server";
1521
import { cn } from "~/utils/cn";
1622
import { EnvironmentParamSchema, v3WaitpointTokensPath } from "~/utils/pathBuilder";
@@ -45,7 +51,11 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
4551
}
4652

4753
try {
48-
const presenter = new WaitpointPresenter(undefined, undefined, {});
54+
const presenter = new WaitpointPresenter(undefined, undefined, {
55+
newClient: runOpsNewReplicaClient as unknown as PrismaClientOrTransaction,
56+
legacyReplica: runOpsLegacyReplica as unknown as PrismaClientOrTransaction,
57+
splitEnabled: runOpsSplitReadEnabled,
58+
});
4959
const result = await presenter.call({
5060
friendlyId: waitpointParam,
5161
environmentId: environment.id,

apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.idempotencyKey.reset.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { type ActionFunction, json } from "@remix-run/node";
2-
import { $replica, prisma } from "~/db.server";
2+
import { prisma } from "~/db.server";
33
import { jsonWithErrorMessage, jsonWithSuccessMessage } from "~/models/message.server";
44
import { logger } from "~/services/logger.server";
55
import { requireUserId } from "~/services/session.server";
@@ -31,7 +31,7 @@ export const action: ActionFunction = async ({ request, params }) => {
3131
return jsonWithErrorMessage({}, request, "Run not found");
3232
}
3333

34-
const authorizedProject = await $replica.project.findFirst({
34+
const authorizedProject = await prisma.project.findFirst({
3535
where: { id: taskRun.projectId, organization: { members: { some: { userId } } } },
3636
select: { id: true },
3737
});

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { LoaderFunctionArgs } from "@remix-run/server-runtime";
22
import { MachinePresetName, prettyPrintPacket, TaskRunError } from "@trigger.dev/core/v3";
33
import { typedjson, UseDataFunctionReturn } from "remix-typedjson";
44
import { RUNNING_STATUSES } from "~/components/runs/v3/TaskRunStatus";
5-
import { $replica } from "~/db.server";
5+
import { $replica, prisma } from "~/db.server";
66
import { requireUserId } from "~/services/session.server";
77
import { v3RunParamsSchema } from "~/utils/pathBuilder";
88
import { machinePresetFromName, machinePresetFromRun } from "~/v3/machinePresets.server";
@@ -82,7 +82,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
8282
throw new Response("Not found", { status: 404 });
8383
}
8484

85-
const authorizedProject = await $replica.project.findFirst({
85+
const authorizedProject = await prisma.project.findFirst({
8686
where: { id: run.projectId, organization: { members: { some: { userId } } } },
8787
select: { id: true },
8888
});

apps/webapp/app/routes/resources.taskruns.$runParam.debug.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { type LoaderFunctionArgs } from "@remix-run/node";
22
import { typedjson } from "remix-typedjson";
33
import { z } from "zod";
4-
import { $replica } from "~/db.server";
4+
import { $replica, prisma } from "~/db.server";
55
import { requireUserId } from "~/services/session.server";
66
import { marqs } from "~/v3/marqs/index.server";
77
import { engine } from "~/v3/runEngine.server";
@@ -42,7 +42,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
4242
// Authorize on the control-plane DB, keyed by the run's project — a non-member (or
4343
// unresolvable project) is indistinguishable from not-found (both 404), matching the
4444
// original scoped where.
45-
const authorizedProject = await $replica.project.findFirst({
45+
const authorizedProject = await prisma.project.findFirst({
4646
where: { id: run.projectId, organization: { members: { some: { userId } } } },
4747
select: { id: true },
4848
});

apps/webapp/app/routes/resources.taskruns.$runParam.replay.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,20 @@ async function resolveRunOrganizationId(runParam: string): Promise<string | null
282282
return entry.orgId;
283283
}
284284

285-
return null;
285+
// Replica lag with the buffer entry already drained: the run can exist in the
286+
// primary while both lookups above miss. Fall back to the primary so the RBAC
287+
// scope is never resolved without an org (which would let the role check run
288+
// unscoped under the RBAC plugin). Keyed by friendlyId so routing still applies.
289+
const primaryRun = await runStore.findRun(
290+
{ friendlyId: runParam },
291+
{ select: { runtimeEnvironmentId: true } },
292+
prisma
293+
);
294+
if (!primaryRun) {
295+
return null;
296+
}
297+
const primaryEnv = await controlPlaneResolver.resolveEnv(primaryRun.runtimeEnvironmentId);
298+
return primaryEnv?.organizationId ?? null;
286299
}
287300

288301
export const action = dashboardAction(

0 commit comments

Comments
 (0)