223 lines
8.4 KiB
Python
223 lines
8.4 KiB
Python
"""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()
|