diff --git a/apps/sim/lib/credentials/google-workload-identity.server.test.ts b/apps/sim/lib/credentials/google-workload-identity.server.test.ts index 0de02046d10..07db28f217f 100644 --- a/apps/sim/lib/credentials/google-workload-identity.server.test.ts +++ b/apps/sim/lib/credentials/google-workload-identity.server.test.ts @@ -415,27 +415,41 @@ describe('getting a keyless Google Cloud token for a run', () => { expect(google.iam[0]?.body).toEqual({ scope: [BIGQUERY], lifetime: '3600s' }) }) - it('refuses domain-wide delegation before contacting Google', async () => { - fakeGoogle(fetch) + it.each([ + ['google-drive', 'google_keyless_delegation'], + ['google-bigquery', 'google_keyless_delegation_unavailable'], + ['google-ads', 'google_keyless_service_unsupported'], + ])( + 'refuses impersonation for %s before contacting Google, offering delegation only where it exists', + async (service, step) => { + fakeGoogle(fetch) - await expect( - resolve({ credentialId: 'cred-dwd', impersonateEmail: 'alice@example.com' }) - ).rejects.toMatchObject({ - code: 'unsupported_use', - logDetail: { step: 'google_keyless_delegation' }, - userMessage: expect.any(String), - }) - expect(fetch).not.toHaveBeenCalled() - }) + await expect( + resolve({ credentialId: 'cred-dwd', service, impersonateEmail: 'alice@example.com' }) + ).rejects.toMatchObject({ + code: 'unsupported_use', + logDetail: { step }, + userMessage: expect.any(String), + }) + expect(fetch).not.toHaveBeenCalled() + } + ) - it.each(['google-drive', 'gmail', 'google-sheets', 'google-ads', undefined])( - 'refuses %s, which keyless credentials do not serve, before contacting Google', - async (service) => { + it.each([ + ['google-drive', 'google_keyless_service'], + ['gmail', 'google_keyless_service'], + ['google-sheets', 'google_keyless_service'], + ['google-email', 'google_keyless_service'], + ['google-ads', 'google_keyless_service_unsupported'], + [undefined, 'google_keyless_service_unsupported'], + ])( + 'refuses %s, which keyless credentials do not serve, offering delegation only where it exists', + async (service, step) => { fakeGoogle(fetch) await expect(resolve({ credentialId: 'cred-workspace', service })).rejects.toMatchObject({ code: 'unsupported_use', - logDetail: { step: 'google_keyless_service' }, + logDetail: { step }, userMessage: expect.any(String), }) expect(fetch).not.toHaveBeenCalled() diff --git a/apps/sim/lib/credentials/google-workload-identity.server.ts b/apps/sim/lib/credentials/google-workload-identity.server.ts index 6d4a75d2957..52627cc8fc9 100644 --- a/apps/sim/lib/credentials/google-workload-identity.server.ts +++ b/apps/sim/lib/credentials/google-workload-identity.server.ts @@ -930,7 +930,9 @@ export async function resolveGoogleWorkloadIdentityToken( input: ResolveGoogleWorkloadIdentityTokenInput ): Promise { const config = readStoredConfig(input.decrypted) - const delegation = googleDelegationServiceFor(input.service) ? config.delegation : undefined + // Reconnecting can add delegation only for the Workspace services keyless delegation covers. + const delegable = googleDelegationServiceFor(input.service) !== undefined + const delegation = delegable ? config.delegation : undefined if (delegation) { if (input.impersonateEmail && input.impersonateEmail.trim().toLowerCase() !== delegation.user) { @@ -941,20 +943,35 @@ export async function resolveGoogleWorkloadIdentityToken( ) } } else { - if (input.impersonateEmail) { + const cloudService = Boolean(input.service && GOOGLE_KEYLESS_SERVICES.has(input.service)) + // Clearing the Impersonated Account helps only where keyless works without it. + if (input.impersonateEmail && delegable) { throw unsupportedUse( 'google_keyless_delegation', 'this keyless Google credential has no domain-wide delegation for this service', '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' ) } - if (!input.service || !GOOGLE_KEYLESS_SERVICES.has(input.service)) { + if (input.impersonateEmail && cloudService) { throw unsupportedUse( - 'google_keyless_service', - 'this keyless Google credential works only with Google Cloud APIs and the Workspace services it was delegated', - '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' + 'google_keyless_delegation_unavailable', + 'keyless Google credentials cannot act as a Workspace user for this service', + 'A keyless Google credential cannot act as a Workspace user for this service. Clear the Impersonated Account, or use a JSON key credential instead' ) } + if (!cloudService) { + throw delegable + ? unsupportedUse( + 'google_keyless_service', + 'this keyless Google credential works only with Google Cloud APIs and the Workspace services it was delegated', + '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' + ) + : unsupportedUse( + 'google_keyless_service_unsupported', + 'keyless Google credentials cannot serve this service', + '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' + ) + } } if (input.scopes.length === 0) { throw new TokenServiceAccountValidationError('invalid_credentials', 400, {