diff --git a/src/pages/admin/liste.py b/src/pages/admin/liste.py index 7967d84..48caccd 100644 --- a/src/pages/admin/liste.py +++ b/src/pages/admin/liste.py @@ -10,6 +10,7 @@ from dash import ( no_update, register_page, ) +from dash.exceptions import PreventUpdate from flask_login import current_user from src.admin.db import log_action @@ -91,6 +92,13 @@ def layout(**_): prevent_initial_call=True, ) def _update_table(selected_table, data, data_previous): + # Les callbacks Dash sont des endpoints serveur globaux (/_dash-update-component) + # invocables indépendamment du layout rendu : la garde is_admin() de layout() + # ne protège QUE l'affichage. Sans ce contrôle, n'importe qui (y compris un + # anonyme) pourrait lire/écrire toute la base via ce callback. Voir issue #110. + if not is_admin(): + raise PreventUpdate + if ctx.triggered_id == "admin-table-select": return ( get_rows(selected_table), diff --git a/tests/admin/test_callback_guard.py b/tests/admin/test_callback_guard.py new file mode 100644 index 0000000..6d820a2 --- /dev/null +++ b/tests/admin/test_callback_guard.py @@ -0,0 +1,42 @@ +"""Régression issue #110 : le callback admin est un endpoint serveur global +(/_dash-update-component), invocable indépendamment du layout. Il DOIT revérifier +is_admin() lui-même, sinon un non-admin (voire un anonyme) lit/écrit toute la base. + +Ces tests appellent le callback directement (sans navigateur) : la garde s'exécute +avant tout accès au `ctx`/à la base, donc un non-admin ne doit rien pouvoir faire. +""" + +import pytest +from dash.exceptions import PreventUpdate + +import src.app # noqa: F401 # instancie l'app → register_page() des pages +from src.pages.admin import liste + + +def test_update_table_read_path_rejects_non_admin(monkeypatch): + monkeypatch.setattr(liste, "is_admin", lambda: False) + # Chemin lecture (sélection d'une table) : ne doit RIEN renvoyer. + with pytest.raises(PreventUpdate): + liste._update_table("users", [], None) + + +def test_update_table_write_path_rejects_non_admin(monkeypatch): + monkeypatch.setattr(liste, "is_admin", lambda: False) + # Chemin écriture (édition de cellule simulée) : ne doit RIEN modifier. + data = [{"id": 1, "email": "a@b.fr"}] + data_previous = [{"id": 1, "email": "victime@b.fr"}] + with pytest.raises(PreventUpdate): + liste._update_table("users", data, data_previous) + + +def test_update_table_allows_admin(monkeypatch): + # Avec is_admin() vrai, la garde ne bloque pas : sans changement de cellule + # détecté, le callback renvoie 4 valeurs (pas de PreventUpdate). + monkeypatch.setattr(liste, "is_admin", lambda: True) + + class _Ctx: + triggered_id = "autre-composant" + + monkeypatch.setattr(liste, "ctx", _Ctx) + result = liste._update_table("users", None, None) + assert len(result) == 4