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.

152 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(
42 user,
43 params.owner,
44 String(form.get("id") ?? ""),
45 decision,
46 String(form.get("comment") ?? "")
47 .trim()
48 .slice(0, 500),
49 );
50 return done.ok ? { ok: true } : { ok: false, error: done.error.message };
51}
52
53const STATE_TONE = { pending: "warn", approved: "accent", denied: "danger", cancelled: "neutral" } as const;
54
55function Review({ request, mine }: { request: BypassRequest; mine: boolean }) {
56 const fetcher = useFetcher<{ ok: boolean; error?: string }>();
57 const busy = fetcher.state !== "idle";
58 return (
59 <fetcher.Form method="post" className="mt-3 flex flex-col gap-2 sm:flex-row sm:items-center">
60 <input type="hidden" name="id" value={request.id} />
61 {!mine && (
62 <input
63 name="comment"
64 maxLength={500}
65 placeholder="Comment (optional)"
66 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"
67 />
68 )}
69 <div className="flex gap-2">
70 {mine ? (
71 <Button type="submit" name="decision" value="cancel" disabled={busy} variant="outline" size="inline" className="px-2.5 py-1 text-xs text-muted">
72 Cancel request
73 </Button>
74 ) : (
75 <>
76 <Button type="submit" name="decision" value="approve" disabled={busy} size="inline" className="px-3 py-1 text-xs">
77 Approve
78 </Button>
79 <Button type="submit" name="decision" value="deny" disabled={busy} variant="destructive" size="inline" className="px-3 py-1 text-xs">
80 Deny
81 </Button>
82 </>
83 )}
84 </div>
85 {fetcher.data?.error && <span className="text-xs text-danger">{fetcher.data.error}</span>}
86 </fetcher.Form>
87 );
88}
89
90export default function BypassRequests({ loaderData, params }: Route.ComponentProps) {
91 const { requests, pending, me } = loaderData;
92 const [search, setSearch] = useSearchParams();
93 const state = search.get("state") ?? "pending";
94 return (
95 <div>
96 <WorkspaceSecurityHeading
97 title="Bypass requests"
98 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."
99 />
100 <WorkspaceSecurityTabs owner={params.owner} pending={pending} />
101 <div className="mb-4">
102 <FilterSelect
103 label="State"
104 value={STATES.has(state) ? state : "all"}
105 options={[
106 ["pending", "Pending"],
107 ["approved", "Approved"],
108 ["denied", "Denied"],
109 ["cancelled", "Cancelled"],
110 ["all", "All"],
111 ]}
112 onChange={(value) => setSearch(value === "pending" ? {} : { state: value }, { replace: true })}
113 />
114 </div>
115 {requests.length === 0 ? (
116 <Card asChild tone="plain" className="border-dashed px-4 py-6 text-sm text-muted">
117 <p>No {state === "all" ? "" : `${state} `}requests. Delegated bypass is turned on in this workspace's Security settings.</p>
118 </Card>
119 ) : (
120 <ul className="space-y-3">
121 {requests.map((request) => (
122 <li key={request.id} className={`${CARD} p-4`}>
123 <div className="flex flex-wrap items-center gap-2">
124 <Link
125 to={`/${request.workspace}/${request.repo}/security/secret-scanning/${request.secretId}`}
126 className="text-sm font-medium first-letter:uppercase hover:underline"
127 >
128 {request.label}
129 </Link>
130 <Badge tone={STATE_TONE[request.state]}>{request.state}</Badge>
131 <span className="font-mono text-xs text-faint">
132 {request.repo} · {request.path}:{request.line}
133 </span>
134 </div>
135 <p className="mt-1.5 text-sm text-muted">
136 {request.requester} asked <TimeAgo at={request.createdAt} />: {request.reason.replaceAll("_", " ")}
137 {request.comment && <> · “{request.comment}”</>}
138 </p>
139 {request.reviewer && (
140 <p className="mt-1 text-xs text-faint">
141 {request.state} by {request.reviewer}
142 {request.reviewComment && <>: “{request.reviewComment}”</>}
143 </p>
144 )}
145 {request.state === "pending" && <Review request={request} mine={request.requester.toLowerCase() === me.toLowerCase()} />}
146 </li>
147 ))}
148 </ul>
149 )}
150 </div>
151 );
152}