Redact devserver database credentials

This commit is contained in:
2026-07-21 03:18:08 +02:00
parent a98475f7bc
commit 844f934379
2 changed files with 27 additions and 2 deletions

View File

@@ -10,6 +10,9 @@ from collections.abc import Iterable, Sequence
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
from sqlalchemy.engine import make_url
from sqlalchemy.exc import ArgumentError
from govoplan_core.core.discovery import iter_module_entry_points from govoplan_core.core.discovery import iter_module_entry_points
from govoplan_core.core.module_management import load_startup_enabled_modules, startup_candidate_module_ids from govoplan_core.core.module_management import load_startup_enabled_modules, startup_candidate_module_ids
from govoplan_core.core.modules import ModuleManifest from govoplan_core.core.modules import ModuleManifest
@@ -112,6 +115,14 @@ def validate_sqlite_database_url(database_url: str) -> None:
) )
def redacted_database_url(database_url: str) -> str:
try:
return make_url(database_url).render_as_string(hide_password=True)
except (ArgumentError, TypeError, ValueError):
scheme = database_url.partition(":")[0].strip()
return f"{scheme}:<redacted>" if scheme else "<redacted>"
def _env_truthy(value: str | None) -> bool: def _env_truthy(value: str | None) -> bool:
return value is not None and value.strip().lower() in {"1", "true", "yes", "on"} return value is not None and value.strip().lower() in {"1", "true", "yes", "on"}
@@ -297,11 +308,11 @@ def print_devserver_summary(state: DevserverState, *, app: str, no_reload: bool)
print(f"Config: {state.config_path or DEFAULT_CONFIG}") print(f"Config: {state.config_path or DEFAULT_CONFIG}")
print(f"Runtime root: {state.runtime_root}") print(f"Runtime root: {state.runtime_root}")
if state.database_url: if state.database_url:
print(f"Database: {state.database_url}") print(f"Database: {redacted_database_url(state.database_url)}")
if state.database_url.startswith("postgresql"): if state.database_url.startswith("postgresql"):
pgtools_url = os.getenv("GOVOPLAN_DATABASE_URL_PGTOOLS") pgtools_url = os.getenv("GOVOPLAN_DATABASE_URL_PGTOOLS")
if pgtools_url: if pgtools_url:
print(f"PostgreSQL tools URL: {pgtools_url}") print(f"PostgreSQL tools URL: {redacted_database_url(pgtools_url)}")
if state.bootstrap_db_path is not None: if state.bootstrap_db_path is not None:
bootstrap_state = "enabled" if getattr(state.config.settings, "dev_bootstrap_enabled", False) else "disabled by DEV_BOOTSTRAP_ENABLED" bootstrap_state = "enabled" if getattr(state.config.settings, "dev_bootstrap_enabled", False) else "disabled by DEV_BOOTSTRAP_ENABLED"
print(f"Dev bootstrap for missing SQLite DB: {bootstrap_state} ({state.bootstrap_db_path})") print(f"Dev bootstrap for missing SQLite DB: {bootstrap_state} ({state.bootstrap_db_path})")

View File

@@ -7,8 +7,22 @@ import tempfile
import unittest import unittest
from pathlib import Path from pathlib import Path
from govoplan_core.devserver import redacted_database_url
class DevserverSmokeTests(unittest.TestCase): class DevserverSmokeTests(unittest.TestCase):
def test_database_url_redaction_hides_passwords(self) -> None:
rendered = redacted_database_url(
"postgresql+psycopg://govoplan:database-secret@db.example.test/govoplan"
)
self.assertEqual(
rendered,
"postgresql+psycopg://govoplan:***@db.example.test/govoplan",
)
self.assertNotIn("database-secret", rendered)
self.assertEqual(redacted_database_url("://database-secret"), "<redacted>")
def test_smoke_mode_bootstraps_missing_local_sqlite_database(self) -> None: def test_smoke_mode_bootstraps_missing_local_sqlite_database(self) -> None:
repo_root = Path(__file__).resolve().parents[1] repo_root = Path(__file__).resolve().parents[1]
src_root = repo_root / "src" src_root = repo_root / "src"