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
+84
View File
@@ -983,3 +983,87 @@ def _migration_row_content_pages(conn: sqlite3.Connection) -> None:
"CREATE INDEX IF NOT EXISTS idx_pages_row "
"ON pages(collection_row_id) WHERE collection_row_id IS NOT NULL"
)
@register(23, "v6.7.0: SSO/SAML enterprise auth")
def _migration_sso_enterprise_auth(conn: sqlite3.Connection) -> None:
"""v6.7.0 — SSO/SAML 2.0 + OIDC enterprise authentication.
``sso_config`` — single active SSO provider (SAML or OIDC), managed
from Settings → Admin → SSO / Enterprise. Secrets
(``client_secret``, SP private key) are encrypted at
rest by ``app.services.sso_provisioning``.
``sso_login_history`` — audit trail of every SSO login attempt (successes
AND rejections — signature failure, replay, no
local account…).
``sso_requests`` — single-use anti-replay store: AuthnRequest ids and
OIDC states, CSRF relay tokens, PKCE verifiers and
the post-login redirect target. One row is consumed
by exactly one callback.
"""
conn.execute(
"""
CREATE TABLE IF NOT EXISTS sso_config (
id INTEGER PRIMARY KEY AUTOINCREMENT,
workspace_id INTEGER REFERENCES workspaces(id) ON DELETE CASCADE,
provider_type TEXT NOT NULL DEFAULT 'saml',
name TEXT NOT NULL DEFAULT 'Company SSO',
entity_id TEXT NOT NULL DEFAULT '',
sso_url TEXT NOT NULL DEFAULT '',
slo_url TEXT DEFAULT '',
x509_certificate TEXT NOT NULL DEFAULT '',
issuer_url TEXT DEFAULT '',
client_id TEXT DEFAULT '',
client_secret TEXT DEFAULT '',
scope TEXT DEFAULT 'openid profile email',
attribute_mapping TEXT NOT NULL DEFAULT '{}',
groups_mapping TEXT NOT NULL DEFAULT '[]',
auto_provision INTEGER NOT NULL DEFAULT 1,
sso_only INTEGER NOT NULL DEFAULT 0,
sign_requests INTEGER NOT NULL DEFAULT 0,
default_workspace_id INTEGER REFERENCES workspaces(id) ON DELETE SET NULL,
sp_private_key TEXT DEFAULT '',
sp_certificate TEXT DEFAULT '',
active INTEGER NOT NULL DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_by INTEGER REFERENCES users(id)
)
"""
)
conn.execute(
"""
CREATE TABLE IF NOT EXISTS 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 DEFAULT 'SSO',
sso_identifier TEXT,
ip_address TEXT DEFAULT '',
user_agent TEXT DEFAULT '',
success INTEGER NOT NULL DEFAULT 0,
error_message TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
"""
)
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_sso_history_user "
"ON sso_login_history(user_id, created_at)"
)
conn.execute(
"""
CREATE TABLE IF NOT EXISTS sso_requests (
id TEXT PRIMARY KEY,
kind TEXT NOT NULL,
relay_state TEXT NOT NULL DEFAULT '',
code_verifier TEXT NOT NULL DEFAULT '',
next_path TEXT NOT NULL DEFAULT '/workspaces',
used INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
"""
)
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_sso_requests_created ON sso_requests(created_at)"
)