feat(stage-1): inactive jail discovery and activation
- Backend: config_file_service.py parses jail.conf/jail.local/jail.d/*
following fail2ban merge order; discovers jails not running in fail2ban
- Backend: 3 new API endpoints (GET /jails/inactive, POST /jails/{name}/activate,
POST /jails/{name}/deactivate); moved /jails/inactive before /jails/{name}
to fix route-ordering conflict
- Frontend: ActivateJailDialog component with optional parameter overrides
- Frontend: JailsTab extended with inactive jail list and InactiveJailDetail pane
- Frontend: JailsPage JailOverviewSection shows inactive jails with toggle
- Tests: 57 service tests + 16 router tests for all new endpoints (all pass)
- Docs: Features.md, Architekture.md, Tasks.md updated; Tasks 1.1-1.5 marked done
This commit is contained in:
@@ -3,15 +3,18 @@
|
||||
Provides endpoints to inspect and edit fail2ban jail configuration and
|
||||
global settings, test regex patterns, add log paths, and preview log files.
|
||||
|
||||
* ``GET /api/config/jails`` — list all jail configs
|
||||
* ``GET /api/config/jails/{name}`` — full config for one jail
|
||||
* ``PUT /api/config/jails/{name}`` — update a jail's config
|
||||
* ``GET /api/config/global`` — global fail2ban settings
|
||||
* ``PUT /api/config/global`` — update global settings
|
||||
* ``POST /api/config/reload`` — reload fail2ban
|
||||
* ``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/jails`` — list all jail configs
|
||||
* ``GET /api/config/jails/{name}`` — full config for one jail
|
||||
* ``PUT /api/config/jails/{name}`` — update a jail's config
|
||||
* ``GET /api/config/jails/inactive`` — list all inactive jails
|
||||
* ``POST /api/config/jails/{name}/activate`` — activate an inactive jail
|
||||
* ``POST /api/config/jails/{name}/deactivate`` — deactivate an active jail
|
||||
* ``GET /api/config/global`` — global fail2ban settings
|
||||
* ``PUT /api/config/global`` — update global settings
|
||||
* ``POST /api/config/reload`` — reload fail2ban
|
||||
* ``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
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -22,9 +25,12 @@ from fastapi import APIRouter, HTTPException, Path, Query, Request, status
|
||||
|
||||
from app.dependencies import AuthDep
|
||||
from app.models.config import (
|
||||
ActivateJailRequest,
|
||||
AddLogPathRequest,
|
||||
GlobalConfigResponse,
|
||||
GlobalConfigUpdate,
|
||||
InactiveJailListResponse,
|
||||
JailActivationResponse,
|
||||
JailConfigListResponse,
|
||||
JailConfigResponse,
|
||||
JailConfigUpdate,
|
||||
@@ -35,7 +41,14 @@ from app.models.config import (
|
||||
RegexTestRequest,
|
||||
RegexTestResponse,
|
||||
)
|
||||
from app.services import config_service, jail_service
|
||||
from app.services import config_file_service, config_service, jail_service
|
||||
from app.services.config_file_service import (
|
||||
ConfigWriteError,
|
||||
JailAlreadyActiveError,
|
||||
JailAlreadyInactiveError,
|
||||
JailNameError,
|
||||
JailNotFoundInConfigError,
|
||||
)
|
||||
from app.services.config_service import (
|
||||
ConfigOperationError,
|
||||
ConfigValidationError,
|
||||
@@ -113,6 +126,33 @@ async def get_jail_configs(
|
||||
raise _bad_gateway(exc) from exc
|
||||
|
||||
|
||||
@router.get(
|
||||
"/jails/inactive",
|
||||
response_model=InactiveJailListResponse,
|
||||
summary="List all inactive jails discovered in config files",
|
||||
)
|
||||
async def get_inactive_jails(
|
||||
request: Request,
|
||||
_auth: AuthDep,
|
||||
) -> InactiveJailListResponse:
|
||||
"""Return all jails defined in fail2ban config files that are not running.
|
||||
|
||||
Parses ``jail.conf``, ``jail.local``, and ``jail.d/`` following the
|
||||
fail2ban merge order. Jails that fail2ban currently reports as running
|
||||
are excluded; only truly inactive entries are returned.
|
||||
|
||||
Args:
|
||||
request: FastAPI request object.
|
||||
_auth: Validated session — enforces authentication.
|
||||
|
||||
Returns:
|
||||
:class:`~app.models.config.InactiveJailListResponse`.
|
||||
"""
|
||||
config_dir: str = request.app.state.settings.fail2ban_config_dir
|
||||
socket_path: str = request.app.state.settings.fail2ban_socket
|
||||
return await config_file_service.list_inactive_jails(config_dir, socket_path)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/jails/{name}",
|
||||
response_model=JailConfigResponse,
|
||||
@@ -495,3 +535,114 @@ async def update_map_color_thresholds(
|
||||
threshold_medium=body.threshold_medium,
|
||||
threshold_low=body.threshold_low,
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/jails/{name}/activate",
|
||||
response_model=JailActivationResponse,
|
||||
summary="Activate an inactive jail",
|
||||
)
|
||||
async def activate_jail(
|
||||
request: Request,
|
||||
_auth: AuthDep,
|
||||
name: _NamePath,
|
||||
body: ActivateJailRequest | None = None,
|
||||
) -> JailActivationResponse:
|
||||
"""Enable an inactive jail and reload fail2ban.
|
||||
|
||||
Writes ``enabled = true`` (plus any override values from the request
|
||||
body) to ``jail.d/{name}.local`` and triggers a full fail2ban reload so
|
||||
the jail starts immediately.
|
||||
|
||||
Args:
|
||||
request: FastAPI request object.
|
||||
_auth: Validated session.
|
||||
name: Name of the jail to activate.
|
||||
body: Optional override values (bantime, findtime, maxretry, port,
|
||||
logpath).
|
||||
|
||||
Returns:
|
||||
:class:`~app.models.config.JailActivationResponse`.
|
||||
|
||||
Raises:
|
||||
HTTPException: 400 if *name* contains invalid characters.
|
||||
HTTPException: 404 if *name* is not found in any config file.
|
||||
HTTPException: 409 if the jail is already active.
|
||||
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
|
||||
req = body if body is not None else ActivateJailRequest()
|
||||
|
||||
try:
|
||||
return await config_file_service.activate_jail(
|
||||
config_dir, socket_path, name, req
|
||||
)
|
||||
except JailNameError as exc:
|
||||
raise _bad_request(str(exc)) from exc
|
||||
except JailNotFoundInConfigError:
|
||||
raise _not_found(name) from None
|
||||
except JailAlreadyActiveError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail=f"Jail {name!r} is already active.",
|
||||
) from None
|
||||
except ConfigWriteError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to write config override: {exc}",
|
||||
) from exc
|
||||
except Fail2BanConnectionError as exc:
|
||||
raise _bad_gateway(exc) from exc
|
||||
|
||||
|
||||
@router.post(
|
||||
"/jails/{name}/deactivate",
|
||||
response_model=JailActivationResponse,
|
||||
summary="Deactivate an active jail",
|
||||
)
|
||||
async def deactivate_jail(
|
||||
request: Request,
|
||||
_auth: AuthDep,
|
||||
name: _NamePath,
|
||||
) -> JailActivationResponse:
|
||||
"""Disable an active jail and reload fail2ban.
|
||||
|
||||
Writes ``enabled = false`` to ``jail.d/{name}.local`` and triggers a
|
||||
full fail2ban reload so the jail stops immediately.
|
||||
|
||||
Args:
|
||||
request: FastAPI request object.
|
||||
_auth: Validated session.
|
||||
name: Name of the jail to deactivate.
|
||||
|
||||
Returns:
|
||||
:class:`~app.models.config.JailActivationResponse`.
|
||||
|
||||
Raises:
|
||||
HTTPException: 400 if *name* contains invalid characters.
|
||||
HTTPException: 404 if *name* is not found in any config file.
|
||||
HTTPException: 409 if the jail is already inactive.
|
||||
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.deactivate_jail(config_dir, socket_path, name)
|
||||
except JailNameError as exc:
|
||||
raise _bad_request(str(exc)) from exc
|
||||
except JailNotFoundInConfigError:
|
||||
raise _not_found(name) from None
|
||||
except JailAlreadyInactiveError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail=f"Jail {name!r} is already inactive.",
|
||||
) from None
|
||||
except ConfigWriteError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to write config override: {exc}",
|
||||
) from exc
|
||||
except Fail2BanConnectionError as exc:
|
||||
raise _bad_gateway(exc) from exc
|
||||
|
||||
Reference in New Issue
Block a user