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.

145 linesCodeBlameRaw
1import { Link } from "react-router";
2
3import type { SsoSettings } from "@g1t/contracts";
4
5import { Badge, EmptyState, Notice, Section, When } from "~/components/ui";
6
7const PROTOCOLS = { oidc: "OpenID Connect", oauth2: "OAuth 2.0", saml: "SAML 2.0" } as const;
8
9/** Which provider a connection is, by its own address. */
10function provider(sso: NonNullable<SsoSettings["connection"]>): string {
11 const config = sso.config;
12 return config.issuer ?? config.idp_entity_id ?? config.authorization_url ?? "—";
13}
14
15/**
16 * A workspace's single sign-on, as its owners see it: the provider, whether
17 * it is required, and who is linked. Read only: owners change it on the
18 * site, and are never held out, so staff never need to.
19 */
20export function SsoSection({ sso, unavailable }: { sso: SsoSettings | null; unavailable: string | null }) {
21 const connection = sso?.connection ?? null;
22 return (
23 <Section
24 id="sso"
25 title="Single sign-on"
26 description="The workspace's own identity provider. Owners set it up and require it at /-/sso; they are never held out."
27 className="mt-6"
28 >
29 {unavailable ? (
30 <Notice tone="error">Single sign-on could not be read: {unavailable}</Notice>
31 ) : !connection ? (
32 <EmptyState title="Not set up" />
33 ) : (
34 <div className="space-y-5">
35 <dl className="grid gap-x-6 gap-y-3 text-sm sm:grid-cols-2">
36 <div>
37 <dt className="text-xs text-faint">Protocol</dt>
38 <dd className="mt-0.5 flex flex-wrap items-center gap-1.5">
39 {PROTOCOLS[connection.protocol]}
40 {sso?.required ? <Badge tone="lavender">Required of members</Badge> : <Badge>Not required</Badge>}
41 {connection.allow_join && <Badge tone="info">Provider's people join</Badge>}
42 </dd>
43 </div>
44 <div>
45 <dt className="text-xs text-faint">Provider</dt>
46 <dd className="mt-0.5 font-mono text-xs break-all">{provider(connection)}</dd>
47 </div>
48 <div>
49 <dt className="text-xs text-faint">Tested</dt>
50 <dd className="mt-0.5">
51 {connection.tested_at ? (
52 <>
53 By{" "}
54 {connection.tested_by ? (
55 <Link to={`/users/${connection.tested_by}`} className="font-mono hover:underline">
56 {connection.tested_by}
57 </Link>
58 ) : (
59 "someone"
60 )}{" "}
61 <When at={connection.tested_at} />
62 </>
63 ) : (
64 <Badge tone="warn">Not yet</Badge>
65 )}
66 </dd>
67 </div>
68 <div>
69 <dt className="text-xs text-faint">Set up</dt>
70 <dd className="mt-0.5">
71 <When at={connection.created_at} />, changed <When at={connection.updated_at} />
72 </dd>
73 </div>
74 {connection.protocol !== "saml" && (
75 <div>
76 <dt className="text-xs text-faint">Client id</dt>
77 <dd className="mt-0.5 font-mono text-xs break-all">
78 {connection.config.client_id ?? "—"} {connection.has_client_secret ? "(secret kept)" : "(no secret)"}
79 </dd>
80 </div>
81 )}
82 {connection.certificates.length > 0 && (
83 <div>
84 <dt className="text-xs text-faint">Signing certificates, SHA-256</dt>
85 <dd className="mt-0.5 space-y-0.5 font-mono text-xs break-all">
86 {connection.certificates.map((certificate) => (
87 <p key={certificate.fingerprint}>{certificate.fingerprint}</p>
88 ))}
89 </dd>
90 </div>
91 )}
92 </dl>
93 <p className="text-sm">
94 <span className="text-xs text-faint">SCIM </span>
95 {sso?.scim ? (
96 <>
97 <Badge tone="info">On</Badge>{" "}
98 <span className="text-muted">
99 {sso.scim.users.filter((user) => user.active).length} active of {sso.scim.users.length} assigned, token ending{" "}
100 <span className="font-mono">{sso.scim.hint}</span>, last used <When at={sso.scim.last_used_at} />
101 </span>
102 </>
103 ) : (
104 <Badge>Off</Badge>
105 )}
106 </p>
107 {sso!.identities.length === 0 ? (
108 <p className="text-sm text-muted">No one has signed in through it yet.</p>
109 ) : (
110 <div className="-mx-4 -mb-4 overflow-x-auto border-t border-line sm:-mx-5 sm:-mb-5">
111 <table className="w-full min-w-120 text-sm">
112 <thead>
113 <tr className="border-b border-line text-left text-xs text-muted">
114 <th className="px-4 py-2 font-medium sm:pl-5">Person</th>
115 <th className="px-4 py-2 font-medium">At the provider</th>
116 <th className="px-4 py-2 font-medium">Linked</th>
117 <th className="px-4 py-2 font-medium sm:pr-5">Last signed in</th>
118 </tr>
119 </thead>
120 <tbody>
121 {sso!.identities.map((identity) => (
122 <tr key={identity.username} className="border-b border-line last:border-0">
123 <td className="px-4 py-2.5 font-mono sm:pl-5">
124 <Link to={`/users/${identity.username}`} className="hover:underline">
125 {identity.username}
126 </Link>
127 </td>
128 <td className="px-4 py-2.5 break-all text-muted">{identity.email ?? identity.subject}</td>
129 <td className="px-4 py-2.5 text-xs whitespace-nowrap text-muted">
130 <When at={identity.linked_at} />
131 </td>
132 <td className="px-4 py-2.5 text-xs whitespace-nowrap text-muted sm:pr-5">
133 <When at={identity.last_used_at} />
134 </td>
135 </tr>
136 ))}
137 </tbody>
138 </table>
139 </div>
140 )}
141 </div>
142 )}
143 </Section>
144 );
145}