Flowdeck 7.85.6 — porte centrale unique (phase 2) : écran réduit à l'entrée ObsiGate SSO + porte de secours administrateur par variables d'environnement, désactivée par défaut
This commit is contained in:
@@ -0,0 +1,222 @@
|
||||
"""FlowDeck — porte centrale unique (phase 2, programme « compte
|
||||
partout »), basée sur les réglages d'accès de la phase 1 (#146).
|
||||
|
||||
Contrat :
|
||||
|
||||
- réglage « porte centrale » **distinct** de celui d'inscription,
|
||||
**désactivé par défaut** à la livraison ;
|
||||
- mode actif : l'écran de connexion ne présente que l'entrée
|
||||
centrale (bouton ObsiGate SSO) et les identifiants locaux
|
||||
ordinaires sont refusés — comptes historiques inclus ; les
|
||||
sessions déjà établies ne sont pas touchées ;
|
||||
- **porte de secours** : UN compte administrateur local désigné,
|
||||
identifiants posés par les variables d'environnement
|
||||
``FLOWDECK_SECOURS_ADMIN_LOGIN`` /
|
||||
``FLOWDECK_SECOURS_ADMIN_MOT_DE_PASSE``, authentification locale
|
||||
uniquement par le chemin discret ``?secours=1`` (drapeau
|
||||
``secours`` à l'envoi) — jamais proposé sur l'écran principal ;
|
||||
- mode inactif : comportement actuel intégralement préservé.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from conftest import anon_csrf
|
||||
|
||||
from app.services import reglages_acces
|
||||
|
||||
LOGIN_SECOURS = "admin-secours"
|
||||
MDP_SECOURS = "mot-de-passe-de-secours-designe"
|
||||
|
||||
|
||||
def _activer():
|
||||
reglages_acces.set_reglages_acces(porte_centrale_active=True)
|
||||
|
||||
|
||||
def _desactiver():
|
||||
reglages_acces.set_reglages_acces(porte_centrale_active=False)
|
||||
|
||||
|
||||
def _creer_compte_local(login: str, mot_de_passe: str, admin: int = 0) -> None:
|
||||
from app.db import get_conn
|
||||
from app.password_utils import hash_password
|
||||
|
||||
with get_conn() as conn:
|
||||
conn.execute(
|
||||
"INSERT OR REPLACE INTO users (login, full_name, email,"
|
||||
" password_hash, is_admin) VALUES (?, ?, ?, ?, ?)",
|
||||
(login, login, f"{login}@test.dev",
|
||||
hash_password(mot_de_passe), admin),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def secours_env(monkeypatch):
|
||||
"""Pose les variables d'environnement de la porte de secours."""
|
||||
monkeypatch.setenv("FLOWDECK_SECOURS_ADMIN_LOGIN", LOGIN_SECOURS)
|
||||
monkeypatch.setenv("FLOWDECK_SECOURS_ADMIN_MOT_DE_PASSE", MDP_SECOURS)
|
||||
_creer_compte_local(LOGIN_SECOURS, "ancien-mot-de-passe-local", admin=1)
|
||||
return monkeypatch
|
||||
|
||||
|
||||
# ── Défaut et distinction des réglages ──────────────────────────────
|
||||
|
||||
def test_porte_centrale_desactivee_par_defaut_dans_le_code():
|
||||
from app.config import settings
|
||||
|
||||
assert settings.porte_centrale_active is False
|
||||
|
||||
|
||||
def test_reglage_distinct_de_celui_d_inscription(client):
|
||||
reglages_acces.set_reglages_acces(
|
||||
inscription_locale_ouverte=True, porte_centrale_active=False)
|
||||
reglages = reglages_acces.get_reglages_acces()
|
||||
assert reglages["inscription_locale_ouverte"] is True
|
||||
assert reglages["porte_centrale_active"] is False
|
||||
|
||||
|
||||
# ── Mode actif : le local ordinaire est refusé ──────────────────────
|
||||
|
||||
def test_mode_actif_refuse_compte_ordinaire(client):
|
||||
_creer_compte_local("ordinaire", "secret123")
|
||||
_activer()
|
||||
r = anon_csrf(client).post("/auth/local-login", json={
|
||||
"email": "ordinaire", "password": "secret123"})
|
||||
assert r.status_code == 403
|
||||
assert "ObsiGate (SSO)" in r.json()["error"]
|
||||
|
||||
|
||||
def test_mode_actif_refuse_compte_local_historique(client):
|
||||
"""Compte créé par inscription publique (quand elle était
|
||||
ouverte), puis porte activée : son mot de passe local ne passe
|
||||
plus."""
|
||||
reglages_acces.set_reglages_acces(
|
||||
inscription_locale_ouverte=True, porte_centrale_active=False)
|
||||
r = client.post("/auth/register", json={
|
||||
"email": "[email protected]", "password": "secret123",
|
||||
"name": "Historique"})
|
||||
assert r.status_code == 200
|
||||
_activer()
|
||||
r = anon_csrf(client).post("/auth/local-login", json={
|
||||
"email": "[email protected]", "password": "secret123"})
|
||||
assert r.status_code == 403
|
||||
|
||||
|
||||
def test_mode_actif_ecran_connexion_que_l_entree_centrale(client):
|
||||
_activer()
|
||||
r = anon_csrf(client).get("/auth/login?provider=local")
|
||||
assert r.status_code == 200
|
||||
# L'entrée centrale, bien visible…
|
||||
assert "Login with ObsiGate (SSO)" in r.text
|
||||
# … et rien d'autre : ni formulaire local, ni onglet Register,
|
||||
# ni lien vers le chemin discret de secours.
|
||||
assert 'id="login-form"' not in r.text
|
||||
assert 'id="password"' not in r.text
|
||||
assert 'id="tab-register"' not in r.text
|
||||
assert "secours" not in r.text
|
||||
|
||||
|
||||
def test_mode_actif_inscription_aussi_fermee(client):
|
||||
reglages_acces.set_reglages_acces(inscription_locale_ouverte=True)
|
||||
_activer()
|
||||
r = client.post("/auth/register", json={
|
||||
"email": "[email protected]", "password": "secret123"})
|
||||
assert r.status_code == 403
|
||||
|
||||
|
||||
def test_mode_actif_session_deja_etablie_non_touchee(client):
|
||||
_creer_compte_local("deja-dedans", "secret123")
|
||||
c = anon_csrf(client)
|
||||
r = c.post("/auth/local-login", json={
|
||||
"email": "deja-dedans", "password": "secret123"})
|
||||
assert r.status_code == 200
|
||||
_activer()
|
||||
r = c.get("/auth/user")
|
||||
assert r.json()["authenticated"] is True
|
||||
|
||||
|
||||
# ── Porte de secours administrateur ─────────────────────────────────
|
||||
|
||||
def test_secours_connexion_admin_par_le_chemin_discret(client, secours_env):
|
||||
_activer()
|
||||
# L'écran discret rend le formulaire, marqué « secours ».
|
||||
r = anon_csrf(client).get("/auth/login?provider=local&secours=1")
|
||||
assert r.status_code == 200
|
||||
assert 'id="login-form"' in r.text
|
||||
assert "secours:true" in r.text
|
||||
# L'authentification elle-même : identifiants d'environnement.
|
||||
r = anon_csrf(client).post("/auth/local-login", json={
|
||||
"email": LOGIN_SECOURS, "password": MDP_SECOURS,
|
||||
"secours": True})
|
||||
assert r.status_code == 200
|
||||
assert r.json()["status"] == "ok"
|
||||
assert "flowdeck_session" in r.headers.get("set-cookie", "")
|
||||
|
||||
|
||||
def test_secours_mauvais_mot_de_passe_environnement_refuse(client, secours_env):
|
||||
_activer()
|
||||
r = anon_csrf(client).post("/auth/local-login", json={
|
||||
"email": LOGIN_SECOURS, "password": "pas-le-bon",
|
||||
"secours": True})
|
||||
assert r.status_code == 401
|
||||
|
||||
|
||||
def test_secours_sans_drapeau_refuse_meme_pour_l_admin(client, secours_env):
|
||||
"""Sans le drapeau du chemin discret, même les identifiants de
|
||||
secours sont traités comme un envoi ordinaire : refusés."""
|
||||
_activer()
|
||||
r = anon_csrf(client).post("/auth/local-login", json={
|
||||
"email": LOGIN_SECOURS, "password": MDP_SECOURS})
|
||||
assert r.status_code == 403
|
||||
|
||||
|
||||
def test_secours_compte_non_admin_refuse(client, secours_env, monkeypatch):
|
||||
"""Le compte désigné doit être administrateur : un compte
|
||||
ordinaire désigné par les variables ne passe pas."""
|
||||
_creer_compte_local("pas-admin", "secret123", admin=0)
|
||||
monkeypatch.setenv("FLOWDECK_SECOURS_ADMIN_LOGIN", "pas-admin")
|
||||
monkeypatch.setenv("FLOWDECK_SECOURS_ADMIN_MOT_DE_PASSE", "secret123")
|
||||
_activer()
|
||||
r = anon_csrf(client).post("/auth/local-login", json={
|
||||
"email": "pas-admin", "password": "secret123", "secours": True})
|
||||
assert r.status_code == 401
|
||||
|
||||
|
||||
def test_secours_sans_variables_environnement_fermee(client, monkeypatch):
|
||||
monkeypatch.delenv("FLOWDECK_SECOURS_ADMIN_LOGIN", raising=False)
|
||||
monkeypatch.delenv("FLOWDECK_SECOURS_ADMIN_MOT_DE_PASSE", raising=False)
|
||||
_creer_compte_local("admin-local", "secret123", admin=1)
|
||||
_activer()
|
||||
r = anon_csrf(client).post("/auth/local-login", json={
|
||||
"email": "admin-local", "password": "secret123", "secours": True})
|
||||
assert r.status_code == 401
|
||||
|
||||
|
||||
# ── Mode inactif : comportement actuel intégralement préservé ───────
|
||||
|
||||
def test_mode_inactif_connexion_locale_comme_avant(client):
|
||||
_desactiver()
|
||||
_creer_compte_local("normal", "secret123")
|
||||
r = anon_csrf(client).post("/auth/local-login", json={
|
||||
"email": "normal", "password": "secret123"})
|
||||
assert r.status_code == 200
|
||||
assert r.json()["status"] == "ok"
|
||||
|
||||
|
||||
def test_mode_inactif_ecran_connexion_complet(client):
|
||||
_desactiver()
|
||||
r = anon_csrf(client).get("/auth/login?provider=local")
|
||||
assert r.status_code == 200
|
||||
assert 'id="login-form"' in r.text
|
||||
assert "Login with ObsiGate (SSO)" in r.text
|
||||
|
||||
|
||||
def test_api_admin_aller_retour_porte_centrale(client):
|
||||
_desactiver()
|
||||
r = client.put("/api/admin/reglages-acces",
|
||||
json={"porte_centrale_active": True})
|
||||
assert r.status_code == 200
|
||||
assert r.json()["porte_centrale_active"] is True
|
||||
r = client.get("/api/admin/reglages-acces")
|
||||
assert r.json()["porte_centrale_active"] is True
|
||||
_desactiver()
|
||||
Reference in New Issue
Block a user