wiki/c-work/4-done/2026-04-infomaniak-connector.md
ValueOn AG 6eeeb962f6 upd
2026-04-29 20:23:03 +02:00

21 KiB

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 Daten aus den Infomaniak-Services bereitstellt. Heute aktiv:

  • 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.

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

  • API-Pfad-Konvention (Adapter-Path != API-Path):
    • kDrive (api.infomaniak.com): Adapter-Path /{driveId}/{fileId}, API /2/drive/{driveId}/files/{fileId}. Wichtig: das naheliegende /2/drive?account_id=...-Listing ist filtered auf Drive-Manager-Admins (account_admin: true) und liefert fuer normale kSuite-Member (role: 'user') sauber 200 [], obwohl sie das Drive lesen koennen. Der korrekte User-zentrische Endpoint ist GET /2/drive/init?with=drives, der ALLE Drives des PAT-Owners zurueckgibt -- inklusive id, name, account_id, role. Der Adapter cached die Liste auf der Instanz (_ensureDrives); ein Browse zahlt also pro Request maximal einen init-Call. /2/drive/init?with=drives braucht NUR den drive- Scope, kein accounts oder user_info.
    • 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)).
    • listAccessibleDrives(token) -> Liste aller Drives, die der PAT-Owner sehen kann (egal mit welcher Rolle). Ein einzelner GET /2/drive/init?with=drives-Call, vom KdriveAdapter benutzt statt des admin-only /2/drive?account_id=... Listings. 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 /2/drive/init?with=drives.
  • Antwort-Wrapping: Erfolgreiche Responses sind als {result: 'success', data: ...} gewrappt -- _unwrapData() normalisiert.
  • Token-Validation im Submit-Endpoint: zwei harte 200-Schritte, jeder probet genau einen Scope:
    1. listAccessibleDrives -> probet drive-Scope und stellt sicher, dass mindestens ein erreichbarer kDrive existiert.
    2. resolveOwnerIdentity -> probet workspace:calendar / workspace:contact-Scope (mindestens einer noetig). Jeder Schritt liefert eine eigene 400-Message, die exakt den fehlenden Scope nennt.
  • 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" 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.
  • 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 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 (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
2026-04-26 Nur DATA_CONNECTION, kein Login User explicitly: "wir benoetigen nur den userconnection auth"
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-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 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

  • AuthAuthority-Enum erweitert
  • InfomaniakConnector + KdriveAdapter (discovert Drives zur Laufzeit ueber listAccessibleDrives() -> /2/drive/init?with=drives, cached die Liste auf der Adapter-Instanz) + CalendarAdapter + ContactAdapter
  • resolveOwnerIdentity() als reiner UI-Identity-Helper (Display-Name + kSuite-account_id fuer das Connection-Label)
  • listAccessibleDrives() als Drive-Discovery-Helper (/2/drive/init?with=drives, drive-Scope-Probe, raised InfomaniakIdentityError mit klarer Scope-Message)
  • ConnectorResolver-Registry (alle Adapter werden uniform mit accessToken konstruiert; keine Sonderbehandlung in getServiceAdapter)
  • Setup-Guide (PAT-basiert, Calendar + Contacts als zusaetzliche aktive Services)
  • PAT-Submit-Endpoint POST /api/infomaniak/connections/{id}/token (Pre-Flight in 2 Schritten: listAccessibleDrives (drive- Scope), resolveOwnerIdentity (workspace:calendar / workspace:contact) -- jeder Schritt mit eigener 400-Message)
  • OAuth-Routen entfernt
  • Infomaniak-Refresh aus tokenManager + tokenRefreshService entfernt
  • Infomaniak-Scopes aus oauthProviderConfig entfernt
  • Service_INFOMANIAK_* aus allen .env-Dateien entfernt
  • DataConnections-Dispatch (Infomaniak-Branch in connect_service -> 400)
  • FE-Hook umgebaut (createInfomaniakConnection + submitInfomaniakToken)
  • FE-OAuth-Popup-Branch fuer Infomaniak entfernt
  • ConnectionsPage-PAT-Modal
  • UDB-Integration (Authority-Icon, Service-Icons, Source-Colors)
  • ClickUp-UDB-Fix
  • Manueller End-to-End-Test (Token im Manager erstellen, in Modal pasten, kDrive browsen, Calendar browsen, Contacts browsen, Datei + .ics + .vcf downloaden)
  • 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 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=kSuiteAccountId, externalUsername=Owner-Anzeigename aus PIM 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
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
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-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
  • PR: tbd
  • Setup-Guide: wiki/d-guides/infomaniak-token-setup.md

Abschluss

  • b-reference/ aktualisiert
  • TOPICS.md aktualisiert
  • Dokument nach z-archive/ verschoben