Kanbante 0.46.6 — porte centrale unique : réglage administrateur, écran réduit à l'entrée SSO et porte de secours discrète
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
"""Porte centrale unique (0.46.6) — réglage, écran, refus, secours.
|
||||
|
||||
Branche de la phase 2 du programme « compte partout » (basée sur
|
||||
la 0.46.5, fusion #145/#144). Couvert ici, sur instance fraîche
|
||||
par test (base temporaire : seul le compte d'amorçage « proprio »,
|
||||
admin, créé depuis l'environnement — même patron que
|
||||
tests/test_amorcage_pont.py) :
|
||||
|
||||
- mode INACTIF (défaut à la livraison) : comportement actuel —
|
||||
formulaire local visible et fonctionnel ;
|
||||
- activation par le PANNEAU d'administration (le réglage
|
||||
``porte_centrale`` se pose et se retire : ``true`` puis
|
||||
``false`` — la conversion booléenne explicite de
|
||||
l'enregistrement est exercée au passage) ;
|
||||
- mode ACTIF : l'écran ne présente que l'entrée centrale (lien
|
||||
SSO, aucun champ local) ; le formulaire local est refusé même
|
||||
avec des identifiants locaux valides ; une session déjà
|
||||
établie reste valide (aucun impact) ;
|
||||
- porte de secours : chemin discret ``/login/secours``, jamais
|
||||
proposé sur l'écran principal ; identifiants posés par les
|
||||
variables d'environnement ``KANBANTE_PORTE_SECOURS_USER`` /
|
||||
``KANBANTE_PORTE_SECOURS_PASSWORD`` ; seul le compte
|
||||
administrateur désigné passe (un membre désigné est refusé) ;
|
||||
sans les variables, le chemin répond 404.
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
import app.main as main
|
||||
from app import accounts, models
|
||||
from app.config import get_settings
|
||||
|
||||
MDP_AMORCE = "mdp-amorce"
|
||||
MDP_MEMBRE = "mdp-membre"
|
||||
MDP_SECOURS = "mdp-secours-test"
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def instance(monkeypatch, tmp_path, fake_gitea):
|
||||
monkeypatch.setenv("KANBANTE_DB_PATH", str(tmp_path / "porte.db"))
|
||||
monkeypatch.setenv("KANBANTE_AUTH_USER", "proprio")
|
||||
monkeypatch.setenv("KANBANTE_AUTH_PASSWORD", MDP_AMORCE)
|
||||
with TestClient(main.app) as client:
|
||||
users = models.list_users()
|
||||
assert len(users) == 1 and users[0]["username"] == "proprio"
|
||||
membre_id = models.create_user(
|
||||
"membre1", accounts.hash_password(MDP_MEMBRE),
|
||||
role="member", display_name="Membre Un",
|
||||
)
|
||||
yield client, users[0], membre_id
|
||||
|
||||
|
||||
def _csrf(client, chemin="/login"):
|
||||
client.get(chemin)
|
||||
return client.cookies.get("kanbante_csrf") or ""
|
||||
|
||||
|
||||
def _connecter(client, username, password, chemin="/login"):
|
||||
token = _csrf(client, chemin)
|
||||
return client.post(
|
||||
chemin,
|
||||
data={"username": username, "password": password,
|
||||
"next": "/", "csrf_token": token},
|
||||
follow_redirects=False,
|
||||
)
|
||||
|
||||
|
||||
def _activer_porte_par_le_panneau(client):
|
||||
"""Connexion admin native, puis POST /admin/settings (true)."""
|
||||
r = _connecter(client, "proprio", MDP_AMORCE)
|
||||
assert r.status_code == 303
|
||||
r = client.get("/admin/settings")
|
||||
assert r.status_code == 200
|
||||
assert "porte_centrale" in r.text # le réglage est au panneau
|
||||
token = client.cookies.get("kanbante_csrf") or ""
|
||||
r = client.post(
|
||||
"/admin/settings",
|
||||
data={"setting_porte_centrale": "true", "csrf_token": token},
|
||||
follow_redirects=False,
|
||||
)
|
||||
assert r.status_code == 303
|
||||
assert get_settings().porte_centrale is True
|
||||
|
||||
|
||||
# ── Mode inactif : comportement actuel ───────────────────────
|
||||
|
||||
def test_mode_inactif_par_defaut_comportement_actuel(instance):
|
||||
client, _amorce, _membre = instance
|
||||
assert get_settings().porte_centrale is False
|
||||
r = client.get("/login")
|
||||
assert r.status_code == 200
|
||||
assert 'name="password"' in r.text # formulaire local présent
|
||||
assert "Se connecter avec ObsiGate (SSO)" in r.text
|
||||
# Le formulaire local fonctionne pour un compte ordinaire.
|
||||
r = _connecter(client, "membre1", MDP_MEMBRE)
|
||||
assert r.status_code == 303
|
||||
assert "kanbante_session" in r.headers.get("set-cookie", "")
|
||||
assert client.get("/").status_code == 200
|
||||
|
||||
|
||||
def test_panneau_active_puis_desactive(instance):
|
||||
client, _amorce, _membre = instance
|
||||
_activer_porte_par_le_panneau(client)
|
||||
# Désactivation : saisir « false » doit RÉELLEMENT désactiver
|
||||
# (conversion booléenne explicite, pas bool("false") == True).
|
||||
token = client.cookies.get("kanbante_csrf") or ""
|
||||
r = client.post(
|
||||
"/admin/settings",
|
||||
data={"setting_porte_centrale": "false", "csrf_token": token},
|
||||
follow_redirects=False,
|
||||
)
|
||||
assert r.status_code == 303
|
||||
assert get_settings().porte_centrale is False
|
||||
# Et le formulaire refonctionne (session admin refermée).
|
||||
client.cookies.delete("kanbante_session")
|
||||
r = _connecter(client, "membre1", MDP_MEMBRE)
|
||||
assert r.status_code == 303
|
||||
|
||||
|
||||
# ── Mode actif : écran central, formulaire refusé ────────────
|
||||
|
||||
def test_mode_actif_ecran_et_formulaire_refuse(instance):
|
||||
client, _amorce, _membre = instance
|
||||
_activer_porte_par_le_panneau(client)
|
||||
# La session DÉJÀ ÉTABLIE (celle de l'admin qui vient
|
||||
# d'activer) n'est pas touchée par l'activation.
|
||||
assert client.get("/").status_code == 200
|
||||
assert client.get("/admin/settings").status_code == 200
|
||||
# Écran de connexion, visiteur déconnecté : entrée centrale seule.
|
||||
client.cookies.delete("kanbante_session")
|
||||
r = client.get("/login")
|
||||
assert r.status_code == 200
|
||||
assert "Se connecter avec ObsiGate (SSO)" in r.text
|
||||
assert 'name="password"' not in r.text
|
||||
assert 'action="/login"' not in r.text
|
||||
# Le chemin de secours n'est JAMAIS proposé sur l'écran principal.
|
||||
assert "/login/secours" not in r.text
|
||||
# Formulaire local refusé, même avec les identifiants valides
|
||||
# d'un compte ordinaire…
|
||||
r = _connecter(client, "membre1", MDP_MEMBRE)
|
||||
assert r.status_code == 403
|
||||
assert "porte centrale est active" in r.text
|
||||
assert "kanbante_session" not in r.headers.get("set-cookie", "")
|
||||
# …et même avec ceux de l'administrateur d'amorçage.
|
||||
r = _connecter(client, "proprio", MDP_AMORCE)
|
||||
assert r.status_code == 403
|
||||
assert "kanbante_session" not in r.headers.get("set-cookie", "")
|
||||
|
||||
|
||||
# ── Porte de secours ─────────────────────────────────────────
|
||||
|
||||
def test_porte_secours_chemin_discret(instance, monkeypatch):
|
||||
client, _amorce, _membre = instance
|
||||
monkeypatch.setenv("KANBANTE_PORTE_SECOURS_USER", "proprio")
|
||||
monkeypatch.setenv("KANBANTE_PORTE_SECOURS_PASSWORD", MDP_SECOURS)
|
||||
_activer_porte_par_le_panneau(client)
|
||||
client.cookies.delete("kanbante_session")
|
||||
# L'écran principal ne propose toujours pas le chemin.
|
||||
assert "/login/secours" not in client.get("/login").text
|
||||
# Le chemin discret, lui, présente son formulaire…
|
||||
r = client.get("/login/secours")
|
||||
assert r.status_code == 200
|
||||
assert 'action="/login/secours"' in r.text
|
||||
# …refuse un mauvais mot de passe…
|
||||
r = _connecter(client, "proprio", "mauvais", chemin="/login/secours")
|
||||
assert r.status_code == 401
|
||||
assert "kanbante_session" not in r.headers.get("set-cookie", "")
|
||||
# …refuse le compte membre (pas le compte désigné)…
|
||||
r = _connecter(client, "membre1", MDP_SECOURS, chemin="/login/secours")
|
||||
assert r.status_code == 401
|
||||
# …et ouvre la session de l'administrateur désigné, en plein
|
||||
# mode porte centrale.
|
||||
r = _connecter(client, "proprio", MDP_SECOURS, chemin="/login/secours")
|
||||
assert r.status_code == 303
|
||||
assert "kanbante_session" in r.headers.get("set-cookie", "")
|
||||
assert client.get("/admin").status_code == 200
|
||||
|
||||
|
||||
def test_porte_secours_membre_designe_refuse(instance, monkeypatch):
|
||||
"""Le compte désigné doit être administrateur : un membre ne passe pas."""
|
||||
client, _amorce, _membre = instance
|
||||
monkeypatch.setenv("KANBANTE_PORTE_SECOURS_USER", "membre1")
|
||||
monkeypatch.setenv("KANBANTE_PORTE_SECOURS_PASSWORD", MDP_SECOURS)
|
||||
r = _connecter(client, "membre1", MDP_SECOURS, chemin="/login/secours")
|
||||
assert r.status_code == 401
|
||||
assert "kanbante_session" not in r.headers.get("set-cookie", "")
|
||||
|
||||
|
||||
def test_porte_secours_non_configuree_404(instance):
|
||||
client, _amorce, _membre = instance
|
||||
# Aucune variable de secours dans l'environnement de test.
|
||||
assert client.get("/login/secours").status_code == 404
|
||||
r = _connecter(client, "proprio", MDP_AMORCE, chemin="/login/secours")
|
||||
assert r.status_code == 404
|
||||
assert "kanbante_session" not in r.headers.get("set-cookie", "")
|
||||
|
||||
|
||||
def test_activation_par_variable_environnement(monkeypatch, tmp_path, fake_gitea):
|
||||
"""L'environnement peut amorcer le réglage (défaut officiel : inactif)."""
|
||||
monkeypatch.setenv("KANBANTE_DB_PATH", str(tmp_path / "porte-env.db"))
|
||||
monkeypatch.setenv("KANBANTE_AUTH_USER", "proprio")
|
||||
monkeypatch.setenv("KANBANTE_AUTH_PASSWORD", MDP_AMORCE)
|
||||
monkeypatch.setenv("KANBANTE_PORTE_CENTRALE", "1")
|
||||
with TestClient(main.app) as client:
|
||||
assert get_settings().porte_centrale is True
|
||||
r = client.get("/login")
|
||||
assert 'name="password"' not in r.text
|
||||
# La surcharge du panneau reste possible par-dessus :
|
||||
# (kv settings_overrides, même écriture que le panneau).
|
||||
models.set_kv(
|
||||
"settings_overrides", json.dumps({"porte_centrale": False})
|
||||
)
|
||||
assert get_settings().porte_centrale is False
|
||||
Reference in New Issue
Block a user