Skip to content

Commit 990ea3a

Browse files
authored
fix(credentials): offer keyless Google delegation only for services that can be delegated (#9992)
1 parent 9bf50c9 commit 990ea3a

2 files changed

Lines changed: 52 additions & 21 deletions

File tree

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

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -415,27 +415,41 @@ describe('getting a keyless Google Cloud token for a run', () => {
415415
expect(google.iam[0]?.body).toEqual({ scope: [BIGQUERY], lifetime: '3600s' })
416416
})
417417

418-
it('refuses domain-wide delegation before contacting Google', async () => {
419-
fakeGoogle(fetch)
418+
it.each([
419+
['google-drive', 'google_keyless_delegation'],
420+
['google-bigquery', 'google_keyless_delegation_unavailable'],
421+
['google-ads', 'google_keyless_service_unsupported'],
422+
])(
423+
'refuses impersonation for %s before contacting Google, offering delegation only where it exists',
424+
async (service, step) => {
425+
fakeGoogle(fetch)
420426

421-
await expect(
422-
resolve({ credentialId: 'cred-dwd', impersonateEmail: 'alice@example.com' })
423-
).rejects.toMatchObject({
424-
code: 'unsupported_use',
425-
logDetail: { step: 'google_keyless_delegation' },
426-
userMessage: expect.any(String),
427-
})
428-
expect(fetch).not.toHaveBeenCalled()
429-
})
427+
await expect(
428+
resolve({ credentialId: 'cred-dwd', service, impersonateEmail: 'alice@example.com' })
429+
).rejects.toMatchObject({
430+
code: 'unsupported_use',
431+
logDetail: { step },
432+
userMessage: expect.any(String),
433+
})
434+
expect(fetch).not.toHaveBeenCalled()
435+
}
436+
)
430437

431-
it.each(['google-drive', 'gmail', 'google-sheets', 'google-ads', undefined])(
432-
'refuses %s, which keyless credentials do not serve, before contacting Google',
433-
async (service) => {
438+
it.each([
439+
['google-drive', 'google_keyless_service'],
440+
['gmail', 'google_keyless_service'],
441+
['google-sheets', 'google_keyless_service'],
442+
['google-email', 'google_keyless_service'],
443+
['google-ads', 'google_keyless_service_unsupported'],
444+
[undefined, 'google_keyless_service_unsupported'],
445+
])(
446+
'refuses %s, which keyless credentials do not serve, offering delegation only where it exists',
447+
async (service, step) => {
434448
fakeGoogle(fetch)
435449

436450
await expect(resolve({ credentialId: 'cred-workspace', service })).rejects.toMatchObject({
437451
code: 'unsupported_use',
438-
logDetail: { step: 'google_keyless_service' },
452+
logDetail: { step },
439453
userMessage: expect.any(String),
440454
})
441455
expect(fetch).not.toHaveBeenCalled()

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

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -930,7 +930,9 @@ export async function resolveGoogleWorkloadIdentityToken(
930930
input: ResolveGoogleWorkloadIdentityTokenInput
931931
): Promise<string> {
932932
const config = readStoredConfig(input.decrypted)
933-
const delegation = googleDelegationServiceFor(input.service) ? config.delegation : undefined
933+
// Reconnecting can add delegation only for the Workspace services keyless delegation covers.
934+
const delegable = googleDelegationServiceFor(input.service) !== undefined
935+
const delegation = delegable ? config.delegation : undefined
934936

935937
if (delegation) {
936938
if (input.impersonateEmail && input.impersonateEmail.trim().toLowerCase() !== delegation.user) {
@@ -941,20 +943,35 @@ export async function resolveGoogleWorkloadIdentityToken(
941943
)
942944
}
943945
} else {
944-
if (input.impersonateEmail) {
946+
const cloudService = Boolean(input.service && GOOGLE_KEYLESS_SERVICES.has(input.service))
947+
// Clearing the Impersonated Account helps only where keyless works without it.
948+
if (input.impersonateEmail && delegable) {
945949
throw unsupportedUse(
946950
'google_keyless_delegation',
947951
'this keyless Google credential has no domain-wide delegation for this service',
948952
'This keyless Google credential has no domain-wide delegation for this service, so it cannot act as a Workspace user. Clear the Impersonated Account, reconnect the credential to delegate this service, or use a JSON key credential instead'
949953
)
950954
}
951-
if (!input.service || !GOOGLE_KEYLESS_SERVICES.has(input.service)) {
955+
if (input.impersonateEmail && cloudService) {
952956
throw unsupportedUse(
953-
'google_keyless_service',
954-
'this keyless Google credential works only with Google Cloud APIs and the Workspace services it was delegated',
955-
'This keyless Google credential works only with Google Cloud tools such as BigQuery and, through domain-wide delegation, with Gmail, Google Drive, Google Docs, Google Sheets, and Google Calendar. Reconnect it to delegate this service, or use a JSON key credential instead'
957+
'google_keyless_delegation_unavailable',
958+
'keyless Google credentials cannot act as a Workspace user for this service',
959+
'A keyless Google credential cannot act as a Workspace user for this service. Clear the Impersonated Account, or use a JSON key credential instead'
956960
)
957961
}
962+
if (!cloudService) {
963+
throw delegable
964+
? unsupportedUse(
965+
'google_keyless_service',
966+
'this keyless Google credential works only with Google Cloud APIs and the Workspace services it was delegated',
967+
'This keyless Google credential works only with Google Cloud tools such as BigQuery and, through domain-wide delegation, with Gmail, Google Drive, Google Docs, Google Sheets, and Google Calendar. Reconnect it to delegate this service, or use a JSON key credential instead'
968+
)
969+
: unsupportedUse(
970+
'google_keyless_service_unsupported',
971+
'keyless Google credentials cannot serve this service',
972+
'Keyless Google credentials work only with Google Cloud tools such as BigQuery and, through domain-wide delegation, with Gmail, Google Drive, Google Docs, Google Sheets, and Google Calendar. Use a JSON key credential for this service'
973+
)
974+
}
958975
}
959976
if (input.scopes.length === 0) {
960977
throw new TokenServiceAccountValidationError('invalid_credentials', 400, {

0 commit comments

Comments
 (0)