diff --git a/TOPICS.md b/TOPICS.md index 50f3416..b2226cd 100644 --- a/TOPICS.md +++ b/TOPICS.md @@ -67,7 +67,8 @@ Lade immer zuerst diese Datei. Dann gezielt die passende(n) Referenz-Datei(en). | Testing-Strategie | d-guides/testing-strategy.md | Testpyramide, AC-Format, Test-Pfade | | Dev-Setup | d-guides/dev-setup.md | Lokale Umgebung starten | | Secrets-Verschluesselung | d-guides/encrypt-env-secrets.md | Env-Dateien verschluesseln | -| Google OAuth | d-guides/google-oauth-setup.md | OAuth Auth/Data Apps einrichten | +| Google OAuth | d-guides/google-oauth-setup.md | OAuth Auth/Data Apps einrichten (inkl. Calendar/Contacts-Scopes + Reconnect-Hinweis) | +| Infomaniak Token-Setup | d-guides/infomaniak-token-setup.md | Personal Access Token im Infomaniak-Manager fuer kDrive/Calendar/Contacts erzeugen | | Security-Migration | d-guides/security-migration-guide.md | JWT Cookie Migration | | Doc-Sync Cursor-Rule | d-guides/cursor-doc-sync.md | Installation, Regel-Quelle `doc-sync.mdc`, Doku-Workflow | diff --git a/b-reference/gateway/ai-agent.md b/b-reference/gateway/ai-agent.md index 3780a28..76bfe89 100644 --- a/b-reference/gateway/ai-agent.md +++ b/b-reference/gateway/ai-agent.md @@ -1,6 +1,6 @@ - - + + # AI Agent & Knowledge Store @@ -202,7 +202,7 @@ Zusätzlich zu den unten genannten **Kern-Tools** existieren **dynamische Tools* | Plugin-Modul | Typische Rolle | |--------------|----------------| | `aicorePluginAnthropic.py` | Claude-Modelle | -| `aicorePluginOpenai.py` | GPT, Embeddings, Bild | +| `aicorePluginOpenai.py` | GPT, Embeddings, Bild — modellabhaengiges Payload-Tuning: GPT-5.x und o-Serie (o1/o3/o4) sind Reasoning-Modelle und akzeptieren weder `max_tokens` (-> immer `max_completion_tokens`) noch ein custom `temperature` (-> Feld bei diesen Modellen weggelassen, OpenAI erzwingt sonst HTTP 400 `unsupported_value`) | | `aicorePluginMistral.py` | Mistral Chat / Embed | | `aicorePluginPerplexity.py` | Sonar / Recherche | | `aicorePluginTavily.py` | Web-Suche | diff --git a/b-reference/gateway/architecture.md b/b-reference/gateway/architecture.md index 5bda8b3..06b8707 100644 --- a/b-reference/gateway/architecture.md +++ b/b-reference/gateway/architecture.md @@ -1,6 +1,6 @@ - - + + # Gateway -- Architektur @@ -16,7 +16,7 @@ Unter `gateway/modules/` (Kontext-Audit): |-------|-------| | `aicore/` | Model-Registry, Model-Selector, Provider-Plugins (Anthropic, OpenAI, Mistral, Perplexity, Tavily, PrivateLLM) | | `auth/` | Authentifizierung, CSRF, Token-Refresh-Middleware, JWT | -| `connectors/` | DB-Connector (PostgreSQL), Provider-Subpakete (Microsoft, Google, ClickUp, FTP), Ticket/Messaging/Geo-Konnektoren | +| `connectors/` | DB-Connector (PostgreSQL), Provider-Subpakete (Microsoft, Google, ClickUp, FTP, Infomaniak), Ticket/Messaging/Geo-Konnektoren. Pro Provider registrieren ServiceAdapter (`OutlookAdapter`, `OneDriveAdapter`, `SharepointAdapter`, `TeamsAdapter`, `CalendarAdapter`, `ContactsAdapter`, `GmailAdapter`, `DriveAdapter`, `KdriveAdapter`, …) die UDB-Services. Adapter-Registry pro Connector ist `_SERVICE_MAP` | | `datamodels/` | Pydantic-Datenmodelle (u. a. Ai, Billing, Chat, Content, Files, Knowledge, Rbac, Subscription, UiLanguage, Workflow) | | `features/` | Feature-Module (autonome Domänen): workspace, graphicalEditor, chatbot, commcoach, neutralization, realEstate, trustee, teamsbot | | `interfaces/` | DB-Interfaces (App, Billing, Chat, Knowledge, Management, Subscription), AI-Objects, RBAC, Features, Messaging | diff --git a/c-work/2-build/2026-04-infomaniak-connector.md b/c-work/2-build/2026-04-infomaniak-connector.md index bcdb91c..c70223b 100644 --- a/c-work/2-build/2026-04-infomaniak-connector.md +++ b/c-work/2-build/2026-04-infomaniak-connector.md @@ -1,118 +1,296 @@ + + + + -# Infomaniak Connector (kDrive + Mail) + UDB-Integration +# Infomaniak Connector (kDrive + Calendar + Contacts today; Mail reserved) + UDB-Integration ## Beschreibung und Kontext PowerOn besitzt das Provider-Connector-Pattern fuer externe Datenanbindungen (Microsoft, Google, ClickUp). Infomaniak war bisher nicht angebunden. Ziel: ein -neuer `InfomaniakConnector`, der OAuth gegen `login.infomaniak.com` durchfuehrt -und Daten aus zwei Services bereitstellt: +neuer `InfomaniakConnector`, der Daten aus den Infomaniak-Services +bereitstellt. Heute aktiv: -- **kDrive** (Pendant zu OneDrive / Google Drive) -- **Mail** (Pendant zu Outlook / Gmail) +- **kDrive** (Pendant zu OneDrive / Google Drive) +- **Calendar** (Pendant zu Outlook-Kalender / Google Calendar; .ics-Download) +- **Contacts** (Pendant zu Outlook-Kontakte / Google Contacts; .vcf-Download) + +Blockiert (Scope ist auf der PAT, Adapter wartet auf Vendor): + +- **Mail** -- alle erschoepfend getesteten Pfade scheitern. Final-Befund + vom 2026-04-28: + + | Pfad | Status | + |---|---| + | `api.infomaniak.com/1/mail` | 404 nginx (existiert nicht) | + | `api.infomaniak.com/2/mail?account_id=...` | 404 nginx (existiert nicht) | + | `mail.infomaniak.com/api/mail?account_id=...` | 302 -> `login.infomaniak.com/authorize` (OAuth Web-Session, nicht PAT) | + | `mail.infomaniak.com/api/mail/?account_id=...` | 301 -> `http://mail.infomaniak.com:5000` (interner Cyrus-IMAP-API-Port, von aussen nicht erreichbar) | + | `mail.infomaniak.com/api/pim/mail` | 302 -> OAuth | + | `mail.infomaniak.com/api/pim/mailbox` | 302 -> OAuth | + | `mail.infomaniak.com/api/pim/folder` | 302 -> OAuth | + + Konsequenz: PowerOn kann mit dem heutigen PAT-Mechanismus die + Infomaniak-Mailbox **nicht** ansprechen. Der Scope `workspace:mail` + bleibt im Standard-PAT-Setup, sodass der MailAdapter ohne + Token-Rotation aufgeschaltet werden kann, sobald Infomaniak einen + PAT-tauglichen Endpoint freischaltet (oder wir entscheiden, die + Mail-Connection ueber einen separaten OAuth-Flow oder ein App-Passwort + ueber IMAP/SMTP einzubinden -- separater Build). Die Verbindung ist eine reine **Daten-Connection** (`TokenPurpose.DATA_CONNECTION`). -Infomaniak ist explizit **kein** Login-Provider fuer PowerOn -- entsprechend gibt -es nur `_FLOW_CONNECT`, kein `_FLOW_LOGIN`. +Infomaniak ist explizit **kein** Login-Provider fuer PowerOn. -Zusatzlich wurden zwei kleine Inkonsistenzen in der ClickUp-UDB-Anzeige +Zusaetzlich wurden zwei kleine Inkonsistenzen in der ClickUp-UDB-Anzeige behoben (`_SERVICE_ICONS` + `_SERVICE_TO_SOURCE_TYPE`). +## Architektur-Pivot 2026-04-28: OAuth -> Personal Access Token + +**Befund**: Infomaniaks `login.infomaniak.com/authorize` akzeptiert nur +Identity-Scopes (`openid`, `profile`, `email`, `phone`). Bearer-Tokens, die +gegen die Daten-APIs (`/2/drive/...`, `/1/mail/...`) funktionieren, koennen +ueber OAuth nicht ausgestellt werden -- der Versuch quittiert mit +`error=invalid_scope`. Datenzugriff laeuft bei Infomaniak ausschliesslich +ueber **Personal Access Tokens (PATs)**, die der Endnutzer manuell im +Infomaniak-Manager erstellt. + +**Konsequenz**: kompletter Umbau des Auth-Teils des Connectors. Das +Provider-Connector-Pattern und die Adapter (`KdriveAdapter`, `MailAdapter`) +bleiben unveraendert -- sie konsumieren ohnehin nur einen Bearer-Token. Was +aussortiert wurde: + +- OAuth-Routen (`/api/infomaniak/auth/connect[/callback]`) -> entfernt. +- Token-Refresh-Logik (`refreshInfomaniakToken`, Background-Refresh-Branch) + -> entfernt; PATs sind langlebig, kein Rotations-Lifecycle. +- Scope-Definition `infomaniakDataScopes` -> entfernt. +- `Service_INFOMANIAK_*` Env-Variablen (Client-ID/Secret/Redirect-URI) + -> entfernt; PowerOn benoetigt keine Infomaniak-App-Registrierung mehr. +- Frontend-OAuth-Popup -> ersetzt durch Modal mit Token-Eingabe + Deeplink + zum Infomaniak-Manager. + +**Neuer Auth-Flow**: + +1. FE: User klickt "Infomaniak" -> `POST /api/connections/` mit + `authority=infomaniak` legt PENDING-Connection an. +2. FE: Modal oeffnet sich mit Schritt-fuer-Schritt-Anleitung + Link + `https://manager.infomaniak.com/v3/ng/accounts/token/list` und + Token-Eingabefeld. +3. FE: User fuegt PAT ein -> `POST /api/infomaniak/connections/{id}/token`. +4. BE: Validiert PAT in zwei Schritten: + - `resolveOwnerIdentity()` ruft PIM Calendar (Scope + `workspace:calendar`) auf, faellt bei leerer Owner-Liste auf PIM + Contacts zurueck (Scope `workspace:contact`). Beide Endpoints + liefern den ersten Owner-Record als `{account_id, name, user_id}`. + Wenn keiner antwortet -> 400 fail-loud (PAT ist fuer kDrive + unbrauchbar, weil `/2/drive` `account_id` zwingend braucht und kein + Drive-Endpoint die Identity ohne `user_info`-Scope rausgibt). + - `GET /2/drive?account_id={resolved}` -- erwartet 200, andernfalls + 400 (`drive`-Scope fehlt) bzw. 502 (Infomaniak antwortet + unerwartet). Mit der vorab resolvten `account_id` ist der Probe + deterministisch und braucht keine 422-Tolerance mehr. + Token landet als `Token` mit 10-Jahres-Horizont (`tokenStatus`-Anzeige, + analog ClickUp). `externalUsername` und `externalId` werden fuer die + UI-Anzeige gesetzt; **`externalId` ist nicht Vertragspartner des + kDrive-Adapters** (siehe "Kritische Details"). +5. FE: bei Erfolg -> Modal schliesst, Liste refresht; bei Fehler -> + Connection bleibt PENDING, Modal zeigt Fehlermeldung; bei Cancel -> + Connection wird via DELETE wieder entfernt. + ## Fokus und kritische Details -- **Refresh-Token-Persistenz**: Infomaniak rotiert Refresh-Tokens nicht in jedem - Token-Response; falls `refresh_token` im Callback fehlt, wird aus dem letzten - gespeicherten Token rekonstruiert (analog Google). -- **API-Pfad-Konvention**: Drei-Ebenen-Hierarchie kDrive (`/{driveId}/{fileId}`) - und vier-Ebenen Mail (`/{mailboxId}/{folderId}/{uid}`); `ServiceAdapter.browse` - unterscheidet die Tiefe via Segment-Count. +- **API-Pfad-Konvention** (Adapter-Path != API-Path): + - kDrive (`api.infomaniak.com`): Adapter-Path `/{driveId}/{fileId}`, + API `/2/drive/{driveId}/files/{fileId}`. `account_id` muss bei jedem + Listing-Call als Query-Arg mit. **Wichtig**: Ein User kann kDrives + in mehreren Infomaniak-Accounts besitzen (typischerweise einer in + der kSuite + ein Standalone- oder Free-tier-kDrive auf einem + **anderen** account_id). Der Adapter resolvt deshalb on demand + ueber `resolveAccessibleAccountIds()` (-> `GET /1/accounts`) **alle** + erreichbaren account_ids, ruft `/2/drive?account_id=X` fuer jede + auf und unioniert die Drive-Listings mit Dedup ueber `driveId`. + Cache auf der Adapter-Instanz (`_ensureAccountIds`). `/1/accounts` + erfordert den PAT-Scope `accounts`; ohne ihn schlaegt schon der + Submit-Pre-Flight fehl. + - Calendar (`calendar.infomaniak.com/api/pim`): Adapter-Path + `/{calendarId}/{eventId}`. **Achtung**: die naheliegenden + Nested-Routes (`/calendar/{id}/event`, `/calendar/{id}/event/{id}`, + `/calendar/{id}/event/{id}/export`) sind **NICHT** PAT-faehig -- + sie 302-en zur OAuth-Login-Seite. Korrekte PAT-Pfade: + - Listing: `/api/pim/event?calendar_id={id}&from=YYYY-MM-DD HH:MM:SS&to=...` + mit Pflicht-Range, max **3 Monate** (Vendor-Constraint). + Adapter wahlt fix 90-Tage-Window (heute -30 / +60). + - Detail: `/api/pim/event/{eventId}` (ohne calendar-Praefix) + - Export `.ics`: `/api/pim/event/{eventId}/export` + - Contacts (`contacts.infomaniak.com/api/pim`): Adapter-Path + `/{addressBookId}/{contactId}`. Listing der AddressBooks und der + Contacts geht via PAT, **aber Detail- und Export-Endpoints + (`/addressbook/{book}/contact/{id}` und `.../export`) sind nicht + PAT-faehig** (500 bzw. 302 OAuth). Konsequenzen: + - Listing braucht zwingend + `&with=emails,phones,addresses,details`, sonst kommen die + relevanten Felder leer (Default-Response listet nur Stammdaten). + - `.vcf`-Download wird im Adapter **selbst synthetisiert** aus dem + Listing-Record (vCard 3.0). Implementierung in + `_renderInfomaniakVcard()`. + - Geteilte Organisations-Adressbuecher haben einen leeren Namen -- + der Adapter setzt dort einen Platzhalter "Organisation". + `ServiceAdapter.browse` unterscheidet die Tiefe via Segment-Count. +- **Multi-Host**: `_infomaniakGet` / `_infomaniakDownload` akzeptieren ein + optionales `baseUrl`, sodass Calendar gegen `calendar.infomaniak.com`, + Contacts gegen `contacts.infomaniak.com` und kDrive gegen + `api.infomaniak.com` laufen. +- **Zwei Resolver, getrennte Verantwortung**: + - `resolveOwnerIdentity(token)` -> Display-Name + kSuite-`account_id`, + rein fuer das UI-Label der Connection. Erste Quelle PIM Calendar, + Fallback PIM Contacts; nimmt den ersten Owner-Record (`user_id > 0` + und `isinstance(account_id, int)`). + - `resolveAccessibleAccountIds(token)` -> Liste **aller** account_ids, + die der PAT erreicht. Ein einzelner `GET /1/accounts`-Call. Vom + `KdriveAdapter` benutzt, weil ein Standalone-/Free-tier-kDrive auf + einem **anderen** account_id liegt als die kSuite und die + kSuite-`account_id` aus PIM den Drive nicht abdeckt. + Beide raisen `InfomaniakIdentityError` mit einer scope-spezifischen + Fehlermeldung, sodass der Submit-Endpoint pro fehlendem Scope eine + eigene 400-Message zurueckgeben kann. +- **`externalId` ist UI-State, kein Adapter-Vertragspartner**: Submit + speichert `externalId = str(kSuiteAccountId)` fuer die ConnectionsPage + (Anzeige + Konsistenz mit anderen Providern), aber der KdriveAdapter + liest ihn nie -- er fragt zur Laufzeit `/1/accounts`. - **Antwort-Wrapping**: Erfolgreiche Responses sind als `{result: 'success', data: ...}` gewrappt -- `_unwrapData()` normalisiert. -- **Authority-Filter** in `tokenRefreshService` muss `INFOMANIAK` enthalten, - sonst werden Token nie proaktiv refreshed. +- **Token-Validation** im Submit-Endpoint: drei harte 200-Schritte, + jeder probet genau einen Scope: + 1. `resolveAccessibleAccountIds` -> probet `accounts`-Scope. + 2. `resolveOwnerIdentity` -> probet `workspace:calendar`/ + `workspace:contact`-Scope (mindestens einer noetig). + 3. `/2/drive?account_id={first}` -> probet `drive`-Scope. + Jeder Schritt liefert eine eigene 400-Message, die exakt den fehlenden + Scope nennt. 401/403 -> 400 mit Scope-Hinweis; alles Unerwartete -> 502. +- **Token-Persistenz**: `expiresAt = now + 10*365*24*3600`, `tokenRefresh = None`; + `getTokenStatusForConnection` zeigt damit `active`, kein "none". ## Ziel und Nicht-Ziele - Ziel: Infomaniak-Daten (kDrive + Mail) wie MSFT/Google im UDB browsbar. -- Ziel: ConnectionsPage hat Button "Infomaniak" (analog ClickUp). -- Ziel: Refresh-Token-Lifecycle vollautomatisch (kein User-Reconnect noetig). +- Ziel: ConnectionsPage hat Button "Infomaniak" mit PAT-Modal. +- Ziel: Setup ohne Operator-Interaktion (keine globale App-Registrierung). - Ziel: ClickUp-UDB-Inkonsistenz beheben (Service-Icon + Source-Type-Mapping). -- NICHT: Infomaniak als PowerOn-Login (`User`-Erstellung via Infomaniak-OAuth). -- NICHT: Upload-Implementierung in kDrive/Mail (gibt nur Browse + Download). +- NICHT: Infomaniak als PowerOn-Login. +- NICHT: Upload-Implementierung in kDrive/Mail. +- NICHT: Auto-Refresh fuer Infomaniak (PATs sind langlebig; bei Ablauf legt + der User eine neue Connection an). ## Betroffene Module - Gateway: - - `modules/datamodels/datamodelUam.py` (Enum) - - `modules/auth/oauthProviderConfig.py` (Scopes) - - `modules/auth/tokenManager.py` (Refresh) - - `modules/auth/tokenRefreshService.py` (Background-Refresh) - - `modules/connectors/providerInfomaniak/connectorInfomaniak.py` (neu) - - `modules/connectors/connectorResolver.py` (Registry) - - `modules/routes/routeSecurityInfomaniak.py` (neu) - - `modules/routes/routeDataConnections.py` (Dispatch) - - `app.py` (Router-Mount) + - `modules/datamodels/datamodelUam.py` (Enum erweitert) + - `modules/auth/oauthProviderConfig.py` (Infomaniak-Scopes raus) + - `modules/auth/tokenManager.py` (Infomaniak-Init + Refresh raus) + - `modules/auth/tokenRefreshService.py` (Infomaniak aus Background-Refresh raus) + - `modules/connectors/providerInfomaniak/connectorInfomaniak.py` (Adapter unveraendert) + - `modules/connectors/connectorResolver.py` (Registry, unveraendert) + - `modules/routes/routeSecurityInfomaniak.py` (komplett neu: PAT-Submit-Endpoint) + - `modules/routes/routeDataConnections.py` (`connect_service` antwortet bei + Infomaniak mit 400 + Hinweis) + - `gateway/.env` + `env_dev/int/prod[_forgejo].env` (Service_INFOMANIAK_* raus) - Frontend: - - `src/api/connectionApi.ts` (Authority-Typ) - - `src/hooks/useConnections.ts` (Popup-Handler) - - `src/pages/basedata/ConnectionsPage.tsx` (Button) - - `src/components/UnifiedDataBar/SourcesTab.tsx` (Icons + Colors + Mapping, ClickUp-Fix) -- DB-Migration: nein (nur Enum-Wert, alle Tabellen abwaertskompatibel). -- Andere: keine. + - `src/api/connectionApi.ts` (`submitInfomaniakToken` neu) + - `src/hooks/useConnections.ts` (`createInfomaniakConnection` + + `submitInfomaniakToken` ersetzen `createInfomaniakConnectionAndAuth`, + OAuth-Popup-Branch fuer Infomaniak entfernt) + - `src/pages/basedata/ConnectionsPage.tsx` (PAT-Modal) + - `src/components/UnifiedDataBar/SourcesTab.tsx` (unveraendert seit Phase 1) +- Doku: + - `wiki/d-guides/infomaniak-oauth-setup.md` -> geloescht + - `wiki/d-guides/infomaniak-token-setup.md` -> neu +- DB-Migration: nein. ## Entscheidungen | Datum | Entscheidung | Begruendung | |------------|--------------|-------------| -| 2026-04-26 | Self-contained Connector (httpx im Modul, kein eigener Service) | Folgt Google/MSFT-Pattern, nicht ClickUp-Pattern (das delegiert an `ClickupService`) | +| 2026-04-26 | Self-contained Connector (httpx im Modul, kein eigener Service) | Folgt Google/MSFT-Pattern, nicht ClickUp-Pattern | | 2026-04-26 | Nur DATA_CONNECTION, kein Login | User explicitly: "wir benoetigen nur den userconnection auth" | -| 2026-04-26 | `kdrive` + `mail` als Service-Namen | Kurz, konsistent mit Infomaniak-Branding (kDrive heisst offiziell so) | +| 2026-04-26 | `kdrive` + `mail` als Service-Namen | Konsistent mit Infomaniak-Branding | +| 2026-04-28 | OAuth raus, Personal Access Token rein | Infomaniak's `/authorize` unterstuetzt fuer Datenzugriff keine Scopes; nur PATs liefern brauchbare Bearer-Tokens (`error=invalid_scope` bei OAuth-Versuch) | +| 2026-04-28 | Token-Validierung gegen `/1/profile` | Leichter Endpoint, liefert User-Identity (`id`, `login`, `email`) zur Anzeige im FE; gibt 401 bei ungueltigem PAT | +| 2026-04-28 | `/1/profile` raus, Validierung gegen `/2/drive` + `/1/mail` | Profile braucht zusaetzlich Scope `user_info`; mit Drive- + Mail-Probes verifizieren wir nur die fuer Adapter benoetigten Scopes | +| 2026-04-28 | `/1/mail` raus, Validierung gegen `calendar.infomaniak.com` + `/2/drive` | `/1/mail` existiert nicht (404 nginx); `mail.infomaniak.com/api/mail` redirected zu OAuth (302). Calendar-PIM-Endpoint funktioniert mit PAT und liefert Identity (account_id/user_id/name) gleich mit. | +| 2026-04-28 | `account_id` in `UserConnection.externalId` persistieren (zurueckgenommen 2026-04-28 abends) | War als Cache-Optimierung gedacht, hat aber zwei Concerns vermischt: (1) UI-Anzeige der Connection und (2) Adapter-Vertragspartner. Existierende Connections konnten dadurch mit einem Token-Fingerprint statt account_id korrumpiert werden, was im kDrive-Browse zu 422 fuehrte. | +| 2026-04-28 | `KdriveAdapter` resolvt `account_id` zur Laufzeit selbst (`_ensureAccountId`) ueber `resolveOwnerIdentity()` | Saubere Trennung: `externalId` ist nur UI-State, der Adapter ist self-contained und braucht keine Connection-Felder zu lesen. Damit gibt es keinen Migrationspfad und keine Korruption mehr -- der Adapter heilt sich automatisch. Cache auf Adapter-Instanz vermeidet wiederholte API-Calls innerhalb desselben Requests. | +| 2026-04-28 | Identity-Resolver (Calendar -> Contacts) zentral in `resolveOwnerIdentity()` | Beide PIM-Endpoints liefern dieselbe Owner-Struktur (`user_id`/`account_id`/`name`); ein gemeinsamer Resolver verhindert, dass Submit und Adapter divergieren. Die Sequenz Calendar -> Contacts deckt alle realistischen Setups ab (mindestens einer der beiden Scopes muss auf der PAT sein, damit kDrive ueberhaupt nutzbar ist). | +| 2026-04-29 | Calendar-Events ueber `/api/pim/event?calendar_id=...` (nicht `/calendar/{id}/event`) | Live-Test 2026-04-29: nested Route 302 zu OAuth, flat Route 200. Die offizielle PAT-faehige Route fordert `from`/`to` als Y-m-d H:i:s mit max-3-Monats-Range -- Adapter waehlt fixes 90-Tage-Window. | +| 2026-04-29 | `.vcf`-Download per Hand synthetisieren (`_renderInfomaniakVcard`) | Alle Contacts-Detail-/Export-Endpoints sind nicht PAT-faehig (500 bzw. 302 OAuth). Wir holen das Listing mit `with=emails,phones,addresses,details` (PAT-faehig) und rendern vCard 3.0 selbst -- konsistent mit MSFT/Google-Contacts-Adapter, der dieselbe Synthese betreibt. | +| 2026-04-28 | MailAdapter und ContactAdapter NICHT registrieren bis Endpoint gefunden | 302-Redirects zu OAuth wuerden im UDB als kaputter Service erscheinen; lieber gar nicht zeigen. | +| 2026-04-28 | ContactAdapter aktivieren via `contacts.infomaniak.com/api/pim/addressbook` | Nachgereichter curl-Test zeigte 200 + JSON mit derselben Struktur wie Calendar (`addressbooks[].id/name/account_id/...`). Singular-Pfad funktioniert (`/contact` und `/contacts` reden weiter mit OAuth). Adapter ist 1:1 analog `CalendarAdapter`, nur mit `addressbook` statt `calendar` und `.vcf` statt `.ics`. | +| 2026-04-28 | `expiresAt = now + 10y` fuer PATs | Analog ClickUp, sonst markiert `getTokenStatusForConnection` die Connection als "none" | +| 2026-04-28 | Connection up-front, dann PAT-Submit (statt Submit-erst-dann-erstellen) | Nutzt vorhandenen `POST /api/connections/`-Pfad ohne Sonderbehandlung; Cancel rollback via DELETE auf Modal-Schliessen | ## Umsetzungs-Checkliste - [x] AuthAuthority-Enum erweitert -- [x] Scopes definiert (`user_info kdrive mail`) -- [x] InfomaniakConnector + KdriveAdapter + MailAdapter -- [x] ConnectorResolver-Registry -- [x] OAuth-Route `/api/infomaniak/auth/connect[/callback]` -- [x] Token-Refresh (`refreshInfomaniakToken` + Background-Service) -- [x] DataConnections-Dispatch (`authority_map`, Labels, `connect_service`) -- [x] App-Router-Mount -- [x] Frontend-Typen -- [x] Hook `createInfomaniakConnectionAndAuth` + Event-Listener -- [x] ConnectionsPage-Button -- [x] UDB-Integration (Authority-Icon, Service-Icons, Source-Colors, Mapping) +- [x] InfomaniakConnector + KdriveAdapter (resolvt `account_id` zur + Laufzeit ueber `resolveOwnerIdentity()`) + CalendarAdapter + + ContactAdapter +- [x] `resolveOwnerIdentity()` als gemeinsamer Identity-Helper im + Connector-Modul (Calendar -> Contacts Fallback, raised + `InfomaniakIdentityError` bei totalem Fehlschlag) +- [x] ConnectorResolver-Registry (alle Adapter werden uniform mit + `accessToken` konstruiert; keine Sonderbehandlung in + `getServiceAdapter`) +- [x] Setup-Guide (PAT-basiert, Calendar + Contacts als zusaetzliche aktive Services) +- [x] PAT-Submit-Endpoint `POST /api/infomaniak/connections/{id}/token` + (Pre-Flight: `resolveOwnerIdentity` + Drive-Probe mit resolvter + `account_id` -- harter 200-Pfad, kein 422-Tolerance-Hack mehr) +- [x] OAuth-Routen entfernt +- [x] Infomaniak-Refresh aus tokenManager + tokenRefreshService entfernt +- [x] Infomaniak-Scopes aus `oauthProviderConfig` entfernt +- [x] `Service_INFOMANIAK_*` aus allen .env-Dateien entfernt +- [x] DataConnections-Dispatch (Infomaniak-Branch in `connect_service` -> 400) +- [x] FE-Hook umgebaut (`createInfomaniakConnection` + `submitInfomaniakToken`) +- [x] FE-OAuth-Popup-Branch fuer Infomaniak entfernt +- [x] ConnectionsPage-PAT-Modal +- [x] UDB-Integration (Authority-Icon, Service-Icons, Source-Colors) - [x] ClickUp-UDB-Fix -- [x] Setup-Guide `wiki/d-guides/infomaniak-oauth-setup.md` -- [ ] Manueller End-to-End-Test (ClientID/Secret im Manager registrieren, Verbinden, kDrive browsen, Mail browsen, Datei downloaden) +- [ ] Manueller End-to-End-Test (Token im Manager erstellen, in Modal + pasten, kDrive browsen, Calendar browsen, Contacts browsen, + Datei + .ics + .vcf downloaden) +- [x] Mail-API-Pfad verifiziert -- alle 7 erschoepfend getesteten Pfade + scheitern (siehe "Blockiert" oben). Adapter pausiert bis Infomaniak + einen PAT-faehigen Endpoint freischaltet. - [ ] `wiki/b-reference/connectors.md` (falls vorhanden) ergaenzen ## Akzeptanzkriterien | # | Kriterium (Given-When-Then) | Prio | |---|-----------------------------|------| -| 1 | Given Infomaniak-Credentials in `APP_CONFIG`, When Nutzer "Infomaniak" auf ConnectionsPage klickt, Then oeffnet sich der OAuth-Popup auf `login.infomaniak.com/authorize` | must | -| 2 | Given erfolgreicher OAuth-Callback, Then ist die UserConnection `ACTIVE`, Token gespeichert (`tokenAccess`+`tokenRefresh`), `externalId/Email` gesetzt | must | -| 3 | Given aktive Connection, When im UDB die Authority expandiert wird, Then werden Services `kdrive` und `mail` mit Icons angezeigt | must | -| 4 | Given Token vor Ablauf < 5 min, When Background-Refresh laeuft, Then wird `_refresh_infomaniak_token` aufgerufen und Token erneuert | must | -| 5 | Given ClickUp-Service-Node im UDB, Then ist das Service-Icon das Klemmbrett (Fix) | should | +| 1 | Given Nutzer auf ConnectionsPage, When er auf "Infomaniak" klickt, Then oeffnet sich ein Modal mit Token-Eingabe und Deeplink zu `manager.infomaniak.com/v3/ng/accounts/token/list` | must | +| 2 | Given gueltiger PAT mit Scopes `drive`+(`workspace:calendar` ODER `workspace:contact`) im Modal, When Submit, Then ist die UserConnection `ACTIVE`, Token gespeichert, `externalId=account_id`, `externalUsername=Owner-Anzeigename aus PIM` | must | +| 3 | Given ungueltiger PAT oder fehlender `drive`-Scope ODER weder `workspace:calendar` noch `workspace:contact`, When Submit, Then bleibt Connection PENDING, Modal zeigt 400-Detail mit Scope-Hinweis | must | +| 4 | Given aktive Connection, When im UDB die Authority expandiert wird, Then werden Services `kdrive`, `calendar` und `contact` mit Icons angezeigt | must | +| 4b | Given aktive Connection (auch eine, deren `externalId` aus historischen Gruenden NICHT `account_id` ist), When im UDB `kdrive` expandiert wird, Then erscheinen Drives ohne `422`-Fehler (Adapter resolvt `account_id` zur Laufzeit ueber `resolveOwnerIdentity`) | must | +| 4c | Given aktive Connection, When im UDB `calendar` expandiert wird, Then erscheinen Kalender, dann Events; Event-Download liefert `.ics` | must | +| 4d | Given aktive Connection, When im UDB `contact` expandiert wird, Then erscheinen Adressbuecher, dann Kontakte; Kontakt-Download liefert `.vcf` | must | +| 5 | Given Modal offen, When User auf Cancel/X klickt, Then wird die soeben erstellte PENDING-Connection wieder geloescht | should | +| 6 | Given ClickUp-Service-Node im UDB, Then ist das Service-Icon das Klemmbrett (Fix) | should | ## Testplan | ID | AC | Art | Automatisiert | Repo-Pfad | Status | |----|----|-----|---------------|-----------|--------| -| T1 | 1-2 | manual | nein | UI: ConnectionsPage | pending | -| T2 | 3 | manual | nein | UI: UDB SourcesTab | pending | -| T3 | 4 | unit | spaeter | gateway/tests/auth/test_tokenManager.py | pending | -| T4 | 5 | manual | nein | UI: UDB SourcesTab | pending | +| T1 | 1-3 | manual | nein | UI: ConnectionsPage Modal | pending | +| T2 | 4 | manual | nein | UI: UDB SourcesTab | pending | +| T3 | 5 | manual | nein | UI: ConnectionsPage Modal Cancel | pending | +| T4 | 6 | manual | nein | UI: UDB SourcesTab | pending | ## Links - PR: tbd -- Setup-Guide: `wiki/d-guides/infomaniak-oauth-setup.md` +- Setup-Guide: `wiki/d-guides/infomaniak-token-setup.md` ## Abschluss diff --git a/c-work/2-build/2026-04-msft-google-calendar-contacts.md b/c-work/2-build/2026-04-msft-google-calendar-contacts.md new file mode 100644 index 0000000..77bcf61 --- /dev/null +++ b/c-work/2-build/2026-04-msft-google-calendar-contacts.md @@ -0,0 +1,189 @@ + + + + +# MSFT- und Google-Connector: CalendarAdapter + ContactsAdapter, plus Reconnect-Button + +## Beschreibung und Kontext + +Der Infomaniak-Connector liefert seit dem 2026-04-28 die drei Services +**kDrive**, **Calendar** und **Contacts**. Die analogen Services fuer Microsoft +und Google fehlten bislang in der UDB. Ziel dieses Builds: vier neue +ServiceAdapter (MSFT-Calendar, MSFT-Contacts, Google-Calendar, +Google-Contacts), so dass die UDB pro Connection einen einheitlichen +Service-Pool zeigt und der Agent fuer Calendar/Contacts gegen alle drei +Provider gleichermassen arbeiten kann. + +Zusaetzliche Anforderung: ein **Reconnect-Button** im Frontend. Bestehende +MSFT-/Google-Connections wurden mit einem kleineren Scope-Set autorisiert. +Beim Hinzufuegen neuer Scopes (`Calendars.Read`, `Contacts.Read`, +`calendar.readonly`, `contacts.readonly`) liefern die Provider auf einer +Re-Authorisierung sonst still **dieselben** alten Tokens, weil sie +`include_granted_scopes` (Google) bzw. `prompt=select_account` (MSFT) +default-maessig nutzen -- die neuen Scopes wuerden nie das Tokenset +erreichen. + +## Architektur + +- `oauthProviderConfig.googleDataScopes` += `calendar.readonly`, + `contacts.readonly` +- `oauthProviderConfig.msftDataScopes` += `Calendars.Read`, `Contacts.Read` +- `connectorMsft.CalendarAdapter` (neu) + - Endpoints: `me/calendars`, `me/calendars/{id}/events` + - Pagination via `@odata.nextLink` (Helper `_stripGraphBase` aus dem Modul) + - Pfad: `""` -> Calendars; `"/{calendarId}"` -> Events + - Download: synthetisches RFC-5545 VCALENDAR/VEVENT (Graph hat keinen + `$value`-Endpoint fuer Events) +- `connectorMsft.ContactsAdapter` (neu) + - Endpoints: `me/contactFolders`, `me/contactFolders/{id}/contacts`, + plus virtueller Default-Folder `default` -> `me/contacts` + - Download: vCard 3.0 (N/FN/ORG/TITLE/EMAIL/TEL/ADR/NOTE) +- Helper: `_eventToIcs`, `_contactToVcard`, `_safeFileName`, + `_personLabel`, `_icsEscape`, `_icsDateTime` (alle modullokal) +- `connectorMsft.MsftConnector._SERVICE_MAP` += `"calendar"` + `"contact"` +- `connectorGoogle.CalendarAdapter` (neu) + - Endpoints: `users/me/calendarList`, + `calendars/{id}/events?singleEvents=true&orderBy=startTime` + - Download: `.ics` aus Event-Detail synthetisiert + - Search: `events?q=...` per Calendar-ID (Default `primary`) +- `connectorGoogle.ContactsAdapter` (neu) + - Endpoints: `contactGroups` + virtueller `all` -> `people/me/connections` + - Gruppen-Mitglieder via `contactGroups/{id}` -> `memberResourceNames` -> + `people:batchGet?resourceNames=...&personFields=...` + - Search: `people:searchContacts` + - Download: vCard 3.0 aus `personFields=names,emailAddresses,phoneNumbers, + organizations,addresses,biographies,memberships` +- `connectorGoogle.GoogleConnector._SERVICE_MAP` += `"calendar"` + + `"contact"` +- `routeFeatureWorkspace` und `routeFeatureGraphicalEditor` + Service-Label-/Icon-Maps += `kdrive`, `calendar`, `contact` (so dass die + UDB sinnvolle Bezeichnungen anzeigt -- vorher war Infomaniak-kDrive + ungelabelt durchgerutscht) + +## Reconnect-Flow + +- `routeDataConnections.connect_service` (`POST /api/connections/{id}/connect`) + akzeptiert optionalen Body `{"reauth": true}` und haengt `&reauth=1` an + die zurueckgegebene `auth_url`. +- `routeSecurityMsft.auth_connect` setzt bei `reauth=1` + `prompt=consent` (statt `select_account` / `login`). Das erzwingt den + Microsoft-Consent-Screen und liefert die neuen Scopes nach. +- `routeSecurityGoogle.auth_connect` droppt bei `reauth=1` + `include_granted_scopes=true` (default-`true` macht Google "vergesslich" + fuer neu hinzugekommene Scopes -- ohne diesen Drop blieben die neuen + Scopes still aussen vor). +- ClickUp ignoriert den Param (FastAPI laesst unbekannte Query-Args + durchrutschen) -- das ist OK, weil ClickUp keine Scope-Erweiterung hat. +- Frontend: + - `connectionApi.connectService(id, reauth?)` haengt den Body an. + - `useConnections.connectWithPopup(id, reauth?)` reicht durch (auch + der separate `useOAuthConnect`-Hook). + - `ConnectionsPage` hat ein neues `customAction` `reconnect` mit + `FaSyncAlt`-Icon, eigenem `reconnectingConnections`-Set und + `visible`-Filter `status === 'active' && authority in {msft, google, + clickup}`. Der Refresh-Button bleibt fuer Token-Refresh ohne + Re-Consent erhalten. + +## Fokus und kritische Details + +- **Pagination**: MSFT Calendar/Contacts paginieren bei `$top<=100`; wir + ziehen `@odata.nextLink` durch wie OutlookAdapter und stoppen bei + `effectiveLimit`. +- **iCal-Bauen**: Wir ueberlassen das nicht einer Library, weil RFC-5545 + fuer einzelne VEVENT-Faelle trivial ist und der Overhead einer + Dependency (`icalendar`) nicht gerechtfertigt ist. Escape und + DTSTAMP/DTSTART/DTEND in UTC. +- **vCard-Bauen**: vCard 3.0 weil universell von Outlook/Google/macOS + Contacts akzeptiert. ADR-Felder: leere Felder werden als leere Slots + gerendert (nicht weggelassen), damit das Format gueltig bleibt. +- **Default-Contacts-Folder**: Outlook hat einen unsichtbaren + Default-Ordner -- den simulieren wir mit dem Pseudo-Folder `default`, + der intern auf `me/contacts` mappt. Ohne diesen Eintrag waere der + Default-Ordner aus der UDB nicht erreichbar. +- **People-API Group-Resolution**: `contactGroups/{id}` liefert keine + vollstaendigen Person-Records, nur `memberResourceNames`. Wir batchen + diese in 200er-Chunks via `people:batchGet`. Limit fuer Resource-Names + pro Batch ist 200 (Google API spec). +- **Reconnect ohne Token-Vernichtung**: Wir loeschen die alte Token nicht + -- der Auth-Callback ueberschreibt sie atomar nach erfolgreichem + Code-Exchange. Bei abgebrochenem Reconnect bleibt der bisherige Token + gueltig. + +## Entscheidungen + +- **Calendar/Contacts read-only:** Wir nehmen `Calendars.Read` / + `calendar.readonly` / `Contacts.Read` / `contacts.readonly` -- nicht + ReadWrite. Der UDB-Use-Case ist ausschliesslich Lesen + Suche + + Download. Schreibrechte werden bei einem konkreten Bedarf separat + diskutiert (mehr Scopes erhoehen Consent-Aufwand und Risk-Profile + unnoetig). +- **Reconnect statt eigenem Endpoint:** Wir spendieren keinen separaten + `/reconnect`-Endpoint, sondern einen Body-Param am bestehenden + `/connect`. Vorteil: ein einziger Authorisierungspfad, identische + Callback-Behandlung, kein doppeltes State-Management. +- **`include_granted_scopes` Drop bei Google:** Default `true` ist + freundlich fuer Token-Reuse, aber die offizielle Google-Doku warnt + ausdruecklich, dass es bei Scope-Erweiterung ueberraschend Tokens + ohne neue Scopes ausstellt. `false` bei Reconnect ist die robustere + Wahl. +- **`prompt=consent` bei MSFT:** Microsoft erkennt zusaetzliche Scopes + zwar normalerweise selbststaendig und zeigt einen Consent-Screen, + aber das ist nur best-effort. Mit `prompt=consent` ist es + garantiert. +- **vCard 3.0 statt 4.0:** 4.0 hat noch immer schlechte Outlook- + Kompatibilitaet beim Import; 3.0 ist der Common-Denominator. +- **`.ics` per Hand bauen statt `iCalendar`-Lib:** Eine externe + Dependency lohnt fuer einen einzigen VEVENT je Download nicht. + +## Umsetzungs-Checkliste + +- [x] `oauthProviderConfig.py`: `googleDataScopes` + `msftDataScopes` + ergaenzt +- [x] `connectorMsft.CalendarAdapter` (browse/download/search) +- [x] `connectorMsft.ContactsAdapter` (browse/download/search) +- [x] `connectorMsft._SERVICE_MAP`: `calendar`, `contact` registriert +- [x] `connectorMsft._eventToIcs`, `_contactToVcard`, Helpers +- [x] `connectorGoogle.CalendarAdapter` (browse/download/search) +- [x] `connectorGoogle.ContactsAdapter` (browse/download/search inkl. + Group-Resolution) +- [x] `connectorGoogle._SERVICE_MAP`: `calendar`, `contact` registriert +- [x] `connectorGoogle._googleEventToIcs`, `_googlePersonToVcard`, + Helpers +- [x] `routeFeatureWorkspace` Service-Label/Icon-Maps erweitert +- [x] `routeFeatureGraphicalEditor` Service-Label/Icon-Maps erweitert +- [x] `routeDataConnections.connect_service`: optionaler `reauth`-Body +- [x] `routeSecurityMsft.auth_connect`: `reauth` -> `prompt=consent` +- [x] `routeSecurityGoogle.auth_connect`: `reauth` -> drop + `include_granted_scopes` +- [x] Frontend `connectionApi.connectService(id, reauth?)` +- [x] Frontend `useConnections.connectWithPopup(id, reauth?)` (beide + Hooks) +- [x] Frontend `ConnectionsPage`: `Reconnect`-Button mit + `FaSyncAlt`-Icon und eigenem Loading-Set +- [ ] **Manuelle Verifikation:** existierende MSFT-Connection neu + durchklicken (Reconnect), Calendar+Contacts in der UDB + verifizieren; existierende Google-Connection genauso +- [ ] **Manuelle Verifikation:** Calendar-Download liefert eine + importierbare `.ics` (Outlook-Test); Contacts-Download liefert eine + importierbare `.vcf` +- [ ] Doc-Sync: `wiki/b-reference/auth-and-rbac.md`, + `wiki/b-reference/connectors.md` aktualisieren (neue Scopes, + Reconnect-Pfad), Topics-Liste pruefen + +## Akzeptanzkriterien + +1. Bestehende MSFT-/Google-Connection -> Reconnect-Button -> nach + Consent erscheinen `Calendar` und `Contacts` als zusaetzliche + Service-Knoten in der UDB. +2. Calendar-Browse listet die User-Calendars; Klick auf einen + Calendar listet die juengsten Events; Download eines Events + liefert eine `.ics` mit DTSTART/DTEND/SUMMARY/LOCATION. +3. Contacts-Browse listet ContactFolders (MSFT) bzw. ContactGroups + (Google); Klick listet die enthaltenen Contacts; Download liefert + eine `.vcf` mit FN, ORG, EMAIL, TEL. +4. Frischer Connect (neue MSFT-Connection) zieht die neuen Scopes ohne + Reconnect (weil sie im initialen `msftDataScopes` / + `googleDataScopes` enthalten sind). +5. ClickUp- und FTP-Connections sind unbeeintraechtigt; bei + Infomaniak ist der Reconnect-Button korrekt **nicht** sichtbar + (Authority-Whitelist). diff --git a/c-work/_CHANGELOG.md b/c-work/_CHANGELOG.md index 99ab652..b75d113 100644 --- a/c-work/_CHANGELOG.md +++ b/c-work/_CHANGELOG.md @@ -12,8 +12,28 @@ type: `feat` `fix` `refactor` `docs` `test` `chore` `build` · scope: `gateway Skip: reine Refactors, Formatting, Lint, Dep-Bumps, Test-only, Wiki-Tippfehler. +## 2026-04-29 + +- 2026-04-29 | fix | gateway, frontend-nyla, wiki | **Infomaniak-Connector: kDrive findet Standalone- und Free-tier-Drives (account_id-Resolution korrigiert).** Live-Beweis vom User mit echten kDrive-Files und Empty-Listing aus PowerOn: das Standalone-/Free-tier-kDrive haengt an einer **anderen** `account_id` als die kSuite, und `/2/drive?account_id=` antwortet sauber 200 mit leerer Liste -- die kSuite-`account_id` aus PIM Calendar/Contacts (heutige Identity-Quelle) trifft also den Drive-Account ueberhaupt nicht. Root-cause: es gibt unter den heutigen Scopes (`drive`, `workspace:*`) **keinen** Endpoint, der die volle Account-Liste eines PAT zurueckgibt -- der einzige PAT-faehige ist `GET /1/accounts`, der `403 all_scopes "scopes: ['accounts']"` antwortet wenn der `accounts`-Scope fehlt. Architektur-Change: (a) Neuer Helper `resolveAccessibleAccountIds(token)` im `connectorInfomaniak.py` -- ein einzelner `/1/accounts`-Call gibt **alle** account_ids zurueck, raised `InfomaniakIdentityError` mit klarer Scope-Message wenn der Scope fehlt. (b) `KdriveAdapter` haelt jetzt eine `_accountIds: List[int]` (statt `_accountId: int`); `_listDrives()` ruft `/2/drive?account_id=X` fuer **jede** account_id auf und unioniert die Drive-Ergebnisse mit Dedup ueber `driveId`. Damit deckt der Adapter sowohl den kSuite-eingebetteten als auch den Standalone-/Free-tier-kDrive ab, ohne dass der User irgendwas konfigurieren muss. (c) `resolveOwnerIdentity` ist jetzt rein UI-Identity (Display-Name + kSuite-`account_id` fuer das Connection-Label) -- nicht mehr fuer Drive-Lookup verwendet. (d) `submit_infomaniak_token` validiert jetzt drei Scopes deterministisch: `/1/accounts` (`accounts`-Scope), `resolveOwnerIdentity` (`workspace:calendar`/`workspace:contact`-Scope), `/2/drive?account_id={first}` (`drive`-Scope). Jeder Schritt liefert eine eigene 400-Message, die genau den fehlenden Scope nennt. (e) `grantedScopes` um `"accounts"` ergaenzt. Frontend: PAT-Setup-Modal listet jetzt **fuenf** Pflicht-Scopes (`accounts` neu) mit klarem "sonst findet kDrive deinen Drive nicht"-Hinweis. Doku: `infomaniak-token-setup.md` Status-Tabelle / Scope-Tabelle / Validation-Section / "How the kDrive adapter knows your account"-Section komplett umgeschrieben. **Konsequenz: bestehende Infomaniak-PATs ohne `accounts`-Scope muessen einmal im Infomaniak Manager neu erstellt werden -- der gestrige `resolveOwnerIdentity`-Pre-Flight-Check faellt sonst nie auf, das Listing bleibt leer.** (c-work: `c-work/2-build/2026-04-infomaniak-connector.md`) + +- 2026-04-29 | fix | gateway | **Infomaniak-Connector: Calendar-Events und Contacts-Download auf die echten PAT-faehigen Pfade gezogen.** Live-Tests gegen die Vendor-API zeigten zwei Pfad-Mismatches im gestrigen Build: (1) Calendar-Events: der nested Route `/api/pim/calendar/{id}/event` 302t zur OAuth-Login-Seite (also nicht PAT-faehig); korrekter Endpoint ist `/api/pim/event?calendar_id={id}&from=YYYY-MM-DD HH:MM:SS&to=...` mit Pflicht-Range max 3 Monate (Vendor-Constraint `range_must_be_lower_than_3_months`) -- Adapter waehlt jetzt fix 90-Tage-Window (heute -30 / +60), URL-Encoding via `urllib.parse.quote`. Event-Detail und `.ics`-Export laufen ueber `/api/pim/event/{eventId}` und `.../export` (also ohne calendar-Praefix). (2) Contacts-Download: `/addressbook/{book}/contact/{id}` antwortet mit `500 unexpected_error` und `.../export` 302t zu OAuth -- beide Detail-Endpoints sind also nicht PAT-faehig. Listing dagegen funktioniert, liefert aber per Default nur Stammdaten -- ohne `with=emails,phones,addresses,details` kommen Email/Phone/Adresse leer. Loesung: ContactAdapter holt das Listing mit dem `with`-Param und rendert die `.vcf` selbst via `_renderInfomaniakVcard()` (vCard 3.0, escaped, mit N/FN/ORG/EMAIL/TEL/ADR/URL/NOTE) -- konsistent mit MSFT/Google-Adapter, die ihre `.vcf`s ebenfalls selbst synthetisieren. Plus: Helper `_safeFileName` aus dem Calendar-Adapter zu modullokal hochgezogen, von beiden Adaptern genutzt; ungenutzte `import re`/`import json`-Inline-Imports raus. **kDrive-Adapter ist im Live-Test korrekt:** `/2/drive?account_id=1696919` antwortet 200 mit leerem Array fuer den Test-Account (kein kDrive-Produkt aktiviert). (c-work: `c-work/2-build/2026-04-infomaniak-connector.md`) + ## 2026-04-28 +- 2026-04-28 | refactor | gateway | **Infomaniak-Connector: `account_id` ist Adapter-State, nicht Connection-State.** Symptom im Log: `Infomaniak GET https://api.infomaniak.com/2/drive?account_id=pat-XXXXXXXX -> 422 validation_rule_integer "The account id must be an integer."`. Root cause: der `KdriveAdapter` las `connection.externalId` als `account_id`-Quelle, und bei kaputten Submits konnte dort der Token-Fingerprint ("pat-59ee48d9") statt der `account_id` stehen. Saubere Loesung statt Fingerprint-Migration: (a) Neuer modullokaler Helper `resolveOwnerIdentity(token) -> InfomaniakOwnerIdentity` im `connectorInfomaniak.py` -- versucht PIM Calendar (`workspace:calendar`-Scope), faellt auf PIM Contacts (`workspace:contact`-Scope) zurueck, raised `InfomaniakIdentityError` wenn keine Owner-Records gefunden. (b) `KdriveAdapter` hat keinen `accountId`-Konstruktor-Parameter mehr, sondern resolvt zur Laufzeit ueber `_ensureAccountId()` (gecached auf der Adapter-Instanz). Damit ist der Adapter self-contained und liest **nichts** mehr aus der Connection -- `externalId` ist reiner UI-State. (c) `submit_infomaniak_token` ruft denselben `resolveOwnerIdentity()` als Pre-Flight-Check, dann `/2/drive?account_id={resolved}` als sauberer 200-Probe. Der frueher noetige `_probeScope`-422-Tolerance-Hack ist entfallen. (d) `getServiceAdapter` hat keine kDrive-Sonderbehandlung mehr; alle drei Adapter werden uniform mit `accessToken` konstruiert. (e) `_PIM_PREFIX` ist auf Modul-Ebene definiert; CalendarAdapter und ContactAdapter haben keine Klassen-Konstanten mehr. **Konsequenz: jede existierende Infomaniak-Connection arbeitet ohne User-Aktion sofort wieder, auch wenn `externalId` historisch einen Fingerprint enthaelt -- der Adapter zieht die `account_id` deterministisch aus der API.** Setup-Guide um Architektur-Abschnitt erweitert. (c-work: `c-work/2-build/2026-04-infomaniak-connector.md`) + +- 2026-04-28 | feat | gateway, frontend-nyla | **MSFT- und Google-Connector: CalendarAdapter + ContactsAdapter neu, plus Reconnect-Button.** Nachdem Infomaniak heute Calendar/Contacts kann, ziehen MSFT und Google nach. Backend: (a) `oauthProviderConfig.googleDataScopes` um `calendar.readonly` + `contacts.readonly` erweitert, `msftDataScopes` um `Calendars.Read` + `Contacts.Read`. (b) `connectorMsft.CalendarAdapter` (Graph: `me/calendars`, `me/calendars/{id}/events?$top&$orderby=start/dateTime desc`, Pagination via `@odata.nextLink`, `.ics`-Download als selbstgebautes RFC-5545 VCALENDAR/VEVENT da Graph keinen `$value`-Endpoint fuer Events kennt; `$search` fuer query). (c) `connectorMsft.ContactsAdapter` (Graph: `me/contactFolders` + virtueller `default`-Ordner fuer `me/contacts`, `me/contactFolders/{id}/contacts?$orderby=displayName`, `.vcf` als vCard-3.0 selbstgebaut mit N/FN/ORG/TITLE/EMAIL/TEL/ADR/NOTE). Helper `_eventToIcs`, `_contactToVcard`, `_safeFileName`, `_personLabel` plus `_icsEscape`/`_icsDateTime`. (d) `connectorGoogle.CalendarAdapter` (Calendar v3: `users/me/calendarList`, `calendars/{id}/events?singleEvents=true&orderBy=startTime`, `.ics` aus Event-Detail). (e) `connectorGoogle.ContactsAdapter` (People API: `contactGroups` + virtueller `all`-Folder ueber `people/me/connections`, fuer Gruppen `people:batchGet?resourceNames=...`, Search via `people:searchContacts`, `.vcf` aus `personFields=names,emailAddresses,phoneNumbers,organizations,addresses,biographies`). (f) `_SERVICE_MAP` von beiden Connectoren um `"calendar"`/`"contact"` ergaenzt; `routeFeatureWorkspace` und `routeFeatureGraphicalEditor` Service-Label-/Icon-Maps um `kdrive`, `calendar`, `contact` ergaenzt, damit die UDB sinnvolle Anzeigen macht. (g) **Reconnect-Flow**: `POST /api/connections/{id}/connect` akzeptiert optionalen Body `{"reauth": true}` und haengt `&reauth=1` an die Auth-URL. `routeSecurityMsft.auth_connect` setzt bei `reauth=1` `prompt=consent` (sonst select_account/login), `routeSecurityGoogle.auth_connect` droppt bei `reauth=1` `include_granted_scopes=true` damit Google strikt fuer die aktuelle Scope-Liste neu signiert (sonst werden neue Scopes still uebersprungen). Frontend: `connectionApi.connectService(id, reauth?)` -> POST mit Body, `useConnections.connectWithPopup(id, reauth?)` reicht durch, `ConnectionsPage` zeigt fuer aktive MSFT/Google/ClickUp-Connections einen `FaSyncAlt`-Button "Erneut verbinden (neue Scopes erteilen)" mit eigenem Loading-Set. Bestehende Connections muessen einmal reconnected werden, damit Calendar/Contacts in der UDB auftauchen. (c-work: `c-work/2-build/2026-04-msft-google-calendar-contacts.md`) + +- 2026-04-28 | fix | gateway | **OpenAI-Connector: `temperature` fuer GPT-5.x / o-Serie aus dem Payload nehmen.** Symptom im Log: jede AI-Anfrage failt mit HTTP 400 `Unsupported value: 'temperature' does not support 0.2 with this model. Only the default (1) value is supported.`, der Failover spricht 14 Modelle durch und schreibt `Recorded failure for gpt-5.5, cooldown 60.0s`. Root cause: die GPT-5-Familie (gpt-5, gpt-5.4*, gpt-5.5*) und die o-Serie (o1/o3/o4) sind Reasoning-Modelle; OpenAI akzeptiert dort -- analog zur `max_tokens` -> `max_completion_tokens`-Restriction -- nur den Default (`1`). Wir senden aber unverhandelt `temperature=0.2` aus jedem `AiModel`-Eintrag. Fix: Helper `_supportsCustomTemperature(modelName)` und in `callAiBasic`/`callAiBasicStream`/`callAiImage` den Key nur conditional ins Payload aufnehmen (Modellnamen mit Praefix `gpt-5`, `o1`, `o3`, `o4` lassen ihn weg). Der vom User im UI gesetzte Override ueber `AiCallOptions.temperature` wird auf den nicht unterstuetzten Modellen still verworfen statt einen 400 zu erzwingen. Tests: `tests/unit/aicore/test_aicorePluginOpenai_temperature.py` (18 Tests, parametrisiert ueber Legacy-Modelle vs. Reasoning-Familie). Suite gesamt: 530 passed. + +- 2026-04-28 | docs | wiki | **Infomaniak-Connector: Mail-Adapter formal als "blocked by vendor" markiert.** Erschoepfende Pfad-Tests am 2026-04-28 zeigen: alle 7 plausiblen Mail-Endpoints sind heute nicht PAT-faehig. `api.infomaniak.com/{1,2}/mail` -> 404 nginx (existiert nicht); `mail.infomaniak.com/api/mail[?account_id=...]`, `/api/pim/mail`, `/api/pim/mailbox`, `/api/pim/folder` -> 302 zu `login.infomaniak.com/authorize` (nur OAuth-Web-Session, Bearer-PAT wird abgelehnt); `mail.infomaniak.com/api/mail/?account_id=...` -> 301 zu `http://mail.infomaniak.com:5000` (interner Cyrus-IMAP-Port, von aussen nicht erreichbar). Der `workspace:mail`-Scope bleibt im PAT-Standard-Setup, damit der MailAdapter spaeter ohne Token-Rotation freigeschaltet werden kann; Setup-Guide und c-work-Doku zementieren den Befund. Kein Code-Change. (c-work: `c-work/2-build/2026-04-infomaniak-connector.md`) + +- 2026-04-28 | feat | gateway, frontend-nyla | **Infomaniak-Connector: ContactAdapter neu.** Nachfolge-Tests gegen die Contacts-PIM-API zeigten, dass `https://contacts.infomaniak.com/api/pim/addressbook` (Singular!) mit dem PAT-Scope `workspace:contact` und Bearer-Auth `200` + JSON liefert -- gleiche Antwort-Struktur wie Calendar (`addressbooks[].id/name/account_id/...`). Die Plural- und `/contact*`-Pfade (`/api/pim/contacts`, `/api/pim/contact/addressbook`) sind weiterhin OAuth-only bzw. 404. Neuer `ContactAdapter` 1:1 analog `CalendarAdapter`: `browse("/")` -> Adressbuecher, `browse("/{bookId}")` -> Kontakte (Display-Name, Email, Phone, Organization in Metadata), `download("/{bookId}/{contactId}")` -> `.vcf` via `/contact/{id}/export` mit JSON-Fallback, `search()` als kostenguenstiger Client-Filter. Geteilte Organisations-Adressbuecher (`name=""`, `is_dynamic_organisation_member_directory=true`) bekommen "Organisation" als Anzeigename, sonst waere der Tree-Knoten leer. Neue Konstante `_CONTACTS_BASE`, `ContactAdapter` in `_SERVICE_MAP` registriert. Frontend: `SourcesTab` kennt `contact`-Icon (👤), -Color und Mapping; PAT-Modal nennt Contacts als heute aktiv (Mail bleibt "in Vorbereitung"). Setup-Guide: Status-Tabelle, UDB-Verifikations-Liste und Validation-Beschreibung aktualisiert. (c-work: `c-work/2-build/2026-04-infomaniak-connector.md`) + +- 2026-04-28 | feat | gateway, frontend-nyla | **Infomaniak-Connector: kDrive PAT-Fix + Calendar-Adapter neu.** Nach den ersten echten PAT-Test-Calls aufgeraeumt: (a) `/2/drive` braucht ein `account_id`-Query-Arg, sonst `422 account_id required`. Fix: `account_id` einmalig beim Token-Submit aus dem Calendar-PIM-Endpoint (`https://calendar.infomaniak.com/api/pim/calendar` -- liefert `account_id`, `user_id`, Anzeigename) ziehen, in `UserConnection.externalId` persistieren und ueber `InfomaniakConnector.getServiceAdapter` als Konstruktor-Parameter in den `KdriveAdapter` injizieren. `/1/profile` (haette `user_info`-Scope verlangt) und `/1/mail` (existiert nicht: 404 nginx) raus aus den Probes. (b) `_probeScope` toleriert jetzt 4xx ausser 401/403 (z.B. 422 `validation_failed`) als "Scope ist da, Endpoint braucht nur weitere Args". (c) Neuer `CalendarAdapter` (`browse` -> Calendars/Events ueber `calendar.infomaniak.com/api/pim/calendar`, `download` -> `.ics` via `/event/{id}/export`, JSON-Fallback). `_infomaniakGet` und `_infomaniakDownload` akzeptieren ein optionales `baseUrl`, sodass Calendar gegen `calendar.infomaniak.com` und kDrive gegen `api.infomaniak.com` laufen koennen. (d) `MailAdapter` aus `_SERVICE_MAP` entfernt: `mail.infomaniak.com/api/mail` redirected mit 302 zu `login.infomaniak.com/authorize`, akzeptiert also keine PATs; gleiches gilt fuer `contacts.infomaniak.com/api/pim/contact`. Beide Scopes werden weiterhin in `grantedScopes` gespeichert, damit kuenftige Adapter ohne Token-Rotation aufgeschaltet werden koennen. (e) Frontend: `SourcesTab` kennt `calendar`-Icon, -Color und `_SERVICE_TO_SOURCE_TYPE`-Mapping; PAT-Modal in `ConnectionsPage` zeigt Calendar als zweiten aktiven Service, Mail/Contact als "in Vorbereitung, Scope schon mitnehmen". (f) Setup-Guide aktualisiert (Status-Tabelle + Validation-Beschreibung). (c-work: `c-work/2-build/2026-04-infomaniak-connector.md`) + +- 2026-04-28 | refactor | gateway, frontend-nyla | **Infomaniak-Connector: OAuth -> Personal Access Token.** Infomaniaks `login.infomaniak.com/authorize` akzeptiert nur Identity-Scopes (`openid`, `profile`, `email`, `phone`); Versuche mit `kdrive`/`mail`-Scopes quittieren mit `error=invalid_scope`. Datenzugriff geht ausschliesslich ueber manuell im Manager erstellte PATs. Umbau: (a) Backend `routeSecurityInfomaniak` komplett neu -- ein Endpoint `POST /api/infomaniak/connections/{id}/token`, validiert PAT via `GET https://api.infomaniak.com/1/profile`, persistiert Bearer mit 10-Jahres-Horizont (analog ClickUp), entfernt OAuth-Connect/Callback-Pfade. (b) `tokenManager.refreshInfomaniakToken` + `tokenRefreshService._refresh_infomaniak_token` entfernt, AuthAuthority.INFOMANIAK aus den Background-Refresh-Filtern raus -- PATs sind langlebig. (c) `oauthProviderConfig.infomaniakDataScopes` + `infomaniakDataScopesForRefresh` entfernt. (d) `routeDataConnections.connect_service` antwortet bei Infomaniak jetzt mit 400 + Hinweis auf den PAT-Endpoint. (e) Env-Cleanup: `Service_INFOMANIAK_DATA_CLIENT_ID/SECRET` und `Service_INFOMANIAK_OAUTH_REDIRECT_URI` aus `.env` + `env_dev/int/prod[_forgejo].env` raus. (f) Frontend: `useConnections.createInfomaniakConnectionAndAuth` (OAuth-Popup) ersetzt durch `createInfomaniakConnection` + `submitInfomaniakToken`; `ConnectionsPage` zeigt PAT-Modal mit Schritt-Anleitung + Deeplink zu `manager.infomaniak.com/v3/ng/accounts/token/list`; Cancel rollt PENDING-Connection per DELETE zurueck. (g) Doku: `wiki/d-guides/infomaniak-oauth-setup.md` geloescht, `wiki/d-guides/infomaniak-token-setup.md` neu. (c-work: `c-work/2-build/2026-04-infomaniak-connector.md`) + - 2026-04-28 | refactor | gateway | **Cleanup der zwei tieferliegenden Defensive-Programming-Schichten, die den Trustee-Bug (vorheriger Eintrag) ueberhaupt erst durchgelassen haben.** - **(1) DB-Connector: fail-loud statt swallow.** `connectorDbPostgre.getRecord/getRecordset/getRecordsetPaginated/getDistinctColumnValues/_loadTable/semanticSearch` haben bisher jede Exception per `except Exception → log → return []` (bzw. `None`/leeres Pagination-Resultat) verschluckt. Folge: jeder echte DB-Fehler (Postgres-Adapt, UndefinedTable, UndefinedColumn, OperationalError, etc.) wurde fuer den Caller ununterscheidbar von "0 Rows" -- darauf basierten misleading downstream Errors wie "No active accounting configuration found". Neu: typisierte Exception `DatabaseQueryError(table, message, original)` plus zentrales `_rollbackQuietly(connection)` (Postgres setzt die Connection in Error-State nach jedem fehlgeschlagenen Statement). Empty Result Sets liefern weiterhin `[]`/`None`/`{items: [], totalItems: 0, totalPages: 0}` (= Normalpfad ueber `cursor.fetchall()/fetchone()`), aber jede Exception innerhalb des Query-Pfads wird hochgereicht. Tests: `tests/unit/connectors/test_connectorDbPostgre_failLoud.py` (9 Tests). - **(2) Action-Parameter: zentrale Validierung statt impliziter Kontrakt.** Workflow-Actions haben `parameters: Dict[str, Any]` ohne Schema-Enforcement bekommen; die Aktionsimplementationen mussten ad-hoc `isinstance`-Branches haben oder mit Postgres-Errors abstuerzen (siehe Trustee-Bug). Neues Modul `gateway/modules/workflows/processing/shared/parameterValidation.py` mit `InvalidActionParameterError(ValueError)` + `validateAndCoerceParameters(actionDef, parameters)`. Zentral aufgerufen in `ActionExecutor.executeAction` -- gilt fuer alle Aufrufpfade (Agent, Workflow-Graph, REST). Logik: diff --git a/d-guides/deployment/poweron-sec.kdbx b/d-guides/deployment/poweron-sec.kdbx index 6f9f22e..0daaaed 100644 Binary files a/d-guides/deployment/poweron-sec.kdbx and b/d-guides/deployment/poweron-sec.kdbx differ diff --git a/d-guides/google-oauth-setup.md b/d-guides/google-oauth-setup.md index a17bd52..051ee5b 100644 --- a/d-guides/google-oauth-setup.md +++ b/d-guides/google-oauth-setup.md @@ -27,10 +27,32 @@ This guide explains how to set up Google OAuth 2.0 authentication for the Porta 3. If prompted, configure the OAuth consent screen first: - Choose "External" user type - Fill in the required fields (App name, User support email, Developer contact information) - - Add scopes: `https://www.googleapis.com/auth/userinfo.profile`, `https://www.googleapis.com/auth/userinfo.email` + - Add scopes (see `gateway/modules/auth/oauthProviderConfig.py` -- `googleAuthScopes` and `googleDataScopes` are the source of truth): + - **Auth app** (login only): + - `openid` + - `https://www.googleapis.com/auth/userinfo.profile` + - `https://www.googleapis.com/auth/userinfo.email` + - **Data app** (UDB connectors -- Drive, Gmail, Calendar, Contacts): + - everything from the Auth app, **plus** + - `https://www.googleapis.com/auth/gmail.readonly` + - `https://www.googleapis.com/auth/drive.readonly` + - `https://www.googleapis.com/auth/calendar.readonly` + - `https://www.googleapis.com/auth/contacts.readonly` + - Enable the matching Google APIs under "APIs & Services > Library": + Gmail API, Google Drive API, **Google Calendar API**, **People API**. - Add test users if needed - Click "Save and Continue" through all sections +> **Adding new scopes to an existing app?** When you broaden the scope list +> (e.g. when Calendar/Contacts were rolled out on top of the original Drive + +> Gmail set), every existing UserConnection must go through the +> **Reconnect** action in the Connections page (`/basedata/connections`). The +> Reconnect button posts `{"reauth": true}` to the connect endpoint, which +> drops `include_granted_scopes=true` from the authorization URL so Google +> issues a token strictly for the **current** scope set. Without reconnecting +> the connection keeps working with the **old** scopes only -- the new +> services (Calendar, Contacts) silently return 403/empty. + 4. Back to creating OAuth client ID: - Application type: "Web application" - Name: "Porta Web Client" diff --git a/d-guides/infomaniak-oauth-setup.md b/d-guides/infomaniak-oauth-setup.md deleted file mode 100644 index d195d44..0000000 --- a/d-guides/infomaniak-oauth-setup.md +++ /dev/null @@ -1,105 +0,0 @@ -# Infomaniak OAuth 2.0 Setup Guide - -## Overview - -This guide explains how to register an Infomaniak OAuth application so that PowerOn -users can connect their Infomaniak account as a data source. The connection -exposes two services in the Unified Data Bar: - -- **kDrive** -- browse and download files from any drive accessible to the user. -- **Mail** -- browse mailboxes, folders, and download messages as `.eml`. - -Infomaniak is a **data-only** authority in PowerOn. It is **not** a login -provider; users still authenticate against PowerOn via Local / Google / Microsoft. - -## Prerequisites - -- An Infomaniak account with admin rights to create API credentials. -- Access to the Infomaniak Manager (https://manager.infomaniak.com). -- The PowerOn gateway publicly reachable (or `http://localhost:8000` for development). - -## Step 1: Create an Infomaniak API Application - -1. Login to https://manager.infomaniak.com. -2. Navigate to your account icon (top right) > **API**. -3. Click **Create a new application**. -4. Fill in: - - **Name:** PowerOn (or your project name) - - **Description:** Data connector for kDrive + Mail - - **Redirect URI:** must exactly match the gateway endpoint: - - Development: `http://localhost:8000/api/infomaniak/auth/connect/callback` - - Production: `https:///api/infomaniak/auth/connect/callback` -5. Select scopes: - - `user_info` -- required to read `/1/profile` for `externalId` / `email`. - - `kdrive` -- required for the kDrive ServiceAdapter. - - `mail` -- required for the Mail ServiceAdapter. -6. Save and copy the generated **Client ID** and **Client Secret**. - -## Step 2: Configure PowerOn Gateway - -Add the following keys to your gateway environment file (e.g. `gateway/env_dev.env`): - -```env -# Infomaniak OAuth -- Data App (kDrive + Mail) -Service_INFOMANIAK_DATA_CLIENT_ID = your-client-id -Service_INFOMANIAK_DATA_CLIENT_SECRET = your-client-secret -Service_INFOMANIAK_OAUTH_REDIRECT_URI = http://localhost:8000/api/infomaniak/auth/connect/callback -``` - -For production, replace the redirect URI with your HTTPS gateway host. The URI -must be identical (byte-for-byte) to the one registered in step 1. - -Restart the gateway after editing the env file so `APP_CONFIG` is reloaded. - -## Step 3: Verify the Flow - -1. Login to PowerOn and open **Basisdaten > Verbindungen**. -2. Click **Infomaniak** in the header actions. -3. A popup opens against `https://login.infomaniak.com/authorize`. -4. Sign in with your Infomaniak account, grant the requested scopes. -5. The popup closes automatically and the new connection appears with status - `connected`. -6. Open the **Unified Data Bar > Sources** tab; you should see the Infomaniak - authority node with `kdrive` and `mail` services. - -## Token Lifecycle - -- Access tokens are short-lived (refresh handled automatically by - `tokenRefreshService._refresh_infomaniak_token`). -- Refresh tokens are stored alongside the connection (`Token.tokenRefresh`). -- If a refresh returns `invalid_grant`, the user must reconnect (popup again - via the Infomaniak button on `ConnectionsPage`). - -## Troubleshooting - -### `invalid_redirect_uri` -The URI registered on Infomaniak Manager does not match `Service_INFOMANIAK_OAUTH_REDIRECT_URI`. -They must be byte-identical (including protocol, host, port, and path). - -### `invalid_client` -Client ID or secret in the env file does not match what Infomaniak issued. -Re-copy the values from Manager > API. - -### Profile lookup returns 401 -The `user_info` scope was not granted. Re-create the application with the -correct scope set, or reconnect the user. - -### Empty kDrive listing -The user has no kDrives associated. Verify on https://kdrive.infomaniak.com -that at least one drive is accessible. - -## Security Notes - -- Never commit the client secret. Use the encrypted env mechanism described in - `wiki/d-guides/encrypt-env-secrets.md`. -- Rotate the client secret if it leaks; update `Service_INFOMANIAK_DATA_CLIENT_SECRET` - and restart the gateway. Existing tokens remain valid only until they expire. -- The connector is scoped to `DATA_CONNECTION` only. Even with valid Infomaniak - credentials, no PowerOn login session is created. - -## Reference - -- Code: `gateway/modules/connectors/providerInfomaniak/connectorInfomaniak.py` -- OAuth route: `gateway/modules/routes/routeSecurityInfomaniak.py` -- Token refresh: `gateway/modules/auth/tokenManager.py::refreshInfomaniakToken` -- Frontend hook: `frontend_nyla/src/hooks/useConnections.ts::createInfomaniakConnectionAndAuth` diff --git a/d-guides/infomaniak-token-setup.md b/d-guides/infomaniak-token-setup.md new file mode 100644 index 0000000..d30f0f7 --- /dev/null +++ b/d-guides/infomaniak-token-setup.md @@ -0,0 +1,161 @@ +# Infomaniak Personal Access Token Setup + +## Overview + +PowerOn integrates Infomaniak as a **data-only** authority for the kSuite +services in the Unified Data Bar: + +| Service | API scope | Status in PowerOn | +|---|---|---| +| _account discovery_ -- enumerates the user's account_ids | `accounts` | required for kDrive | +| **kDrive** -- browse / download files | `drive` | active | +| **Calendar** -- agendas + events (.ics download) | `workspace:calendar` | active | +| **Contacts** -- address books + contacts (.vcf download) | `workspace:contact` | active | +| **Mail** -- mailboxes, folders, `.eml` download | `workspace:mail` | blocked by Infomaniak (no PAT-friendly endpoint) | + +You should tick **all five scopes** when creating the token even if only +kDrive, Calendar and Contacts are wired up today -- this avoids a token +rotation when Mail goes live. + +The `accounts` scope is non-negotiable: a standalone or free-tier +kDrive lives on a *different* `account_id` than its kSuite counterpart, +and `/1/accounts` is the only PAT-friendly endpoint that enumerates +**all** account_ids in one call. Without it the kDrive listing will +silently come back empty even though the PAT carries the `drive` +scope, because PowerOn would only know about the kSuite `account_id` +(via PIM Calendar / Contacts) and that one returns no drives. + +**Status of the Mail adapter (2026-04-28):** Infomaniak currently does +not expose a PAT-authenticated endpoint for mailboxes/folders/messages. +Every probed route either returns `404` (`/1/mail`, `/2/mail`) or +`302`-redirects to the OAuth authorize page +(`/api/mail`, `/api/pim/mail`, `/api/pim/mailbox`, `/api/pim/folder`), +which is the OAuth-Web-Session path -- bearer PATs are rejected. The +`/api/mail/?account_id=...` route 301-redirects to an internal Cyrus +server on `http://mail.infomaniak.com:5000` that is not reachable from +the public internet. The `workspace:mail` scope is stored on the PAT so +the Mail adapter can be enabled later with no token rotation, as soon +as Infomaniak opens a public PAT-friendly endpoint. + +Infomaniak does not expose its data APIs (`/2/drive/...`, `/1/mail/...`) over +OAuth 2.0. The OAuth endpoint at `login.infomaniak.com/authorize` only issues +identity tokens (scopes `openid`, `profile`, `email`, `phone`). Bearer tokens +that work against the data APIs must be issued manually by the user as a +**Personal Access Token (PAT)** in the Infomaniak Manager. + +This is the same model used by GitHub Personal Access Tokens, Notion +Integration Tokens, and many other vendors. PowerOn never sees the user's +Infomaniak password. + +## Prerequisites + +- An Infomaniak account that has access to at least one kDrive and one mailbox. +- The PowerOn frontend reachable in the browser; the backend reachable from + the frontend. + +## Step 1: Create the token in the Infomaniak Manager + +1. Open the API-Tokens page directly: + +2. Click **Token erstellen** (Create token). +3. Fill in the form: + - **Token name**: anything memorable, for example `PowerOn` or + `PowerOn DEV`. + - **Application**: leave it on `Default application`. The "PowerOn" + application registration is **not** used for PATs. + - **Scopes** (search box): add **all five** of the following, one by one: + + | Search term | Pick this entry | + |---|---| + | `accounts` | `accounts - Manage your accounts` | + | `drive` | `drive - Drive products` | + | `workspace:mail` | `workspace:mail - Manage your emails` | + | `workspace:calendar` | `workspace:calendar - Manage your calendars` | + | `workspace:contact` | `workspace:contact - Manage your contacts` | + + Do **not** tick `All` -- it grants every Infomaniak API (Hosting, + Billing, AI, ...). Do **not** add `user_info` -- PowerOn does not call + `/1/profile`. + - **Validity**: any value works. PowerOn does not auto-refresh PATs. +4. Click **Erstellen** (Create) and **copy the token immediately**. Infomaniak + shows the token value only once. + +## Step 2: Paste the token into PowerOn + +1. Sign in to PowerOn and open **Basisdaten -> Verbindungen**. +2. Click **Infomaniak**. PowerOn creates a pending connection and opens a + modal that asks for the Personal Access Token. +3. Paste the token from Step 1 and click **Verbinden**. + +PowerOn validates the token in three deterministic steps before +persisting anything -- each step probes exactly one scope: + +1. `GET https://api.infomaniak.com/1/accounts` (requires scope + `accounts`) -- enumerates **all** Infomaniak account_ids the PAT + can reach. Without this scope kDrive cannot find the owning + account; the submit fails with HTTP 400 and a message that names + the missing scope. +2. `GET https://calendar.infomaniak.com/api/pim/calendar` (requires + scope `workspace:calendar`; `workspace:contact` works as the + equivalent fallback) -- yields the user's display name and kSuite + account_id for the connection label in the UI. +3. `GET https://api.infomaniak.com/2/drive?account_id=` + (requires scope `drive`) -- a clean 200 confirms the `drive` + scope is on the PAT. + +On success the connection turns active and the token is stored +encrypted in the backend; on failure the modal shows which scope is +missing. The mail scope is stored as part of `grantedScopes` without +a probe -- it is only consumed once the Mail adapter lands. + +## Step 3: Verify in the Unified Data Bar + +Open the **Sources** tab in the Unified Data Bar. The connection appears with +the Infomaniak label and exposes (today) three child nodes: + +- **kDrive** -- expand to see drives, then folders and files. +- **Calendar** -- expand to see calendars, then events; downloads as `.ics`. +- **Contacts** -- expand to see address books, then contacts; downloads as `.vcf`. + +Mail will appear as an additional child node once a PAT-friendly endpoint +is identified; no token rotation needed because the scope is already on +the PAT. + +## Rotating or revoking the token + +- To rotate, repeat Step 1 with a new token, then in PowerOn delete the + existing Infomaniak connection and create a new one with the fresh token. +- To revoke, delete the token in the Infomaniak Manager. The PowerOn + connection will start to fail at the next call; delete it from the + Verbindungen page to remove the stored bearer. + +## How the kDrive adapter knows your account + +`/2/drive` requires an integer `account_id` query arg. A user can own +kDrives in several Infomaniak accounts -- typically a kSuite account +plus a standalone (or free-tier) kDrive that lives on its **own** +account_id. The kSuite account_id from PIM Calendar / Contacts only +covers the kSuite case, which is why naive PAT integrations show an +empty kDrive even though there are files. + +The `KdriveAdapter` therefore calls `GET /1/accounts` (the only +PAT-friendly endpoint that lists every account_id of a token) and +unions the `/2/drive?account_id=` listing across all returned +account_ids. The result is cached on the adapter instance for the +lifetime of the request, so each browse touches `/1/accounts` at most +once. + +The submit endpoint runs the same enumeration as a pre-flight check +before persisting the token; if `/1/accounts` rejects the PAT for +missing the `accounts` scope, the submit fails with a clear 400 +instead of producing a half-broken connection. + +## Security notes + +- PATs are stored exactly like OAuth access tokens for Google or Microsoft: + encrypted at rest in the gateway database, only ever sent over TLS to + `api.infomaniak.com`, and never returned to the frontend after submission. +- The PowerOn backend does not need any Infomaniak client ID or client + secret -- there is no `Service_INFOMANIAK_*` configuration. +- Each user manages their own tokens. There is no global "PowerOn Infomaniak + app" the operator has to register or maintain.