Redact devserver database credentials
This commit is contained in:
@@ -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})")
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user