Skip to content

Commit 8a1b530

Browse files
committed
feat(platform): offer #9869's credential methods where the Platform connects a provider
Every connection method staging's #9869 added is now a choice in the Platform's existing setup dialogs, as AWS and Databricks are: an Authentication select, each choice's own fields, and for keyless sign-in the issuer, audience, subject and setup snippet read from the workload-identity setup. - Snowflake: a config descriptor offers the keyless service user (with its CREATE USER statement) beside the programmatic access token, which still submits apiToken and domain; a saved Search account keeps the token form. Labelled "Add Snowflake connection". - Microsoft app credential (Entra ID, SharePoint, Dataverse, Dynamics 365): a config descriptor with federated (keyless), client secret and certificate. - Google keyless: the Google dialog offers Workload Identity Federation beside the JSON key, only for a Google Cloud service (BigQuery, Vertex AI). The OAuth service reaches it from the catalog connection on the Integrations page and from the block's serviceId in the workflow editor's credential picker. - A reconnect of Snowflake, Microsoft or Google opens on keyless only when the setup reports the stored credential is keyless; new Google credentials start on the JSON key. - GitHub and Bitbucket tokens and DocuSign JWT Grant come through the existing token and client-credential forms; GitHub and Bitbucket follow GitLab's service-account token (personal_token stays GitLab-only on the server). - The catalog generator lists the new methods and carries each service account's OAuth service; the field renderer is shared by the key form and the Google dialog.
1 parent 7303d70 commit 8a1b530

18 files changed

Lines changed: 976 additions & 194 deletions

File tree

Lines changed: 131 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
'use client'
22

3-
import { TextAreaField, TextField } from '@sim/emcn/field'
3+
import { SelectField, TextAreaField, TextField } from '@sim/emcn/field'
44
import { FileField } from '@sim/emcn/field/file-field'
5+
import { CopyField, SecretField } from '@sim/emcn/field/value-fields'
56
import { Modal } from '@sim/emcn/modal'
6-
import { type ResourceScope, useGoogleServiceAccount } from '@/platform/lib/data/credentials'
7+
import {
8+
type ResourceScope,
9+
type ServiceAccountFormField,
10+
useGoogleServiceAccount,
11+
} from '@/platform/lib/data/credentials'
712

813
interface GoogleServiceAccountModalProps {
914
open: boolean
@@ -13,21 +18,25 @@ interface GoogleServiceAccountModalProps {
1318
serviceName: string
1419
/** Receives the new credential id so the picker can select it. */
1520
onCreated: (credentialId: string) => void
21+
/** The OAuth service the account is for; a Google Cloud service also offers keyless setup. */
22+
oauthServiceId?: string
1623
}
1724

1825
/**
19-
* Adds a Google service account from its JSON key, pasted or chosen as a `.json` file. The display
20-
* name fills in from the key's `client_email` until the user types one, and the key is checked
21-
* against the server's schema before it is sent.
26+
* Adds a Google service account from its JSON key, pasted or chosen as a `.json` file, or for a
27+
* Google Cloud service keyless through Workload Identity Federation, chosen in the Authentication
28+
* select. The display name fills in from the key's `client_email` until the user types one, and the
29+
* key is checked against the server's schema before it is sent.
2230
*/
2331
export function GoogleServiceAccountModal({
2432
open,
2533
onOpenChange,
2634
scope,
2735
serviceName,
2836
onCreated,
37+
oauthServiceId,
2938
}: GoogleServiceAccountModalProps) {
30-
const form = useGoogleServiceAccount({ open, scope, onCreated })
39+
const form = useGoogleServiceAccount({ open, scope, onCreated, oauthServiceId })
3140
const docsUrl = form.docsUrl
3241

3342
return (
@@ -45,34 +54,43 @@ export function GoogleServiceAccountModal({
4554
onClick: () => window.open(docsUrl, '_blank', 'noopener,noreferrer'),
4655
}}
4756
>
48-
<TextAreaField
49-
label='JSON key'
50-
value={form.jsonInput}
51-
onChange={form.setJsonInput}
52-
placeholder='Paste your service account JSON key here'
53-
autoComplete='off'
54-
spellCheck={false}
55-
defaultSize='expanded'
56-
required
57-
/>
58-
<FileField
59-
label='Or upload a file'
60-
accept='.json'
61-
prompt='Drag & drop a .json file, or click to browse'
62-
selection={
63-
form.uploadedFile && {
64-
name: form.uploadedFile.name,
65-
detail: 'Click or drop to replace',
66-
}
67-
}
68-
error={form.fileError}
69-
onFileChange={(file) => void form.chooseFile(file)}
70-
/>
57+
<ServiceAccountFormFields fields={form.keylessFields} onChange={form.setKeylessField} />
58+
{!form.keyless && (
59+
<>
60+
<TextAreaField
61+
label='JSON key'
62+
value={form.jsonInput}
63+
onChange={form.setJsonInput}
64+
placeholder='Paste your service account JSON key here'
65+
autoComplete='off'
66+
spellCheck={false}
67+
defaultSize='expanded'
68+
required
69+
/>
70+
<FileField
71+
label='Or upload a file'
72+
accept='.json'
73+
prompt='Drag & drop a .json file, or click to browse'
74+
selection={
75+
form.uploadedFile && {
76+
name: form.uploadedFile.name,
77+
detail: 'Click or drop to replace',
78+
}
79+
}
80+
error={form.fileError}
81+
onFileChange={(file) => void form.chooseFile(file)}
82+
/>
83+
</>
84+
)}
7185
<TextField
7286
label='Display name'
7387
value={form.displayName}
7488
onChange={form.setDisplayName}
75-
placeholder='Auto-populated from client_email'
89+
placeholder={
90+
form.keyless
91+
? 'Defaults to the service account email'
92+
: 'Auto-populated from client_email'
93+
}
7694
autoComplete='off'
7795
optional
7896
/>
@@ -87,3 +105,86 @@ export function GoogleServiceAccountModal({
87105
</Modal>
88106
)
89107
}
108+
109+
interface ServiceAccountFormFieldsProps {
110+
fields: readonly ServiceAccountFormField[]
111+
onChange: (id: string, value: string) => void
112+
}
113+
114+
/**
115+
* A service-account setup form's fields: a choice as a select, a value Sim issues as a copy field
116+
* (multiline as a read-only text area), a secret masked, and every other value as text. The shared
117+
* key form renders its fields with it too; it lives here so the dialogs add no module of their own.
118+
*/
119+
export function ServiceAccountFormFields({ fields, onChange }: ServiceAccountFormFieldsProps) {
120+
return (
121+
<>
122+
{fields.map((field) =>
123+
field.copy ? (
124+
field.multiline ? (
125+
<TextAreaField
126+
key={field.id}
127+
label={field.label}
128+
value={field.value}
129+
placeholder={field.placeholder}
130+
viewOnly
131+
spellCheck={false}
132+
/>
133+
) : (
134+
<CopyField key={field.id} label={field.label} value={field.value} />
135+
)
136+
) : field.options ? (
137+
<SelectField
138+
key={field.id}
139+
label={field.label}
140+
value={field.value}
141+
onValueChange={(value) => onChange(field.id, value)}
142+
options={field.options}
143+
error={field.error}
144+
optional={!field.required}
145+
/>
146+
) : field.multiline ? (
147+
<TextAreaField
148+
key={field.id}
149+
label={field.label}
150+
value={field.value}
151+
onChange={(value) => onChange(field.id, value)}
152+
placeholder={field.placeholder}
153+
autoComplete='off'
154+
spellCheck={false}
155+
required={field.required}
156+
optional={!field.required}
157+
hint={field.formatWarning}
158+
error={field.error}
159+
/>
160+
) : field.secret ? (
161+
<SecretField
162+
key={field.id}
163+
label={field.label}
164+
value={field.value}
165+
onChange={(value) => onChange(field.id, value)}
166+
placeholder={field.placeholder}
167+
autoComplete='new-password'
168+
required={field.required}
169+
optional={!field.required}
170+
hint={field.formatWarning}
171+
error={field.error}
172+
/>
173+
) : (
174+
<TextField
175+
key={field.id}
176+
label={field.label}
177+
value={field.value}
178+
onChange={(value) => onChange(field.id, value)}
179+
placeholder={field.placeholder}
180+
autoComplete='off'
181+
required={field.required}
182+
optional={!field.required}
183+
hint={field.formatWarning}
184+
error={field.error}
185+
/>
186+
)
187+
)}
188+
</>
189+
)
190+
}

0 commit comments

Comments
 (0)