Add ensure_jail_configs startup check for required jail config files
On startup BanGUI now verifies that the four fail2ban jail config files required by its two custom jails (manual-Jail and blocklist-import) are present in `$fail2ban_config_dir/jail.d`. Any missing file is created with the correct default content; existing files are never overwritten. Files managed: - manual-Jail.conf (enabled=false template) - manual-Jail.local (enabled=true override) - blocklist-import.conf (enabled=false template) - blocklist-import.local (enabled=true override) The check runs in the lifespan hook immediately after logging is configured, before the database is opened.
This commit is contained in:
@@ -330,6 +330,7 @@ class TestLifespanDatabaseDirectoryCreation:
|
||||
patch("app.tasks.geo_cache_flush.register"),
|
||||
patch("app.tasks.geo_re_resolve.register"),
|
||||
patch("app.main.AsyncIOScheduler", return_value=mock_scheduler),
|
||||
patch("app.main.ensure_jail_configs"),
|
||||
):
|
||||
async with _lifespan(app):
|
||||
assert nested_db.parent.exists(), (
|
||||
@@ -372,6 +373,7 @@ class TestLifespanDatabaseDirectoryCreation:
|
||||
patch("app.tasks.geo_cache_flush.register"),
|
||||
patch("app.tasks.geo_re_resolve.register"),
|
||||
patch("app.main.AsyncIOScheduler", return_value=mock_scheduler),
|
||||
patch("app.main.ensure_jail_configs"),
|
||||
):
|
||||
# Should not raise FileExistsError or similar.
|
||||
async with _lifespan(app):
|
||||
|
||||
134
backend/tests/test_utils/test_jail_config.py
Normal file
134
backend/tests/test_utils/test_jail_config.py
Normal file
@@ -0,0 +1,134 @@
|
||||
"""Tests for app.utils.jail_config.ensure_jail_configs."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from app.utils.jail_config import (
|
||||
_BLOCKLIST_IMPORT_CONF,
|
||||
_BLOCKLIST_IMPORT_LOCAL,
|
||||
_MANUAL_JAIL_CONF,
|
||||
_MANUAL_JAIL_LOCAL,
|
||||
ensure_jail_configs,
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Expected filenames
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_MANUAL_CONF = "manual-Jail.conf"
|
||||
_MANUAL_LOCAL = "manual-Jail.local"
|
||||
_BLOCKLIST_CONF = "blocklist-import.conf"
|
||||
_BLOCKLIST_LOCAL = "blocklist-import.local"
|
||||
|
||||
_ALL_FILES = [_MANUAL_CONF, _MANUAL_LOCAL, _BLOCKLIST_CONF, _BLOCKLIST_LOCAL]
|
||||
|
||||
_CONTENT_MAP: dict[str, str] = {
|
||||
_MANUAL_CONF: _MANUAL_JAIL_CONF,
|
||||
_MANUAL_LOCAL: _MANUAL_JAIL_LOCAL,
|
||||
_BLOCKLIST_CONF: _BLOCKLIST_IMPORT_CONF,
|
||||
_BLOCKLIST_LOCAL: _BLOCKLIST_IMPORT_LOCAL,
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _read(jail_d: Path, filename: str) -> str:
|
||||
return (jail_d / filename).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests: ensure_jail_configs
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestEnsureJailConfigs:
|
||||
def test_all_missing_creates_all_four(self, tmp_path: Path) -> None:
|
||||
"""All four files are created when the directory is empty."""
|
||||
jail_d = tmp_path / "jail.d"
|
||||
ensure_jail_configs(jail_d)
|
||||
|
||||
for name in _ALL_FILES:
|
||||
assert (jail_d / name).exists(), f"{name} should have been created"
|
||||
assert _read(jail_d, name) == _CONTENT_MAP[name]
|
||||
|
||||
def test_all_missing_creates_correct_content(self, tmp_path: Path) -> None:
|
||||
"""Each created file has exactly the expected default content."""
|
||||
jail_d = tmp_path / "jail.d"
|
||||
ensure_jail_configs(jail_d)
|
||||
|
||||
# .conf files must set enabled = false
|
||||
for conf_file in (_MANUAL_CONF, _BLOCKLIST_CONF):
|
||||
content = _read(jail_d, conf_file)
|
||||
assert "enabled = false" in content
|
||||
|
||||
# .local files must set enabled = true and nothing else
|
||||
for local_file in (_MANUAL_LOCAL, _BLOCKLIST_LOCAL):
|
||||
content = _read(jail_d, local_file)
|
||||
assert "enabled = true" in content
|
||||
|
||||
def test_all_present_overwrites_nothing(self, tmp_path: Path) -> None:
|
||||
"""Existing files are never overwritten."""
|
||||
jail_d = tmp_path / "jail.d"
|
||||
jail_d.mkdir()
|
||||
|
||||
sentinel = "# EXISTING CONTENT — must not be replaced\n"
|
||||
for name in _ALL_FILES:
|
||||
(jail_d / name).write_text(sentinel, encoding="utf-8")
|
||||
|
||||
ensure_jail_configs(jail_d)
|
||||
|
||||
for name in _ALL_FILES:
|
||||
assert _read(jail_d, name) == sentinel, (
|
||||
f"{name} should not have been overwritten"
|
||||
)
|
||||
|
||||
def test_only_local_files_missing_creates_only_locals(
|
||||
self, tmp_path: Path
|
||||
) -> None:
|
||||
"""Only the .local files are created when the .conf files already exist."""
|
||||
jail_d = tmp_path / "jail.d"
|
||||
jail_d.mkdir()
|
||||
|
||||
sentinel = "# pre-existing conf\n"
|
||||
for conf_file in (_MANUAL_CONF, _BLOCKLIST_CONF):
|
||||
(jail_d / conf_file).write_text(sentinel, encoding="utf-8")
|
||||
|
||||
ensure_jail_configs(jail_d)
|
||||
|
||||
# .conf files must remain unchanged
|
||||
for conf_file in (_MANUAL_CONF, _BLOCKLIST_CONF):
|
||||
assert _read(jail_d, conf_file) == sentinel
|
||||
|
||||
# .local files must have been created with correct content
|
||||
for local_file, expected in (
|
||||
(_MANUAL_LOCAL, _MANUAL_JAIL_LOCAL),
|
||||
(_BLOCKLIST_LOCAL, _BLOCKLIST_IMPORT_LOCAL),
|
||||
):
|
||||
assert (jail_d / local_file).exists(), f"{local_file} should have been created"
|
||||
assert _read(jail_d, local_file) == expected
|
||||
|
||||
def test_creates_jail_d_directory_if_missing(self, tmp_path: Path) -> None:
|
||||
"""The jail.d directory is created automatically when absent."""
|
||||
jail_d = tmp_path / "nested" / "jail.d"
|
||||
assert not jail_d.exists()
|
||||
ensure_jail_configs(jail_d)
|
||||
assert jail_d.is_dir()
|
||||
|
||||
def test_idempotent_on_repeated_calls(self, tmp_path: Path) -> None:
|
||||
"""Calling ensure_jail_configs twice does not alter any file."""
|
||||
jail_d = tmp_path / "jail.d"
|
||||
ensure_jail_configs(jail_d)
|
||||
|
||||
# Record content after first call
|
||||
first_pass = {name: _read(jail_d, name) for name in _ALL_FILES}
|
||||
|
||||
ensure_jail_configs(jail_d)
|
||||
|
||||
for name in _ALL_FILES:
|
||||
assert _read(jail_d, name) == first_pass[name], (
|
||||
f"{name} changed on second call"
|
||||
)
|
||||
Reference in New Issue
Block a user