feat: v6.7.0 — SSO/SAML + OIDC entreprise (SP SAML signé + OIDC PKCE, auto-provisioning, groupes IdP → rôles workspace, SSO only, onglet Settings « SSO / Enterprise », API /api/v2/sso/*, help, migration 23, docs + OpenAPI 439 chemins) · 802 tests verts
This commit is contained in:
@@ -1,5 +1,80 @@
|
||||
# Changelog - FlowDeck
|
||||
|
||||
## v6.7.0 (2026-09-24) — SSO / SAML + OIDC entreprise (Enterprise Auth)
|
||||
|
||||
> Dernière feature de la roadmap v6.0.0 : authentification fédérée via un IdP
|
||||
> d'entreprise (SAML 2.0 ou OpenID Connect + PKCE), auto-provisioning des
|
||||
> comptes, mapping des groupes de l'IdP vers les rôles workspace, mode
|
||||
> « SSO only » et configuration admin dans Settings. Design :
|
||||
> `docs/V6_SSO_SAML_Enterprise_Auth.md` (checklist §9 cochée).
|
||||
|
||||
### Added
|
||||
|
||||
- **SAML 2.0 SP** — `app/auth/providers/saml_provider.py` (python3-saml 1.16.0) :
|
||||
login SP-initié `GET /auth/saml/login` (RelayState = id AuthnRequest + jeton CSRF
|
||||
en DB), ACS `POST /auth/saml/callback` (signature, audience, destination,
|
||||
InResponseTo, anti-replay `sso_requests`), SP metadata `GET /auth/saml/metadata`,
|
||||
SLO `GET|POST /auth/saml/logout` (relais LogoutRequest/Response vers l'IdP) ;
|
||||
`allowSingleLabelDomains` activé pour les hôts homelab/LAN
|
||||
- **OIDC + PKCE** — `app/auth/providers/oidc_provider.py` (authlib 1.8.0) :
|
||||
`GET /auth/oidc/login` (`state` + `code_verifier` stockés en DB),
|
||||
`GET|POST /auth/oidc/callback` (code → token → userinfo ; ID token vérifié via
|
||||
JWKS : aud/iss/nonce/exp), discovery + JWKS mis en cache 1 h,
|
||||
`GET|POST /auth/oidc/logout` (`end_session_endpoint` si présent)
|
||||
- **Provisioning** — `app/services/sso_provisioning.py` : création automatique de
|
||||
l'utilisateur au 1er login (`auth_method=saml|oidc`, lien via email), rôle par
|
||||
défaut + `default_workspace_id`, mapping groupes IdP → rôles workspace
|
||||
(re-synchronisé à chaque login + `POST /api/v2/sso/sync`), mode **SSO only**
|
||||
(login local refusé sauf administrateurs — design §7.1), secrets chiffrés
|
||||
Fernet dans `sso_config` (champ secret vide = conserver l'existant), détection
|
||||
de rejeu via `sso_requests` (TTL 15 min), `sso_login_history` (succès + échecs),
|
||||
`safe_next_path` (pas de redirection externe)
|
||||
- **Routeur** — `app/routers/sso.py` : parcours navigateur + API
|
||||
`GET|POST|PUT|DELETE /api/v2/sso/config` (admin, header `X-CSRF-Token`,
|
||||
`audit_log`), `GET /api/v2/sso/providers` (public, boutons de la page de login),
|
||||
`GET /api/v2/sso/workspaces`, `POST /api/v2/sso/sync`, `GET /api/v2/sso/history` ;
|
||||
CSRF exclut les préfixes `/auth/saml` et `/auth/oidc` (POST IdP cross-site)
|
||||
- **Migration 23** — tables `sso_config`, `sso_login_history`, `sso_requests`
|
||||
- **UI admin** — onglet « SSO / Enterprise » dans Settings : statut, config
|
||||
SAML (Entity ID, SSO/SLO URL, certificat PEM, mapping attributs JSON, signature
|
||||
des AuthnRequests) et OIDC (issuer, client id/secret, scope), provisioning
|
||||
(auto-provision, SSO only, espace par défaut), tableau groupes → rôles,
|
||||
URL des métadonnées SP copiable, bouton « Re-sync des groupes », désactivation ;
|
||||
bouton SSO sur la page de login (nom dynamique, masqué si non configuré)
|
||||
- **`PermissionManager`** (design §7.2) — `is_sso_only_workspace()`,
|
||||
`get_sso_roles()`, `sync_sso_permissions()` (les grants SSO sont des lignes
|
||||
`workspace_members` explicites ; le fallback « viewer » implicite n'en est pas un)
|
||||
- **Help** — section « Enterprise SSO » + badge `.help-badge.sso` dans `/help`
|
||||
- **`.env` (fallback bootstrap)** — `SSO_PROVIDER`, `SSO_NAME`, `SSO_ONLY`,
|
||||
`SSO_AUTO_PROVISION`, `SSO_ENTITY_ID`, `SSO_SSO_URL`, `SSO_SLO_URL`,
|
||||
`SSO_X509_CERTIFICATE`, `SSO_SIGN_REQUESTS`, `SSO_ISSUER_URL`, `SSO_CLIENT_ID`,
|
||||
`SSO_CLIENT_SECRET`, `SSO_SCOPE`, `SSO_ATTRIBUTE_MAPPING`, `SSO_GROUPS_MAPPING`,
|
||||
`SSO_DEFAULT_WORKSPACE_ID` (la config admin prime sur l'environnement ;
|
||||
section ajoutée dans `.env.example`)
|
||||
- **Dépendances** — `python3-saml==1.16.0`, `authlib==1.8.0`, `cryptography>=42.0`
|
||||
|
||||
### Changed
|
||||
|
||||
- `/logout` délègue la déconnexion à l'IdP (SLO) quand la session vient de SAML,
|
||||
puis détruit la session locale ; sinon comportement existant
|
||||
- OpenAPI régénéré : `docs/openapi-v2.json` → **439 chemins**, `info.version = 6.7.0`
|
||||
(operation IDs uniques — les routes multi-méthodes sont enregistrées en deux
|
||||
routes mono-méthode)
|
||||
|
||||
### Tests
|
||||
|
||||
- `tests/test_v67_sso.py` : **38 tests** — API de configuration admin
|
||||
(validation, CSRF, secret jamais renvoyé, secret vide préservé, réinitialisation,
|
||||
DELETE), métadonnées SP, login SAML complet contre un IdP mock signé (y compris
|
||||
SLO), OIDC complet (discovery/token/userinfo/JWKS mockés), cas négatifs
|
||||
(mauvais audience/issuer/destination/nonce/state, rejeu, signature altérée,
|
||||
assertion expirée), auto-provisioning, group mapping (nom → rôle workspace),
|
||||
sso_only, historique, rate limit, endpoint public `providers`, extension
|
||||
`PermissionManager`
|
||||
- `pytest -n auto` → **802 passed, 0 skipped** · `ruff check app tests` OK · `eslint static/js` 0 problème
|
||||
|
||||
---
|
||||
|
||||
## v6.6.0 (2026-09-24) — Agent phase 5 : API publique agent & skill marketplace
|
||||
|
||||
> Dernière phase du plan agent en 5 phases (« Plateforme ») : l'agent devient
|
||||
|
||||
Reference in New Issue
Block a user