Kostenlos testen
Handbuch

Google Workspace

Drive, Docs, Sheets, Calendar, Gmail via zentralem Service Account — oder Sheets im Drive des Kunden per OAuth.

Google-APIs laufen zentral über einen Service Account der Plattform. Du deklarierst nur den API-Adapter in apis.php mit einem auth-Block — der ToolRunner holt vor jedem Call einen frischen Bearer-Token und injiziert ihn transparent. Kein Handler-Code für OAuth, kein manuelles JWT-Signing, kein Google_Client in Tool-Handlern.

Auth-Modell — ein zentraler SA für alle Tenants

Die Plattform betreibt genau einen Service Account. Der Zugriff auf einzelne Ressourcen (Kalender, Sheets, Drive-Ordner) wird pro Ressource durch Freigabe an die SA-Email geregelt — nicht durch pro-Tenant-Keys.

Konsequenz für den Betreiber: Jeder neue Tenant, der Google-Ressourcen anbindet, muss die konkreten Dokumente / Kalender an die SA-Email freigeben. Die Email steht in config/app.php unter Google.ServiceAccount.client_email.

Konsequenz für Tool-Autoren: Nichts. auth-Block in apis.<name> setzen, fertig.

Sheets im Google Drive des Kunden (OAuth, empfohlen für neue Tabellen)

Neue Tabellen legt der Builder im Drive des Kunden an statt sie den Kunden bauen und an den Service Account freigeben zu lassen:

  • createCustomerSheet(template, title?, extra_columns?, include_examples?) — Vorlagen haccp, inventur, reklamationen, kunden (config/sheet_templates.php). Erst Vorschau + Ja, dann ein spreadsheets.create mit dem Token der Google-Verbindung des Workspaces. Das Sheet wird in google_documents registriert (source = user_drive, oauth_connection_id).
  • Google nicht verbunden: connectGoogleDrive bzw. createCustomerSheet blendet im Chat „Mit Google verbinden“ ein (Popup, Plattform-App, Scope drive.file — nicht sensibel). In der Verwaltung: Verbindungen (/manage/connections) → Karte „Google“ (Status, Trennen).
  • Danach per Slug: readFromGoogleSheet, writeToGoogleSheet, listGoogleSheetTabs nehmen für solche Sheets automatisch den Nutzer-Token. Der Service Account wird für sie nie benutzt.
  • drive.file heißt: TaskMonkey sieht nur Dateien, die es selbst angelegt hat. Bestehende Sheets des Kunden gehen weiter über die Freigabe an den Service Account (unten).

Deklarativ (eigene Tools gegen die Sheets-/Drive-API mit dem Nutzer-Token):

'apis.sheets_kunde' => [
    'base_url' => 'https://sheets.googleapis.com/v4/',
    'auth'     => ['type' => 'google_user'],
]

google_user nimmt die Verbindung, die das per slug (slug/sheet_slug/document_slug im Aufruf) gemeinte Dokument angelegt hat, sonst die Drive-Verbindung des Workspaces. Steht in einem Aufruf an eine google_service_account-API ein Slug aus dem Kunden-Drive, schaltet der ToolRunner ebenfalls auf den Nutzer-Token um. Tokens gehen nur an https://*.googleapis.com.

Einrichtung der Plattform-App (Consent-Screen, Markenprüfung, env): docs/plans/google-oauth-drive-setup.md.

Deklaratives Muster — API-Adapter

Jede Google-API bekommt einen Eintrag in config/tenants/<tenant>/apis.php:

'apis.google_calendar' => [
    'base_url' => 'https://www.googleapis.com/calendar/v3/',
    'auth'     => [
        'type'   => 'google_service_account',
        'scopes' => ['https://www.googleapis.com/auth/calendar'],
    ],
    'headers'  => [
        'Accept'       => 'application/json',
        'Content-Type' => 'application/json',
    ],
]

Die drei entscheidenden Felder:

Feld Zweck
base_url Wurzel-URL, an die der endpoint des Tools angehängt wird
auth.type google_service_account — löst den Token-Holer aus
auth.scopes OAuth-Scopes, die der Token abdecken muss (siehe API-Docs)

Der ToolRunner ruft vor jedem Call App\Service\GoogleAuthTokenProvider::getAccessToken($tenant, $scopes) auf, cached den Token 55 Minuten pro tenant + scope-hash und setzt Authorization: Bearer <token> als Header. Du siehst davon nichts.

Zusätzliche Felder, die kein Auth sind, aber praktisch — sie leben unter apis.<name> und sind über $ctx['config']['apis']['<name>'] im preprocess verfügbar:

'apis.google_calendar' => [
    'base_url'    => 'https://www.googleapis.com/calendar/v3/',
    'auth'        => [ 'type' => 'google_service_account', 'scopes' => [...] ],
    'headers'     => [ 'Content-Type' => 'application/json' ],
    'calendar_id' => 'termine@meco-media.com',  // <— eigenes Feld, nicht-sensitive Metadaten
]

Tool-Muster — GET mit Query-Params

'listCalendarEvents' => [
    'description' => 'Bevorstehende Termine im Google Calendar.',
    'internal'    => true,
    'api'         => 'google_calendar',
    'endpoint'    => 'calendars/{calendar_id}/events',
    'method'      => 'GET',
    'retry'       => ['max' => 3, 'on_status' => [429, 500, 502, 503, 504], 'delay' => 2],
    'parameters'  => [
        'type' => 'object',
        'properties' => [
            'days'        => ['type' => 'integer', 'description' => 'default 7, max 30'],
            'max_results' => ['type' => 'integer', 'description' => 'default 20, max 50'],
        ],
    ],
    'preprocess' => function (array $args, array $ctx): array {
        $days = min(max(1, (int)($args['days'] ?? 7)), 30);
        $max  = min(max(1, (int)($args['max_results'] ?? 20)), 50);
        $calendarId = (string)($ctx['config']['apis']['google_calendar']['calendar_id'] ?? '');
        return [
            'calendar_id' => rawurlencode($calendarId),
            '_query'      => [
                'timeMin'      => (new \DateTime())->format(\DateTime::RFC3339),
                'timeMax'      => (new \DateTime("+{$days} days"))->format(\DateTime::RFC3339),
                'maxResults'   => $max,
                'orderBy'      => 'startTime',
                'singleEvents' => 'true',
            ],
        ];
    },
]

preprocess befüllt zwei Dinge:

  • Endpoint-Placeholder ({calendar_id} im Endpoint wird durch den Return-Wert ersetzt).
  • _query — assoziatives Array, das der ToolRunner als URL-Query-String an den Endpoint anhängt.

Tool-Muster — POST mit JSON-Body

'createCalendarEvent' => [
    'description' => 'Legt einen Termin im Google Calendar an.',
    'internal'    => true,
    'api'         => 'google_calendar',
    'endpoint'    => 'calendars/{calendar_id}/events',
    'method'      => 'POST',
    'json'        => true,
    'parameters'  => [
        'type' => 'object',
        'properties' => [
            'title'    => ['type' => 'string'],
            'start'    => ['type' => 'string', 'description' => 'ISO 8601 mit Timezone'],
            'end'      => ['type' => 'string', 'description' => 'ISO 8601 mit Timezone'],
            'timezone' => ['type' => 'string', 'description' => 'IANA, default Europe/Berlin'],
        ],
        'required' => ['title', 'start', 'end'],
    ],
    'preprocess' => function (array $args, array $ctx): array {
        $calendarId = (string)($ctx['config']['apis']['google_calendar']['calendar_id'] ?? '');
        return ['calendar_id' => rawurlencode($calendarId)];
    },
    'body_transform' => function (array $args): array {
        $tz = (string)($args['timezone'] ?? 'Europe/Berlin');
        return [
            'summary' => (string)($args['title'] ?? ''),
            'start'   => ['dateTime' => (string)($args['start'] ?? ''), 'timeZone' => $tz],
            'end'     => ['dateTime' => (string)($args['end']   ?? ''), 'timeZone' => $tz],
        ];
    },
]

body_transform erzeugt den JSON-Body aus den Tool-Args. Ohne den geht der komplette $args-Array 1:1 als Body raus — bei Google APIs praktisch immer verschieden von dem was der Assistant liefert.

Scopes — Cheat-Sheet

API Scope
Calendar (Lesen + Schreiben) https://www.googleapis.com/auth/calendar
Calendar (nur Lesen) https://www.googleapis.com/auth/calendar.readonly
Sheets (Lesen + Schreiben) https://www.googleapis.com/auth/spreadsheets
Drive (voll) https://www.googleapis.com/auth/drive
Drive (nur was die App angelegt hat) https://www.googleapis.com/auth/drive.file
Gmail (Versand) https://mail.google.com/
Gmail (nur Lesen) https://www.googleapis.com/auth/gmail.readonly

Faustregel: so wenig Scopes wie möglich pro Adapter. Einen Drive-Adapter nur für "Reports ablegen" nutzt drive.file, keinen vollen drive-Scope.

Onboarding — was passieren muss damit ein neuer Google-Adapter funktioniert

  1. API im GCP-Projekt aktiviert. Einmalig pro API (Calendar API, Gmail API, ...) im Projekt der Plattform-SA. Steuert der Betreiber.
  2. apis.<name>-Block in der Tenant-Config eingetragen (siehe oben).
  3. Ressource freigegeben an die SA-Email. Konkret:
    • Kalender: Google Calendar → Kalendereinstellungen → "Freigabe & Berechtigungen" → SA-Email als "Termine ändern" hinzufügen.
    • Sheets / Docs / Drive-Ordner: Datei / Ordner → "Freigeben" → SA-Email einladen.
    • Gmail: geht nicht per Service Account gegen persönliche Postfächer — dafür braucht es domain-wide-delegation im Workspace oder OAuth-User-Flow. Als Absender für Transaktionsmails ist SendGrid/Resend meistens sinnvoller.

Rate-Limits

Google hat pro-Projekt-Limits (Calendar ~1M req/Tag, Sheets 300 req/min pro Projekt). Bei Scheduled Tasks mit vielen Calls:

  • retry: {max: 3, on_status: [429, 500, 502, 503, 504], delay: 2} im Tool setzen.
  • interval der Task hochsetzen (10min+).
  • Bei echtem Volumen: eigenes GCP-Projekt für den Tenant, damit die Limits pro Kunde separat gelten. Dann zusätzlicher SA-Config-Block nötig — Betreiber-Thema, nicht Tool-Autor.

Debug

tm logs                            # HTTP-Response-Bodies bei API-Fehlern
tm test-tool listCalendarEvents    # Tool isoliert ausführen

Häufige Fehler:

  • 401 Unauthorized — SA hat keinen Zugriff auf die Ressource. Kalender / Doc / Ordner an die SA-Email freigeben.
  • 403 API not enabled — API im GCP-Projekt nicht aktiviert (Betreiber-Task).
  • 403 insufficient authentication scopes — der scopes-Wert im apis.<name>.auth deckt den Endpoint nicht ab. In Google-API-Docs den passenden Scope nachsehen.
  • error: 'Google-Auth fehlgeschlagen' — Konfig-Check: Configure::read('Google.ServiceAccount') liefert ein Array mit client_email + private_key? Wenn nicht: config/.env prüfen.

Nicht empfohlen

  • Handler-Code mit \Google_Client in Tenant-Tools. Der ToolCodeAudit lehnt das ab, weil die Klasse nicht in der Whitelist von vendor-klassen.md steht. Der deklarative Weg ersetzt das komplett.
  • Eigene App\Service\GoogleXxxService-Helper. Existieren nicht. Der GoogleAuthTokenProvider ist Plattform-Code, nicht aus Tools nutzbar.
  • Eigener OAuth-Flow im Tool-Code. Für Sheets im Kunden-Drive gibt es createCustomerSheet + auth.type = google_user (oben); für bestehende, freigegebene Dateien den Service Account. Der Plattform-SA wird nur für freigeschaltete Workspaces benutzt (Platform.google.service_account_tenants), auch in den Sheet-Handlern.
Zuletzt aktualisiert: 2026-10-01