Add filter write/create/delete and jail-filter assign endpoints (Task 2.2)
- PUT /api/config/filters/{name}: updates failregex/ignoreregex/datepattern/
journalmatch in filter.d/{name}.local; validates regex via re.compile();
supports ?reload=true
- POST /api/config/filters: creates filter.d/{name}.local from FilterCreateRequest;
returns 409 if file already exists
- DELETE /api/config/filters/{name}: deletes .local only; returns 409 for
conf-only (readonly) filters
- POST /api/config/jails/{name}/filter: assigns filter to jail by writing
'filter = {name}' to jail.d/{jail}.local; supports ?reload=true
- New models: FilterUpdateRequest, FilterCreateRequest, AssignFilterRequest
- New service helpers: _safe_filter_name, _validate_regex_patterns,
_write_filter_local_sync, _set_jail_local_key_sync
- Fixed .local-only filter discovery in _parse_filters_sync (5-tuple return)
- Fixed get_filter extension stripping (.local is 6 chars not 5)
- Renamed file_config.py raw-write routes to /raw suffix
(PUT /filters/{name}/raw, POST /filters/raw) to avoid routing conflicts
- Full service + router tests; all 930 tests pass
This commit is contained in:
@@ -660,11 +660,12 @@ class TestParseFiltersSync:
|
||||
result = _parse_filters_sync(filter_d)
|
||||
|
||||
assert len(result) == 1
|
||||
name, filename, content, has_local = result[0]
|
||||
name, filename, content, has_local, source_path = result[0]
|
||||
assert name == "nginx"
|
||||
assert filename == "nginx.conf"
|
||||
assert "failregex" in content
|
||||
assert has_local is False
|
||||
assert source_path.endswith("nginx.conf")
|
||||
|
||||
def test_local_override_detected(self, tmp_path: Path) -> None:
|
||||
from app.services.config_file_service import _parse_filters_sync
|
||||
@@ -675,7 +676,7 @@ class TestParseFiltersSync:
|
||||
|
||||
result = _parse_filters_sync(filter_d)
|
||||
|
||||
_, _, _, has_local = result[0]
|
||||
_, _, _, has_local, _ = result[0]
|
||||
assert has_local is True
|
||||
|
||||
def test_local_content_appended_to_content(self, tmp_path: Path) -> None:
|
||||
@@ -687,7 +688,7 @@ class TestParseFiltersSync:
|
||||
|
||||
result = _parse_filters_sync(filter_d)
|
||||
|
||||
_, _, content, _ = result[0]
|
||||
_, _, content, _, _ = result[0]
|
||||
assert "local tweak" in content
|
||||
|
||||
def test_sorted_alphabetically(self, tmp_path: Path) -> None:
|
||||
@@ -852,3 +853,637 @@ class TestGetFilter:
|
||||
result = await get_filter(str(tmp_path), "/fake.sock", "sshd")
|
||||
|
||||
assert result.has_local_override is True
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _parse_filters_sync — .local-only filters (Task 2.2)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestParseFiltersSyncLocalOnly:
|
||||
"""Verify that .local-only user-created filters appear in results."""
|
||||
|
||||
def test_local_only_included(self, tmp_path: Path) -> None:
|
||||
from app.services.config_file_service import _parse_filters_sync
|
||||
|
||||
filter_d = tmp_path / "filter.d"
|
||||
_write(filter_d / "custom.local", "[Definition]\nfailregex = ^fail\n")
|
||||
|
||||
result = _parse_filters_sync(filter_d)
|
||||
|
||||
assert len(result) == 1
|
||||
name, filename, content, has_local, source_path = result[0]
|
||||
assert name == "custom"
|
||||
assert filename == "custom.local"
|
||||
assert has_local is False # .local-only: no conf to override
|
||||
assert source_path.endswith("custom.local")
|
||||
|
||||
def test_local_only_not_duplicated_when_conf_exists(self, tmp_path: Path) -> None:
|
||||
from app.services.config_file_service import _parse_filters_sync
|
||||
|
||||
filter_d = tmp_path / "filter.d"
|
||||
_write(filter_d / "sshd.conf", _FILTER_CONF)
|
||||
_write(filter_d / "sshd.local", "[Definition]\n")
|
||||
|
||||
result = _parse_filters_sync(filter_d)
|
||||
|
||||
# sshd should appear exactly once (conf + local, not as separate entry)
|
||||
names = [r[0] for r in result]
|
||||
assert names.count("sshd") == 1
|
||||
_, _, _, has_local, _ = result[0]
|
||||
assert has_local is True # conf + local → has_local_override
|
||||
|
||||
def test_local_only_sorted_with_conf_filters(self, tmp_path: Path) -> None:
|
||||
from app.services.config_file_service import _parse_filters_sync
|
||||
|
||||
filter_d = tmp_path / "filter.d"
|
||||
_write(filter_d / "zzz.conf", _FILTER_CONF)
|
||||
_write(filter_d / "aaa.local", "[Definition]\nfailregex = x\n")
|
||||
|
||||
result = _parse_filters_sync(filter_d)
|
||||
|
||||
names = [r[0] for r in result]
|
||||
assert names == ["aaa", "zzz"]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# get_filter — .local-only filter (Task 2.2)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestGetFilterLocalOnly:
|
||||
"""Verify that get_filter handles .local-only user-created filters."""
|
||||
|
||||
async def test_returns_local_only_filter(self, tmp_path: Path) -> None:
|
||||
from app.services.config_file_service import get_filter
|
||||
|
||||
filter_d = tmp_path / "filter.d"
|
||||
_write(
|
||||
filter_d / "custom.local",
|
||||
"[Definition]\nfailregex = ^fail from <HOST>\n",
|
||||
)
|
||||
|
||||
with patch(
|
||||
"app.services.config_file_service._get_active_jail_names",
|
||||
new=AsyncMock(return_value=set()),
|
||||
):
|
||||
result = await get_filter(str(tmp_path), "/fake.sock", "custom")
|
||||
|
||||
assert result.name == "custom"
|
||||
assert result.has_local_override is False
|
||||
assert result.source_file.endswith("custom.local")
|
||||
assert len(result.failregex) == 1
|
||||
|
||||
async def test_raises_when_neither_conf_nor_local(self, tmp_path: Path) -> None:
|
||||
from app.services.config_file_service import FilterNotFoundError, get_filter
|
||||
|
||||
with patch(
|
||||
"app.services.config_file_service._get_active_jail_names",
|
||||
new=AsyncMock(return_value=set()),
|
||||
), pytest.raises(FilterNotFoundError):
|
||||
await get_filter(str(tmp_path), "/fake.sock", "nonexistent")
|
||||
|
||||
async def test_accepts_local_extension(self, tmp_path: Path) -> None:
|
||||
from app.services.config_file_service import get_filter
|
||||
|
||||
filter_d = tmp_path / "filter.d"
|
||||
_write(filter_d / "custom.local", "[Definition]\nfailregex = x\n")
|
||||
|
||||
with patch(
|
||||
"app.services.config_file_service._get_active_jail_names",
|
||||
new=AsyncMock(return_value=set()),
|
||||
):
|
||||
result = await get_filter(str(tmp_path), "/fake.sock", "custom.local")
|
||||
|
||||
assert result.name == "custom"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _validate_regex_patterns (Task 2.2)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestValidateRegexPatterns:
|
||||
def test_valid_patterns_pass(self) -> None:
|
||||
from app.services.config_file_service import _validate_regex_patterns
|
||||
|
||||
_validate_regex_patterns([r"^fail from \S+", r"\d+\.\d+"])
|
||||
|
||||
def test_empty_list_passes(self) -> None:
|
||||
from app.services.config_file_service import _validate_regex_patterns
|
||||
|
||||
_validate_regex_patterns([])
|
||||
|
||||
def test_invalid_pattern_raises(self) -> None:
|
||||
from app.services.config_file_service import (
|
||||
FilterInvalidRegexError,
|
||||
_validate_regex_patterns,
|
||||
)
|
||||
|
||||
with pytest.raises(FilterInvalidRegexError) as exc_info:
|
||||
_validate_regex_patterns([r"[unclosed"])
|
||||
|
||||
assert "[unclosed" in exc_info.value.pattern
|
||||
|
||||
def test_mixed_valid_invalid_raises_on_first_invalid(self) -> None:
|
||||
from app.services.config_file_service import (
|
||||
FilterInvalidRegexError,
|
||||
_validate_regex_patterns,
|
||||
)
|
||||
|
||||
with pytest.raises(FilterInvalidRegexError) as exc_info:
|
||||
_validate_regex_patterns([r"\d+", r"[bad", r"\w+"])
|
||||
|
||||
assert "[bad" in exc_info.value.pattern
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _write_filter_local_sync (Task 2.2)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestWriteFilterLocalSync:
|
||||
def test_writes_file(self, tmp_path: Path) -> None:
|
||||
from app.services.config_file_service import _write_filter_local_sync
|
||||
|
||||
filter_d = tmp_path / "filter.d"
|
||||
filter_d.mkdir()
|
||||
_write_filter_local_sync(filter_d, "myfilter", "[Definition]\n")
|
||||
|
||||
local = filter_d / "myfilter.local"
|
||||
assert local.is_file()
|
||||
assert "[Definition]" in local.read_text()
|
||||
|
||||
def test_creates_filter_d_if_missing(self, tmp_path: Path) -> None:
|
||||
from app.services.config_file_service import _write_filter_local_sync
|
||||
|
||||
filter_d = tmp_path / "filter.d"
|
||||
_write_filter_local_sync(filter_d, "test", "[Definition]\n")
|
||||
assert (filter_d / "test.local").is_file()
|
||||
|
||||
def test_overwrites_existing_file(self, tmp_path: Path) -> None:
|
||||
from app.services.config_file_service import _write_filter_local_sync
|
||||
|
||||
filter_d = tmp_path / "filter.d"
|
||||
filter_d.mkdir()
|
||||
(filter_d / "myfilter.local").write_text("old content")
|
||||
|
||||
_write_filter_local_sync(filter_d, "myfilter", "[Definition]\nnew=1\n")
|
||||
|
||||
assert "new=1" in (filter_d / "myfilter.local").read_text()
|
||||
assert "old content" not in (filter_d / "myfilter.local").read_text()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _set_jail_local_key_sync (Task 2.2)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestSetJailLocalKeySync:
|
||||
def test_creates_new_local_file(self, tmp_path: Path) -> None:
|
||||
from app.services.config_file_service import _set_jail_local_key_sync
|
||||
|
||||
_set_jail_local_key_sync(tmp_path, "sshd", "filter", "myfilter")
|
||||
|
||||
local = tmp_path / "jail.d" / "sshd.local"
|
||||
assert local.is_file()
|
||||
content = local.read_text()
|
||||
assert "filter" in content
|
||||
assert "myfilter" in content
|
||||
|
||||
def test_updates_existing_local_file(self, tmp_path: Path) -> None:
|
||||
from app.services.config_file_service import _set_jail_local_key_sync
|
||||
|
||||
jail_d = tmp_path / "jail.d"
|
||||
jail_d.mkdir()
|
||||
(jail_d / "sshd.local").write_text(
|
||||
"[sshd]\nenabled = true\n"
|
||||
)
|
||||
|
||||
_set_jail_local_key_sync(tmp_path, "sshd", "filter", "newfilter")
|
||||
|
||||
content = (jail_d / "sshd.local").read_text()
|
||||
assert "newfilter" in content
|
||||
# Existing key is preserved
|
||||
assert "enabled" in content
|
||||
|
||||
def test_overwrites_existing_key(self, tmp_path: Path) -> None:
|
||||
from app.services.config_file_service import _set_jail_local_key_sync
|
||||
|
||||
jail_d = tmp_path / "jail.d"
|
||||
jail_d.mkdir()
|
||||
(jail_d / "sshd.local").write_text("[sshd]\nfilter = old\n")
|
||||
|
||||
_set_jail_local_key_sync(tmp_path, "sshd", "filter", "newfilter")
|
||||
|
||||
content = (jail_d / "sshd.local").read_text()
|
||||
assert "newfilter" in content
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# update_filter (Task 2.2)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
_FILTER_CONF_WITH_REGEX = """\
|
||||
[Definition]
|
||||
|
||||
failregex = ^fail from <HOST>
|
||||
^error from <HOST>
|
||||
|
||||
ignoreregex =
|
||||
"""
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestUpdateFilter:
|
||||
async def test_writes_local_override(self, tmp_path: Path) -> None:
|
||||
from app.models.config import FilterUpdateRequest
|
||||
from app.services.config_file_service import update_filter
|
||||
|
||||
filter_d = tmp_path / "filter.d"
|
||||
_write(filter_d / "sshd.conf", _FILTER_CONF_WITH_REGEX)
|
||||
|
||||
with patch(
|
||||
"app.services.config_file_service._get_active_jail_names",
|
||||
new=AsyncMock(return_value=set()),
|
||||
):
|
||||
result = await update_filter(
|
||||
str(tmp_path),
|
||||
"/fake.sock",
|
||||
"sshd",
|
||||
FilterUpdateRequest(failregex=[r"^new pattern <HOST>"]),
|
||||
)
|
||||
|
||||
local = filter_d / "sshd.local"
|
||||
assert local.is_file()
|
||||
assert result.name == "sshd"
|
||||
assert any("new pattern" in p for p in result.failregex)
|
||||
|
||||
async def test_accepts_conf_extension(self, tmp_path: Path) -> None:
|
||||
from app.models.config import FilterUpdateRequest
|
||||
from app.services.config_file_service import update_filter
|
||||
|
||||
filter_d = tmp_path / "filter.d"
|
||||
_write(filter_d / "sshd.conf", _FILTER_CONF_WITH_REGEX)
|
||||
|
||||
with patch(
|
||||
"app.services.config_file_service._get_active_jail_names",
|
||||
new=AsyncMock(return_value=set()),
|
||||
):
|
||||
result = await update_filter(
|
||||
str(tmp_path),
|
||||
"/fake.sock",
|
||||
"sshd.conf",
|
||||
FilterUpdateRequest(datepattern="%Y-%m-%d"),
|
||||
)
|
||||
|
||||
assert result.name == "sshd"
|
||||
|
||||
async def test_raises_filter_not_found(self, tmp_path: Path) -> None:
|
||||
from app.models.config import FilterUpdateRequest
|
||||
from app.services.config_file_service import FilterNotFoundError, update_filter
|
||||
|
||||
with patch(
|
||||
"app.services.config_file_service._get_active_jail_names",
|
||||
new=AsyncMock(return_value=set()),
|
||||
), pytest.raises(FilterNotFoundError):
|
||||
await update_filter(
|
||||
str(tmp_path),
|
||||
"/fake.sock",
|
||||
"missing",
|
||||
FilterUpdateRequest(),
|
||||
)
|
||||
|
||||
async def test_raises_on_invalid_regex(self, tmp_path: Path) -> None:
|
||||
from app.models.config import FilterUpdateRequest
|
||||
from app.services.config_file_service import (
|
||||
FilterInvalidRegexError,
|
||||
update_filter,
|
||||
)
|
||||
|
||||
filter_d = tmp_path / "filter.d"
|
||||
_write(filter_d / "sshd.conf", _FILTER_CONF_WITH_REGEX)
|
||||
|
||||
with patch(
|
||||
"app.services.config_file_service._get_active_jail_names",
|
||||
new=AsyncMock(return_value=set()),
|
||||
), pytest.raises(FilterInvalidRegexError):
|
||||
await update_filter(
|
||||
str(tmp_path),
|
||||
"/fake.sock",
|
||||
"sshd",
|
||||
FilterUpdateRequest(failregex=[r"[unclosed"]),
|
||||
)
|
||||
|
||||
async def test_raises_filter_name_error_for_invalid_name(self, tmp_path: Path) -> None:
|
||||
from app.models.config import FilterUpdateRequest
|
||||
from app.services.config_file_service import FilterNameError, update_filter
|
||||
|
||||
with pytest.raises(FilterNameError):
|
||||
await update_filter(
|
||||
str(tmp_path),
|
||||
"/fake.sock",
|
||||
"../etc/passwd",
|
||||
FilterUpdateRequest(),
|
||||
)
|
||||
|
||||
async def test_triggers_reload_when_requested(self, tmp_path: Path) -> None:
|
||||
from app.models.config import FilterUpdateRequest
|
||||
from app.services.config_file_service import update_filter
|
||||
|
||||
filter_d = tmp_path / "filter.d"
|
||||
_write(filter_d / "sshd.conf", _FILTER_CONF)
|
||||
|
||||
with patch(
|
||||
"app.services.config_file_service._get_active_jail_names",
|
||||
new=AsyncMock(return_value=set()),
|
||||
), patch(
|
||||
"app.services.config_file_service.jail_service.reload_all",
|
||||
new=AsyncMock(),
|
||||
) as mock_reload:
|
||||
await update_filter(
|
||||
str(tmp_path),
|
||||
"/fake.sock",
|
||||
"sshd",
|
||||
FilterUpdateRequest(journalmatch="_SYSTEMD_UNIT=sshd.service"),
|
||||
do_reload=True,
|
||||
)
|
||||
|
||||
mock_reload.assert_awaited_once()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# create_filter (Task 2.2)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestCreateFilter:
|
||||
async def test_creates_local_file(self, tmp_path: Path) -> None:
|
||||
from app.models.config import FilterCreateRequest
|
||||
from app.services.config_file_service import create_filter
|
||||
|
||||
with patch(
|
||||
"app.services.config_file_service._get_active_jail_names",
|
||||
new=AsyncMock(return_value=set()),
|
||||
):
|
||||
result = await create_filter(
|
||||
str(tmp_path),
|
||||
"/fake.sock",
|
||||
FilterCreateRequest(
|
||||
name="my-custom",
|
||||
failregex=[r"^fail from <HOST>"],
|
||||
),
|
||||
)
|
||||
|
||||
local = tmp_path / "filter.d" / "my-custom.local"
|
||||
assert local.is_file()
|
||||
assert result.name == "my-custom"
|
||||
assert result.source_file.endswith("my-custom.local")
|
||||
|
||||
async def test_raises_already_exists_when_conf_exists(self, tmp_path: Path) -> None:
|
||||
from app.models.config import FilterCreateRequest
|
||||
from app.services.config_file_service import FilterAlreadyExistsError, create_filter
|
||||
|
||||
filter_d = tmp_path / "filter.d"
|
||||
_write(filter_d / "sshd.conf", _FILTER_CONF)
|
||||
|
||||
with patch(
|
||||
"app.services.config_file_service._get_active_jail_names",
|
||||
new=AsyncMock(return_value=set()),
|
||||
), pytest.raises(FilterAlreadyExistsError):
|
||||
await create_filter(
|
||||
str(tmp_path),
|
||||
"/fake.sock",
|
||||
FilterCreateRequest(name="sshd"),
|
||||
)
|
||||
|
||||
async def test_raises_already_exists_when_local_exists(self, tmp_path: Path) -> None:
|
||||
from app.models.config import FilterCreateRequest
|
||||
from app.services.config_file_service import FilterAlreadyExistsError, create_filter
|
||||
|
||||
filter_d = tmp_path / "filter.d"
|
||||
_write(filter_d / "custom.local", "[Definition]\n")
|
||||
|
||||
with patch(
|
||||
"app.services.config_file_service._get_active_jail_names",
|
||||
new=AsyncMock(return_value=set()),
|
||||
), pytest.raises(FilterAlreadyExistsError):
|
||||
await create_filter(
|
||||
str(tmp_path),
|
||||
"/fake.sock",
|
||||
FilterCreateRequest(name="custom"),
|
||||
)
|
||||
|
||||
async def test_raises_invalid_regex(self, tmp_path: Path) -> None:
|
||||
from app.models.config import FilterCreateRequest
|
||||
from app.services.config_file_service import FilterInvalidRegexError, create_filter
|
||||
|
||||
with patch(
|
||||
"app.services.config_file_service._get_active_jail_names",
|
||||
new=AsyncMock(return_value=set()),
|
||||
), pytest.raises(FilterInvalidRegexError):
|
||||
await create_filter(
|
||||
str(tmp_path),
|
||||
"/fake.sock",
|
||||
FilterCreateRequest(name="bad", failregex=[r"[unclosed"]),
|
||||
)
|
||||
|
||||
async def test_raises_filter_name_error_for_invalid_name(self, tmp_path: Path) -> None:
|
||||
from app.models.config import FilterCreateRequest
|
||||
from app.services.config_file_service import FilterNameError, create_filter
|
||||
|
||||
with pytest.raises(FilterNameError):
|
||||
await create_filter(
|
||||
str(tmp_path),
|
||||
"/fake.sock",
|
||||
FilterCreateRequest(name="../etc/evil"),
|
||||
)
|
||||
|
||||
async def test_triggers_reload_when_requested(self, tmp_path: Path) -> None:
|
||||
from app.models.config import FilterCreateRequest
|
||||
from app.services.config_file_service import create_filter
|
||||
|
||||
with patch(
|
||||
"app.services.config_file_service._get_active_jail_names",
|
||||
new=AsyncMock(return_value=set()),
|
||||
), patch(
|
||||
"app.services.config_file_service.jail_service.reload_all",
|
||||
new=AsyncMock(),
|
||||
) as mock_reload:
|
||||
await create_filter(
|
||||
str(tmp_path),
|
||||
"/fake.sock",
|
||||
FilterCreateRequest(name="newfilter"),
|
||||
do_reload=True,
|
||||
)
|
||||
|
||||
mock_reload.assert_awaited_once()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# delete_filter (Task 2.2)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestDeleteFilter:
|
||||
async def test_deletes_local_file_when_conf_and_local_exist(
|
||||
self, tmp_path: Path
|
||||
) -> None:
|
||||
from app.services.config_file_service import delete_filter
|
||||
|
||||
filter_d = tmp_path / "filter.d"
|
||||
_write(filter_d / "sshd.conf", _FILTER_CONF)
|
||||
_write(filter_d / "sshd.local", "[Definition]\n")
|
||||
|
||||
await delete_filter(str(tmp_path), "sshd")
|
||||
|
||||
assert not (filter_d / "sshd.local").exists()
|
||||
assert (filter_d / "sshd.conf").exists()
|
||||
|
||||
async def test_deletes_local_only_filter(self, tmp_path: Path) -> None:
|
||||
from app.services.config_file_service import delete_filter
|
||||
|
||||
filter_d = tmp_path / "filter.d"
|
||||
_write(filter_d / "custom.local", "[Definition]\n")
|
||||
|
||||
await delete_filter(str(tmp_path), "custom")
|
||||
|
||||
assert not (filter_d / "custom.local").exists()
|
||||
|
||||
async def test_raises_readonly_for_conf_only(self, tmp_path: Path) -> None:
|
||||
from app.services.config_file_service import FilterReadonlyError, delete_filter
|
||||
|
||||
filter_d = tmp_path / "filter.d"
|
||||
_write(filter_d / "sshd.conf", _FILTER_CONF)
|
||||
|
||||
with pytest.raises(FilterReadonlyError):
|
||||
await delete_filter(str(tmp_path), "sshd")
|
||||
|
||||
async def test_raises_not_found_for_missing_filter(self, tmp_path: Path) -> None:
|
||||
from app.services.config_file_service import FilterNotFoundError, delete_filter
|
||||
|
||||
with pytest.raises(FilterNotFoundError):
|
||||
await delete_filter(str(tmp_path), "nonexistent")
|
||||
|
||||
async def test_accepts_filter_name_error_for_invalid_name(
|
||||
self, tmp_path: Path
|
||||
) -> None:
|
||||
from app.services.config_file_service import FilterNameError, delete_filter
|
||||
|
||||
with pytest.raises(FilterNameError):
|
||||
await delete_filter(str(tmp_path), "../evil")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# assign_filter_to_jail (Task 2.2)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestAssignFilterToJail:
|
||||
async def test_writes_filter_key_to_jail_local(self, tmp_path: Path) -> None:
|
||||
from app.models.config import AssignFilterRequest
|
||||
from app.services.config_file_service import assign_filter_to_jail
|
||||
|
||||
# Setup: jail.conf with sshd jail, filter.conf for "myfilter"
|
||||
_write(tmp_path / "jail.conf", JAIL_CONF)
|
||||
_write(tmp_path / "filter.d" / "myfilter.conf", _FILTER_CONF)
|
||||
|
||||
with patch(
|
||||
"app.services.config_file_service._get_active_jail_names",
|
||||
new=AsyncMock(return_value=set()),
|
||||
):
|
||||
await assign_filter_to_jail(
|
||||
str(tmp_path),
|
||||
"/fake.sock",
|
||||
"sshd",
|
||||
AssignFilterRequest(filter_name="myfilter"),
|
||||
)
|
||||
|
||||
local = tmp_path / "jail.d" / "sshd.local"
|
||||
assert local.is_file()
|
||||
content = local.read_text()
|
||||
assert "myfilter" in content
|
||||
|
||||
async def test_raises_jail_not_found(self, tmp_path: Path) -> None:
|
||||
from app.models.config import AssignFilterRequest
|
||||
from app.services.config_file_service import (
|
||||
JailNotFoundInConfigError,
|
||||
assign_filter_to_jail,
|
||||
)
|
||||
|
||||
_write(tmp_path / "filter.d" / "sshd.conf", _FILTER_CONF)
|
||||
|
||||
with pytest.raises(JailNotFoundInConfigError):
|
||||
await assign_filter_to_jail(
|
||||
str(tmp_path),
|
||||
"/fake.sock",
|
||||
"nonexistent-jail",
|
||||
AssignFilterRequest(filter_name="sshd"),
|
||||
)
|
||||
|
||||
async def test_raises_filter_not_found(self, tmp_path: Path) -> None:
|
||||
from app.models.config import AssignFilterRequest
|
||||
from app.services.config_file_service import FilterNotFoundError, assign_filter_to_jail
|
||||
|
||||
_write(tmp_path / "jail.conf", JAIL_CONF)
|
||||
|
||||
with pytest.raises(FilterNotFoundError):
|
||||
await assign_filter_to_jail(
|
||||
str(tmp_path),
|
||||
"/fake.sock",
|
||||
"sshd",
|
||||
AssignFilterRequest(filter_name="nonexistent-filter"),
|
||||
)
|
||||
|
||||
async def test_raises_jail_name_error_for_invalid_name(self, tmp_path: Path) -> None:
|
||||
from app.models.config import AssignFilterRequest
|
||||
from app.services.config_file_service import JailNameError, assign_filter_to_jail
|
||||
|
||||
with pytest.raises(JailNameError):
|
||||
await assign_filter_to_jail(
|
||||
str(tmp_path),
|
||||
"/fake.sock",
|
||||
"../etc/evil",
|
||||
AssignFilterRequest(filter_name="sshd"),
|
||||
)
|
||||
|
||||
async def test_raises_filter_name_error_for_invalid_filter(
|
||||
self, tmp_path: Path
|
||||
) -> None:
|
||||
from app.models.config import AssignFilterRequest
|
||||
from app.services.config_file_service import FilterNameError, assign_filter_to_jail
|
||||
|
||||
with pytest.raises(FilterNameError):
|
||||
await assign_filter_to_jail(
|
||||
str(tmp_path),
|
||||
"/fake.sock",
|
||||
"sshd",
|
||||
AssignFilterRequest(filter_name="../etc/evil"),
|
||||
)
|
||||
|
||||
async def test_triggers_reload_when_requested(self, tmp_path: Path) -> None:
|
||||
from app.models.config import AssignFilterRequest
|
||||
from app.services.config_file_service import assign_filter_to_jail
|
||||
|
||||
_write(tmp_path / "jail.conf", JAIL_CONF)
|
||||
_write(tmp_path / "filter.d" / "myfilter.conf", _FILTER_CONF)
|
||||
|
||||
with patch(
|
||||
"app.services.config_file_service.jail_service.reload_all",
|
||||
new=AsyncMock(),
|
||||
) as mock_reload:
|
||||
await assign_filter_to_jail(
|
||||
str(tmp_path),
|
||||
"/fake.sock",
|
||||
"sshd",
|
||||
AssignFilterRequest(filter_name="myfilter"),
|
||||
do_reload=True,
|
||||
)
|
||||
|
||||
mock_reload.assert_awaited_once()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user