refactor(db)!: adopt project-routed pooled databases
Reorganize WNDB by responsibility and remove legacy scheme endpoints.\n\nRoute analysis and time-series access through project pools, preserve transactional realtime replacement, and refresh GIS materialized views after writes.\n\nAdd database architecture documentation, live pooling coverage, API contract updates, and executable container verification.\n\nBREAKING CHANGE: legacy scheme APIs and flat app.native.wndb module imports are removed.
This commit is contained in:
@@ -1,4 +1,9 @@
|
||||
from app.native.wndb import s2_junctions
|
||||
import ast
|
||||
from pathlib import Path
|
||||
|
||||
from app.infra.db.postgresql import scada_assets
|
||||
from app.native.wndb.core.database import ChangeSet, sql_literal
|
||||
from app.native.wndb.model import controls, junctions, patterns
|
||||
|
||||
|
||||
def test_get_junction_binds_untrusted_identifier(monkeypatch) -> None:
|
||||
@@ -9,13 +14,104 @@ def test_get_junction_binds_untrusted_identifier(monkeypatch) -> None:
|
||||
calls.append((name, statement, params))
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(s2_junctions, "try_read", fake_try_read)
|
||||
monkeypatch.setattr(junctions, "try_read", fake_try_read)
|
||||
|
||||
assert s2_junctions.get_junction("project_a", malicious_id) == {}
|
||||
assert calls == [
|
||||
(
|
||||
"project_a",
|
||||
"select * from junctions where id = %s",
|
||||
(malicious_id,),
|
||||
)
|
||||
]
|
||||
assert junctions.get_junction("project_a", malicious_id) == {}
|
||||
assert len(calls) == 1
|
||||
name, statement, params = calls[0]
|
||||
assert name == "project_a"
|
||||
assert "network.junctions" in statement
|
||||
assert "WHERE n.id = %s" in statement
|
||||
assert malicious_id not in statement
|
||||
assert params == (malicious_id,)
|
||||
|
||||
|
||||
def test_get_all_junctions_reads_materialized_view(monkeypatch) -> None:
|
||||
statements: list[str] = []
|
||||
|
||||
def fake_read_all(_name, statement):
|
||||
statements.append(statement)
|
||||
return []
|
||||
|
||||
monkeypatch.setattr(junctions, "read_all", fake_read_all)
|
||||
monkeypatch.setattr(junctions, "get_all_node_links", lambda _name: {})
|
||||
|
||||
assert junctions.get_all_junctions("project_a") == []
|
||||
assert "FROM gis.junctions" in statements[0]
|
||||
|
||||
|
||||
def test_get_all_scada_info_reads_materialized_view(monkeypatch) -> None:
|
||||
statements: list[str] = []
|
||||
|
||||
def fake_read_all(_name, statement):
|
||||
statements.append(statement)
|
||||
return []
|
||||
|
||||
monkeypatch.setattr(scada_assets, "read_all", fake_read_all)
|
||||
|
||||
assert scada_assets.get_all_scada_info("project_a") == []
|
||||
assert "FROM gis.scada_devices" in statements[0]
|
||||
|
||||
|
||||
def test_sql_literal_keeps_attacker_text_inside_one_postgres_literal() -> None:
|
||||
malicious = "x'); DROP SCHEMA network CASCADE; --"
|
||||
|
||||
assert sql_literal("O'Brien") == "'O''Brien'"
|
||||
assert sql_literal(malicious) == "'x''); DROP SCHEMA network CASCADE; --'"
|
||||
assert sql_literal(None) == "NULL"
|
||||
|
||||
|
||||
def test_pattern_command_quotes_malicious_identifier() -> None:
|
||||
malicious = "x'); DROP SCHEMA network CASCADE; --"
|
||||
change = ChangeSet({"id": malicious, "factors": [1.0]})
|
||||
|
||||
command = patterns._add_pattern("project_a", change).sql
|
||||
|
||||
assert f"values ({sql_literal(malicious)})" in command
|
||||
assert "values ('x'); DROP SCHEMA" not in command
|
||||
|
||||
|
||||
def test_inp_control_quotes_embedded_apostrophe() -> None:
|
||||
command = controls.inp_in_control("LINK P-1 STATUS 'OPEN'; DELETE")
|
||||
|
||||
assert "''OPEN''" in command
|
||||
assert "STATUS 'OPEN'; DELETE')" not in command
|
||||
|
||||
|
||||
def test_wndb_sql_fstrings_do_not_quote_formatted_values_directly() -> None:
|
||||
root = Path(__file__).resolve().parents[2] / "app" / "native" / "wndb"
|
||||
violations: list[str] = []
|
||||
for path in root.rglob("*.py"):
|
||||
tree = ast.parse(path.read_text(encoding="utf-8"))
|
||||
for node in ast.walk(tree):
|
||||
if not isinstance(node, ast.JoinedStr):
|
||||
continue
|
||||
static_text = "".join(
|
||||
value.value
|
||||
for value in node.values
|
||||
if isinstance(value, ast.Constant) and isinstance(value.value, str)
|
||||
).lower()
|
||||
if not any(
|
||||
keyword in static_text
|
||||
for keyword in ("select ", "insert into", "update ", "delete from")
|
||||
):
|
||||
continue
|
||||
for index, value in enumerate(node.values):
|
||||
if not isinstance(value, ast.FormattedValue):
|
||||
continue
|
||||
before = node.values[index - 1] if index else None
|
||||
after = node.values[index + 1] if index + 1 < len(node.values) else None
|
||||
left_quote = (
|
||||
isinstance(before, ast.Constant)
|
||||
and isinstance(before.value, str)
|
||||
and before.value.endswith("'")
|
||||
)
|
||||
right_quote = (
|
||||
isinstance(after, ast.Constant)
|
||||
and isinstance(after.value, str)
|
||||
and after.value.startswith("'")
|
||||
)
|
||||
if left_quote and right_quote:
|
||||
violations.append(f"{path.name}:{node.lineno}")
|
||||
|
||||
assert violations == []
|
||||
|
||||
Reference in New Issue
Block a user