Remove FastAPI dependency from jail config service signatures

This commit is contained in:
2026-04-14 15:01:05 +02:00
parent 41f8c1f6cb
commit 86fa271c40
4 changed files with 28 additions and 40 deletions

View File

@@ -119,6 +119,8 @@ The inverted import creates a hidden circular-dependency risk: if `health_check`
### TASK-04 — Remove `FastAPI` app instance from service signatures 🟠 ### TASK-04 — Remove `FastAPI` app instance from service signatures 🟠
**Status:** Completed ✅
**Where:** **Where:**
`backend/app/services/jail_config_service.py` — `activate_jail()` and `deactivate_jail()` function signatures accept `app: FastAPI` (guarded by `TYPE_CHECKING` on line 48, used at runtime on lines ~295, ~571). `backend/app/services/jail_config_service.py` — `activate_jail()` and `deactivate_jail()` function signatures accept `app: FastAPI` (guarded by `TYPE_CHECKING` on line 48, used at runtime on lines ~295, ~571).

View File

@@ -47,6 +47,12 @@ from app.services import (
jail_config_service, jail_config_service,
) )
from app.utils.fail2ban_client import Fail2BanConnectionError from app.utils.fail2ban_client import Fail2BanConnectionError
from app.utils.runtime_state import (
clear_activation_record,
clear_pending_recovery,
create_pending_recovery,
record_activation,
)
router: APIRouter = APIRouter(prefix="/jails", tags=["Jail Config"]) router: APIRouter = APIRouter(prefix="/jails", tags=["Jail Config"])
@@ -376,8 +382,10 @@ async def activate_jail(
""" """
req = body if body is not None else ActivateJailRequest() req = body if body is not None else ActivateJailRequest()
activation_time = record_activation(app, name)
try: try:
result = await jail_config_service.activate_jail(app, config_dir, socket_path, name, req) result = await jail_config_service.activate_jail(config_dir, socket_path, name, req)
except JailNameError as exc: except JailNameError as exc:
raise _bad_request(str(exc)) from exc raise _bad_request(str(exc)) from exc
except JailNotFoundInConfigError: except JailNotFoundInConfigError:
@@ -406,7 +414,6 @@ async def activate_jail(
summary="Deactivate an active jail", summary="Deactivate an active jail",
) )
async def deactivate_jail( async def deactivate_jail(
app: AppDep,
_auth: AuthDep, _auth: AuthDep,
config_dir: Fail2BanConfigDirDep, config_dir: Fail2BanConfigDirDep,
socket_path: Fail2BanSocketDep, socket_path: Fail2BanSocketDep,
@@ -418,7 +425,6 @@ async def deactivate_jail(
full fail2ban reload so the jail stops immediately. full fail2ban reload so the jail stops immediately.
Args: Args:
app: FastAPI application instance.
_auth: Validated session. _auth: Validated session.
name: Name of the jail to deactivate. name: Name of the jail to deactivate.
@@ -433,7 +439,7 @@ async def deactivate_jail(
""" """
try: try:
result = await jail_config_service.deactivate_jail(app, config_dir, socket_path, name) result = await jail_config_service.deactivate_jail(config_dir, socket_path, name)
except JailNameError as exc: except JailNameError as exc:
raise _bad_request(str(exc)) from exc raise _bad_request(str(exc)) from exc
except JailNotFoundInConfigError: except JailNotFoundInConfigError:
@@ -585,7 +591,7 @@ async def rollback_jail(
start_cmd_parts: list[str] = start_cmd.split() start_cmd_parts: list[str] = start_cmd.split()
try: try:
result = await jail_config_service.rollback_jail(app, config_dir, socket_path, name, start_cmd_parts) result = await jail_config_service.rollback_jail(config_dir, socket_path, name, start_cmd_parts)
except JailNameError as exc: except JailNameError as exc:
raise _bad_request(str(exc)) from exc raise _bad_request(str(exc)) from exc
except ConfigWriteError as exc: except ConfigWriteError as exc:
@@ -594,6 +600,10 @@ async def rollback_jail(
detail=f"Failed to write config override: {exc}", detail=f"Failed to write config override: {exc}",
) from exc ) from exc
if result.fail2ban_running:
clear_pending_recovery(app)
clear_activation_record(app)
return result return result

View File

@@ -14,7 +14,7 @@ import os
import re import re
import tempfile import tempfile
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING, cast from typing import cast
import structlog import structlog
@@ -37,15 +37,6 @@ from app.models.config import (
from app.services import health_service from app.services import health_service
from app.utils.async_utils import run_blocking from app.utils.async_utils import run_blocking
from app.utils.fail2ban_client import Fail2BanClient from app.utils.fail2ban_client import Fail2BanClient
from app.utils.runtime_state import (
clear_activation_record,
clear_pending_recovery,
create_pending_recovery,
record_activation,
)
if TYPE_CHECKING: # pragma: no cover
from fastapi import FastAPI
log: structlog.stdlib.BoundLogger = structlog.get_logger() log: structlog.stdlib.BoundLogger = structlog.get_logger()
@@ -274,29 +265,18 @@ async def list_inactive_jails(
async def activate_jail( async def activate_jail(
app: FastAPI,
config_dir: str, config_dir: str,
socket_path: str, socket_path: str,
name: str, name: str,
req: ActivateJailRequest, req: ActivateJailRequest,
) -> JailActivationResponse: ) -> JailActivationResponse:
"""Activate a jail and manage crash recovery state. """Activate a jail and update the health-check cache.
This wrapper records the activation timestamp, delegates the actual This wrapper delegates the file-based activation workflow to the
file-based activation workflow to the lower-level implementation, and lower-level implementation and runs an immediate probe so the UI
updates the health-check cache immediately so the UI reflects the reflects the current fail2ban state.
current fail2ban state.
""" """
activation_time = record_activation(app, name)
result = await _activate_jail(config_dir, socket_path, name, req) result = await _activate_jail(config_dir, socket_path, name, req)
if not result.fail2ban_running:
create_pending_recovery(
app,
jail_name=name,
activated_at=activation_time,
)
await run_probe(socket_path) await run_probe(socket_path)
return result return result
@@ -561,7 +541,6 @@ async def _rollback_activation_async(
async def deactivate_jail( async def deactivate_jail(
app: FastAPI,
config_dir: str, config_dir: str,
socket_path: str, socket_path: str,
name: str, name: str,
@@ -707,20 +686,17 @@ async def validate_jail_config(
async def rollback_jail( async def rollback_jail(
app: FastAPI,
config_dir: str, config_dir: str,
socket_path: str, socket_path: str,
name: str, name: str,
start_cmd_parts: list[str], start_cmd_parts: list[str],
) -> RollbackResponse: ) -> RollbackResponse:
"""Rollback a jail and clear pending recovery state on success.""" """Rollback a jail and return the result.
result = await _rollback_jail(config_dir, socket_path, name, start_cmd_parts)
if result.fail2ban_running: The caller is responsible for clearing pending recovery state when the
clear_pending_recovery(app) operation succeeds.
clear_activation_record(app) """
return await _rollback_jail(config_dir, socket_path, name, start_cmd_parts)
return result
async def _rollback_jail( async def _rollback_jail(

View File

@@ -807,7 +807,7 @@ class TestActivateJail:
assert resp.status_code == 200 assert resp.status_code == 200
# Verify the override values were passed to the service # Verify the override values were passed to the service
called_req = mock_activate.call_args.args[4] called_req = mock_activate.call_args.args[3]
assert called_req.bantime == "1h" assert called_req.bantime == "1h"
assert called_req.maxretry == 3 assert called_req.maxretry == 3