Refactor router dependencies to use explicit fail2ban socket and HTTP session injection

This commit is contained in:
2026-04-06 16:38:17 +02:00
parent 42c030c706
commit 3b58179845
2 changed files with 34 additions and 64 deletions

View File

@@ -12,15 +12,17 @@ Also provides ``GET /api/dashboard/bans`` for the dashboard ban-list table,
from __future__ import annotations
from typing import TYPE_CHECKING, Literal
if TYPE_CHECKING:
import aiohttp
from typing import Literal
from fastapi import APIRouter, Query, Request
from app import __version__
from app.dependencies import AuthDep, DbDep
from app.dependencies import (
AuthDep,
DbDep,
Fail2BanSocketDep,
HttpSessionDep,
)
from app.models.ban import (
BanOrigin,
BansByCountryResponse,
@@ -80,9 +82,10 @@ async def get_server_status(
summary="Return a paginated list of recent bans",
)
async def get_dashboard_bans(
request: Request,
_auth: AuthDep,
db: DbDep,
socket_path: Fail2BanSocketDep,
http_session: HttpSessionDep,
range: TimeRange = Query(default=_DEFAULT_RANGE, description="Time-range preset."),
source: Literal["fail2ban", "archive"] = Query(
default="fail2ban",
@@ -104,7 +107,6 @@ async def get_dashboard_bans(
GET request.
Args:
request: The incoming request (used to access ``app.state``).
_auth: Validated session dependency.
range: Time-range preset — ``"24h"``, ``"7d"``, ``"30d"``, or
``"365d"``.
@@ -116,9 +118,6 @@ async def get_dashboard_bans(
:class:`~app.models.ban.DashboardBanListResponse` with paginated
ban items and the total count for the selected window.
"""
socket_path: str = request.app.state.settings.fail2ban_socket
http_session: aiohttp.ClientSession = request.app.state.http_session
return await ban_service.list_bans(
socket_path,
range,
@@ -138,9 +137,10 @@ async def get_dashboard_bans(
summary="Return ban counts aggregated by country",
)
async def get_bans_by_country(
request: Request,
_auth: AuthDep,
db: DbDep,
socket_path: Fail2BanSocketDep,
http_session: HttpSessionDep,
range: TimeRange = Query(default=_DEFAULT_RANGE, description="Time-range preset."),
source: Literal["fail2ban", "archive"] = Query(
default="fail2ban",
@@ -164,7 +164,6 @@ async def get_bans_by_country(
during this GET request.
Args:
request: The incoming request.
_auth: Validated session dependency.
range: Time-range preset.
origin: Optional filter by ban origin.
@@ -173,9 +172,6 @@ async def get_bans_by_country(
:class:`~app.models.ban.BansByCountryResponse` with per-country
aggregation and the companion ban list.
"""
socket_path: str = request.app.state.settings.fail2ban_socket
http_session: aiohttp.ClientSession = request.app.state.http_session
return await ban_service.bans_by_country(
socket_path,
range,
@@ -195,9 +191,9 @@ async def get_bans_by_country(
summary="Return ban counts aggregated into time buckets",
)
async def get_ban_trend(
request: Request,
_auth: AuthDep,
db: DbDep,
socket_path: Fail2BanSocketDep,
range: TimeRange = Query(default=_DEFAULT_RANGE, description="Time-range preset."),
source: Literal["fail2ban", "archive"] = Query(
default="fail2ban",
@@ -223,7 +219,6 @@ async def get_ban_trend(
* ``365d`` → 7-day buckets (~53 total)
Args:
request: The incoming request (used to access ``app.state``).
_auth: Validated session dependency.
range: Time-range preset.
origin: Optional filter by ban origin.
@@ -232,8 +227,6 @@ async def get_ban_trend(
:class:`~app.models.ban.BanTrendResponse` with the ordered bucket
list and the bucket-size label.
"""
socket_path: str = request.app.state.settings.fail2ban_socket
return await ban_service.ban_trend(
socket_path,
range,
@@ -249,9 +242,9 @@ async def get_ban_trend(
summary="Return ban counts aggregated by jail",
)
async def get_bans_by_jail(
request: Request,
_auth: AuthDep,
db: DbDep,
socket_path: Fail2BanSocketDep,
range: TimeRange = Query(default=_DEFAULT_RANGE, description="Time-range preset."),
source: Literal["fail2ban", "archive"] = Query(
default="fail2ban",
@@ -269,7 +262,6 @@ async def get_bans_by_jail(
distribution bar chart.
Args:
request: The incoming request (used to access ``app.state``).
_auth: Validated session dependency.
range: Time-range preset — ``"24h"``, ``"7d"``, ``"30d"``, or
``"365d"``.
@@ -279,8 +271,6 @@ async def get_bans_by_jail(
:class:`~app.models.ban.BansByJailResponse` with per-jail counts
sorted descending and the total for the selected window.
"""
socket_path: str = request.app.state.settings.fail2ban_socket
return await ban_service.bans_by_jail(
socket_path,
range,

View File

@@ -21,9 +21,15 @@ from __future__ import annotations
from typing import Annotated
from fastapi import APIRouter, Body, HTTPException, Path, Request, status
from fastapi import APIRouter, Body, HTTPException, Path, status
from app.dependencies import AuthDep, DbDep
from app.dependencies import (
AuthDep,
DbDep,
Fail2BanSocketDep,
HttpSessionDep,
)
from app.exceptions import JailNotFoundError, JailOperationError
from app.models.ban import JailBannedIpsResponse
from app.models.jail import (
IgnoreIpRequest,
@@ -32,7 +38,6 @@ from app.models.jail import (
JailListResponse,
)
from app.services import geo_service, jail_service
from app.exceptions import JailNotFoundError, JailOperationError
from app.utils.fail2ban_client import Fail2BanConnectionError
router: APIRouter = APIRouter(prefix="/api/jails", tags=["Jails"])
@@ -100,8 +105,8 @@ def _conflict(message: str) -> HTTPException:
summary="List all active fail2ban jails",
)
async def get_jails(
request: Request,
_auth: AuthDep,
socket_path: Fail2BanSocketDep,
) -> JailListResponse:
"""Return a summary of every active fail2ban jail.
@@ -110,13 +115,11 @@ async def get_jails(
for each jail.
Args:
request: Incoming request (used to access ``app.state``).
_auth: Validated session — enforces authentication.
Returns:
:class:`~app.models.jail.JailListResponse` with all active jails.
"""
socket_path: str = request.app.state.settings.fail2ban_socket
try:
return await jail_service.list_jails(socket_path)
except Fail2BanConnectionError as exc:
@@ -129,9 +132,9 @@ async def get_jails(
summary="Return full detail for a single jail",
)
async def get_jail(
request: Request,
_auth: AuthDep,
name: _NamePath,
socket_path: Fail2BanSocketDep,
) -> JailDetailResponse:
"""Return the complete configuration and runtime state for one jail.
@@ -140,7 +143,6 @@ async def get_jail(
counters.
Args:
request: Incoming request (used to access ``app.state``).
_auth: Validated session — enforces authentication.
name: Jail name.
@@ -151,7 +153,6 @@ async def get_jail(
HTTPException: 404 when the jail does not exist.
HTTPException: 502 when fail2ban is unreachable.
"""
socket_path: str = request.app.state.settings.fail2ban_socket
try:
return await jail_service.get_jail(socket_path, name)
except JailNotFoundError:
@@ -171,8 +172,8 @@ async def get_jail(
summary="Reload all fail2ban jails",
)
async def reload_all_jails(
request: Request,
_auth: AuthDep,
socket_path: Fail2BanSocketDep,
) -> JailCommandResponse:
"""Reload every fail2ban jail to apply configuration changes.
@@ -180,7 +181,6 @@ async def reload_all_jails(
jails simultaneously.
Args:
request: Incoming request (used to access ``app.state``).
_auth: Validated session — enforces authentication.
Returns:
@@ -190,7 +190,6 @@ async def reload_all_jails(
HTTPException: 502 when fail2ban is unreachable.
HTTPException: 409 when fail2ban reports the operation failed.
"""
socket_path: str = request.app.state.settings.fail2ban_socket
try:
await jail_service.reload_all(socket_path)
return JailCommandResponse(message="All jails reloaded successfully.", jail="*")
@@ -206,14 +205,13 @@ async def reload_all_jails(
summary="Start a stopped jail",
)
async def start_jail(
request: Request,
_auth: AuthDep,
name: _NamePath,
socket_path: Fail2BanSocketDep,
) -> JailCommandResponse:
"""Start a fail2ban jail that is currently stopped.
Args:
request: Incoming request (used to access ``app.state``).
_auth: Validated session — enforces authentication.
name: Jail name.
@@ -225,7 +223,6 @@ async def start_jail(
HTTPException: 409 when fail2ban reports the operation failed.
HTTPException: 502 when fail2ban is unreachable.
"""
socket_path: str = request.app.state.settings.fail2ban_socket
try:
await jail_service.start_jail(socket_path, name)
return JailCommandResponse(message=f"Jail {name!r} started.", jail=name)
@@ -243,9 +240,9 @@ async def start_jail(
summary="Stop a running jail",
)
async def stop_jail(
request: Request,
_auth: AuthDep,
name: _NamePath,
socket_path: Fail2BanSocketDep,
) -> JailCommandResponse:
"""Stop a running fail2ban jail.
@@ -254,7 +251,6 @@ async def stop_jail(
jail is already stopped the request succeeds silently (idempotent).
Args:
request: Incoming request (used to access ``app.state``).
_auth: Validated session — enforces authentication.
name: Jail name.
@@ -265,7 +261,6 @@ async def stop_jail(
HTTPException: 409 when fail2ban reports the operation failed.
HTTPException: 502 when fail2ban is unreachable.
"""
socket_path: str = request.app.state.settings.fail2ban_socket
try:
await jail_service.stop_jail(socket_path, name)
return JailCommandResponse(message=f"Jail {name!r} stopped.", jail=name)
@@ -281,9 +276,9 @@ async def stop_jail(
summary="Toggle idle mode for a jail",
)
async def toggle_idle(
request: Request,
_auth: AuthDep,
name: _NamePath,
socket_path: Fail2BanSocketDep,
on: bool = Body(..., description="``true`` to enable idle, ``false`` to disable."),
) -> JailCommandResponse:
"""Enable or disable idle mode for a fail2ban jail.
@@ -292,7 +287,6 @@ async def toggle_idle(
preserving all existing bans.
Args:
request: Incoming request (used to access ``app.state``).
_auth: Validated session — enforces authentication.
name: Jail name.
on: ``true`` to enable idle, ``false`` to disable.
@@ -305,7 +299,6 @@ async def toggle_idle(
HTTPException: 409 when fail2ban reports the operation failed.
HTTPException: 502 when fail2ban is unreachable.
"""
socket_path: str = request.app.state.settings.fail2ban_socket
state_str = "on" if on else "off"
try:
await jail_service.set_idle(socket_path, name, on=on)
@@ -327,14 +320,13 @@ async def toggle_idle(
summary="Reload a single jail",
)
async def reload_jail(
request: Request,
_auth: AuthDep,
name: _NamePath,
socket_path: Fail2BanSocketDep,
) -> JailCommandResponse:
"""Reload a single fail2ban jail to pick up configuration changes.
Args:
request: Incoming request (used to access ``app.state``).
_auth: Validated session — enforces authentication.
name: Jail name.
@@ -346,7 +338,6 @@ async def reload_jail(
HTTPException: 409 when fail2ban reports the operation failed.
HTTPException: 502 when fail2ban is unreachable.
"""
socket_path: str = request.app.state.settings.fail2ban_socket
try:
await jail_service.reload_jail(socket_path, name)
return JailCommandResponse(message=f"Jail {name!r} reloaded.", jail=name)
@@ -377,14 +368,13 @@ class _IgnoreSelfRequest(IgnoreIpRequest):
summary="List the ignore IPs for a jail",
)
async def get_ignore_list(
request: Request,
_auth: AuthDep,
name: _NamePath,
socket_path: Fail2BanSocketDep,
) -> list[str]:
"""Return the current ignore list (IP whitelist) for a fail2ban jail.
Args:
request: Incoming request (used to access ``app.state``).
_auth: Validated session — enforces authentication.
name: Jail name.
@@ -395,7 +385,6 @@ async def get_ignore_list(
HTTPException: 404 when the jail does not exist.
HTTPException: 502 when fail2ban is unreachable.
"""
socket_path: str = request.app.state.settings.fail2ban_socket
try:
return await jail_service.get_ignore_list(socket_path, name)
except JailNotFoundError:
@@ -411,10 +400,10 @@ async def get_ignore_list(
summary="Add an IP or network to the ignore list",
)
async def add_ignore_ip(
request: Request,
_auth: AuthDep,
name: _NamePath,
body: IgnoreIpRequest,
socket_path: Fail2BanSocketDep,
) -> JailCommandResponse:
"""Add an IP address or CIDR network to a jail's ignore list.
@@ -422,7 +411,6 @@ async def add_ignore_ip(
trigger the configured fail regex.
Args:
request: Incoming request (used to access ``app.state``).
_auth: Validated session — enforces authentication.
name: Jail name.
body: Payload containing the IP or CIDR to add.
@@ -436,7 +424,6 @@ async def add_ignore_ip(
HTTPException: 409 when fail2ban reports the operation failed.
HTTPException: 502 when fail2ban is unreachable.
"""
socket_path: str = request.app.state.settings.fail2ban_socket
try:
await jail_service.add_ignore_ip(socket_path, name, body.ip)
return JailCommandResponse(
@@ -462,15 +449,14 @@ async def add_ignore_ip(
summary="Remove an IP or network from the ignore list",
)
async def del_ignore_ip(
request: Request,
_auth: AuthDep,
name: _NamePath,
body: IgnoreIpRequest,
socket_path: Fail2BanSocketDep,
) -> JailCommandResponse:
"""Remove an IP address or CIDR network from a jail's ignore list.
Args:
request: Incoming request (used to access ``app.state``).
_auth: Validated session — enforces authentication.
name: Jail name.
body: Payload containing the IP or CIDR to remove.
@@ -483,7 +469,6 @@ async def del_ignore_ip(
HTTPException: 409 when fail2ban reports the operation failed.
HTTPException: 502 when fail2ban is unreachable.
"""
socket_path: str = request.app.state.settings.fail2ban_socket
try:
await jail_service.del_ignore_ip(socket_path, name, body.ip)
return JailCommandResponse(
@@ -504,9 +489,9 @@ async def del_ignore_ip(
summary="Toggle the ignoreself option for a jail",
)
async def toggle_ignore_self(
request: Request,
_auth: AuthDep,
name: _NamePath,
socket_path: Fail2BanSocketDep,
on: bool = Body(..., description="``true`` to enable ignoreself, ``false`` to disable."),
) -> JailCommandResponse:
"""Toggle the ``ignoreself`` flag for a fail2ban jail.
@@ -515,7 +500,6 @@ async def toggle_ignore_self(
own IP addresses to the ignore list so the host can never ban itself.
Args:
request: Incoming request (used to access ``app.state``).
_auth: Validated session — enforces authentication.
name: Jail name.
on: ``true`` to enable, ``false`` to disable.
@@ -528,7 +512,6 @@ async def toggle_ignore_self(
HTTPException: 409 when fail2ban reports the operation failed.
HTTPException: 502 when fail2ban is unreachable.
"""
socket_path: str = request.app.state.settings.fail2ban_socket
state_str = "enabled" if on else "disabled"
try:
await jail_service.set_ignore_self(socket_path, name, on=on)
@@ -555,10 +538,11 @@ async def toggle_ignore_self(
summary="Return paginated currently-banned IPs for a single jail",
)
async def get_jail_banned_ips(
request: Request,
_auth: AuthDep,
db: DbDep,
name: _NamePath,
socket_path: Fail2BanSocketDep,
http_session: HttpSessionDep,
page: int = 1,
page_size: int = 25,
search: str | None = None,
@@ -570,7 +554,6 @@ async def get_jail_banned_ips(
geo-enriched exclusively for that page slice.
Args:
request: Incoming request (used to access ``app.state``).
_auth: Validated session — enforces authentication.
name: Jail name.
page: 1-based page number (default 1, min 1).
@@ -596,9 +579,6 @@ async def get_jail_banned_ips(
detail="page_size must be between 1 and 100.",
)
socket_path: str = request.app.state.settings.fail2ban_socket
http_session = getattr(request.app.state, "http_session", None)
try:
return await jail_service.get_jail_banned_ips(
socket_path=socket_path,