fix(admin): revérifier is_admin() dans le callback _update_table (#110)
Le callback Dash _update_table est un endpoint serveur global (/_dash-update-component) invocable indépendamment du layout : la garde is_admin() de layout() ne protégeait que l'affichage. Sans contrôle dans le callback, un non-admin — voire un anonyme sur le chemin lecture — pouvait lire/écrire toute la base SQLite utilisateurs. Ajoute la garde en tête du callback + test de non-régression. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,7 @@ from dash import (
|
|||||||
no_update,
|
no_update,
|
||||||
register_page,
|
register_page,
|
||||||
)
|
)
|
||||||
|
from dash.exceptions import PreventUpdate
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
|
|
||||||
from src.admin.db import log_action
|
from src.admin.db import log_action
|
||||||
@@ -91,6 +92,13 @@ def layout(**_):
|
|||||||
prevent_initial_call=True,
|
prevent_initial_call=True,
|
||||||
)
|
)
|
||||||
def _update_table(selected_table, data, data_previous):
|
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":
|
if ctx.triggered_id == "admin-table-select":
|
||||||
return (
|
return (
|
||||||
get_rows(selected_table),
|
get_rows(selected_table),
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in New Issue
Block a user