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
FlowDeck CI / lint (push) Successful in 1m28s
FlowDeck CI / test (push) Successful in 11m52s
FlowDeck CI / docker (push) Successful in 1m51s

This commit is contained in:
2026-09-24 13:32:17 -04:00
parent 9562f30366
commit d074689b18
25 changed files with 4507 additions and 31 deletions
+38 -2
View File
@@ -7,6 +7,10 @@
> marketplace de skills (`/api/v2/skills/*`) dans `app/routers/api_v2_agent.py`, logique
> partagée `app/services/skill_gallery.py`, OpenAPI régénéré (**427 chemins**), 15 tests dédiés
> (`tests/test_v66_agent_api.py`). Voir §2.4.
> **v6.7.0 (2026-09-24)** — **SSO / SAML + OIDC entreprise** : parcours navigateur
> `/auth/saml/*` + `/auth/oidc/*` (hors scope `/api/v2`), API admin de configuration
> `/api/v2/sso/*` (session + admin + CSRF), logique partagée `app/services/sso_provisioning.py`,
> OpenAPI régénéré, 38 tests dédiés (`tests/test_v67_sso.py`). Voir §2.5.
> Les sections ci-dessous décrivent les conventions cibles et restent la référence de conception.
> **Dernière mise à jour** : 2026-09-24
> **Portée** : inventaire de l'API existante, conventions cibles, design CRUD par ressource, webhooks, sécurité, checklist d'implémentation.
@@ -259,9 +263,41 @@ Mêmes endpoints (session cookie) côté interne : `/api/agent/skills/gallery`,
(catalogue `app/services/webhook_outbound.py`, abonnement `agent.*` possible).
4. Chaque mutation écrit `api_audit_log` (`agent.create`, `agent.run`, `skill.import`, …).
---
### 2.5 SSO / Enterprise auth (`app/routers/sso.py`, v6.7.0)
## 3. Conventions cibles pour l'API v2
> **Authentification fédérée** : un IdP d'entreprise (SAML 2.0 ou OpenID Connect + PKCE)
> ouvre une session FlowDeck ; les comptes sont créés au premier login et les groupes de
> l'IdP deviennent des rôles workspace. Design : `docs/V6_SSO_SAML_Enterprise_Auth.md`.
**Parcours navigateur (session cookie, CSRF exclu — POST IdP cross-site)**
| Méthode | Route | Description |
|---------|-------|-------------|
| GET | `/auth/saml/login` | Redirection SP-initiée vers l'IdP (`RelayState` = next sûr) |
| POST | `/auth/saml/callback` | Assertion Consumer Service — signature/aud/dest/InResponseTo validés |
| GET | `/auth/saml/metadata` | Métadonnées SP XML (EntityID, ACS, SLO, certificat) |
| GET | `/auth/saml/logout` | SLO — relaye `SAMLRequest` à l'IdP puis détruit la session (POST accepté pour l'IdP) |
| GET | `/auth/oidc/login` | Redirection authorize (`state` + `code_verifier` en DB) |
| GET | `/auth/oidc/callback` | Code → token → userinfo ; ID token vérifié (JWKS, aud/iss/nonce/exp) |
| GET/POST | `/auth/oidc/logout` | Déconnexion OIDC (relaye à l'IdP si `end_session_endpoint`) |
**API admin (session + rôle admin + header `X-CSRF-Token`)**
| Méthode | Route | Description |
|---------|-------|-------------|
| GET | `/api/v2/sso/providers` | **Public** (page de login) : `{providers[{type,name,icon,login_url}], sso_only, base_url}` |
| GET | `/api/v2/sso/config` | Config publique (`{configured, source: db\|env, client_secret_set, provisioned_users}` — **jamais** le secret) |
| POST/PUT | `/api/v2/sso/config` | Crée/met à jour (`{configured:false}` = actif) ; champ secret vide = conserver |
| DELETE | `/api/v2/sso/config` | Désactive le SSO (les comptes SSO existants restent) |
| GET | `/api/v2/sso/workspaces` | Épingles pour le mapping (workspaces + `provisioned_users`) |
| POST | `/api/v2/sso/sync` | Re-synchronise les groupes de tous les utilisateurs SSO |
| GET | `/api/v2/sso/history` | Journal d'audit des tentatives (`limit` ≤ 200) |
Règles : secrets chiffrés Fernet (`sso_config`), anti-replay `sso_requests` (TTL 15 min),
historique `sso_login_history` (succès/échecs), mode SSO only (login local refusé sauf admins),
bootstrap possible par variables `SSO_*` du `.env` (la config admin prime).
---
### 3.1 Auth & tokens
+16 -14
View File
@@ -1,6 +1,6 @@
# V6.0.0 — SSO / SAML : Enterprise Authentication
> **Statut** : Conception détaillée — v6.0.0
> **Statut** : ✅ **Livré en v6.7.0** (2026-09-24) — conception historique ci-dessous
> **Date** : 2026-09-15
> **Route** : `feat/v6-sso-saml` → `develop` → `main`
> **Dépendances** : v4.0.0 Accounts & Integrations (OAuth2 Gitea/GitHub existant)
@@ -181,7 +181,7 @@ 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_X509_CERTIFICATE="-----BEGIN CERTIFICATE-----\n..."
SSO_ATTRIBUTE_MAPPING={"login":"nameid","email":"email","full_name":"name"}
SSO_AUTO_PROVISION=true
SSO_DEFAULT_WORKSPACE_ID=1
@@ -432,18 +432,20 @@ ALTER TABLE users ADD COLUMN auth_method TEXT DEFAULT 'local';
## 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
> ✅ **Tout est livré en v6.7.0** (`tests/test_v67_sso.py`, 38 tests) :
1. ✅ **`auth/providers/saml_provider.py`** — wrapper python3-saml (`app/auth/providers/saml_provider.py`)
2. ✅ **`auth/providers/oidc_provider.py`** — wrapper OIDC authlib (`app/auth/providers/oidc_provider.py`)
3. ✅ **`routers/sso.py`** — endpoints SSO/OIDC (`app/routers/sso.py` + API admin `/api/v2/sso/*`)
4. ✅ **Migration 23 `sso_config`** + `sso_login_history` + `sso_requests` + colonne `auth_method` sur `users` (déjà présente)
5. ✅ **`services/sso_provisioning.py`** — auto-provision + group mapping
6. ✅ **Extension `settings.html`** — UI admin SSO (onglet « SSO / Enterprise »)
7. ✅ **Extension page de login** — boutons SSO (nom dynamique)
8. ✅ **SP metadata endpoint** (`/auth/saml/metadata`)
9. ✅ **Security** — validation assertions (sign/aud/dest/InResponseTo), anti-replay `sso_requests`, rate limit login, historique
10. ✅ **Tests** — `tests/test_v67_sso.py` : 38 scénarios (flots SAML/OIDC, négatifs, groupes, sso_only, SLO)
11. ✅ **Documentation utilisateur** — `/help` section « SSO (Enterprise) »
12. ✅ **Dépendance** — `python3-saml==1.16.0`, `authlib==1.8.0`, `cryptography>=42.0` dans requirements.txt
---
+504 -2
View File
@@ -2,7 +2,7 @@
"openapi": "3.1.0",
"info": {
"title": "FlowDeck",
"version": "6.6.0"
"version": "6.7.0"
},
"paths": {
"/auth/register": {
@@ -161,7 +161,7 @@
"auth"
],
"summary": "Logout",
"description": "Clear session and redirect to login page.",
"description": "Clear session and redirect to login page.\n\nSAML sessions additionally hand over to the IdP's Single Logout when one\nis configured (the actual cookie clearing happens on the SLO route).",
"operationId": "logout_auth_logout_get",
"responses": {
"200": {
@@ -195,6 +195,508 @@
}
}
},
"/auth/saml/login": {
"get": {
"tags": [
"sso"
],
"summary": "Saml Login",
"description": "SP-initiated SSO: issue an AuthnRequest and redirect to the IdP.",
"operationId": "saml_login_auth_saml_login_get",
"parameters": [
{
"name": "next",
"in": "query",
"required": false,
"schema": {
"type": "string",
"default": "/workspaces",
"title": "Next"
}
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/auth/saml/callback": {
"post": {
"tags": [
"sso"
],
"summary": "Saml Callback",
"description": "Assertion Consumer Service — validate the SAMLResponse and open a session.",
"operationId": "saml_callback_auth_saml_callback_post",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {}
}
}
}
}
}
},
"/auth/saml/metadata": {
"get": {
"tags": [
"sso"
],
"summary": "Saml Metadata",
"description": "SP metadata XML — paste into the IdP (Azure AD / Okta / Keycloak…).",
"operationId": "saml_metadata_auth_saml_metadata_get",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {}
}
}
}
}
}
},
"/auth/saml/logout": {
"get": {
"tags": [
"sso"
],
"summary": "Saml Logout",
"description": "SP-initiated Single Logout (GET) — hands the browser to the IdP.",
"operationId": "saml_logout_auth_saml_logout_get",
"parameters": [
{
"name": "next",
"in": "query",
"required": false,
"schema": {
"type": "string",
"default": "/auth/login?provider=local",
"title": "Next"
}
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
},
"post": {
"tags": [
"sso"
],
"summary": "Saml Logout Post",
"description": "IdP-initiated Single Logout (POST with SAMLRequest/SAMLResponse).",
"operationId": "saml_logout_post_auth_saml_logout_post",
"parameters": [
{
"name": "next",
"in": "query",
"required": false,
"schema": {
"type": "string",
"default": "/auth/login?provider=local",
"title": "Next"
}
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/auth/oidc/login": {
"get": {
"tags": [
"sso"
],
"summary": "Oidc Login",
"description": "Redirect to the OIDC provider (authorization code + PKCE).",
"operationId": "oidc_login_auth_oidc_login_get",
"parameters": [
{
"name": "next",
"in": "query",
"required": false,
"schema": {
"type": "string",
"default": "/workspaces",
"title": "Next"
}
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/auth/oidc/callback": {
"get": {
"tags": [
"sso"
],
"summary": "Oidc Callback",
"description": "OIDC callback (GET, authorization code in the query string).",
"operationId": "oidc_callback_auth_oidc_callback_get",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {}
}
}
}
}
},
"post": {
"tags": [
"sso"
],
"summary": "Oidc Callback Post",
"description": "OIDC callback (POST, form_post response mode).",
"operationId": "oidc_callback_post_auth_oidc_callback_post",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {}
}
}
}
}
}
},
"/auth/oidc/logout": {
"get": {
"tags": [
"sso"
],
"summary": "Oidc Logout",
"description": "OIDC logout (GET) — local session first, then the IdP end-session URL.",
"operationId": "oidc_logout_auth_oidc_logout_get",
"parameters": [
{
"name": "next",
"in": "query",
"required": false,
"schema": {
"type": "string",
"default": "/auth/login?provider=local",
"title": "Next"
}
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
},
"post": {
"tags": [
"sso"
],
"summary": "Oidc Logout Post",
"description": "OIDC logout (POST).",
"operationId": "oidc_logout_post_auth_oidc_logout_post",
"parameters": [
{
"name": "next",
"in": "query",
"required": false,
"schema": {
"type": "string",
"default": "/auth/login?provider=local",
"title": "Next"
}
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/api/v2/sso/providers": {
"get": {
"tags": [
"sso"
],
"summary": "Sso Providers",
"description": "Public: what the login page should show (button list + sso_only flag).",
"operationId": "sso_providers_api_v2_sso_providers_get",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {}
}
}
}
}
}
},
"/api/v2/sso/config": {
"get": {
"tags": [
"sso"
],
"summary": "Get Sso Config Api",
"description": "Read the current SSO configuration (secrets never returned).",
"operationId": "get_sso_config_api_api_v2_sso_config_get",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {}
}
}
}
}
},
"put": {
"tags": [
"sso"
],
"summary": "Save Sso Config Api",
"description": "Create/replace the SSO configuration (admin, scope write).",
"operationId": "save_sso_config_api_api_v2_sso_config_put",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {}
}
}
}
}
},
"post": {
"tags": [
"sso"
],
"summary": "Save Sso Config Api",
"description": "Create/replace the SSO configuration (admin, scope write).",
"operationId": "save_sso_config_api_api_v2_sso_config_post",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {}
}
}
}
}
},
"delete": {
"tags": [
"sso"
],
"summary": "Delete Sso Config Api",
"description": "Disable SSO — local logins keep working (design §8 « SSO disable »).",
"operationId": "delete_sso_config_api_api_v2_sso_config_delete",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {}
}
}
}
}
}
},
"/api/v2/sso/workspaces": {
"get": {
"tags": [
"sso"
],
"summary": "Sso Workspaces",
"description": "Workspaces available for default assignment / group mapping.",
"operationId": "sso_workspaces_api_v2_sso_workspaces_get",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {}
}
}
}
}
}
},
"/api/v2/sso/sync": {
"post": {
"tags": [
"sso"
],
"summary": "Sso Sync",
"description": "Re-apply group → workspace role mapping for every SSO user.",
"operationId": "sso_sync_api_v2_sso_sync_post",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {}
}
}
}
}
}
},
"/api/v2/sso/history": {
"get": {
"tags": [
"sso"
],
"summary": "Sso History",
"description": "Audit trail of SSO login attempts (successes and rejections).",
"operationId": "sso_history_api_v2_sso_history_get",
"parameters": [
{
"name": "limit",
"in": "query",
"required": false,
"schema": {
"type": "integer",
"default": 50,
"title": "Limit"
}
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/trash": {
"get": {
"tags": [