feat: connecteurs Google + Microsoft 365 (OAuth2 PKCE) — phase 6/8 (v7.56.0)
- app/services/oauth_connectors.py : flow OAuth2 complet PKCE (S256) pour
2 fournisseurs décrits par 1 dict — Google (Drive/Gmail/Calendar en lecture
seule) et Microsoft 365 (Graph Files.Read / Mail.Read / Calendars.Read) ;
begin() = URL d'autorisation + state + code_verifier, complete() = échange du
code, access_token() = refresh automatique (60 s de marge, refresh_token
conservé si absent de la réponse), api_get() = path absolu refusé + validation
SSRF + borne 20 000 car.
- Tokens chiffrés Fernet en réutilisant calendar_sync._encrypt_tokens (zéro
dépendance) dans la table connector_tokens (migration 33, PK (kind, user_id)).
- 4 routes /api/agent/connectors/oauth/{kind}/… : status, authorize (cookies
d'état HttpOnly 10 min, retour same-origin validé), callback (GET safe, state
comparé en temps constant, tokens stockés puis cookies purgés, redirection
?oauth=connected / ?oauth_error=), disconnect. OpenAPI 523 chemins.
- Config + .env.example : GOOGLE_CLIENT_ID/SECRET, MS_CLIENT_ID/SECRET (vidés =
« non configuré »), redirect URI dérivé d'APP_BASE_URL.
- Catalogue : google/ms365 en natifs avec badge connecté/non connecté ;
connector_fetch et Tester passent par l'API du fournisseur avec le token de
l'utilisateur (user_id transmis par l'outil LLM).
- Menu + : « Se connecter » / « Déconnecter » sur la fiche, toast au retour du
flux (URL nettoyée par history.replaceState). État dans la fiche du menu
plutôt qu'une page dédiée.
- Tests : tests/test_v756_oauth_connectors.py (13), 0 appel réseau réel
(_post_form / _api_get monkeypatchés) — state forgé refusé sans échange,
tokens chiffrés en base, refresh, URL absolue refusée, 401/404, câblage menu ;
test_v755 adapté (5 natifs). Suite complète 1319 verts (-n auto), ruff 0,
eslint 0 erreur (19 warnings préexistants hors fichiers touchés).
This commit is contained in:
@@ -1,5 +1,50 @@
|
||||
# Changelog - FlowDeck
|
||||
|
||||
## v7.56.0 (2026-10-06) — Connecteurs : Google + Microsoft 365 (phase 6/8)
|
||||
|
||||
### Added
|
||||
|
||||
- **`app/services/oauth_connectors.py`** — flow OAuth2 complet **PKCE (S256)** pour
|
||||
2 fournisseurs décrits par 1 dict :
|
||||
- **Google** : Drive / Gmail / Calendar **en lecture seule** ;
|
||||
- **Microsoft 365** : Graph `Files.Read` / `Mail.Read` / `Calendars.Read` ;
|
||||
- `begin()` → URL d'autorisation + state + code_verifier ; `complete()` →
|
||||
échange du code ; `access_token()` → **refresh automatique** (60 s avant
|
||||
expiration, `refresh_token` conservé si le fournisseur n'en renvoie pas) ;
|
||||
`api_get()` → lecture bornée, `path` absolu refusé + validation SSRF ;
|
||||
- tokens chiffrés **Fernet** via `_encrypt_tokens` de `calendar_sync`
|
||||
(réutilisation, aucune nouvelle dépendance) dans la table
|
||||
**`connector_tokens`** (migration **33**, PK `(kind, user_id)`).
|
||||
- **4 routes OAuth** (`/api/agent/connectors/oauth/{kind}/…`) : `status`,
|
||||
`authorize` (cookies d'état HttpOnly 10 min + retour same-origin validé),
|
||||
`callback` (GET = SAFE_METHODS, `state` comparé en temps constant, tokens
|
||||
stockés puis cookies purgés, redirection `?oauth=connected` /
|
||||
`?oauth_error=…`), `disconnect`. **OpenAPI : 523 chemins**.
|
||||
- **Config + `.env.example`** : `GOOGLE_CLIENT_ID/SECRET`, `MS_CLIENT_ID/SECRET`
|
||||
(vidés = « non configuré »), redirect URI dérivé d'APP_BASE_URL.
|
||||
- **Catalogue** : les 2 nouveaux natifs apparaissent avec le badge
|
||||
« non connecté — lancer la connexion OAuth » / « connecté · N scope(s) » ;
|
||||
`connector_fetch` et `Tester` passent par l'API Graph/Google avec le token de
|
||||
l'utilisateur (outil LLM = `user_id` désormais transmis).
|
||||
- **Menu +** : « Se connecter » (redirige vers le fournisseur) / « Déconnecter »
|
||||
sur la fiche du connecteur, + toast au retour du flux (URL nettoyée via
|
||||
`history.replaceState`).
|
||||
|
||||
### Tests
|
||||
|
||||
- `tests/test_v756_oauth_connectors.py` — **13 tests** : URL d'autorisation
|
||||
(PKCE, scopes, redirect URI, cookies), 400 non configuré, callback → tokens
|
||||
**chiffrés** en base + redirection + `status.connected`, **state forgé refusé
|
||||
sans aucun appel réseau**, callback sans session → `oauth_error=session`,
|
||||
refresh (grant_type, conservation du refresh_token), `api_get` (Bearer + hôte
|
||||
fixe + URL absolue refusée), non connecté, déconnexion, catalogue, outil LLM,
|
||||
401 sans session, 404 kind inconnu, câblage menu. Aucun appel réseau réel
|
||||
(`_post_form` / `_api_get` monkeypatchés).
|
||||
- `test_v755` adapté : **5 natifs** (gitea, github, web, google, ms365).
|
||||
- **Suite complète : 1319 passed / 0 failed** (`-n auto`), `ruff` 0,
|
||||
`eslint` 0 problème.
|
||||
|
||||
|
||||
## v7.55.0 (2026-10-06) — Menu + : Connecteurs — socle (phase 5/8)
|
||||
|
||||
### Added
|
||||
|
||||
Reference in New Issue
Block a user