fixes infomaniac different than in doc

This commit is contained in:
ValueOn AG 2026-04-29 00:57:26 +02:00
parent 53f28d47a1
commit 67aa9d2113
3 changed files with 64 additions and 76 deletions

View file

@ -3,7 +3,7 @@
<!-- pivoted: 2026-04-28 (OAuth -> Personal Access Token) --> <!-- pivoted: 2026-04-28 (OAuth -> Personal Access Token) -->
<!-- pivoted: 2026-04-28 (Mail-Endpoint nicht PAT-faehig -> Calendar als zweiter aktiver Service) --> <!-- pivoted: 2026-04-28 (Mail-Endpoint nicht PAT-faehig -> Calendar als zweiter aktiver Service) -->
<!-- pivoted: 2026-04-28 (Contacts-PIM-Endpoint funktioniert -> Contacts als dritter aktiver Service) --> <!-- pivoted: 2026-04-28 (Contacts-PIM-Endpoint funktioniert -> Contacts als dritter aktiver Service) -->
<!-- pivoted: 2026-04-29 (kDrive findet Standalone/Free-tier-Drives nicht ueber kSuite-account_id -> `accounts`-Scope + `/1/accounts`-Resolver) --> <!-- pivoted: 2026-04-29 (kDrive-Listing fuer non-admin-User leer -> `/2/drive/init?with=drives` als Discovery) -->
<!-- component: gateway, frontend-nyla --> <!-- component: gateway, frontend-nyla -->
# Infomaniak Connector (kDrive + Calendar + Contacts today; Mail reserved) + UDB-Integration # Infomaniak Connector (kDrive + Calendar + Contacts today; Mail reserved) + UDB-Integration
@ -104,17 +104,17 @@ aussortiert wurde:
- **API-Pfad-Konvention** (Adapter-Path != API-Path): - **API-Pfad-Konvention** (Adapter-Path != API-Path):
- kDrive (`api.infomaniak.com`): Adapter-Path `/{driveId}/{fileId}`, - kDrive (`api.infomaniak.com`): Adapter-Path `/{driveId}/{fileId}`,
API `/2/drive/{driveId}/files/{fileId}`. `account_id` muss bei jedem API `/2/drive/{driveId}/files/{fileId}`. **Wichtig**: das
Listing-Call als Query-Arg mit. **Wichtig**: Ein User kann kDrives naheliegende `/2/drive?account_id=...`-Listing ist filtered auf
in mehreren Infomaniak-Accounts besitzen (typischerweise einer in Drive-Manager-Admins (`account_admin: true`) und liefert fuer
der kSuite + ein Standalone- oder Free-tier-kDrive auf einem normale kSuite-Member (`role: 'user'`) sauber `200 []`, obwohl
**anderen** account_id). Der Adapter resolvt deshalb on demand sie das Drive lesen koennen. Der korrekte User-zentrische
ueber `resolveAccessibleAccountIds()` (-> `GET /1/accounts`) **alle** Endpoint ist `GET /2/drive/init?with=drives`, der ALLE Drives
erreichbaren account_ids, ruft `/2/drive?account_id=X` fuer jede des PAT-Owners zurueckgibt -- inklusive `id`, `name`, `account_id`,
auf und unioniert die Drive-Listings mit Dedup ueber `driveId`. `role`. Der Adapter cached die Liste auf der Instanz
Cache auf der Adapter-Instanz (`_ensureAccountIds`). `/1/accounts` (`_ensureDrives`); ein Browse zahlt also pro Request maximal einen
erfordert den PAT-Scope `accounts`; ohne ihn schlaegt schon der `init`-Call. `/2/drive/init?with=drives` braucht NUR den `drive`-
Submit-Pre-Flight fehl. Scope, kein `accounts` oder `user_info`.
- Calendar (`calendar.infomaniak.com/api/pim`): Adapter-Path - Calendar (`calendar.infomaniak.com/api/pim`): Adapter-Path
`/{calendarId}/{eventId}`. **Achtung**: die naheliegenden `/{calendarId}/{eventId}`. **Achtung**: die naheliegenden
Nested-Routes (`/calendar/{id}/event`, `/calendar/{id}/event/{id}`, Nested-Routes (`/calendar/{id}/event`, `/calendar/{id}/event/{id}`,
@ -148,28 +148,27 @@ aussortiert wurde:
rein fuer das UI-Label der Connection. Erste Quelle PIM Calendar, rein fuer das UI-Label der Connection. Erste Quelle PIM Calendar,
Fallback PIM Contacts; nimmt den ersten Owner-Record (`user_id > 0` Fallback PIM Contacts; nimmt den ersten Owner-Record (`user_id > 0`
und `isinstance(account_id, int)`). und `isinstance(account_id, int)`).
- `resolveAccessibleAccountIds(token)` -> Liste **aller** account_ids, - `listAccessibleDrives(token)` -> Liste **aller** Drives, die der
die der PAT erreicht. Ein einzelner `GET /1/accounts`-Call. Vom PAT-Owner sehen kann (egal mit welcher Rolle). Ein einzelner
`KdriveAdapter` benutzt, weil ein Standalone-/Free-tier-kDrive auf `GET /2/drive/init?with=drives`-Call, vom `KdriveAdapter` benutzt
einem **anderen** account_id liegt als die kSuite und die statt des admin-only `/2/drive?account_id=...` Listings.
kSuite-`account_id` aus PIM den Drive nicht abdeckt.
Beide raisen `InfomaniakIdentityError` mit einer scope-spezifischen Beide raisen `InfomaniakIdentityError` mit einer scope-spezifischen
Fehlermeldung, sodass der Submit-Endpoint pro fehlendem Scope eine Fehlermeldung, sodass der Submit-Endpoint pro fehlendem Scope eine
eigene 400-Message zurueckgeben kann. eigene 400-Message zurueckgeben kann.
- **`externalId` ist UI-State, kein Adapter-Vertragspartner**: Submit - **`externalId` ist UI-State, kein Adapter-Vertragspartner**: Submit
speichert `externalId = str(kSuiteAccountId)` fuer die ConnectionsPage speichert `externalId = str(kSuiteAccountId)` fuer die ConnectionsPage
(Anzeige + Konsistenz mit anderen Providern), aber der KdriveAdapter (Anzeige + Konsistenz mit anderen Providern), aber der KdriveAdapter
liest ihn nie -- er fragt zur Laufzeit `/1/accounts`. liest ihn nie -- er fragt zur Laufzeit `/2/drive/init?with=drives`.
- **Antwort-Wrapping**: Erfolgreiche Responses sind als - **Antwort-Wrapping**: Erfolgreiche Responses sind als
`{result: 'success', data: ...}` gewrappt -- `_unwrapData()` normalisiert. `{result: 'success', data: ...}` gewrappt -- `_unwrapData()` normalisiert.
- **Token-Validation** im Submit-Endpoint: drei harte 200-Schritte, - **Token-Validation** im Submit-Endpoint: zwei harte 200-Schritte,
jeder probet genau einen Scope: jeder probet genau einen Scope:
1. `resolveAccessibleAccountIds` -> probet `accounts`-Scope. 1. `listAccessibleDrives` -> probet `drive`-Scope und stellt sicher,
dass mindestens ein erreichbarer kDrive existiert.
2. `resolveOwnerIdentity` -> probet `workspace:calendar` / 2. `resolveOwnerIdentity` -> probet `workspace:calendar` /
`workspace:contact`-Scope (mindestens einer noetig). `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 Jeder Schritt liefert eine eigene 400-Message, die exakt den fehlenden
Scope nennt. 401/403 -> 400 mit Scope-Hinweis; alles Unerwartete -> 502. Scope nennt.
- **Token-Persistenz**: `expiresAt = now + 10*365*24*3600`, `tokenRefresh = None`; - **Token-Persistenz**: `expiresAt = now + 10*365*24*3600`, `tokenRefresh = None`;
`getTokenStatusForConnection` zeigt damit `active`, kein "none". `getTokenStatusForConnection` zeigt damit `active`, kein "none".
@ -225,6 +224,7 @@ aussortiert wurde:
| 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-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 | 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-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-29 | `KdriveAdapter` discoverte Drives ueber `/2/drive/init?with=drives` statt `/2/drive?account_id=X` | Live-Beweis vom User: Drive 2980592 existiert in account 1696919 mit Files (PDF + 2 Folders), `/2/drive?account_id=1696919` antwortet trotzdem `200 []`, `/2/drive/2980592/files` zeigt alle Files. Diagnose: `/2/drive` ist Drive-Manager-Admin-Sicht (filtered auf `account_admin: true`), normaler kSuite-Member (`role: 'user'`) sieht dort nichts -- ist nicht Vendor-Bug, sondern Spec. Loesung gefunden via Endpoint-Probing: `/2/drive/init?with=drives` ist die User-zentrische Drive-Liste, die ALLE zugaenglichen Drives unabhaengig von der Admin-Rolle liefert (verifiziert mit PAT der `accounts`-Scope explizit nicht hat -> 200, alle Drives drin). Damit wird der zwischenzeitlich eingefuehrte `accounts`-Scope-Workaround wieder entfernt -- er war eine Sackgasse, weil `/1/accounts` die Manager-Organizations listet (1 pro User in den meisten Faellen), nicht die Drive-Accounts. Der `init`-Endpoint braucht nur den `drive`-Scope, der bereits im PAT-Standard-Setup ist; keine Token-Rotation noetig. |
| 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 | 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 | 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 | `expiresAt = now + 10y` fuer PATs | Analog ClickUp, sonst markiert `getTokenStatusForConnection` die Connection als "none" |
@ -233,19 +233,23 @@ aussortiert wurde:
## Umsetzungs-Checkliste ## Umsetzungs-Checkliste
- [x] AuthAuthority-Enum erweitert - [x] AuthAuthority-Enum erweitert
- [x] InfomaniakConnector + KdriveAdapter (resolvt `account_id` zur - [x] InfomaniakConnector + KdriveAdapter (discovert Drives zur
Laufzeit ueber `resolveOwnerIdentity()`) + CalendarAdapter Laufzeit ueber `listAccessibleDrives()` -> `/2/drive/init?with=drives`,
cached die Liste auf der Adapter-Instanz) + CalendarAdapter
+ ContactAdapter + ContactAdapter
- [x] `resolveOwnerIdentity()` als gemeinsamer Identity-Helper im - [x] `resolveOwnerIdentity()` als reiner UI-Identity-Helper
Connector-Modul (Calendar -> Contacts Fallback, raised (Display-Name + kSuite-account_id fuer das Connection-Label)
`InfomaniakIdentityError` bei totalem Fehlschlag) - [x] `listAccessibleDrives()` als Drive-Discovery-Helper
(`/2/drive/init?with=drives`, `drive`-Scope-Probe, raised
`InfomaniakIdentityError` mit klarer Scope-Message)
- [x] ConnectorResolver-Registry (alle Adapter werden uniform mit - [x] ConnectorResolver-Registry (alle Adapter werden uniform mit
`accessToken` konstruiert; keine Sonderbehandlung in `accessToken` konstruiert; keine Sonderbehandlung in
`getServiceAdapter`) `getServiceAdapter`)
- [x] Setup-Guide (PAT-basiert, Calendar + Contacts als zusaetzliche aktive Services) - [x] Setup-Guide (PAT-basiert, Calendar + Contacts als zusaetzliche aktive Services)
- [x] PAT-Submit-Endpoint `POST /api/infomaniak/connections/{id}/token` - [x] PAT-Submit-Endpoint `POST /api/infomaniak/connections/{id}/token`
(Pre-Flight: `resolveOwnerIdentity` + Drive-Probe mit resolvter (Pre-Flight in 2 Schritten: `listAccessibleDrives` (`drive`-
`account_id` -- harter 200-Pfad, kein 422-Tolerance-Hack mehr) Scope), `resolveOwnerIdentity` (`workspace:calendar` /
`workspace:contact`) -- jeder Schritt mit eigener 400-Message)
- [x] OAuth-Routen entfernt - [x] OAuth-Routen entfernt
- [x] Infomaniak-Refresh aus tokenManager + tokenRefreshService entfernt - [x] Infomaniak-Refresh aus tokenManager + tokenRefreshService entfernt
- [x] Infomaniak-Scopes aus `oauthProviderConfig` entfernt - [x] Infomaniak-Scopes aus `oauthProviderConfig` entfernt
@ -269,10 +273,10 @@ aussortiert wurde:
| # | Kriterium (Given-When-Then) | Prio | | # | Kriterium (Given-When-Then) | Prio |
|---|-----------------------------|------| |---|-----------------------------|------|
| 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 | | 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 | | 2 | Given gueltiger PAT mit Scopes `drive`+(`workspace:calendar` ODER `workspace:contact`) im Modal, When Submit, Then ist die UserConnection `ACTIVE`, Token gespeichert, `externalId=kSuiteAccountId`, `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 | | 3 | Given ungueltiger PAT oder fehlender Scope (`drive` ODER weder `workspace:calendar` noch `workspace:contact`), When Submit, Then bleibt Connection PENDING, Modal zeigt 400-Detail das den fehlenden Scope namentlich nennt | must |
| 4 | Given aktive Connection, When im UDB die Authority expandiert wird, Then werden Services `kdrive`, `calendar` und `contact` mit Icons angezeigt | 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 | | 4b | Given aktive Connection, When im UDB `kdrive` expandiert wird, Then erscheinen alle Drives die der User sehen kann -- auch wenn er dort nur `role: 'user'` hat (Adapter discovert ueber `/2/drive/init?with=drives`, nicht ueber das admin-only `/2/drive?account_id=...` Listing) | must |
| 4c | Given aktive Connection, When im UDB `calendar` expandiert wird, Then erscheinen Kalender, dann Events; Event-Download liefert `.ics` | 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 | | 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 | | 5 | Given Modal offen, When User auf Cancel/X klickt, Then wird die soeben erstellte PENDING-Connection wieder geloescht | should |

View file

@ -14,7 +14,7 @@ Skip: reine Refactors, Formatting, Lint, Dep-Bumps, Test-only, Wiki-Tippfehler.
## 2026-04-29 ## 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=<kSuite-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, frontend-nyla, wiki | **Infomaniak-Connector: kDrive findet jetzt auch Drives, in denen der User nur `role: user` ist (statt `account_admin`).** Live-Beweis vom User mit Files in `https://ksuite.infomaniak.com/1696919/kdrive/app/drive/2980592/files/11`: das Drive haengt korrekt an account_id 1696919, aber `/2/drive?account_id=1696919` antwortet trotzdem `200 [] -- empty`, weil dieser Endpoint zur Drive-Manager-Admin-Sicht gehoert (filtered auf `account_admin: true`) und nicht zur Endbenutzer-Sicht. Direkt-Aufrufe wie `/2/drive/2980592/files` funktionieren fuer denselben User mit `role: user` einwandfrei (PDF "Start_with_kDrive.pdf", Ordner "Nyla-Analysen" und "Onboarding" alle sichtbar). Root-Endpoint gefunden: `GET /2/drive/init?with=drives` -- enumeriert ALLE Drives die der User sehen kann, unabhaengig von der Admin-Rolle, braucht NUR den `drive`-Scope (verifiziert mit PAT der `accounts`-Scope explizit nicht hat). Implementierung: (a) Neuer Helper `listAccessibleDrives(token) -> List[dict]` im `connectorInfomaniak.py` -- ein einzelner `/2/drive/init?with=drives`-Call, raised `InfomaniakIdentityError` mit Scope-Message wenn `drive`-Scope fehlt. (b) `KdriveAdapter` haelt jetzt `_drives: Optional[List[Dict]]` (statt `_accountId`/`_accountIds`); `_listDrives()` mappt direkt aus dem gecachten Init-Response. (c) `submit_infomaniak_token` validiert in zwei Schritten: `listAccessibleDrives` (`drive`-Scope) und `resolveOwnerIdentity` (`workspace:calendar`/`workspace:contact`-Scope). (d) Der zwischenzeitliche `resolveAccessibleAccountIds()` + `accounts`-Scope-Workaround wieder ENTFERNT (war eine Sackgasse: `/1/accounts` listet Manager-Organizations, nicht Drive-Accounts). (e) `_probeDriveScope()`-httpx-Helper im Submit-Route entfernt; `httpx`-Import + `INFOMANIAK_API_BASE`-Konstante raus. Frontend: PAT-Setup-Modal wieder auf 4 Pflicht-Scopes zurueck (`accounts` raus). Doku: Status-Tabelle, Validation-Section und "How the kDrive adapter discovers your drives" komplett auf den `init`-Endpoint umgeschrieben. **Bestehende Connections sollten ohne User-Aktion sofort funktionieren -- es ist kein neuer Scope und keine Token-Rotation noetig, der Adapter wechselt nur den Discovery-Endpoint.** (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-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`)

View file

@ -7,24 +7,15 @@ services in the Unified Data Bar:
| Service | API scope | Status in PowerOn | | Service | API scope | Status in PowerOn |
|---|---|---| |---|---|---|
| _account discovery_ -- enumerates the user's account_ids | `accounts` | required for kDrive |
| **kDrive** -- browse / download files | `drive` | active | | **kDrive** -- browse / download files | `drive` | active |
| **Calendar** -- agendas + events (.ics download) | `workspace:calendar` | active | | **Calendar** -- agendas + events (.ics download) | `workspace:calendar` | active |
| **Contacts** -- address books + contacts (.vcf download) | `workspace:contact` | active | | **Contacts** -- address books + contacts (.vcf download) | `workspace:contact` | active |
| **Mail** -- mailboxes, folders, `.eml` download | `workspace:mail` | blocked by Infomaniak (no PAT-friendly endpoint) | | **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 You should tick **all four scopes** when creating the token even if only
kDrive, Calendar and Contacts are wired up today -- this avoids a token kDrive, Calendar and Contacts are wired up today -- this avoids a token
rotation when Mail goes live. 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 **Status of the Mail adapter (2026-04-28):** Infomaniak currently does
not expose a PAT-authenticated endpoint for mailboxes/folders/messages. not expose a PAT-authenticated endpoint for mailboxes/folders/messages.
Every probed route either returns `404` (`/1/mail`, `/2/mail`) or Every probed route either returns `404` (`/1/mail`, `/2/mail`) or
@ -63,19 +54,18 @@ Infomaniak password.
`PowerOn DEV`. `PowerOn DEV`.
- **Application**: leave it on `Default application`. The "PowerOn" - **Application**: leave it on `Default application`. The "PowerOn"
application registration is **not** used for PATs. application registration is **not** used for PATs.
- **Scopes** (search box): add **all five** of the following, one by one: - **Scopes** (search box): add **all four** of the following, one by one:
| Search term | Pick this entry | | Search term | Pick this entry |
|---|---| |---|---|
| `accounts` | `accounts - Manage your accounts` |
| `drive` | `drive - Drive products` | | `drive` | `drive - Drive products` |
| `workspace:mail` | `workspace:mail - Manage your emails` | | `workspace:mail` | `workspace:mail - Manage your emails` |
| `workspace:calendar` | `workspace:calendar - Manage your calendars` | | `workspace:calendar` | `workspace:calendar - Manage your calendars` |
| `workspace:contact` | `workspace:contact - Manage your contacts` | | `workspace:contact` | `workspace:contact - Manage your contacts` |
Do **not** tick `All` -- it grants every Infomaniak API (Hosting, Do **not** tick `All` -- it grants every Infomaniak API (Hosting,
Billing, AI, ...). Do **not** add `user_info` -- PowerOn does not call Billing, AI, ...). Do **not** add `user_info` or `accounts` --
`/1/profile`. PowerOn does not need the Manager-level scopes.
- **Validity**: any value works. PowerOn does not auto-refresh PATs. - **Validity**: any value works. PowerOn does not auto-refresh PATs.
4. Click **Erstellen** (Create) and **copy the token immediately**. Infomaniak 4. Click **Erstellen** (Create) and **copy the token immediately**. Infomaniak
shows the token value only once. shows the token value only once.
@ -87,21 +77,20 @@ Infomaniak password.
modal that asks for the Personal Access Token. modal that asks for the Personal Access Token.
3. Paste the token from Step 1 and click **Verbinden**. 3. Paste the token from Step 1 and click **Verbinden**.
PowerOn validates the token in three deterministic steps before PowerOn validates the token in two deterministic steps before
persisting anything -- each step probes exactly one scope: persisting anything:
1. `GET https://api.infomaniak.com/1/accounts` (requires scope 1. `GET https://api.infomaniak.com/2/drive/init?with=drives`
`accounts`) -- enumerates **all** Infomaniak account_ids the PAT (requires scope `drive`) -- enumerates every kDrive the PAT can
can reach. Without this scope kDrive cannot find the owning reach, including drives where the user only has `role: 'user'`.
account; the submit fails with HTTP 400 and a message that names The "official" listing endpoint (`/2/drive?account_id=...`) is
the missing scope. filtered to drives where the caller is **Drive-Manager admin** and
would silently return an empty array for a regular kSuite member,
so we deliberately avoid it.
2. `GET https://calendar.infomaniak.com/api/pim/calendar` (requires 2. `GET https://calendar.infomaniak.com/api/pim/calendar` (requires
scope `workspace:calendar`; `workspace:contact` works as the scope `workspace:calendar`; `workspace:contact` works as the
equivalent fallback) -- yields the user's display name and kSuite equivalent fallback) -- yields the user's display name and kSuite
account_id for the connection label in the UI. account_id for the connection label in the UI.
3. `GET https://api.infomaniak.com/2/drive?account_id=<first>`
(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 On success the connection turns active and the token is stored
encrypted in the backend; on failure the modal shows which scope is encrypted in the backend; on failure the modal shows which scope is
@ -129,26 +118,21 @@ the PAT.
connection will start to fail at the next call; delete it from the connection will start to fail at the next call; delete it from the
Verbindungen page to remove the stored bearer. Verbindungen page to remove the stored bearer.
## How the kDrive adapter knows your account ## How the kDrive adapter discovers your drives
`/2/drive` requires an integer `account_id` query arg. A user can own The official `/2/drive?account_id=<id>` listing is **filtered to
kDrives in several Infomaniak accounts -- typically a kSuite account Drive-Manager admins** -- a regular kSuite member with `role: 'user'`
plus a standalone (or free-tier) kDrive that lives on its **own** sees an empty array even though they can read every file in the
account_id. The kSuite account_id from PIM Calendar / Contacts only drive. PowerOn therefore uses `/2/drive/init?with=drives`, which
covers the kSuite case, which is why naive PAT integrations show an returns every drive the PAT can reach regardless of admin role and
empty kDrive even though there are files. includes the drive's `id`, `name`, `account_id` and the caller's
`role` in one payload.
The `KdriveAdapter` therefore calls `GET /1/accounts` (the only The `KdriveAdapter` calls this endpoint once per request and caches
PAT-friendly endpoint that lists every account_id of a token) and the resulting drive list on the adapter instance. The submit endpoint
unions the `/2/drive?account_id=<X>` listing across all returned runs the same call as a pre-flight scope check before persisting the
account_ids. The result is cached on the adapter instance for the token; if the PAT does not carry the `drive` scope, the submit fails
lifetime of the request, so each browse touches `/1/accounts` at most with a clear 400 instead of producing a half-broken connection.
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 ## Security notes