Skip to content

Commit 5ae0adf

Browse files
carderneTrigger.dev RepoOps
authored andcommitted
fix(webapp): require permission to manage private connections
Mono-RevId: 8711c9639a3d9afa5857a418af147ca88df5014f
1 parent 174b64f commit 5ae0adf

2 files changed

Lines changed: 177 additions & 100 deletions

File tree

  • apps/webapp/app/routes
    • _app.orgs.$organizationSlug.settings.private-connections._index
    • _app.orgs.$organizationSlug.settings.private-connections.new

‎apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.private-connections._index/route.tsx‎

Lines changed: 90 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
TrashIcon,
66
} from "@heroicons/react/20/solid";
77
import { Form, useRevalidator } from "@remix-run/react";
8-
import { json, type ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/server-runtime";
8+
import { json, type LoaderFunctionArgs } from "@remix-run/server-runtime";
99
import { tryCatch } from "@trigger.dev/core/utils";
1010
import type { PrivateLinkConnectionStatus } from "@trigger.dev/platform";
1111
import { useMemo, useState } from "react";
@@ -20,8 +20,11 @@ import { Header2 } from "~/components/primitives/Headers";
2020
import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader";
2121
import { Paragraph } from "~/components/primitives/Paragraph";
2222
import { prisma } from "~/db.server";
23+
import { rbac } from "~/services/rbac.server";
2324
import { useInterval } from "~/hooks/useInterval";
2425
import { redirectWithErrorMessage, redirectWithSuccessMessage } from "~/models/message.server";
26+
import { resolveOrgIdFromSlug } from "~/models/organization.server";
27+
import { dashboardAction } from "~/services/routeBuilders/dashboardBuilder";
2528
import { logger } from "~/services/logger.server";
2629
import { deletePrivateLink, getPrivateLinks } from "~/services/platform.v3.server";
2730
import { requireUserId } from "~/services/session.server";
@@ -54,6 +57,13 @@ export async function loader({ params, request }: LoaderFunctionArgs) {
5457
throw new Response(null, { status: 404, statusText: "Organization not found" });
5558
}
5659

60+
const sessionAuth = await rbac.authenticateSession(request, {
61+
userId,
62+
organizationId: organization.id,
63+
});
64+
const canManageConnections =
65+
sessionAuth.ok && sessionAuth.ability.can("write", { type: "privateConnections" });
66+
5767
const [error, connections] = await tryCatch(getPrivateLinks(organization.id));
5868
if (error) {
5969
logger.error("Error loading private link connections", {
@@ -65,52 +75,63 @@ export async function loader({ params, request }: LoaderFunctionArgs) {
6575
return typedjson({
6676
connections: connections?.connections ?? [],
6777
organizationId: organization.id,
78+
canManageConnections,
6879
});
6980
}
7081

71-
export const action = async ({ request, params }: ActionFunctionArgs) => {
72-
const userId = await requireUserId(request);
73-
const { organizationSlug } = OrganizationParamsSchema.parse(params);
82+
export const action = dashboardAction(
83+
{
84+
params: OrganizationParamsSchema,
85+
context: async (params) => {
86+
const organizationId = await resolveOrgIdFromSlug(params.organizationSlug);
87+
return organizationId ? { organizationId } : {};
88+
},
89+
authorization: { action: "write", resource: { type: "privateConnections" } },
90+
},
91+
async ({ request, params }) => {
92+
const userId = await requireUserId(request);
93+
const { organizationSlug } = params;
7494

75-
if (request.method !== "DELETE" && request.method !== "POST") {
76-
return json({ error: "Method not allowed" }, { status: 405 });
77-
}
95+
if (request.method !== "DELETE" && request.method !== "POST") {
96+
return json({ error: "Method not allowed" }, { status: 405 });
97+
}
7898

79-
const formData = await request.formData();
80-
const connectionId = formData.get("connectionId");
81-
const intent = formData.get("intent");
99+
const formData = await request.formData();
100+
const connectionId = formData.get("connectionId");
101+
const intent = formData.get("intent");
82102

83-
if (intent !== "delete" || typeof connectionId !== "string") {
84-
return json({ error: "Invalid request" }, { status: 400 });
85-
}
103+
if (intent !== "delete" || typeof connectionId !== "string") {
104+
return json({ error: "Invalid request" }, { status: 400 });
105+
}
86106

87-
const organization = await prisma.organization.findFirst({
88-
where: { slug: organizationSlug, members: { some: { userId } } },
89-
});
107+
const organization = await prisma.organization.findFirst({
108+
where: { slug: organizationSlug, members: { some: { userId } } },
109+
});
90110

91-
if (!organization) {
92-
return redirectWithErrorMessage(
93-
v3PrivateConnectionsPath({ slug: organizationSlug }),
94-
request,
95-
"Organization not found"
96-
);
97-
}
111+
if (!organization) {
112+
return redirectWithErrorMessage(
113+
v3PrivateConnectionsPath({ slug: organizationSlug }),
114+
request,
115+
"Organization not found"
116+
);
117+
}
98118

99-
const [error] = await tryCatch(deletePrivateLink(organization.id, connectionId));
100-
if (error) {
101-
return redirectWithErrorMessage(
119+
const [error] = await tryCatch(deletePrivateLink(organization.id, connectionId));
120+
if (error) {
121+
return redirectWithErrorMessage(
122+
v3PrivateConnectionsPath({ slug: organizationSlug }),
123+
request,
124+
`Failed to delete connection: ${error.message}`
125+
);
126+
}
127+
128+
return redirectWithSuccessMessage(
102129
v3PrivateConnectionsPath({ slug: organizationSlug }),
103130
request,
104-
`Failed to delete connection: ${error.message}`
131+
"Connection deletion initiated"
105132
);
106133
}
107-
108-
return redirectWithSuccessMessage(
109-
v3PrivateConnectionsPath({ slug: organizationSlug }),
110-
request,
111-
"Connection deletion initiated"
112-
);
113-
};
134+
);
114135

115136
const STATUS_COLORS: Record<PrivateLinkConnectionStatus, string> = {
116137
PENDING: "bg-amber-500/10 text-amber-700 dark:text-amber-400 system:text-warning",
@@ -153,7 +174,7 @@ function CopyButton({ value }: { value: string }) {
153174
const TERMINAL_STATUSES: PrivateLinkConnectionStatus[] = ["ACTIVE", "ERROR"];
154175

155176
export default function Page() {
156-
const { connections } = useTypedLoaderData<typeof loader>();
177+
const { connections, canManageConnections } = useTypedLoaderData<typeof loader>();
157178
const plan = useCurrentPlan();
158179
const revalidator = useRevalidator();
159180

@@ -176,6 +197,7 @@ export default function Page() {
176197
const hasPrivateNetworking = plan?.v3Subscription?.plan?.limits?.hasPrivateNetworking ?? false;
177198
const limit = plan?.v3Subscription?.plan?.limits?.privateLinkConnectionLimit ?? 2;
178199
const canAdd = connections.filter((c) => c.status !== "DELETING").length < limit;
200+
const permissionTooltip = "You don't have permission to manage private connections";
179201

180202
return (
181203
<PageContainer>
@@ -190,9 +212,16 @@ export default function Page() {
190212
Private connection docs
191213
</LinkButton>
192214
{hasPrivateNetworking && canAdd && (
193-
<LinkButton variant="primary/small" LeadingIcon={PlusIcon} to="new">
194-
Add Connection
195-
</LinkButton>
215+
<span title={!canManageConnections ? permissionTooltip : undefined}>
216+
<LinkButton
217+
variant="primary/small"
218+
LeadingIcon={PlusIcon}
219+
to="new"
220+
disabled={!canManageConnections}
221+
>
222+
Add Connection
223+
</LinkButton>
224+
</span>
196225
)}
197226
</PageAccessories>
198227
</NavBar>
@@ -220,9 +249,16 @@ export default function Page() {
220249
No private connections yet. Add your first connection to securely reach your AWS
221250
resources from task pods.
222251
</Paragraph>
223-
<LinkButton variant="primary/small" LeadingIcon={PlusIcon} to="new">
224-
Add Connection
225-
</LinkButton>
252+
<span title={!canManageConnections ? permissionTooltip : undefined}>
253+
<LinkButton
254+
variant="primary/small"
255+
LeadingIcon={PlusIcon}
256+
to="new"
257+
disabled={!canManageConnections}
258+
>
259+
Add Connection
260+
</LinkButton>
261+
</span>
226262
</div>
227263
) : (
228264
<div className="flex flex-col gap-3">
@@ -238,8 +274,9 @@ export default function Page() {
238274
<input type="hidden" name="intent" value="delete" />
239275
<button
240276
type="submit"
241-
className="text-text-dimmed transition hover:text-rose-400"
242-
title="Delete connection"
277+
disabled={!canManageConnections}
278+
className="text-text-dimmed transition hover:text-rose-400 disabled:cursor-not-allowed disabled:opacity-50"
279+
title={canManageConnections ? "Delete connection" : permissionTooltip}
243280
onClick={(e) => {
244281
if (
245282
!confirm(
@@ -293,9 +330,16 @@ export default function Page() {
293330

294331
{canAdd && (
295332
<div className="flex justify-center pt-2">
296-
<LinkButton variant="primary/small" LeadingIcon={PlusIcon} to="new">
297-
Add Connection
298-
</LinkButton>
333+
<span title={!canManageConnections ? permissionTooltip : undefined}>
334+
<LinkButton
335+
variant="primary/small"
336+
LeadingIcon={PlusIcon}
337+
to="new"
338+
disabled={!canManageConnections}
339+
>
340+
Add Connection
341+
</LinkButton>
342+
</span>
299343
</div>
300344
)}
301345

0 commit comments

Comments
 (0)