Skip to content

Feat/google service account#3828

Draft
TheodoreSpeaks wants to merge 14 commits intostagingfrom
feat/google-service-account
Draft

Feat/google service account#3828
TheodoreSpeaks wants to merge 14 commits intostagingfrom
feat/google-service-account

Conversation

@TheodoreSpeaks
Copy link
Copy Markdown
Collaborator

@TheodoreSpeaks TheodoreSpeaks commented Mar 28, 2026

Summary

Add google service support as a integration. Allows users with admin credentials to assume roles on behalf of their google workspace users.

Created new credential type service_account.

Added documentation to sim docs.

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation
  • Other: ___________

Testing

  • Created sim workflow using copilot with all google blocks supported by this.
  • Validated workflows ran with google service account credentials.
  • Validated workflows show impersonate user account when google service account was selected
  • Validated workflows don't show impersonate user account when google account is not service account.

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

Screenshots/Videos

@vercel
Copy link
Copy Markdown

vercel bot commented Mar 28, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Apr 1, 2026 2:09am

Request Review

@TheodoreSpeaks
Copy link
Copy Markdown
Collaborator Author

@cursor review

@TheodoreSpeaks
Copy link
Copy Markdown
Collaborator Author

@greptile review

@cursor
Copy link
Copy Markdown

cursor bot commented Mar 28, 2026

PR Summary

High Risk
Adds a new credential type that stores encrypted private keys and introduces a JWT-based token minting path used across multiple Google integrations; mistakes here could expose secrets or broaden access if authorization/scoping is wrong.

Overview
Enables Google Workspace service account credentials (domain-wide delegation) as a first-class credential type, including DB support for storing an encrypted JSON key and workspace-unique naming.

Updates token/credential resolution to handle service_account credentials by minting short-lived Google access tokens via the JWT bearer flow (with optional user impersonation), and wires this into Google tools/selectors (Drive, Gmail labels, Sheets, Calendar, Tasks, BigQuery, Vertex, and provider proxy) with improved error handling.

Extends the UI to let admins add/delete service accounts from Integrations (paste/upload JSON, validation, encrypted storage) and updates Google blocks to show an Impersonated Account field only when a service account credential is selected; adds docs for setup and required scopes.

Written by Cursor Bugbot for commit 54683d7. This will update automatically on new commits. Configure here.

Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 3 potential issues.

Autofix Details

Bugbot Autofix prepared fixes for all 3 issues found in the latest run.

  • ✅ Fixed: Impersonation field shown for non-service-account credentials
    • The impersonation input now renders only when the selected credential is a service account by gating on isServiceAccount instead of provider-level support.
  • ✅ Fixed: Return type mismatch: undefined instead of false
    • hasExternalApiCredentials now coalesces the optional chaining result with ?? false so it always returns a boolean.
  • ✅ Fixed: Serializer orphan logic is broader than intended
    • The orphan serialization path was narrowed to only include the known impersonateUserEmail key instead of all orphan sub-blocks with values.

Create PR

Or push these changes by commenting:

@cursor push 035b79165e
Preview (035b79165e)
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/credential-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/credential-selector.tsx
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/credential-selector.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/credential-selector.tsx
@@ -10,7 +10,6 @@
 import {
   getCanonicalScopesForProvider,
   getProviderIdFromServiceId,
-  getServiceAccountProviderForProviderId,
   OAUTH_PROVIDERS,
   type OAuthProvider,
   parseProvider,
@@ -122,11 +121,6 @@
     [selectedCredential]
   )
 
-  const supportsServiceAccount = useMemo(
-    () => !!getServiceAccountProviderForProviderId(effectiveProviderId),
-    [effectiveProviderId]
-  )
-
   const selectedCredentialSet = useMemo(
     () => credentialSets.find((cs) => cs.id === selectedCredentialSetId),
     [credentialSets, selectedCredentialSetId]
@@ -377,7 +371,7 @@
         className={overlayContent ? 'pl-7' : ''}
       />
 
-      {supportsServiceAccount && !isPreview && (
+      {isServiceAccount && !isPreview && (
         <div className='mt-2.5 flex flex-col gap-2.5'>
           <div className='flex items-center gap-1.5 pl-0.5'>
             <Label>

diff --git a/apps/sim/lib/auth/hybrid.ts b/apps/sim/lib/auth/hybrid.ts
--- a/apps/sim/lib/auth/hybrid.ts
+++ b/apps/sim/lib/auth/hybrid.ts
@@ -25,7 +25,7 @@
 export function hasExternalApiCredentials(headers: Headers): boolean {
   if (headers.has(API_KEY_HEADER)) return true
   const auth = headers.get('authorization')
-  return auth?.startsWith(BEARER_PREFIX)
+  return auth?.startsWith(BEARER_PREFIX) ?? false
 }
 
 export interface AuthResult {

diff --git a/apps/sim/serializer/index.ts b/apps/sim/serializer/index.ts
--- a/apps/sim/serializer/index.ts
+++ b/apps/sim/serializer/index.ts
@@ -347,14 +347,17 @@
           )
         )
 
-      const isOrphanWithValue =
-        matchingConfigs.length === 0 && subBlock.value != null && subBlock.value !== ''
+      const isImpersonateUserEmailOrphanWithValue =
+        id === 'impersonateUserEmail' &&
+        matchingConfigs.length === 0 &&
+        subBlock.value != null &&
+        subBlock.value !== ''
 
       if (
         (matchingConfigs.length > 0 && shouldInclude) ||
         hasStarterInputFormatValues ||
         isLegacyAgentField ||
-        isOrphanWithValue
+        isImpersonateUserEmailOrphanWithValue
       ) {
         params[id] = subBlock.value
       }

This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 28, 2026

Greptile Summary

This PR introduces Google Service Account credentials as a first-class integration type, enabling Google Workspace domain-wide delegation for automated workflows without per-user OAuth consent flows. It adds a service_account credential type to the DB schema, a UI flow for uploading/managing service account JSON keys, a custom RS256 JWT flow for token exchange, and plumbs impersonateEmail through all Google tool API routes and block definitions.

Key changes:

  • New encrypted_service_account_key column, service_account enum value, and DB constraints
  • getServiceAccountToken implements the two-legged JWT bearer (RFC 7523) flow via Node's createSign
  • All Google block definitions gain hidden isServiceAccount + conditional impersonateUserEmail fields; the credential selector syncs the boolean automatically
  • Every updated tool route passes impersonateEmail and provider-specific scopes through to the token exchange
  • P1: gmail/labels and gmail/label routes only verify workspace membership for service accounts, skipping the credentialMember check that all other updated routes enforce via authorizeCredentialUse. Any workspace member can call these routes to obtain a service account token regardless of explicit credential grant.
  • P2: refreshAccessTokenIfNeeded return type inconsistency; PUT validation looser than POST; no token caching.

Confidence Score: 3/5

Not safe to merge — Gmail label routes bypass per-credential access control for service accounts

One confirmed P1: gmail/labels and gmail/label routes check only workspace membership for service accounts, skipping the credentialMember table check enforced by all other updated routes. Any workspace member can obtain a service account token via these endpoints regardless of explicit credential grant. Fix is mechanical but required before merge. Remaining findings are P2.

apps/sim/app/api/tools/gmail/labels/route.ts and apps/sim/app/api/tools/gmail/label/route.ts

Important Files Changed

Filename Overview
apps/sim/app/api/tools/gmail/labels/route.ts Service account path only checks workspace membership, skipping per-credential credentialMember access control — P1 authorization gap
apps/sim/app/api/tools/gmail/label/route.ts Same credential-member authorization gap as labels route for the service account path
apps/sim/app/api/auth/oauth/utils.ts Core service account JWT flow; return-type inconsistency and no token caching are the main concerns
apps/sim/lib/auth/credential-access.ts Full service account authorization logic added correctly; minor: membership checked before workspace permission
apps/sim/app/api/credentials/route.ts POST creation with thorough Zod validation, encryption, and workspace member propagation looks correct
apps/sim/app/api/credentials/[id]/route.ts PUT update handler for service accounts; validation slightly looser than POST (accepts empty strings for required fields)
apps/sim/app/api/auth/oauth/token/route.ts Service account token exchange path properly authorizes before issuing tokens
packages/db/schema.ts New service_account enum value, encryptedServiceAccountKey column, unique index, and check constraint look correct
apps/sim/lib/oauth/oauth.ts serviceAccountProviderId links added to all Google services; new google-service-account provider config looks correct
apps/sim/app/workspace/[workspaceId]/settings/components/integrations/integrations-manager.tsx New service account creation UI with JSON drag-and-drop and client-side validation looks solid
apps/sim/lib/copilot/orchestrator/tool-executor/integration-tools.ts Service account path sets oauthCredential correctly; impersonateEmail not forwarded — copilot Workspace API calls won't impersonate users

Sequence Diagram

sequenceDiagram
    participant UI as Integrations UI
    participant CredAPI as /api/credentials
    participant TokenAPI as /api/auth/oauth/token
    participant ToolAPI as /api/tools/gmail/*
    participant CredAccess as credential-access.ts
    participant SAUtils as getServiceAccountToken
    participant Google as Google OAuth2

    UI->>CredAPI: POST type:service_account, serviceAccountJson
    CredAPI->>CredAPI: Validate JSON
    CredAPI->>CredAPI: encryptSecret
    CredAPI->>DB: INSERT credential
    CredAPI->>DB: INSERT credentialMember for workspace members

    Note over UI,CredAPI: Workflow execution

    UI->>TokenAPI: POST credentialId, scopes, impersonateEmail
    TokenAPI->>DB: resolveOAuthAccountId
    DB-->>TokenAPI: credentialType:service_account
    TokenAPI->>CredAccess: authorizeCredentialUse
    CredAccess->>DB: Check workspace membership
    CredAccess->>DB: Check credentialMember membership
    CredAccess-->>TokenAPI: ok:true
    TokenAPI->>SAUtils: getServiceAccountToken
    SAUtils->>DB: SELECT encryptedServiceAccountKey
    SAUtils->>SAUtils: decryptSecret, sign RS256 JWT
    SAUtils->>Google: POST /token jwt-bearer
    Google-->>SAUtils: access_token
    SAUtils-->>TokenAPI: accessToken
    TokenAPI-->>UI: accessToken

    Note over ToolAPI,Google: Gmail label routes P1 gap
    UI->>ToolAPI: GET /gmail/labels?credentialId
    ToolAPI->>DB: resolveOAuthAccountId
    ToolAPI->>DB: getUserEntityPermissions workspace only
    ToolAPI->>SAUtils: getServiceAccountToken
    SAUtils->>Google: POST /token
    Google-->>SAUtils: access_token
    SAUtils-->>ToolAPI: accessToken
    ToolAPI->>Google: GET /gmail/v1/users/me/labels
    Google-->>ToolAPI: labels
    ToolAPI-->>UI: labels
Loading

Comments Outside Diff (1)

  1. apps/sim/app/api/tools/gmail/labels/route.ts, line 60-79 (link)

    P1 Service account credential membership check is missing

    The workspace permission check at line 60–70 only verifies that the requesting user belongs to the same workspace as the credential. It does not verify that the user is an explicit member of this specific service account credential (via the credentialMember table).

    Every other route updated for service account support (google_calendar/calendars, google_sheets/sheets, drive/files, drive/file, google_bigquery/datasets, google_bigquery/tables, google_tasks/task-lists) delegates to authorizeCredentialUse, which performs the full check including credentialMember membership. For example, google_calendar/calendars/route.ts:

    const authz = await authorizeCredentialUse(request, {
      credentialId,
      workflowId,
      requireWorkflowIdForInternal: false,
    })
    if (!authz.ok) {
      return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
    }

    The same gap exists in apps/sim/app/api/tools/gmail/label/route.ts at lines 56–75.

    As a result, any workspace member who is NOT explicitly added as a credentialMember of this service account can still call these routes, obtain a service account access token, and enumerate Gmail labels for any domain user the service account can impersonate. This violates the per-credential access control model introduced in this PR.

Reviews (3): Last reviewed commit: "Address comments" | Re-trigger Greptile

@TheodoreSpeaks
Copy link
Copy Markdown
Collaborator Author

@BugBot review

@TheodoreSpeaks
Copy link
Copy Markdown
Collaborator Author

@greptile review

@TheodoreSpeaks
Copy link
Copy Markdown
Collaborator Author

@BugBot review

@TheodoreSpeaks
Copy link
Copy Markdown
Collaborator Author

@greptile review

Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 3 potential issues.

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant