feat: v6.2.0 Web Clipper — extension navigateur (capture article/selection/bookmark/screenshot)
FlowDeck CI / lint (push) Successful in 1m11s
FlowDeck CI / test (push) Failing after 8m32s
FlowDeck CI / docker (push) Skipped

- Service app/services/web_clipper.py: sanitize HTML, html->blocks, extraction article, creation page workspace-aware, rate limit 50/h, device registration
- Router app/routers/web_clipper.py: POST /api/v2/web-clipper/clip, GET /status, POST /auth/verify, GET/DELETE /devices, GET /extensions (download page), auth via session ou Bearer (api_tokens / extension_devices)
- Migration 19: extension_devices + extension_clips (+ indexes)
- Extension Manifest V3: content.js (floating button, selection), background.js (clip + contextMenus), popup.html/js, clipper.css, icons
- Settings UI: onglet Extensions (liste devices, revoke, test clip, liens download), page /extensions
- Tests: 16 tests web_clipper (sanitize, blocks, article/bookmark/selection/screenshot, bearer, rate-limit, devices, extensions page)
- Bump version 6.1.0 -> 6.2.0
This commit is contained in:
2026-09-19 23:26:03 -04:00
parent 13d5f8625a
commit 0b251649e5
31 changed files with 3072 additions and 3 deletions
+472
View File
@@ -0,0 +1,472 @@
# V6.0.0 — SSO / SAML : Enterprise Authentication
> **Statut** : Conception détaillée — v6.0.0
> **Date** : 2026-09-15
> **Route** : `feat/v6-sso-saml` → `develop` → `main`
> **Dépendances** : v4.0.0 Accounts & Integrations (OAuth2 Gitea/GitHub existant)
---
## 1. Vision & objectifs
Ajouter le support **SSO/SAML 2.0** pour permettre aux entreprises d'intégrer FlowDeck dans leur infrastructure d'authentification existante. Les utilisateurs d'une organisation peuvent se connecter via leur fournisseur d'identité (IdP) d'entreprise sans avoir de compte local séparé.
### Objectifs
| Critère | Cible |
|---------|-------|
| Protocoles | SAML 2.0 (principal), OIDC (complémentaire) |
| Fournisseurs supportés | Azure AD, Okta, Google Workspace, OneLogin, Keycloak, Auth0 |
| Temps de connexion SSO | < 3 s (redirect + callback) |
| Gestion des utilisateurs | Auto-provisioning à la première connexion |
| Déconnexion | SLO (Single Logout) supporté |
| Fallback | Login local toujours disponible (admin activé/désactivable) |
---
## 2. Architecture SSO/SAML
### 2.1 Flux SAML 2.0
```
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Utilisateur │ │ FlowDeck │ │ IdP │
│ (navigateur) │ │ (SP) │ │ (Azure AD / │
│ │ │ │ │ Okta / etc.)│
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
│ │ │
│ 1. Accède à FlowDeck │ │
│───────────────────────►│ │
│ │ │
│ 2. Redirige vers IdP │ │
│◄───────────────────────│ │
│ │ │
│ 3. SSO redirect │ │
│───────────────────────►│────────────────────────►│
│ │ │
│ 5. Callback avec │ │
│ SAMLResponse │◄────────────────────────│
│ │ │
│ 6. POST /auth/saml/callback │
│───────────────────────►│ │
│ │ │
│ 7. Validate SAML │ │
│ → Create/Update │ │
│ user → Session │ │
│◄───────────────────────│ │
│ │ │
│ 8. Authentifié │ │
│◄───────────────────────│ │
```
### 2.2 Composants côté serveur
```
┌───────────────────────────────────────────────────────────┐
│ FASTAPI (Python) │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ auth/routers/sso.py │ │
│ │ ├─ GET /auth/saml/login — initie la requête │ │
│ │ ├─ POST /auth/saml/callback — traite la réponse │ │
│ │ ├─ GET /auth/saml/metadata — SP metadata endpoint │ │
│ │ ├─ GET /auth/oidc/login — flux OIDC │ │
│ │ ├─ POST /auth/oidc/callback │ │
│ │ └─ POST /auth/sso/logout — SLO │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ auth/providers/saml_provider.py │ │
│ │ ├─ SAMLProvider — wrapper python3-saml │ │
│ │ └─ validate_saml_response() │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ auth/providers/oidc_provider.py │ │
│ │ ├─ OIDCProvider — wrapper httpx + OIDC lib │ │
│ │ └─ validate_oidc_token() │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ auth/session.py — SessionManager étendu │ │
│ │ ├─ create_sso_session() │ │
│ │ ├─ handle_sso_user() │ │
│ │ └─ enforce_sso_restriction() │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ services/sso_provisioning.py │ │
│ │ ├─ auto_provision_user() │ │
│ │ ├─ sync_user_attributes() │ │
│ │ └─ map_sso_groups_to_workspaces() │ │
│ └─────────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────┘
```
---
## 3. Configuration SAML
### 3.1 Nouveau table `sso_config`
```sql
CREATE TABLE sso_config (
id INTEGER PRIMARY KEY AUTOINCREMENT,
workspace_id INTEGER REFERENCES workspaces(id) ON DELETE CASCADE,
-- NULL = workspace-level; specific workspace; global if NULL+global flag
provider_type TEXT NOT NULL,
-- 'saml', 'oidc'
entity_id TEXT NOT NULL,
-- SAML: Entity ID of the IdP (e.g., "http://www.microsoft.com/...")
sso_url TEXT NOT NULL,
-- SAML: IdP SSO URL (HTTP-POST binding)
slo_url TEXT,
-- SAML: IdP Single Logout URL
x509_certificate TEXT NOT NULL,
-- SAML: IdP signing certificate (PEM format)
-- OIDC: issuer URL
issuer_url TEXT,
-- OIDC: OIDC issuer identifier
client_id TEXT,
-- OIDC: client_id
client_secret TEXT,
-- OIDC: client_secret (encrypted at rest)
scope TEXT DEFAULT 'openid profile email',
-- OIDC: scopes requested
attribute_mapping TEXT NOT NULL DEFAULT '{}',
-- JSON: {"email": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress", ...}
-- Maps SAML/OIDC attributes to FlowDeck user fields
auto_provision BOOLEAN NOT NULL DEFAULT 1,
-- Auto-create user on first SSO login
default_workspace_id INTEGER,
-- Workspace to assign new SSO users
groups_mapping TEXT DEFAULT '[]',
-- JSON: [{"sso_group": "Admins", "workspace_role": "admin"}, ...]
active BOOLEAN NOT NULL DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_by INTEGER REFERENCES users(id)
);
```
### 3.2 Mapping des attributs
Par défaut pour SAML (mappings standard OASIS) :
```json
{
"login": "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
"email": "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
"full_name": "urn:oasis:names:tc:SAML:attribute:displayName",
"avatar_url": "urn:oasis:names:tc:SAML:attribute:thumbnail"
}
```
Par défaut pour OIDC :
```json
{
"login": "sub",
"email": "email",
"full_name": "name",
"avatar_url": "picture"
}
```
### 3.3 Configuration `.env` (exemples)
```env
# SAML — Azure AD
SSO_PROVIDER=saml
SSO_ENTITY_ID=https://sts.windows.net/{tenant-id}/
SSO_SSO_URL=https://login.microsoftonline.com/{tenant-id}/saml2
SSO_SLO_URL=https://login.microsoftonline.com/{tenant-id}/saml2/logout
SSO_X509_CERT="-----BEGIN CERTIFICATE-----\n..."
SSO_ATTRIBUTE_MAPPING={"login":"nameid","email":"email","full_name":"name"}
SSO_AUTO_PROVISION=true
SSO_DEFAULT_WORKSPACE_ID=1
SSO_GROUPS_MAPPING=[{"sso_group":"FlowDeck Admins","workspace_role":"admin"}]
# OIDC — Google Workspace
SSO_PROVIDER=oidc
SSO_ISSUER_URL=https://accounts.google.com
SSO_CLIENT_ID=xxx.apps.googleusercontent.com
SSO_CLIENT_SECRET=yyy
SSO_SCOPE=openid profile email
```
---
## 4. Endpoints API
### 4.1 Routeur `app/routers/sso.py`
```
GET /auth/saml/login — Redirige vers l'IdP SAML
POST /auth/saml/callback — Traite le SAMLResponse, crée la session
GET /auth/saml/metadata — Retourne le SP metadata XML (pour configurer l'IdP)
POST /auth/saml/logout — Initie le SLO (Single Logout)
GET /auth/oidc/login — Redirige vers l'IdP OIDC
POST /auth/oidc/callback — Traite le token OIDC, crée la session
POST /auth/oidc/logout — SLO OIDC
POST /api/v2/sso/config — Créer/configurer SSO (admin, scope admin)
GET /api/v2/sso/config — Lire la config SSO courante
PUT /api/v2/sso/config — Mettre à jour la config
DELETE /api/v2/sso/config — Supprimer la config SSO (désactiver SSO)
GET /api/v2/sso/workspaces — Lister les workspaces avec SSO actif
POST /api/v2/sso/sync — Forcer la synchro des groupes/attributs
```
### 4.2 Service `app/services/sso_provisioning.py`
```python
class SSOService:
"""Provisioning et gestion des utilisateurs SSO."""
def handle_sso_login(self, sso_data: dict, provider_type: str) -> dict:
"""Traite un login SSO : trouve/crée l'utilisateur, crée la session."""
# 1. Extraire les attributs selon le provider
# 2. Chercher l'utilisateur par email/login SSO
# 3. Si trouvé → mise à jour des attributs
# 4. Si non trouvé et auto_provision → création
# 5. Assigner le workspace par défaut ou le premier disponible
# 6. Vérifier les groupes SSO → rôle workspace
# 7. Créer la session
def sync_sso_groups(self, user_id: int, sso_groups: list[str]) -> None:
"""Sync les groupes SSO vers les rôles workspace."""
# Comparer sso_groups avec groups_mapping
# Mettre à jour workspace_members.role
def enforce_sso_restriction(self, workspace_id: int) -> bool:
"""Vérifie si le workspace est en mode SSO-only."""
# Si config.sso_only = True → interdire le login local
```
---
## 5. Sécurité
### 5.1 Validation des assertions SAML
| Validation | Détail |
|------------|--------|
| Signature | Vérifier la signature avec le certificat IdP |
| `NotBefore` / `NotOnOrAfter` | Rejeter les assertions expirées |
| `Audience` | Vérifier que l'audience match l'entity_id du SP |
| `Destination` | Vérifier que le destination match notre callback URL |
| `Issuer` | Vérifier que l'émetteur est le IdP attendu |
| `InResponseTo` | Prévenir le replay attack |
### 5.2 Protection supplémentaire
- **CSRF sur le callback** : le `RelayState` contient un token CSRF validé
- **Rate limiting** sur les endpoints SSO : max 5 tentatives/minute
- **Logging audit** : chaque login SSO est enregistré (user, provider, IP, timestamp)
- **Session fixation** : nouvelle session créée après chaque SSO login
- **Encryption** : `client_secret` chiffré avec `app_secret_key` avant stockage
### 5.3 Table `sso_login_history`
```sql
CREATE TABLE sso_login_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
provider_type TEXT NOT NULL,
provider_name TEXT NOT NULL,
sso_identifier TEXT, -- email ou subject du SSO
ip_address TEXT,
user_agent TEXT,
success BOOLEAN NOT NULL,
error_message TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_sso_history_user ON sso_login_history(user_id, created_at);
```
---
## 6. Interface utilisateur
### 6.1 Login page — ajout SSO buttons
```html
<!-- Extension de login.html — boutons SSO -->
<div class="sso-providers" x-show="ssoProviders.length > 0">
<div class="sso-divider">ou continuez avec</div>
<template x-for="provider in ssoProviders">
<button @click="ssoLogin(provider.type)" class="sso-btn">
<span x-text="provider.icon"></span>
<span x-text="provider.name"></span>
</button>
</template>
</div>
```
### 6.2 Settings — Configuration SSO (admin)
Nouvelle section dans `settings.html` → onglet **"SSO / Enterprise"** :
```
┌──────────────────────────────────────────────────────┐
│ SSO / Enterprise Authentication │
│───────────────────────────────────────────────────────│
│ Provider: [▼ SAML / OIDC] │
│ │
│ SAML Configuration: │
│ ├── Entity ID: [___________________________] │
│ ├── SSO URL: [___________________________] │
│ ├── SLO URL: [___________________________] │
│ ├── Certificate: [text area - PEM] │
│ └── Attribute Mapping: [JSON editor] │
│ │
│ OIDC Configuration: │
│ ├── Issuer URL: [_________________________] │
│ ├── Client ID: [_________________________] │
│ ├── Client Secret: [_______________________] │
│ └── Scope: [openid profile email] │
│ │
│ User Provisioning: │
│ ├── Auto-provision new users [✓] │
│ ├── Default workspace: [dropdown] │
│ └── Group mapping: [table editor] │
│ │
│ ┌─ Group Mappings ───────────────────────────────────┐ │
│ │ SSO Group │ FlowDeck Role │ Workspace │ │
│ │ FlowDeck Admins │ admin │ Main │ │
│ │ FlowDeck Members │ editor │ Main │ │
│ └────────────────────┴────────────────┴──────────────┘ │
│ │
│ [Save SSO Configuration] [Disable SSO] │
│ Status: ✅ Active — 12 users provisioned via SSO │
└──────────────────────────────────────────────────────┘
```
### 6.3 Métadonnées SP pour l'IdP
Endpoint `GET /auth/saml/metadata` retourne le XML SAML metadata :
```xml
<EntityDescriptor entityID="https://flowdeck.local/saml/metadata">
<SPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
<KeyDescriptor use="signing">
<KeyInfo>...</KeyInfo>
</KeyDescriptor>
<AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
Location="https://flowdeck.local/auth/saml/callback" />
<SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
Location="https://flowdeck.local/auth/saml/logout" />
</SPSSODescriptor>
</EntityDescriptor>
```
---
## 7. Intégration avec le système existant
### 7.1 Comportement selon le type de compte
```
SSO ACTIVÉ + local auth ENABLED:
→ Boutons SSO visibles sur la login page
→ Login local disponible (optionnel, admin-toggle)
→ Les utilisateurs SSO et locaux coexistent
SSO ACTIVÉ + local auth DISABLED:
→ Seuls les boutons SSO sont affichés
→ Login local caché / 403 pour /auth/local-login
→ Admin du workspace peut garder le sien
SSO ACTIVÉ + compte local existant:
→ Email match → fusion automatique des sessions
→ Email ne match pas → login local séparé (deux comptes)
```
### 7.2 Interaction avec le PermissionManager
Le `PermissionManager` existant (`app/services/permission_manager.py`) est étendu :
```python
class PermissionManager:
# Méthodes existantes conservées
# + Nouvelles méthodes SSO :
def is_sso_only_workspace(self, workspace_id: int) -> bool:
"""Vérifie si le workspace exige SSO."""
def get_sso_roles(self, user_id: int, workspace_id: int) -> list[str]:
"""Retourne les rôles issus des groupes SSO."""
def sync_sso_permissions(self, user_id: int, sso_groups: list[str], workspace_id: int):
"""Met à jour les rôles basés sur les groupes SSO."""
```
### 7.3 Cookie de session SSO
Le cookie `flowdeck_session` existant est utilisé tel quel. Une colonne supplémentaire dans la table `users` distingue les utilisateurs SSO :
```sql
ALTER TABLE users ADD COLUMN auth_method TEXT DEFAULT 'local';
-- Valeurs: 'local', 'gitea', 'github', 'saml', 'oidc'
```
---
## 8. Tests
| Test | Description | Outil |
|------|-------------|-------|
| SAML login flow | Redirection complète IdP → callback → session | Playwright + mock IdP |
| SAML metadata | Endpoint retourne XML valide | Unit test |
| SAML assertion validation | Signature invalide → rejet | Unit test |
| SAML replay attack | Même assertion utilisée 2× → rejet | Unit test |
| OIDC login flow | Authorization code → token → session | Playwright + mock OIDC |
| Auto-provision | Nouveau user SSO → création auto | Unit test |
| Group mapping | Groupes SSO → rôles workspace | Unit test |
| SSO disable | Désactiver SSO → tous les users restants gardent accès | Unit test |
| SLO | Logout → redirection IdP SLO | Playwright |
| Mixed auth | Login SSO + login local dans le même workspace | Integration test |
---
## 9. Checklist d'implémentation
1. **`auth/providers/saml_provider.py`** — wrapper python3-saml ou pysaml2
2. **`auth/providers/oidc_provider.py`** — wrapper OIDC (authlib ou httpx)
3. **`auth/routers/sso.py`** — endpoints SSO/OIDC
4. **Migration `sso_config`** + `sso_login_history` + colonne `auth_method` sur `users`
5. **`services/sso_provisioning.py`** — auto-provision + group mapping
6. **Extension `settings.html`** — UI admin SSO
7. **Extension `login.html`** — boutons SSO
8. **SP metadata endpoint** (`/auth/saml/metadata`)
9. **Security** — validation assertions, rate limiting, audit log
10. **Tests** — tous les scénarios SSO
11. **Documentation utilisateur** — `/help` section SSO setup
12. **Dépendance** — `python3-saml` ou `pysaml2`, `authlib` dans requirements.txt
---
## 10. Dépendances Python
```
# requirements.txt additions pour v6.0.0 SSO
python3-saml>=1.16.0 # SAML 2.0 SP
authlib>=1.3.0 # OIDC client
cryptography>=42.0 # Signature/encryption
xmlsec>=1.3.0 # XML signature validation (optionnel)
```
---
## 11. Références
- [OASIS SAML 2.0 Core Specification](https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf)
- [SAML 2.0 for Dummies (Simplified)](https://www.onelogin.com/sites/default/files/resources/SAML_2.0_for_Dummies.pdf)
- [OpenID Connect Core 1.0](https://openid.net/specs/openid-connect-core-1_0.html)
- [Azure AD SAML integration](https://learn.microsoft.com/en-us/azure/active-directory/hybrid/how-to-connect-saml-idp)
- [Authlib — OIDC client](https://docs.authlib.org/en/latest/client/oidc.html)
- `app/auth/providers.py` — Architecture provider existante
- `app/services/permission_manager.py` — Extension des rôles
- `docs/API_GUIDE_V6.md` — Référence API v2
- `ROADMAP.md` — v6.0.0 SSO/SAML item