369 lines
16 KiB
Python
369 lines
16 KiB
Python
"""Minimal guarded Firebird write executor for the MVP.
|
|
|
|
Scope is intentionally narrow: a single allowlisted table/field
|
|
(C000025.IPI), a single authorized database file, and an optimistic
|
|
compare-before-write. This is not a generic ORM/executor framework.
|
|
|
|
Per CLAUDE.md's "Firebird write policy": writes are only ever permitted
|
|
against the disposable test database TESTESOFICIAIS.FDB. Any other path,
|
|
or any table/field outside the allowlist, fails closed before touching
|
|
the connection.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
AUTHORIZED_WRITE_DATABASE_PATH = Path(r"D:\PROJETOS\GTISISTEMAS\Projeto\TESTESOFICIAIS.FDB")
|
|
|
|
# table -> allowed writable fields. Anything not listed here is refused.
|
|
_FIELD_ALLOWLIST: dict[str, set[str]] = {
|
|
# C000025.ALIQ_IPI is VARCHAR(1) and unused (0/521 rows) — confirmed via
|
|
# the vendor's own DDL scripts and live schema inspection to NOT be the
|
|
# IPI rate field. C000025.IPI NUMERIC(15,2) is. See Mission notes.
|
|
"C000025": {"IPI"},
|
|
}
|
|
# Local completion mission: every field with WRITE_ALLOWED=true in docs/FISCAL_FIELD_CONTRACT_V1.json (kept equal to
|
|
# central.app.field_contract.WRITE_FIELDS by a test). Still C000025 only; still exact-path authorized.
|
|
_LOCAL_COMPLETION_FIELDS = {"CLASSIFICACAO_FISCAL", "IPI", "EX_TIPI", "CEST", "SITUACAO_TRIBUTARIA", "CFOP", "CFOP_ENTRADA", "CST", "CST_ENTRADA", "CSOSN", "ALIQUOTA", "COD_ORIGEM_MERCADORIA",
|
|
"CST_IPI", "COD_ENTRADA_PIS", "COD_SAIDA_PIS", "ALIQ_PIS", "COD_ENTRADA_COFINS", "COD_SAIDA_COFINS", "ALIQ_COFINS", "INCIDENCIA_PISCOFINS", "PISCOFINS", "IAT", "IPPT"}
|
|
_LOCAL_ALLOWLIST_FILE = Path(__file__).resolve().parents[1] / "docs" / "LOCAL_WRITE_ALLOWLIST_V1.json"
|
|
|
|
_KEY_FIELD = "CODIGO"
|
|
|
|
|
|
class WriteRefused(Exception):
|
|
"""Raised whenever a guard rejects the write before it reaches the database."""
|
|
|
|
|
|
def _normalized_path(path: os.PathLike[str] | str) -> str:
|
|
return os.path.normcase(os.path.normpath(str(Path(path).resolve(strict=False))))
|
|
|
|
|
|
def assert_authorized_write_path(
|
|
path: os.PathLike[str] | str,
|
|
authorized_path: os.PathLike[str] | str | None = None,
|
|
) -> Path:
|
|
"""Fail closed unless `path` is exactly the authorized write target.
|
|
|
|
`authorized_path` defaults to the development disposable test database
|
|
(`AUTHORIZED_WRITE_DATABASE_PATH`) for backward compatibility with the
|
|
dev environment. A pilot/client installation MUST pass its own
|
|
`authorized_path` explicitly (from that installation's own config,
|
|
e.g. `AUTHORIZED_WRITE_DATABASE_PATH` env var) — authorization is never
|
|
inferred from the filename or extension alone, and never defaults to
|
|
"any .FDB is fine". If a caller sets FIREBIRD_WRITES=1 in a pilot
|
|
environment without also setting an explicit authorized_path, this
|
|
still falls back to the dev-only path and therefore fails closed for
|
|
any real client database, which is the safe behavior.
|
|
"""
|
|
candidate = Path(path)
|
|
authorized = Path(authorized_path) if authorized_path else AUTHORIZED_WRITE_DATABASE_PATH
|
|
if _normalized_path(candidate) != _normalized_path(authorized):
|
|
raise WriteRefused(f"path is not the authorized write target: {candidate}")
|
|
return authorized
|
|
|
|
|
|
def assert_schema_fingerprint_authorized(observed_fingerprint: str | None, expected_fingerprint: str | None) -> None:
|
|
"""Fail closed unless the live schema fingerprint matches what this
|
|
installation was registered/configured to expect. Prevents writing
|
|
against a database whose schema silently changed or was never verified."""
|
|
if not expected_fingerprint:
|
|
raise WriteRefused("no expected schema fingerprint configured for this installation")
|
|
if observed_fingerprint != expected_fingerprint:
|
|
raise WriteRefused(f"schema fingerprint mismatch: expected {expected_fingerprint!r}, observed {observed_fingerprint!r}")
|
|
|
|
|
|
def assert_allowed_field(table: str, field: str) -> None:
|
|
allowed = _FIELD_ALLOWLIST.get(table.upper())
|
|
if not allowed or field.upper() not in allowed:
|
|
raise WriteRefused(f"{table}.{field} is not in the write allowlist")
|
|
|
|
|
|
def assert_write_eligible(proposal: dict[str, Any]) -> None:
|
|
if not proposal.get("write_eligible"):
|
|
raise WriteRefused(
|
|
f"proposal for {proposal.get('product_code')}/{proposal.get('field')} "
|
|
f"is not write_eligible (status={proposal.get('status')})"
|
|
)
|
|
|
|
|
|
def assert_schema_ok(cursor: Any, table: str, field: str, proposed_value: Any = None) -> None:
|
|
"""Confirm the target column exists and, if it is a fixed-length text field,
|
|
that the proposed value actually fits without truncation. Never coerce or
|
|
truncate a value to make it fit."""
|
|
cursor.execute(
|
|
"SELECT f.RDB$FIELD_TYPE, f.RDB$FIELD_LENGTH FROM RDB$RELATION_FIELDS rf "
|
|
"JOIN RDB$FIELDS f ON f.RDB$FIELD_NAME = rf.RDB$FIELD_SOURCE "
|
|
"WHERE rf.RDB$RELATION_NAME = ? AND rf.RDB$FIELD_NAME = ?",
|
|
(table, field),
|
|
)
|
|
row = cursor.fetchone()
|
|
if row is None:
|
|
raise WriteRefused(f"schema guard failed: {table}.{field} not found in live schema")
|
|
field_type, field_length = row
|
|
# Firebird character types (CHAR=14, VARCHAR=37): reject a value that would truncate.
|
|
if proposed_value is not None and field_type in (14, 37) and field_length is not None:
|
|
if len(str(proposed_value)) > field_length:
|
|
raise WriteRefused(
|
|
f"schema guard failed: {table}.{field} is {field_length} char(s) wide, "
|
|
f"proposed value {proposed_value!r} ({len(str(proposed_value))} chars) would truncate"
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class WriteAuditRecord:
|
|
product_code: str
|
|
field: str
|
|
before_value: Any
|
|
proposed_value: Any
|
|
after_value: Any
|
|
rowcount: int
|
|
source_dataset: str | None
|
|
source_version: str | None
|
|
timestamp: str
|
|
|
|
def as_dict(self) -> dict[str, Any]:
|
|
return {
|
|
"product_code": self.product_code,
|
|
"field": self.field,
|
|
"before_value": self.before_value,
|
|
"proposed_value": self.proposed_value,
|
|
"after_value": self.after_value,
|
|
"rowcount": self.rowcount,
|
|
"source_dataset": self.source_dataset,
|
|
"source_version": self.source_version,
|
|
"timestamp": self.timestamp,
|
|
}
|
|
|
|
|
|
def execute_guarded_update(
|
|
connection: Any,
|
|
*,
|
|
database_path: os.PathLike[str] | str,
|
|
proposal: dict[str, Any],
|
|
table: str = "C000025",
|
|
key_field: str = _KEY_FIELD,
|
|
now: str,
|
|
authorized_path: os.PathLike[str] | str | None = None,
|
|
) -> WriteAuditRecord:
|
|
"""Perform one allowlisted, compare-before-write UPDATE and verify it.
|
|
|
|
Never commits if any guard fails. Returns an audit record with no secrets.
|
|
`authorized_path` defaults to the dev disposable test database; a pilot
|
|
installation must pass its own registered path explicitly.
|
|
"""
|
|
assert_authorized_write_path(database_path, authorized_path=authorized_path)
|
|
assert_write_eligible(proposal)
|
|
field = proposal["field"]
|
|
assert_allowed_field(table, field)
|
|
|
|
product_code = proposal["product_code"]
|
|
expected_before = proposal["current_value"]
|
|
proposed_value = proposal["proposed_value"]
|
|
if proposed_value is None:
|
|
raise WriteRefused("refuse to write a null proposed_value")
|
|
|
|
cursor = connection.cursor()
|
|
try:
|
|
assert_schema_ok(cursor, table, field, proposed_value)
|
|
|
|
cursor.execute(
|
|
f"SELECT {key_field}, {field} FROM {table} WHERE {key_field} = ?",
|
|
(product_code,),
|
|
)
|
|
row = cursor.fetchone()
|
|
if row is None:
|
|
raise WriteRefused(f"product {product_code} not found in {table}")
|
|
live_before = row[1]
|
|
if live_before != expected_before:
|
|
raise WriteRefused(
|
|
f"drift detected for {product_code}.{field}: expected {expected_before!r}, "
|
|
f"found {live_before!r}"
|
|
)
|
|
|
|
if expected_before is None:
|
|
sql = f"UPDATE {table} SET {field} = ? WHERE {key_field} = ? AND {field} IS NULL"
|
|
params = (proposed_value, product_code)
|
|
else:
|
|
sql = f"UPDATE {table} SET {field} = ? WHERE {key_field} = ? AND {field} = ?"
|
|
params = (proposed_value, product_code, expected_before)
|
|
cursor.execute(sql, params)
|
|
rowcount = cursor.rowcount
|
|
if rowcount != 1:
|
|
connection.rollback()
|
|
raise WriteRefused(f"expected rowcount 1, got {rowcount}; rolled back")
|
|
|
|
connection.commit()
|
|
|
|
cursor.execute(f"SELECT {field} FROM {table} WHERE {key_field} = ?", (product_code,))
|
|
after_row = cursor.fetchone()
|
|
after_value = after_row[0] if after_row else None
|
|
if after_value != proposed_value:
|
|
raise WriteRefused(
|
|
f"post-write verification failed for {product_code}.{field}: "
|
|
f"expected {proposed_value!r}, found {after_value!r}"
|
|
)
|
|
|
|
return WriteAuditRecord(
|
|
product_code=product_code,
|
|
field=field,
|
|
before_value=live_before,
|
|
proposed_value=proposed_value,
|
|
after_value=after_value,
|
|
rowcount=rowcount,
|
|
source_dataset=proposal.get("source_dataset"),
|
|
source_version=proposal.get("source_version"),
|
|
timestamp=now,
|
|
)
|
|
except Exception:
|
|
try:
|
|
connection.rollback()
|
|
except Exception:
|
|
pass
|
|
raise
|
|
finally:
|
|
cursor.close()
|
|
|
|
|
|
def execute_guarded_restore(
|
|
connection: Any,
|
|
*,
|
|
database_path: os.PathLike[str] | str,
|
|
table: str,
|
|
field: str,
|
|
key_field: str,
|
|
product_code: str,
|
|
written_value: Any,
|
|
restore_value: Any,
|
|
now: str,
|
|
) -> WriteAuditRecord:
|
|
"""Restore a field to `restore_value`, guarded the same way as the forward write."""
|
|
assert_authorized_write_path(database_path)
|
|
assert_allowed_field(table, field)
|
|
|
|
cursor = connection.cursor()
|
|
try:
|
|
assert_schema_ok(cursor, table, field)
|
|
cursor.execute(f"SELECT {field} FROM {table} WHERE {key_field} = ?", (product_code,))
|
|
row = cursor.fetchone()
|
|
if row is None:
|
|
raise WriteRefused(f"product {product_code} not found in {table}")
|
|
if row[0] != written_value:
|
|
raise WriteRefused(f"drift before restore: expected {written_value!r}, found {row[0]!r}")
|
|
|
|
if restore_value is None:
|
|
sql = f"UPDATE {table} SET {field} = NULL WHERE {key_field} = ? AND {field} = ?"
|
|
params = (product_code, written_value)
|
|
else:
|
|
sql = f"UPDATE {table} SET {field} = ? WHERE {key_field} = ? AND {field} = ?"
|
|
params = (restore_value, product_code, written_value)
|
|
cursor.execute(sql, params)
|
|
rowcount = cursor.rowcount
|
|
if rowcount != 1:
|
|
connection.rollback()
|
|
raise WriteRefused(f"expected rowcount 1 on restore, got {rowcount}; rolled back")
|
|
connection.commit()
|
|
|
|
cursor.execute(f"SELECT {field} FROM {table} WHERE {key_field} = ?", (product_code,))
|
|
after_row = cursor.fetchone()
|
|
after_value = after_row[0] if after_row else None
|
|
if after_value != restore_value:
|
|
raise WriteRefused(
|
|
f"post-restore verification failed: expected {restore_value!r}, found {after_value!r}"
|
|
)
|
|
|
|
return WriteAuditRecord(
|
|
product_code=product_code,
|
|
field=field,
|
|
before_value=written_value,
|
|
proposed_value=restore_value,
|
|
after_value=after_value,
|
|
rowcount=rowcount,
|
|
source_dataset=None,
|
|
source_version=None,
|
|
timestamp=now,
|
|
)
|
|
except Exception:
|
|
try:
|
|
connection.rollback()
|
|
except Exception:
|
|
pass
|
|
raise
|
|
finally:
|
|
cursor.close()
|
|
|
|
|
|
def load_local_allowlist(path=None):
|
|
"""Exact canonical paths authorized for the local completion mission (never by directory/extension)."""
|
|
import json
|
|
doc = json.loads(Path(path or _LOCAL_ALLOWLIST_FILE).read_text(encoding="utf-8"))
|
|
return {os.path.normcase(os.path.abspath(d["path"])) for d in doc["databases"]}
|
|
|
|
|
|
def assert_local_completion_target(database_path, allowlist=None) -> None:
|
|
allow = allowlist if allowlist is not None else load_local_allowlist()
|
|
if os.path.normcase(os.path.abspath(str(database_path))) not in allow:
|
|
raise WriteRefused(f"{database_path} is not in the exact local-completion write allowlist")
|
|
|
|
|
|
def execute_guarded_product_update(connection, *, database_path, product_code, changes: dict, now: str, allowlist=None, table: str = "C000025", key_field: str = _KEY_FIELD, restore_before: bool = False):
|
|
"""One transaction per product: every field is compare-before-write, ONE UPDATE, rowcount must be 1, then an independent re-read.
|
|
changes: {field: (expected_before, proposed)}. restore_before=True (rollback of an audited write to its recorded BEFORE) additionally allows a NULL target. Refuses any field outside the local-completion field set, any null proposal,
|
|
any path outside the exact allowlist, and any value that would truncate. Returns an audit dict (no secrets)."""
|
|
assert_local_completion_target(database_path, allowlist)
|
|
if table != "C000025":
|
|
raise WriteRefused("only C000025 may be written")
|
|
if not changes:
|
|
raise WriteRefused("no changes")
|
|
for field, (_, proposed) in changes.items():
|
|
if field not in _LOCAL_COMPLETION_FIELDS:
|
|
raise WriteRefused(f"{table}.{field} is not a contract write target")
|
|
if proposed is None and not restore_before:
|
|
raise WriteRefused(f"refuse to write a null proposed_value for {field}")
|
|
cursor = connection.cursor()
|
|
try:
|
|
for field, (_, proposed) in changes.items():
|
|
if proposed is not None:
|
|
assert_schema_ok(cursor, table, field, proposed)
|
|
fields = list(changes)
|
|
cursor.execute(f"SELECT {', '.join(fields)} FROM {table} WHERE {key_field} = ?", (product_code,))
|
|
row = cursor.fetchone()
|
|
if row is None:
|
|
raise WriteRefused(f"product {product_code} not found in {table}")
|
|
for field, live in zip(fields, row):
|
|
expected = changes[field][0]
|
|
if _norm(live) != _norm(expected):
|
|
raise WriteRefused(f"drift detected for {product_code}.{field}: expected {expected!r}, found {live!r}")
|
|
sets = ", ".join(f"{f} = ?" for f in fields)
|
|
cursor.execute(f"UPDATE {table} SET {sets} WHERE {key_field} = ?", tuple(changes[f][1] for f in fields) + (product_code,))
|
|
if cursor.rowcount != 1:
|
|
connection.rollback()
|
|
raise WriteRefused(f"expected rowcount 1, got {cursor.rowcount}; rolled back")
|
|
connection.commit()
|
|
cursor.execute(f"SELECT {', '.join(fields)} FROM {table} WHERE {key_field} = ?", (product_code,))
|
|
after = cursor.fetchone()
|
|
bad = [f for f, a in zip(fields, after) if _norm(a) != _norm(changes[f][1])]
|
|
if bad:
|
|
raise WriteRefused(f"post-write verification failed for {product_code}: {bad}")
|
|
return {"product_code": product_code, "rowcount": 1, "fields": {f: {"before": changes[f][0], "proposed": changes[f][1], "after": a} for f, a in zip(fields, after)}, "timestamp": now}
|
|
except Exception:
|
|
try:
|
|
connection.rollback()
|
|
except Exception:
|
|
pass
|
|
raise
|
|
finally:
|
|
cursor.close()
|
|
|
|
|
|
def _norm(v):
|
|
"""Comparable form: text trimmed, numbers as float (Decimal/int/float from the driver), None stays None."""
|
|
if v is None:
|
|
return None
|
|
if isinstance(v, (int, float)) or v.__class__.__name__ == "Decimal":
|
|
return round(float(v), 6)
|
|
return str(v).strip()
|