Skip to content

Commit f5b5a0b

Browse files
committed
fix(credentials): offer keyless Google setup only in a Google Cloud context
Reconnecting a keyless Google service account from a Workspace integration (Gmail, Drive, ...) offered the keyless form, saving a credential that integration can never use. The dialog now offers keyless only for a Google Cloud service (new or reconnect) or on the credential's own page; from a Workspace integration it explains the credential works only with BigQuery and Vertex AI and does not save. The chat connect control passes its service so it gets the same gate.
1 parent c70981a commit f5b5a0b

4 files changed

Lines changed: 97 additions & 5 deletions

File tree

‎apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2540,6 +2540,7 @@ function ServiceAccountConnectDisplay({
25402540
atlassianProduct={match?.providerId === 'confluence' ? 'confluence' : 'jira'}
25412541
serviceName={target.serviceName}
25422542
serviceIcon={target.serviceIcon}
2543+
oauthServiceId={service?.providerId}
25432544
credentialId={reconnectCredentialId}
25442545
credentialDisplayName={reconnectCredential?.displayName ?? undefined}
25452546
onCreated={() => {

‎apps/sim/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/connect-service-account-modal.tsx‎

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { type ComponentType, useState } from 'react'
44
import {
55
ChipModal,
66
ChipModalBody,
7+
ChipModalDescription,
78
ChipModalError,
89
ChipModalField,
910
ChipModalFooter,
@@ -25,7 +26,10 @@ import {
2526
type ClientCredentialAccountProviderId,
2627
getClientCredentialAccountDescriptor,
2728
} from '@/lib/credentials/client-credential-accounts/descriptors'
28-
import { GOOGLE_KEYLESS_SERVICES } from '@/lib/credentials/google-workload-identity'
29+
import {
30+
GOOGLE_KEYLESS_SERVICES,
31+
googleKeylessOffer,
32+
} from '@/lib/credentials/google-workload-identity'
2933
import type { ServiceAccountSecretFields } from '@/lib/credentials/service-account-secret'
3034
import {
3135
getTokenServiceAccountDescriptor,
@@ -668,7 +672,12 @@ function GoogleServiceAccountModalForm({
668672
onCreated,
669673
onClose: () => onOpenChange(false),
670674
})
671-
const offerKeyless = Boolean(keyless.available) && (cloudService || keyless.reconnectsKeyless)
675+
const keylessOffer = googleKeylessOffer({
676+
oauthServiceId,
677+
available: Boolean(keyless.available),
678+
reconnectsKeyless: keyless.reconnectsKeyless,
679+
})
680+
const offerKeyless = keylessOffer === 'offer'
672681
const [selectedAuthType, setSelectedAuthType] = useState<GoogleAuthType | null>(null)
673682
const authType: GoogleAuthType = offerKeyless
674683
? (selectedAuthType ?? (keyless.reconnectsKeyless ? 'workload_identity' : 'json_key'))
@@ -764,10 +773,12 @@ function GoogleServiceAccountModalForm({
764773

765774
const isPending =
766775
createCredential.isPending || updateCredential.isPending || keylessSubmit.isPending
776+
const cloudOnly = keylessOffer === 'cloud_only'
767777
const isDisabled =
768-
authType === 'workload_identity'
778+
cloudOnly ||
779+
(authType === 'workload_identity'
769780
? isPending || !keyless.ready || !isGoogleKeylessInputComplete(keylessInput)
770-
: !jsonInput.trim() || isPending
781+
: !jsonInput.trim() || isPending)
771782
const submitError = authType === 'workload_identity' ? keylessSubmit.error : error
772783

773784
return (
@@ -817,7 +828,14 @@ function GoogleServiceAccountModalForm({
817828
/>
818829
)}
819830

820-
{authType === 'json_key' && (
831+
{cloudOnly && (
832+
<ChipModalDescription>
833+
This keyless credential works only with Google Cloud services (BigQuery, Vertex AI).
834+
Reconnect it from one of those integrations or from the credential’s own page.
835+
</ChipModalDescription>
836+
)}
837+
838+
{authType === 'json_key' && !cloudOnly && (
821839
<>
822840
<ChipModalField
823841
type='textarea'
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/**
2+
* @vitest-environment node
3+
*/
4+
import { describe, expect, it } from 'vitest'
5+
import { googleKeylessOffer } from '@/lib/credentials/google-workload-identity'
6+
7+
describe('keyless setup in the Google service account dialog', () => {
8+
it.each(['google-drive', 'gmail', 'google-sheets'])(
9+
'never offers the keyless form when reconnecting a keyless credential from %s',
10+
(oauthServiceId) => {
11+
expect(googleKeylessOffer({ oauthServiceId, available: true, reconnectsKeyless: true })).toBe(
12+
'cloud_only'
13+
)
14+
}
15+
)
16+
17+
it('offers JSON keys only for a new connection from a Workspace service', () => {
18+
expect(
19+
googleKeylessOffer({ oauthServiceId: 'gmail', available: true, reconnectsKeyless: false })
20+
).toBe('none')
21+
})
22+
23+
it.each([true, false])(
24+
'offers keyless for BigQuery whether or not the reconnected credential is keyless (%s)',
25+
(reconnectsKeyless) => {
26+
expect(
27+
googleKeylessOffer({
28+
oauthServiceId: 'google-bigquery',
29+
available: true,
30+
reconnectsKeyless,
31+
})
32+
).toBe('offer')
33+
}
34+
)
35+
36+
it('keeps a keyless credential keyless when reconnected from its own page', () => {
37+
expect(googleKeylessOffer({ available: true, reconnectsKeyless: true })).toBe('offer')
38+
expect(googleKeylessOffer({ available: true, reconnectsKeyless: false })).toBe('none')
39+
})
40+
41+
it('offers nothing keyless where this deployment cannot sign tokens', () => {
42+
expect(
43+
googleKeylessOffer({
44+
oauthServiceId: 'google-bigquery',
45+
available: false,
46+
reconnectsKeyless: true,
47+
})
48+
).toBe('none')
49+
})
50+
})

‎apps/sim/lib/credentials/google-workload-identity.ts‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,29 @@ export const GOOGLE_KEYLESS_SERVICES: ReadonlySet<string> = new Set([
2121
'vertex-ai',
2222
])
2323

24+
/**
25+
* What a Google service account connect dialog offers for keyless setup.
26+
*
27+
* - `offer`: a Google Cloud service (new or reconnect), or a keyless credential
28+
* reconnected from a context that names no service (its own detail page).
29+
* - `cloud_only`: a keyless credential reconnected from a Google Workspace
30+
* service, which can never use it; the dialog explains instead of saving.
31+
* - `none`: JSON key only.
32+
*/
33+
export function googleKeylessOffer(input: {
34+
/** The OAuth service the dialog was opened for, when it was opened for one. */
35+
oauthServiceId?: string
36+
/** This deployment can sign workload-identity tokens for this owner. */
37+
available: boolean
38+
/** The credential being reconnected is keyless. */
39+
reconnectsKeyless: boolean
40+
}): 'offer' | 'cloud_only' | 'none' {
41+
if (!input.available) return 'none'
42+
if (input.oauthServiceId && GOOGLE_KEYLESS_SERVICES.has(input.oauthServiceId)) return 'offer'
43+
if (!input.reconnectsKeyless) return 'none'
44+
return input.oauthServiceId ? 'cloud_only' : 'offer'
45+
}
46+
2447
/** The `type` of a keyless credential's stored secret; a JSON key's is `service_account`. */
2548
export const GOOGLE_WORKLOAD_IDENTITY_SECRET_TYPE = 'google_workload_identity_v1' as const
2649

0 commit comments

Comments
 (0)