feat(expenses): backfill historical claims into expense cases
This commit is contained in:
237
server/src/app/db/maintenance_database_target.py
Normal file
237
server/src/app/db/maintenance_database_target.py
Normal file
@@ -0,0 +1,237 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy.engine import URL, make_url
|
||||
from sqlalchemy.exc import ArgumentError
|
||||
|
||||
DEFAULT_POSTGRESQL_PORT = 5432
|
||||
DISPOSABLE_TARGET_MARKERS = ("migration-probe", "disposable-probe")
|
||||
SENSITIVE_QUERY_KEY_PARTS = (
|
||||
"credential",
|
||||
"passfile",
|
||||
"password",
|
||||
"secret",
|
||||
"token",
|
||||
)
|
||||
FORBIDDEN_ROUTING_QUERY_KEYS = frozenset(
|
||||
{
|
||||
"database",
|
||||
"dbname",
|
||||
"host",
|
||||
"hostaddr",
|
||||
"options",
|
||||
"port",
|
||||
"service",
|
||||
"servicefile",
|
||||
"user",
|
||||
"username",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class MaintenanceDatabaseTargetError(ValueError):
|
||||
"""维护命令数据库目标不满足安全约束。"""
|
||||
|
||||
def __init__(self, code: str, message: str) -> None:
|
||||
super().__init__(message)
|
||||
self.code = code
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class MaintenanceDatabaseTarget:
|
||||
host: str
|
||||
port: int
|
||||
database: str
|
||||
username: str | None
|
||||
sanitized_url: str
|
||||
exact_target: str
|
||||
is_disposable: bool
|
||||
|
||||
|
||||
def _raise_target_error(code: str, message: str) -> None:
|
||||
raise MaintenanceDatabaseTargetError(code, message)
|
||||
|
||||
|
||||
def _normalize_host(value: str) -> str:
|
||||
normalized = str(value or "").strip().lower()
|
||||
if normalized.startswith("[") and normalized.endswith("]"):
|
||||
normalized = normalized[1:-1]
|
||||
return normalized.rstrip(".")
|
||||
|
||||
|
||||
def _normalize_probe_component(value: str) -> str:
|
||||
return re.sub(r"[^a-z0-9]+", "-", str(value or "").lower()).strip("-")
|
||||
|
||||
|
||||
def _matches_disposable_marker(value: str) -> bool:
|
||||
normalized = _normalize_probe_component(value)
|
||||
return any(
|
||||
normalized == marker
|
||||
or normalized.startswith(f"{marker}-")
|
||||
or normalized.startswith(f"x-financial-{marker}-")
|
||||
for marker in DISPOSABLE_TARGET_MARKERS
|
||||
)
|
||||
|
||||
|
||||
def is_disposable_maintenance_target(*, host: str, database: str) -> bool:
|
||||
"""只有主机名和数据库名都带显式 probe 标记时才视为一次性目标。"""
|
||||
|
||||
return _matches_disposable_marker(host) and _matches_disposable_marker(database)
|
||||
|
||||
|
||||
def _sanitize_query(url: URL) -> URL:
|
||||
sanitized_query: dict[str, Any] = {}
|
||||
for key, value in url.query.items():
|
||||
normalized_key = str(key).lower()
|
||||
if any(part in normalized_key for part in SENSITIVE_QUERY_KEY_PARTS):
|
||||
if isinstance(value, tuple):
|
||||
sanitized_query[key] = tuple("***" for _ in value)
|
||||
else:
|
||||
sanitized_query[key] = "***"
|
||||
continue
|
||||
sanitized_query[key] = value
|
||||
return url.set(query=sanitized_query)
|
||||
|
||||
|
||||
def _format_target_host(host: str) -> str:
|
||||
return f"[{host}]" if ":" in host else host
|
||||
|
||||
|
||||
def parse_maintenance_database_target(
|
||||
database_url: str,
|
||||
*,
|
||||
expected_host: str,
|
||||
expected_database: str,
|
||||
) -> MaintenanceDatabaseTarget:
|
||||
"""解析显式数据库 URL,并核对操作人声明的目标主机和数据库。"""
|
||||
|
||||
raw_url = str(database_url or "").strip()
|
||||
if not raw_url:
|
||||
_raise_target_error(
|
||||
"database_url_required",
|
||||
"维护命令必须显式提供 DATABASE_URL,禁止回退到环境文件或默认配置。",
|
||||
)
|
||||
|
||||
try:
|
||||
parsed_url = make_url(raw_url)
|
||||
except (ArgumentError, TypeError, ValueError) as exc:
|
||||
raise MaintenanceDatabaseTargetError(
|
||||
"invalid_database_url",
|
||||
"DATABASE_URL 不是有效的 SQLAlchemy 数据库 URL。",
|
||||
) from exc
|
||||
|
||||
if parsed_url.get_backend_name() != "postgresql":
|
||||
_raise_target_error(
|
||||
"postgresql_required",
|
||||
"维护命令只允许连接 PostgreSQL 数据库。",
|
||||
)
|
||||
|
||||
routing_query_keys = sorted(
|
||||
str(key).lower()
|
||||
for key in parsed_url.query
|
||||
if str(key).lower() in FORBIDDEN_ROUTING_QUERY_KEYS
|
||||
)
|
||||
if routing_query_keys:
|
||||
_raise_target_error(
|
||||
"database_routing_query_forbidden",
|
||||
f"DATABASE_URL 查询参数不得覆盖连接目标或 schema:{', '.join(routing_query_keys)}",
|
||||
)
|
||||
|
||||
host = _normalize_host(parsed_url.host or "")
|
||||
if not host:
|
||||
_raise_target_error(
|
||||
"database_host_required",
|
||||
"DATABASE_URL 必须包含显式 PostgreSQL 主机名。",
|
||||
)
|
||||
|
||||
database = str(parsed_url.database or "").strip()
|
||||
if not database:
|
||||
_raise_target_error(
|
||||
"database_name_required",
|
||||
"DATABASE_URL 必须包含显式数据库名。",
|
||||
)
|
||||
|
||||
normalized_expected_host = _normalize_host(expected_host)
|
||||
if not normalized_expected_host:
|
||||
_raise_target_error(
|
||||
"expected_host_required",
|
||||
"必须通过 expected_host 声明预期数据库主机。",
|
||||
)
|
||||
if host != normalized_expected_host:
|
||||
_raise_target_error(
|
||||
"expected_host_mismatch",
|
||||
f"DATABASE_URL 主机与预期不一致:actual={host}; expected={normalized_expected_host}",
|
||||
)
|
||||
|
||||
normalized_expected_database = str(expected_database or "").strip()
|
||||
if not normalized_expected_database:
|
||||
_raise_target_error(
|
||||
"expected_database_required",
|
||||
"必须通过 expected_database 声明预期数据库名。",
|
||||
)
|
||||
if database != normalized_expected_database:
|
||||
_raise_target_error(
|
||||
"expected_database_mismatch",
|
||||
"DATABASE_URL 数据库名与预期不一致:"
|
||||
f"actual={database}; expected={normalized_expected_database}",
|
||||
)
|
||||
|
||||
port = int(parsed_url.port or DEFAULT_POSTGRESQL_PORT)
|
||||
exact_target = f"{_format_target_host(host)}:{port}/{database}"
|
||||
sanitized_url = _sanitize_query(parsed_url).render_as_string(hide_password=True)
|
||||
|
||||
return MaintenanceDatabaseTarget(
|
||||
host=host,
|
||||
port=port,
|
||||
database=database,
|
||||
username=parsed_url.username,
|
||||
sanitized_url=sanitized_url,
|
||||
exact_target=exact_target,
|
||||
is_disposable=is_disposable_maintenance_target(host=host, database=database),
|
||||
)
|
||||
|
||||
|
||||
def validate_maintenance_database_target(
|
||||
database_url: str,
|
||||
*,
|
||||
expected_host: str,
|
||||
expected_database: str,
|
||||
apply: bool = False,
|
||||
allow_non_disposable: bool = False,
|
||||
confirm_target: str | None = None,
|
||||
) -> MaintenanceDatabaseTarget:
|
||||
"""校验维护目标;非一次性数据库 apply 必须显式放行并精确确认目标。"""
|
||||
|
||||
target = parse_maintenance_database_target(
|
||||
database_url,
|
||||
expected_host=expected_host,
|
||||
expected_database=expected_database,
|
||||
)
|
||||
if not apply:
|
||||
return target
|
||||
|
||||
normalized_confirmation = str(confirm_target or "").strip()
|
||||
if target.is_disposable:
|
||||
if normalized_confirmation and normalized_confirmation != target.exact_target:
|
||||
_raise_target_error(
|
||||
"confirm_target_mismatch",
|
||||
"confirm_target 与解析后的数据库目标不一致:"
|
||||
f"actual={normalized_confirmation}; expected={target.exact_target}",
|
||||
)
|
||||
return target
|
||||
|
||||
if not allow_non_disposable:
|
||||
_raise_target_error(
|
||||
"non_disposable_apply_forbidden",
|
||||
"非一次性数据库 apply 必须显式启用 allow_non_disposable。",
|
||||
)
|
||||
if normalized_confirmation != target.exact_target:
|
||||
_raise_target_error(
|
||||
"confirm_target_mismatch",
|
||||
"非一次性数据库 apply 必须通过 confirm_target 精确确认:"
|
||||
f"expected={target.exact_target}",
|
||||
)
|
||||
return target
|
||||
Reference in New Issue
Block a user