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:
2026-03-13 18:13:03 +01:00
parent 4c138424a5
commit e15ad8fb62
8 changed files with 1885 additions and 64 deletions

View File

@@ -370,6 +370,79 @@ class FilterConfigUpdate(BaseModel):
journalmatch: str | None = Field(default=None)
class FilterUpdateRequest(BaseModel):
"""Payload for ``PUT /api/config/filters/{name}``.
Accepts only the user-editable ``[Definition]`` fields. Fields left as
``None`` are not changed; the existing value from the merged conf/local is
preserved.
"""
model_config = ConfigDict(strict=True)
failregex: list[str] | None = Field(
default=None,
description="Updated failure-detection regex patterns. ``None`` = keep existing.",
)
ignoreregex: list[str] | None = Field(
default=None,
description="Updated bypass-ban regex patterns. ``None`` = keep existing.",
)
datepattern: str | None = Field(
default=None,
description="Custom date-parsing pattern. ``None`` = keep existing.",
)
journalmatch: str | None = Field(
default=None,
description="Systemd journal match expression. ``None`` = keep existing.",
)
class FilterCreateRequest(BaseModel):
"""Payload for ``POST /api/config/filters``.
Creates a new user-defined filter at ``filter.d/{name}.local``.
"""
model_config = ConfigDict(strict=True)
name: str = Field(
...,
description="Filter base name (e.g. ``my-custom-filter``). Must not already exist in ``filter.d/``.",
)
failregex: list[str] = Field(
default_factory=list,
description="Failure-detection regex patterns.",
)
ignoreregex: list[str] = Field(
default_factory=list,
description="Regex patterns that bypass ban logic.",
)
prefregex: str | None = Field(
default=None,
description="Prefix regex prepended to every failregex.",
)
datepattern: str | None = Field(
default=None,
description="Custom date-parsing pattern.",
)
journalmatch: str | None = Field(
default=None,
description="Systemd journal match expression.",
)
class AssignFilterRequest(BaseModel):
"""Payload for ``POST /api/config/jails/{jail_name}/filter``."""
model_config = ConfigDict(strict=True)
filter_name: str = Field(
...,
description="Filter base name to assign to the jail (e.g. ``sshd``).",
)
class FilterListResponse(BaseModel):
"""Response for ``GET /api/config/filters``."""