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?)— Vorlagenhaccp,inventur,reklamationen,kunden(config/sheet_templates.php). Erst Vorschau + Ja, dann einspreadsheets.createmit dem Token der Google-Verbindung des Workspaces. Das Sheet wird ingoogle_documentsregistriert (source = user_drive,oauth_connection_id).- Google nicht verbunden:
connectGoogleDrivebzw.createCustomerSheetblendet im Chat „Mit Google verbinden“ ein (Popup, Plattform-App, Scopedrive.file— nicht sensibel). In der Verwaltung: Verbindungen (/manage/connections) → Karte „Google“ (Status, Trennen). - Danach per Slug:
readFromGoogleSheet,writeToGoogleSheet,listGoogleSheetTabsnehmen für solche Sheets automatisch den Nutzer-Token. Der Service Account wird für sie nie benutzt. drive.fileheiß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
- API im GCP-Projekt aktiviert. Einmalig pro API (Calendar API, Gmail API, ...) im Projekt der Plattform-SA. Steuert der Betreiber.
apis.<name>-Block in der Tenant-Config eingetragen (siehe oben).- 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.intervalder 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— derscopes-Wert imapis.<name>.authdeckt 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 mitclient_email+private_key? Wenn nicht:config/.envprüfen.
Nicht empfohlen
- Handler-Code mit
\Google_Clientin Tenant-Tools. Der ToolCodeAudit lehnt das ab, weil die Klasse nicht in der Whitelist vonvendor-klassen.mdsteht. Der deklarative Weg ersetzt das komplett. - Eigene
App\Service\GoogleXxxService-Helper. Existieren nicht. DerGoogleAuthTokenProviderist 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.