Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 29 additions & 15 deletions apps/sim/lib/credentials/google-workload-identity.server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
29 changes: 23 additions & 6 deletions apps/sim/lib/credentials/google-workload-identity.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -930,7 +930,9 @@ export async function resolveGoogleWorkloadIdentityToken(
input: ResolveGoogleWorkloadIdentityTokenInput
): Promise<string> {
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) {
Expand All @@ -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, {
Expand Down
Loading