SQL router
This commit is contained in:
78
memory.py
78
memory.py
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user