Add filter discovery endpoints with active/inactive status (Task 2.1)
- Add list_filters() and get_filter() to config_file_service.py:
scans filter.d/, parses [Definition] + [Init] sections, merges .local
overrides, and cross-references running jails to set active/used_by_jails
- Add FilterConfig.active, used_by_jails, source_file, has_local_override
fields to the Pydantic model; add FilterListResponse and FilterNotFoundError
- Add GET /api/config/filters and GET /api/config/filters/{name} to config.py
- Remove the shadowed GET /api/config/filters list route from file_config.py;
rename GET /api/config/filters/{name} raw variant to /filters/{name}/raw
- Update frontend: fetchFilterFiles() adapts FilterListResponse -> ConfFilesResponse;
add fetchFilters() and fetchFilter() to api/config.ts; remove unused
fetchFilterFiles/fetchActionFiles calls from useConfigActiveStatus
- Fix ConfigPageLogPath test mock to include fetchInactiveJails and related
exports introduced by Stage 1
- Backend: 169 tests pass, mypy --strict clean, ruff clean
- Frontend: 63 tests pass, tsc --noEmit clean, eslint clean
This commit is contained in:
@@ -15,6 +15,8 @@ global settings, test regex patterns, add log paths, and preview log files.
|
||||
* ``POST /api/config/regex-test`` — test a regex pattern
|
||||
* ``POST /api/config/jails/{name}/logpath`` — add a log path to a jail
|
||||
* ``POST /api/config/preview-log`` — preview log matches
|
||||
* ``GET /api/config/filters`` — list all filters with active/inactive status
|
||||
* ``GET /api/config/filters/{name}`` — full parsed detail for one filter
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -27,6 +29,8 @@ from app.dependencies import AuthDep
|
||||
from app.models.config import (
|
||||
ActivateJailRequest,
|
||||
AddLogPathRequest,
|
||||
FilterConfig,
|
||||
FilterListResponse,
|
||||
GlobalConfigResponse,
|
||||
GlobalConfigUpdate,
|
||||
InactiveJailListResponse,
|
||||
@@ -44,6 +48,7 @@ from app.models.config import (
|
||||
from app.services import config_file_service, config_service, jail_service
|
||||
from app.services.config_file_service import (
|
||||
ConfigWriteError,
|
||||
FilterNotFoundError,
|
||||
JailAlreadyActiveError,
|
||||
JailAlreadyInactiveError,
|
||||
JailNameError,
|
||||
@@ -646,3 +651,83 @@ async def deactivate_jail(
|
||||
) from exc
|
||||
except Fail2BanConnectionError as exc:
|
||||
raise _bad_gateway(exc) from exc
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Filter discovery endpoints (Task 2.1)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@router.get(
|
||||
"/filters",
|
||||
response_model=FilterListResponse,
|
||||
summary="List all available filters with active/inactive status",
|
||||
)
|
||||
async def list_filters(
|
||||
request: Request,
|
||||
_auth: AuthDep,
|
||||
) -> FilterListResponse:
|
||||
"""Return all filters discovered in ``filter.d/`` with active/inactive status.
|
||||
|
||||
Scans ``{config_dir}/filter.d/`` for ``.conf`` files, merges any
|
||||
corresponding ``.local`` overrides, and cross-references each filter's
|
||||
name against the ``filter`` fields of currently running jails to determine
|
||||
whether it is active.
|
||||
|
||||
Active filters (those used by at least one running jail) are sorted to the
|
||||
top of the list; inactive filters follow. Both groups are sorted
|
||||
alphabetically within themselves.
|
||||
|
||||
Args:
|
||||
request: FastAPI request object.
|
||||
_auth: Validated session — enforces authentication.
|
||||
|
||||
Returns:
|
||||
:class:`~app.models.config.FilterListResponse` with all discovered
|
||||
filters.
|
||||
"""
|
||||
config_dir: str = request.app.state.settings.fail2ban_config_dir
|
||||
socket_path: str = request.app.state.settings.fail2ban_socket
|
||||
result = await config_file_service.list_filters(config_dir, socket_path)
|
||||
# Sort: active first (by name), then inactive (by name).
|
||||
result.filters.sort(key=lambda f: (not f.active, f.name.lower()))
|
||||
return result
|
||||
|
||||
|
||||
@router.get(
|
||||
"/filters/{name}",
|
||||
response_model=FilterConfig,
|
||||
summary="Return full parsed detail for a single filter",
|
||||
)
|
||||
async def get_filter(
|
||||
request: Request,
|
||||
_auth: AuthDep,
|
||||
name: Annotated[str, Path(description="Filter base name, e.g. ``sshd`` or ``sshd.conf``.")],
|
||||
) -> FilterConfig:
|
||||
"""Return the full parsed configuration and active/inactive status for one filter.
|
||||
|
||||
Reads ``{config_dir}/filter.d/{name}.conf``, merges any corresponding
|
||||
``.local`` override, and annotates the result with ``active``,
|
||||
``used_by_jails``, ``source_file``, and ``has_local_override``.
|
||||
|
||||
Args:
|
||||
request: FastAPI request object.
|
||||
_auth: Validated session — enforces authentication.
|
||||
name: Filter base name (with or without ``.conf`` extension).
|
||||
|
||||
Returns:
|
||||
:class:`~app.models.config.FilterConfig`.
|
||||
|
||||
Raises:
|
||||
HTTPException: 404 if the filter is not found in ``filter.d/``.
|
||||
HTTPException: 502 if fail2ban is unreachable.
|
||||
"""
|
||||
config_dir: str = request.app.state.settings.fail2ban_config_dir
|
||||
socket_path: str = request.app.state.settings.fail2ban_socket
|
||||
try:
|
||||
return await config_file_service.get_filter(config_dir, socket_path, name)
|
||||
except FilterNotFoundError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Filter not found: {name!r}",
|
||||
) from None
|
||||
|
||||
Reference in New Issue
Block a user