SQL router

This commit is contained in:
2026-04-27 16:51:16 +02:00
parent 881c49c0c4
commit dfdc8d26bb
2 changed files with 196 additions and 23 deletions

View File

@@ -4,6 +4,22 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parent
ALLOWED_READ_TABLES = {"clients", "projects", "tasks", "repositories", "memories"}
FORBIDDEN_SQL_WORDS = {
"insert",
"update",
"delete",
"drop",
"alter",
"create",
"replace",
"truncate",
"attach",
"detach",
"pragma",
"vacuum",
"reindex",
}
def get_memory_db():
@@ -85,6 +101,50 @@ def row_to_dict(row):
return dict(row) if row else None
def execute_read_query(sql, limit=100):
sql = normalize_read_sql(sql)
limited_sql = f"SELECT * FROM ({sql}) AS readonly_query LIMIT ?"
with connect() as db:
db.set_authorizer(readonly_authorizer)
rows = db.execute(limited_sql, (limit,)).fetchall()
return [row_to_dict(row) for row in rows]
def normalize_read_sql(sql):
sql = clean_text(sql).rstrip(";")
sql_lower = sql.lower()
if not sql_lower.startswith("select "):
raise ValueError("Seules les requetes SELECT sont autorisees")
if ";" in sql:
raise ValueError("Une seule requete SELECT est autorisee")
if "--" in sql or "/*" in sql or "*/" in sql:
raise ValueError("Les commentaires SQL ne sont pas autorises")
for word in FORBIDDEN_SQL_WORDS:
if re_word_match(sql_lower, word):
raise ValueError(f"Mot SQL interdit: {word}")
return sql
def re_word_match(text, word):
import re
return re.search(rf"\b{re.escape(word)}\b", text) is not None
def readonly_authorizer(action, arg1, arg2, db_name, trigger_name):
if action == sqlite3.SQLITE_SELECT:
return sqlite3.SQLITE_OK
if action == sqlite3.SQLITE_READ:
table_name = arg1 or ""
if table_name in ALLOWED_READ_TABLES:
return sqlite3.SQLITE_OK
return sqlite3.SQLITE_DENY
if action == sqlite3.SQLITE_FUNCTION:
return sqlite3.SQLITE_OK
return sqlite3.SQLITE_DENY
def find_client(db, name):
if not name:
return None
@@ -229,6 +289,24 @@ def list_clients(limit=100):
return [row_to_dict(row) for row in rows]
def list_open_tasks(limit=100):
with connect() as db:
rows = db.execute(
"""
SELECT t.*, c.name AS client_name, p.name AS project_name
FROM tasks t
LEFT JOIN clients c ON c.id = t.client_id
LEFT JOIN projects p ON p.id = t.project_id
WHERE t.done = 0
AND (c.id IS NULL OR c.status != 'inactif')
ORDER BY COALESCE(c.name, p.name, 'zzzz') COLLATE NOCASE, t.created_at DESC
LIMIT ?
""",
(limit,),
).fetchall()
return [row_to_dict(row) for row in rows]
def get_memory_snapshot(limit=25):
with connect() as db:
clients = db.execute(