Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

137 linesCodeBlameRaw
1import { Link, data, useFetcher, useSearchParams } from "react-router";
2
3import type { BypassRequest } from "@g1t/contracts";
4
5import type { Route } from "./+types/security-bypass";
6import { page } from "../../lib/meta";
7import { CARD, FilterSelect } from "../../components/security-suite";
8import { WorkspaceSecurityHeading, WorkspaceSecurityTabs } from "../../components/workspace-security-tabs";
9import { TimeAgo } from "../../components/ui";
10import { Badge } from "../../components/ui/badge";
11import { Button } from "../../components/ui/button";
12import { Card } from "../../components/ui/card";
13import { securitySuite } from "../../lib/services.server";
14import { assertSameOrigin, getViewer, requireUser, roleIn, unwrap } from "../../lib/session.server";
15import { isFailed, part } from "../../lib/partial";
16
17export function meta({ params, ...args }: Route.MetaArgs) {
18 return page(args, { title: `Bypass requests · ${params.owner} · g1t` });
19}
20
21const STATES = new Set(["pending", "approved", "denied", "cancelled"]);
22
23export async function loader({ params, context, request }: Route.LoaderArgs) {
24 const viewer = getViewer(context);
25 if (!roleIn(viewer, params.owner)) throw data(null, { status: 404 });
26 const state = new URL(request.url).searchParams.get("state") ?? "pending";
27 const [list, pending] = await Promise.all([
28 securitySuite.bypassRequests(params.owner, viewer, STATES.has(state) ? state : null),
29 // Only the tabs' badge: without it, the badge goes without a count.
30 part(securitySuite.bypassRequests(params.owner, viewer, "pending"), "Pending bypass requests"),
31 ]);
32 return { requests: unwrap(list), pending: !isFailed(pending) && pending.ok ? pending.value.length : 0, me: viewer?.username ?? "" };
33}
34
35export async function action({ params, context, request }: Route.ActionArgs) {
36 assertSameOrigin(request);
37 const user = requireUser(context, request);
38 const form = await request.formData();
39 const decision = String(form.get("decision") ?? "");
40 if (decision !== "approve" && decision !== "deny" && decision !== "cancel") return { ok: false, error: "Choose approve or deny." };
41 const done = await securitySuite.reviewBypass(user, params.owner, String(form.get("id") ?? ""), decision, String(form.get("comment") ?? "").trim().slice(0, 500));
42 return done.ok ? { ok: true } : { ok: false, error: done.error.message };
43}
44
45const STATE_TONE = { pending: "warn", approved: "accent", denied: "danger", cancelled: "neutral" } as const;
46
47function Review({ request, mine }: { request: BypassRequest; mine: boolean }) {
48 const fetcher = useFetcher<{ ok: boolean; error?: string }>();
49 const busy = fetcher.state !== "idle";
50 return (
51 <fetcher.Form method="post" className="mt-3 flex flex-col gap-2 sm:flex-row sm:items-center">
52 <input type="hidden" name="id" value={request.id} />
53 {!mine && (
54 <input
55 name="comment"
56 maxLength={500}
57 placeholder="Comment (optional)"
58 className="h-8 min-w-0 grow rounded-md border border-line bg-bg px-2.5 text-[0.8125rem] outline-none hover:border-line-strong focus:border-accent-dim"
59 />
60 )}
61 <div className="flex gap-2">
62 {mine ? (
63 <Button type="submit" name="decision" value="cancel" disabled={busy} variant="outline" size="inline" className="px-2.5 py-1 text-xs text-muted">
64 Cancel request
65 </Button>
66 ) : (
67 <>
68 <Button type="submit" name="decision" value="approve" disabled={busy} size="inline" className="px-3 py-1 text-xs">
69 Approve
70 </Button>
71 <Button type="submit" name="decision" value="deny" disabled={busy} variant="destructive" size="inline" className="px-3 py-1 text-xs">
72 Deny
73 </Button>
74 </>
75 )}
76 </div>
77 {fetcher.data?.error && <span className="text-xs text-danger">{fetcher.data.error}</span>}
78 </fetcher.Form>
79 );
80}
81
82export default function BypassRequests({ loaderData, params }: Route.ComponentProps) {
83 const { requests, pending, me } = loaderData;
84 const [search, setSearch] = useSearchParams();
85 const state = search.get("state") ?? "pending";
86 return (
87 <div>
88 <WorkspaceSecurityHeading
89 title="Bypass requests"
90 about="With delegated bypass on, people who push a blocked secret ask to push it anyway, and the workspace's owners and the repository's admins decide. Each decision is recorded on the alert and in the audit log."
91 />
92 <WorkspaceSecurityTabs owner={params.owner} pending={pending} />
93 <div className="mb-4">
94 <FilterSelect
95 label="State"
96 value={STATES.has(state) ? state : "all"}
97 options={[["pending", "Pending"], ["approved", "Approved"], ["denied", "Denied"], ["cancelled", "Cancelled"], ["all", "All"]]}
98 onChange={(value) => setSearch(value === "pending" ? {} : { state: value }, { replace: true })}
99 />
100 </div>
101 {requests.length === 0 ? (
102 <Card asChild tone="plain" className="border-dashed px-4 py-6 text-sm text-muted">
103 <p>
104 No {state === "all" ? "" : `${state} `}requests. Delegated bypass is turned on in this workspace's Security settings.
105 </p>
106 </Card>
107 ) : (
108 <ul className="space-y-3">
109 {requests.map((request) => (
110 <li key={request.id} className={`${CARD} p-4`}>
111 <div className="flex flex-wrap items-center gap-2">
112 <Link to={`/${request.workspace}/${request.repo}/security/secret-scanning/${request.secretId}`} className="text-sm font-medium first-letter:uppercase hover:underline">
113 {request.label}
114 </Link>
115 <Badge tone={STATE_TONE[request.state]}>{request.state}</Badge>
116 <span className="font-mono text-xs text-faint">
117 {request.repo} · {request.path}:{request.line}
118 </span>
119 </div>
120 <p className="mt-1.5 text-sm text-muted">
121 {request.requester} asked <TimeAgo at={request.createdAt} />: {request.reason.replaceAll("_", " ")}
122 {request.comment && <> · “{request.comment}”</>}
123 </p>
124 {request.reviewer && (
125 <p className="mt-1 text-xs text-faint">
126 {request.state} by {request.reviewer}
127 {request.reviewComment && <>: “{request.reviewComment}”</>}
128 </p>
129 )}
130 {request.state === "pending" && <Review request={request} mine={request.requester.toLowerCase() === me.toLowerCase()} />}
131 </li>
132 ))}
133 </ul>
134 )}
135 </div>
136 );
137}