refactor: Remove duplicate router-level exception helpers
All routers now let domain exceptions propagate to the global handlers in main.py instead of catching and converting them to HTTPException. This eliminates: - Duplicate exception-to-HTTP-status mappings across 8 routers - Duplicate helper functions (_bad_gateway, _not_found, _conflict, etc.) - Inconsistent error response formats Changes: - Removed all try/except blocks from routers that catch domain exceptions - Removed duplicate helper functions from all routers - Added missing exception handlers to main.py for: * ActionNameError * FilterNameError * JailNameError * JailNotFoundInConfigError * FilterInvalidRegexError - Removed unused imports from affected routers All domain exceptions now propagate to the single authoritative mapping in main.py, ensuring consistent error codes, messages, and logging across the API. Affected routers: - action_config.py: Removed _action_not_found, _bad_request, _not_found helpers - bans.py: Removed try/except in ban/unban endpoints - config_misc.py: Removed try/except blocks - file_config.py: Removed 6 try/except blocks and _service_unavailable helper - filter_config.py: Removed try/except blocks - geo.py: Removed try/except in lookup_ip endpoint - jail_config.py: Removed try/except blocks - jails.py: Removed try/except blocks - server.py: Removed try/except blocks Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -32,24 +32,6 @@ _NamePath = Annotated[
|
||||
Path(description='Jail name as configured in fail2ban.'),
|
||||
]
|
||||
|
||||
|
||||
def _action_not_found(name: str) -> HTTPException:
|
||||
return HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Action not found: {name!r}",
|
||||
)
|
||||
|
||||
|
||||
def _bad_request(message: str) -> HTTPException:
|
||||
return HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=message)
|
||||
|
||||
|
||||
def _not_found(name: str) -> HTTPException:
|
||||
return HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Jail not found: {name!r}",
|
||||
)
|
||||
|
||||
@router.get(
|
||||
"",
|
||||
response_model=ActionListResponse,
|
||||
@@ -116,10 +98,7 @@ async def get_action(
|
||||
Raises:
|
||||
HTTPException: 404 if the action is not found in ``action.d/``.
|
||||
"""
|
||||
try:
|
||||
return await action_config_service.get_action(config_dir, socket_path, name)
|
||||
except ActionNotFoundError:
|
||||
raise _action_not_found(name) from None
|
||||
return await action_config_service.get_action(config_dir, socket_path, name)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -163,17 +142,7 @@ async def update_action(
|
||||
HTTPException: 404 if the action does not exist.
|
||||
HTTPException: 500 if writing the ``.local`` file fails.
|
||||
"""
|
||||
try:
|
||||
return await action_config_service.update_action(config_dir, socket_path, name, body, do_reload=reload)
|
||||
except ActionNameError as exc:
|
||||
raise _bad_request(str(exc)) from exc
|
||||
except ActionNotFoundError:
|
||||
raise _action_not_found(name) from None
|
||||
except ConfigWriteError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to write action override: {exc}",
|
||||
) from exc
|
||||
return await action_config_service.update_action(config_dir, socket_path, name, body, do_reload=reload)
|
||||
|
||||
|
||||
|
||||
@@ -211,20 +180,7 @@ async def create_action(
|
||||
HTTPException: 409 if the action already exists.
|
||||
HTTPException: 500 if writing fails.
|
||||
"""
|
||||
try:
|
||||
return await action_config_service.create_action(config_dir, socket_path, body, do_reload=reload)
|
||||
except ActionNameError as exc:
|
||||
raise _bad_request(str(exc)) from exc
|
||||
except ActionAlreadyExistsError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail=f"Action {exc.name!r} already exists.",
|
||||
) from exc
|
||||
except ConfigWriteError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to write action: {exc}",
|
||||
) from exc
|
||||
return await action_config_service.create_action(config_dir, socket_path, body, do_reload=reload)
|
||||
|
||||
|
||||
|
||||
@@ -256,22 +212,7 @@ async def delete_action(
|
||||
HTTPException: 409 if the action is a shipped default (conf-only).
|
||||
HTTPException: 500 if deletion fails.
|
||||
"""
|
||||
try:
|
||||
await action_config_service.delete_action(config_dir, name)
|
||||
except ActionNameError as exc:
|
||||
raise _bad_request(str(exc)) from exc
|
||||
except ActionNotFoundError:
|
||||
raise _action_not_found(name) from None
|
||||
except ActionReadonlyError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail=str(exc),
|
||||
) from exc
|
||||
except ConfigWriteError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to delete action: {exc}",
|
||||
) from exc
|
||||
await action_config_service.delete_action(config_dir, name)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -28,21 +28,6 @@ from app.exceptions import Fail2BanConnectionError
|
||||
router: APIRouter = APIRouter(prefix="/api/bans", tags=["Bans"])
|
||||
|
||||
|
||||
def _bad_gateway(exc: Exception) -> HTTPException:
|
||||
"""Return a 502 response when fail2ban is unreachable.
|
||||
|
||||
Args:
|
||||
exc: The underlying connection error.
|
||||
|
||||
Returns:
|
||||
:class:`fastapi.HTTPException` with status 502.
|
||||
"""
|
||||
return HTTPException(
|
||||
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||
detail=f"Cannot reach fail2ban: {exc}",
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/active",
|
||||
response_model=ActiveBanListResponse,
|
||||
@@ -71,15 +56,12 @@ async def get_active_bans(
|
||||
Raises:
|
||||
HTTPException: 502 when fail2ban is unreachable.
|
||||
"""
|
||||
try:
|
||||
return await ban_service.get_active_bans(
|
||||
socket_path,
|
||||
geo_batch_lookup=geo_batch_lookup,
|
||||
http_session=http_session,
|
||||
app_db=db,
|
||||
)
|
||||
except Fail2BanConnectionError as exc:
|
||||
raise _bad_gateway(exc) from exc
|
||||
return await ban_service.get_active_bans(
|
||||
socket_path,
|
||||
geo_batch_lookup=geo_batch_lookup,
|
||||
http_session=http_session,
|
||||
app_db=db,
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
@@ -113,29 +95,11 @@ async def ban_ip(
|
||||
HTTPException: 409 when fail2ban reports the ban failed.
|
||||
HTTPException: 502 when fail2ban is unreachable.
|
||||
"""
|
||||
try:
|
||||
await ban_service.ban_ip(socket_path, body.jail, body.ip)
|
||||
return JailCommandResponse(
|
||||
message=f"IP {body.ip!r} banned in jail {body.jail!r}.",
|
||||
jail=body.jail,
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=str(exc),
|
||||
) from exc
|
||||
except JailNotFoundError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Jail not found: {body.jail!r}",
|
||||
) from None
|
||||
except JailOperationError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail=str(exc),
|
||||
) from exc
|
||||
except Fail2BanConnectionError as exc:
|
||||
raise _bad_gateway(exc) from exc
|
||||
await ban_service.ban_ip(socket_path, body.jail, body.ip)
|
||||
return JailCommandResponse(
|
||||
message=f"IP {body.ip!r} banned in jail {body.jail!r}.",
|
||||
jail=body.jail,
|
||||
)
|
||||
|
||||
|
||||
@router.delete(
|
||||
@@ -173,30 +137,12 @@ async def unban_ip(
|
||||
# Determine target jail (None means all jails).
|
||||
target_jail: str | None = None if (body.unban_all or body.jail is None) else body.jail
|
||||
|
||||
try:
|
||||
await ban_service.unban_ip(socket_path, body.ip, jail=target_jail)
|
||||
scope = f"jail {target_jail!r}" if target_jail else "all jails"
|
||||
return JailCommandResponse(
|
||||
message=f"IP {body.ip!r} unbanned from {scope}.",
|
||||
jail=target_jail or "*",
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=str(exc),
|
||||
) from exc
|
||||
except JailNotFoundError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Jail not found: {target_jail!r}",
|
||||
) from None
|
||||
except JailOperationError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail=str(exc),
|
||||
) from exc
|
||||
except Fail2BanConnectionError as exc:
|
||||
raise _bad_gateway(exc) from exc
|
||||
await ban_service.unban_ip(socket_path, body.ip, jail=target_jail)
|
||||
scope = f"jail {target_jail!r}" if target_jail else "all jails"
|
||||
return JailCommandResponse(
|
||||
message=f"IP {body.ip!r} unbanned from {scope}.",
|
||||
jail=target_jail or "*",
|
||||
)
|
||||
|
||||
|
||||
@router.delete(
|
||||
@@ -225,11 +171,8 @@ async def unban_all(
|
||||
Raises:
|
||||
HTTPException: 502 when fail2ban is unreachable.
|
||||
"""
|
||||
try:
|
||||
count: int = await jail_service.unban_all_ips(socket_path)
|
||||
return UnbanAllResponse(
|
||||
message=f"All bans cleared. {count} IP address{'es' if count != 1 else ''} unbanned.",
|
||||
count=count,
|
||||
)
|
||||
except Fail2BanConnectionError as exc:
|
||||
raise _bad_gateway(exc) from exc
|
||||
count: int = await jail_service.unban_all_ips(socket_path)
|
||||
return UnbanAllResponse(
|
||||
message=f"All bans cleared. {count} IP address{'es' if count != 1 else ''} unbanned.",
|
||||
count=count,
|
||||
)
|
||||
|
||||
@@ -39,20 +39,6 @@ log: structlog.stdlib.BoundLogger = structlog.get_logger()
|
||||
router: APIRouter = APIRouter(tags=["Config Misc"])
|
||||
|
||||
|
||||
def _bad_gateway(exc: Exception) -> HTTPException:
|
||||
return HTTPException(
|
||||
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||
detail=f"Cannot reach fail2ban: {exc}",
|
||||
)
|
||||
|
||||
|
||||
def _bad_request(message: str) -> HTTPException:
|
||||
return HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=message,
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/global",
|
||||
response_model=GlobalConfigResponse,
|
||||
@@ -77,10 +63,7 @@ async def get_global_config(
|
||||
Raises:
|
||||
HTTPException: 502 when fail2ban is unreachable.
|
||||
"""
|
||||
try:
|
||||
return await config_service.get_global_config(socket_path)
|
||||
except Fail2BanConnectionError as exc:
|
||||
raise _bad_gateway(exc) from exc
|
||||
return await config_service.get_global_config(socket_path)
|
||||
|
||||
|
||||
@router.put(
|
||||
@@ -105,12 +88,7 @@ async def update_global_config(
|
||||
HTTPException: 400 when a set command is rejected.
|
||||
HTTPException: 502 when fail2ban is unreachable.
|
||||
"""
|
||||
try:
|
||||
await config_service.update_global_config(socket_path, body)
|
||||
except ConfigOperationError as exc:
|
||||
raise _bad_request(str(exc)) from exc
|
||||
except Fail2BanConnectionError as exc:
|
||||
raise _bad_gateway(exc) from exc
|
||||
await config_service.update_global_config(socket_path, body)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -139,15 +117,7 @@ async def reload_fail2ban(
|
||||
HTTPException: 409 when fail2ban reports the reload failed.
|
||||
HTTPException: 502 when fail2ban is unreachable.
|
||||
"""
|
||||
try:
|
||||
await jail_service.reload_all(socket_path)
|
||||
except JailOperationError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail=f"fail2ban reload failed: {exc}",
|
||||
) from exc
|
||||
except Fail2BanConnectionError as exc:
|
||||
raise _bad_gateway(exc) from exc
|
||||
await jail_service.reload_all(socket_path)
|
||||
|
||||
|
||||
# Restart endpoint
|
||||
@@ -186,18 +156,10 @@ async def restart_fail2ban(
|
||||
"""
|
||||
start_cmd_parts: list[str] = start_cmd.split()
|
||||
|
||||
try:
|
||||
restarted = await jail_service.restart_daemon(
|
||||
socket_path,
|
||||
start_cmd_parts,
|
||||
)
|
||||
except JailOperationError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail=f"fail2ban stop command failed: {exc}",
|
||||
) from exc
|
||||
except Fail2BanConnectionError as exc:
|
||||
raise _bad_gateway(exc) from exc
|
||||
restarted = await jail_service.restart_daemon(
|
||||
socket_path,
|
||||
start_cmd_parts,
|
||||
)
|
||||
|
||||
if not restarted:
|
||||
raise HTTPException(
|
||||
@@ -323,11 +285,7 @@ async def update_map_color_thresholds(
|
||||
HTTPException: 400 if validation fails (thresholds not
|
||||
properly ordered).
|
||||
"""
|
||||
try:
|
||||
await config_service.update_map_color_thresholds(db, body)
|
||||
except ValueError as exc:
|
||||
raise _bad_request(str(exc)) from exc
|
||||
|
||||
await config_service.update_map_color_thresholds(db, body)
|
||||
return await config_service.get_map_color_thresholds(db)
|
||||
|
||||
|
||||
@@ -379,12 +337,7 @@ async def get_fail2ban_log(
|
||||
the allowed directory.
|
||||
HTTPException: 502 when fail2ban is unreachable.
|
||||
"""
|
||||
try:
|
||||
return await log_service.read_fail2ban_log(socket_path, lines, filter_)
|
||||
except ConfigOperationError as exc:
|
||||
raise _bad_request(str(exc)) from exc
|
||||
except Fail2BanConnectionError as exc:
|
||||
raise _bad_gateway(exc) from exc
|
||||
return await log_service.read_fail2ban_log(socket_path, lines, filter_)
|
||||
|
||||
|
||||
@router.get(
|
||||
@@ -415,10 +368,7 @@ async def get_service_status(
|
||||
"""
|
||||
from app.services import health_service
|
||||
|
||||
try:
|
||||
return await health_service.get_service_status(
|
||||
socket_path,
|
||||
probe_fn=health_service.probe,
|
||||
)
|
||||
except Fail2BanConnectionError as exc:
|
||||
raise _bad_gateway(exc) from exc
|
||||
return await health_service.get_service_status(
|
||||
socket_path,
|
||||
probe_fn=health_service.probe,
|
||||
)
|
||||
|
||||
@@ -31,7 +31,7 @@ from __future__ import annotations
|
||||
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Path, status
|
||||
from fastapi import APIRouter, Path, status
|
||||
|
||||
from app.dependencies import AuthDep, Fail2BanConfigDirDep
|
||||
from app.models.config import (
|
||||
@@ -52,13 +52,6 @@ from app.models.file_config import (
|
||||
JailConfigFilesResponse,
|
||||
)
|
||||
from app.services import raw_config_io_service
|
||||
from app.exceptions import (
|
||||
ConfigDirError,
|
||||
ConfigFileExistsError,
|
||||
ConfigFileNameError,
|
||||
ConfigFileNotFoundError,
|
||||
ConfigFileWriteError,
|
||||
)
|
||||
|
||||
router: APIRouter = APIRouter(prefix="/api/config", tags=["Config"])
|
||||
|
||||
@@ -73,39 +66,6 @@ _NamePath = Annotated[
|
||||
str, Path(description="Base name with or without extension (e.g. ``sshd`` or ``sshd.conf``).")
|
||||
]
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Error helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _not_found(filename: str) -> HTTPException:
|
||||
return HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Config file not found: {filename!r}",
|
||||
)
|
||||
|
||||
|
||||
def _bad_request(message: str) -> HTTPException:
|
||||
return HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=message,
|
||||
)
|
||||
|
||||
|
||||
def _conflict(filename: str) -> HTTPException:
|
||||
return HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail=f"Config file already exists: {filename!r}",
|
||||
)
|
||||
|
||||
|
||||
def _service_unavailable(message: str) -> HTTPException:
|
||||
return HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail=message,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Jail config file endpoints (Task 4a)
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -132,12 +92,7 @@ async def list_jail_config_files(
|
||||
Returns:
|
||||
:class:`~app.models.file_config.JailConfigFilesResponse`.
|
||||
"""
|
||||
try:
|
||||
return await raw_config_io_service.list_jail_config_files(config_dir)
|
||||
except ConfigDirError as exc:
|
||||
raise _service_unavailable(str(exc)) from exc
|
||||
|
||||
|
||||
return await raw_config_io_service.list_jail_config_files(config_dir)
|
||||
@router.get(
|
||||
"/jail-files/{filename}",
|
||||
response_model=JailConfigFileContent,
|
||||
@@ -163,16 +118,7 @@ async def get_jail_config_file(
|
||||
HTTPException: 404 if the file does not exist.
|
||||
HTTPException: 503 if the config directory is unavailable.
|
||||
"""
|
||||
try:
|
||||
return await raw_config_io_service.get_jail_config_file(config_dir, filename)
|
||||
except ConfigFileNameError as exc:
|
||||
raise _bad_request(str(exc)) from exc
|
||||
except ConfigFileNotFoundError:
|
||||
raise _not_found(filename) from None
|
||||
except ConfigDirError as exc:
|
||||
raise _service_unavailable(str(exc)) from exc
|
||||
|
||||
|
||||
return await raw_config_io_service.get_jail_config_file(config_dir, filename)
|
||||
@router.put(
|
||||
"/jail-files/{filename}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
@@ -200,18 +146,7 @@ async def write_jail_config_file(
|
||||
HTTPException: 404 if the file does not exist.
|
||||
HTTPException: 503 if the config directory is unavailable.
|
||||
"""
|
||||
try:
|
||||
await raw_config_io_service.write_jail_config_file(config_dir, filename, body)
|
||||
except ConfigFileNameError as exc:
|
||||
raise _bad_request(str(exc)) from exc
|
||||
except ConfigFileNotFoundError:
|
||||
raise _not_found(filename) from None
|
||||
except ConfigFileWriteError as exc:
|
||||
raise _bad_request(str(exc)) from exc
|
||||
except ConfigDirError as exc:
|
||||
raise _service_unavailable(str(exc)) from exc
|
||||
|
||||
|
||||
await raw_config_io_service.write_jail_config_file(config_dir, filename, body)
|
||||
@router.put(
|
||||
"/jail-files/{filename}/enabled",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
@@ -239,20 +174,9 @@ async def set_jail_config_file_enabled(
|
||||
HTTPException: 404 if the file does not exist.
|
||||
HTTPException: 503 if the config directory is unavailable.
|
||||
"""
|
||||
try:
|
||||
await raw_config_io_service.set_jail_config_enabled(
|
||||
config_dir, filename, body.enabled
|
||||
)
|
||||
except ConfigFileNameError as exc:
|
||||
raise _bad_request(str(exc)) from exc
|
||||
except ConfigFileNotFoundError:
|
||||
raise _not_found(filename) from None
|
||||
except ConfigFileWriteError as exc:
|
||||
raise _bad_request(str(exc)) from exc
|
||||
except ConfigDirError as exc:
|
||||
raise _service_unavailable(str(exc)) from exc
|
||||
|
||||
|
||||
await raw_config_io_service.set_jail_config_enabled(
|
||||
config_dir, filename, body.enabled
|
||||
)
|
||||
@router.post(
|
||||
"/jail-files",
|
||||
response_model=ConfFileContent,
|
||||
@@ -279,17 +203,7 @@ async def create_jail_config_file(
|
||||
HTTPException: 409 if a file with that name already exists.
|
||||
HTTPException: 503 if the config directory is unavailable.
|
||||
"""
|
||||
try:
|
||||
filename = await raw_config_io_service.create_jail_config_file(config_dir, body)
|
||||
except ConfigFileNameError as exc:
|
||||
raise _bad_request(str(exc)) from exc
|
||||
except ConfigFileExistsError:
|
||||
raise _conflict(body.name) from None
|
||||
except ConfigFileWriteError as exc:
|
||||
raise _bad_request(str(exc)) from exc
|
||||
except ConfigDirError as exc:
|
||||
raise _service_unavailable(str(exc)) from exc
|
||||
|
||||
filename = await raw_config_io_service.create_jail_config_file(config_dir, body)
|
||||
return ConfFileContent(
|
||||
name=body.name,
|
||||
filename=filename,
|
||||
@@ -331,16 +245,7 @@ async def get_filter_file_raw(
|
||||
HTTPException: 404 if the file does not exist.
|
||||
HTTPException: 503 if the config directory is unavailable.
|
||||
"""
|
||||
try:
|
||||
return await raw_config_io_service.get_filter_file(config_dir, name)
|
||||
except ConfigFileNameError as exc:
|
||||
raise _bad_request(str(exc)) from exc
|
||||
except ConfigFileNotFoundError:
|
||||
raise _not_found(name) from None
|
||||
except ConfigDirError as exc:
|
||||
raise _service_unavailable(str(exc)) from exc
|
||||
|
||||
|
||||
return await raw_config_io_service.get_filter_file(config_dir, name)
|
||||
@router.put(
|
||||
"/filters/{name}/raw",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
@@ -365,18 +270,7 @@ async def write_filter_file(
|
||||
HTTPException: 404 if the file does not exist.
|
||||
HTTPException: 503 if the config directory is unavailable.
|
||||
"""
|
||||
try:
|
||||
await raw_config_io_service.write_filter_file(config_dir, name, body)
|
||||
except ConfigFileNameError as exc:
|
||||
raise _bad_request(str(exc)) from exc
|
||||
except ConfigFileNotFoundError:
|
||||
raise _not_found(name) from None
|
||||
except ConfigFileWriteError as exc:
|
||||
raise _bad_request(str(exc)) from exc
|
||||
except ConfigDirError as exc:
|
||||
raise _service_unavailable(str(exc)) from exc
|
||||
|
||||
|
||||
await raw_config_io_service.write_filter_file(config_dir, name, body)
|
||||
@router.post(
|
||||
"/filters/raw",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
@@ -403,17 +297,7 @@ async def create_filter_file(
|
||||
HTTPException: 409 if a file with that name already exists.
|
||||
HTTPException: 503 if the config directory is unavailable.
|
||||
"""
|
||||
try:
|
||||
filename = await raw_config_io_service.create_filter_file(config_dir, body)
|
||||
except ConfigFileNameError as exc:
|
||||
raise _bad_request(str(exc)) from exc
|
||||
except ConfigFileExistsError:
|
||||
raise _conflict(body.name) from None
|
||||
except ConfigFileWriteError as exc:
|
||||
raise _bad_request(str(exc)) from exc
|
||||
except ConfigDirError as exc:
|
||||
raise _service_unavailable(str(exc)) from exc
|
||||
|
||||
filename = await raw_config_io_service.create_filter_file(config_dir, body)
|
||||
return ConfFileContent(
|
||||
name=body.name,
|
||||
filename=filename,
|
||||
@@ -444,12 +328,7 @@ async def list_action_files(
|
||||
Returns:
|
||||
:class:`~app.models.file_config.ConfFilesResponse`.
|
||||
"""
|
||||
try:
|
||||
return await raw_config_io_service.list_action_files(config_dir)
|
||||
except ConfigDirError as exc:
|
||||
raise _service_unavailable(str(exc)) from exc
|
||||
|
||||
|
||||
return await raw_config_io_service.list_action_files(config_dir)
|
||||
@router.get(
|
||||
"/actions/{name}/raw",
|
||||
response_model=ConfFileContent,
|
||||
@@ -475,16 +354,7 @@ async def get_action_file(
|
||||
HTTPException: 404 if the file does not exist.
|
||||
HTTPException: 503 if the config directory is unavailable.
|
||||
"""
|
||||
try:
|
||||
return await raw_config_io_service.get_action_file(config_dir, name)
|
||||
except ConfigFileNameError as exc:
|
||||
raise _bad_request(str(exc)) from exc
|
||||
except ConfigFileNotFoundError:
|
||||
raise _not_found(name) from None
|
||||
except ConfigDirError as exc:
|
||||
raise _service_unavailable(str(exc)) from exc
|
||||
|
||||
|
||||
return await raw_config_io_service.get_action_file(config_dir, name)
|
||||
@router.put(
|
||||
"/actions/{name}/raw",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
@@ -509,18 +379,7 @@ async def write_action_file(
|
||||
HTTPException: 404 if the file does not exist.
|
||||
HTTPException: 503 if the config directory is unavailable.
|
||||
"""
|
||||
try:
|
||||
await raw_config_io_service.write_action_file(config_dir, name, body)
|
||||
except ConfigFileNameError as exc:
|
||||
raise _bad_request(str(exc)) from exc
|
||||
except ConfigFileNotFoundError:
|
||||
raise _not_found(name) from None
|
||||
except ConfigFileWriteError as exc:
|
||||
raise _bad_request(str(exc)) from exc
|
||||
except ConfigDirError as exc:
|
||||
raise _service_unavailable(str(exc)) from exc
|
||||
|
||||
|
||||
await raw_config_io_service.write_action_file(config_dir, name, body)
|
||||
@router.post(
|
||||
"/actions",
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
@@ -547,17 +406,7 @@ async def create_action_file(
|
||||
HTTPException: 409 if a file with that name already exists.
|
||||
HTTPException: 503 if the config directory is unavailable.
|
||||
"""
|
||||
try:
|
||||
filename = await raw_config_io_service.create_action_file(config_dir, body)
|
||||
except ConfigFileNameError as exc:
|
||||
raise _bad_request(str(exc)) from exc
|
||||
except ConfigFileExistsError:
|
||||
raise _conflict(body.name) from None
|
||||
except ConfigFileWriteError as exc:
|
||||
raise _bad_request(str(exc)) from exc
|
||||
except ConfigDirError as exc:
|
||||
raise _service_unavailable(str(exc)) from exc
|
||||
|
||||
filename = await raw_config_io_service.create_action_file(config_dir, body)
|
||||
return ConfFileContent(
|
||||
name=body.name,
|
||||
filename=filename,
|
||||
@@ -599,16 +448,7 @@ async def get_parsed_filter(
|
||||
HTTPException: 404 if the file does not exist.
|
||||
HTTPException: 503 if the config directory is unavailable.
|
||||
"""
|
||||
try:
|
||||
return await raw_config_io_service.get_parsed_filter_file(config_dir, name)
|
||||
except ConfigFileNameError as exc:
|
||||
raise _bad_request(str(exc)) from exc
|
||||
except ConfigFileNotFoundError:
|
||||
raise _not_found(name) from None
|
||||
except ConfigDirError as exc:
|
||||
raise _service_unavailable(str(exc)) from exc
|
||||
|
||||
|
||||
return await raw_config_io_service.get_parsed_filter_file(config_dir, name)
|
||||
@router.put(
|
||||
"/filters/{name}/parsed",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
@@ -636,18 +476,7 @@ async def update_parsed_filter(
|
||||
HTTPException: 404 if the file does not exist.
|
||||
HTTPException: 503 if the config directory is unavailable.
|
||||
"""
|
||||
try:
|
||||
await raw_config_io_service.update_parsed_filter_file(config_dir, name, body)
|
||||
except ConfigFileNameError as exc:
|
||||
raise _bad_request(str(exc)) from exc
|
||||
except ConfigFileNotFoundError:
|
||||
raise _not_found(name) from None
|
||||
except ConfigFileWriteError as exc:
|
||||
raise _bad_request(str(exc)) from exc
|
||||
except ConfigDirError as exc:
|
||||
raise _service_unavailable(str(exc)) from exc
|
||||
|
||||
|
||||
await raw_config_io_service.update_parsed_filter_file(config_dir, name, body)
|
||||
# ---------------------------------------------------------------------------
|
||||
# Parsed action endpoints (Task 3.1)
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -682,16 +511,7 @@ async def get_parsed_action(
|
||||
HTTPException: 404 if the file does not exist.
|
||||
HTTPException: 503 if the config directory is unavailable.
|
||||
"""
|
||||
try:
|
||||
return await raw_config_io_service.get_parsed_action_file(config_dir, name)
|
||||
except ConfigFileNameError as exc:
|
||||
raise _bad_request(str(exc)) from exc
|
||||
except ConfigFileNotFoundError:
|
||||
raise _not_found(name) from None
|
||||
except ConfigDirError as exc:
|
||||
raise _service_unavailable(str(exc)) from exc
|
||||
|
||||
|
||||
return await raw_config_io_service.get_parsed_action_file(config_dir, name)
|
||||
@router.put(
|
||||
"/actions/{name}/parsed",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
@@ -719,18 +539,7 @@ async def update_parsed_action(
|
||||
HTTPException: 404 if the file does not exist.
|
||||
HTTPException: 503 if the config directory is unavailable.
|
||||
"""
|
||||
try:
|
||||
await raw_config_io_service.update_parsed_action_file(config_dir, name, body)
|
||||
except ConfigFileNameError as exc:
|
||||
raise _bad_request(str(exc)) from exc
|
||||
except ConfigFileNotFoundError:
|
||||
raise _not_found(name) from None
|
||||
except ConfigFileWriteError as exc:
|
||||
raise _bad_request(str(exc)) from exc
|
||||
except ConfigDirError as exc:
|
||||
raise _service_unavailable(str(exc)) from exc
|
||||
|
||||
|
||||
await raw_config_io_service.update_parsed_action_file(config_dir, name, body)
|
||||
# ---------------------------------------------------------------------------
|
||||
# Parsed jail file endpoints (Task 6.1)
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -765,16 +574,7 @@ async def get_parsed_jail_file(
|
||||
HTTPException: 404 if the file does not exist.
|
||||
HTTPException: 503 if the config directory is unavailable.
|
||||
"""
|
||||
try:
|
||||
return await raw_config_io_service.get_parsed_jail_file(config_dir, filename)
|
||||
except ConfigFileNameError as exc:
|
||||
raise _bad_request(str(exc)) from exc
|
||||
except ConfigFileNotFoundError:
|
||||
raise _not_found(filename) from None
|
||||
except ConfigDirError as exc:
|
||||
raise _service_unavailable(str(exc)) from exc
|
||||
|
||||
|
||||
return await raw_config_io_service.get_parsed_jail_file(config_dir, filename)
|
||||
@router.put(
|
||||
"/jail-files/{filename}/parsed",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
@@ -802,13 +602,4 @@ async def update_parsed_jail_file(
|
||||
HTTPException: 404 if the file does not exist.
|
||||
HTTPException: 503 if the config directory is unavailable.
|
||||
"""
|
||||
try:
|
||||
await raw_config_io_service.update_parsed_jail_file(config_dir, filename, body)
|
||||
except ConfigFileNameError as exc:
|
||||
raise _bad_request(str(exc)) from exc
|
||||
except ConfigFileNotFoundError:
|
||||
raise _not_found(filename) from None
|
||||
except ConfigFileWriteError as exc:
|
||||
raise _bad_request(str(exc)) from exc
|
||||
except ConfigDirError as exc:
|
||||
raise _service_unavailable(str(exc)) from exc
|
||||
await raw_config_io_service.update_parsed_jail_file(config_dir, filename, body)
|
||||
|
||||
@@ -23,38 +23,11 @@ from app.services import filter_config_service
|
||||
|
||||
router: APIRouter = APIRouter(prefix="/filters", tags=["Filter Config"])
|
||||
|
||||
_NamePath = Annotated[
|
||||
str,
|
||||
Path(description='Jail name as configured in fail2ban.'),
|
||||
]
|
||||
|
||||
_FilterNamePath = Annotated[
|
||||
str,
|
||||
Path(description='Filter base name, e.g. ``sshd`` or ``sshd.conf``.'),
|
||||
]
|
||||
|
||||
|
||||
def _filter_not_found(name: str) -> HTTPException:
|
||||
return HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Filter not found: {name!r}",
|
||||
)
|
||||
|
||||
|
||||
def _bad_request(message: str) -> HTTPException:
|
||||
return HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=message)
|
||||
|
||||
|
||||
def _unprocessable(message: str) -> HTTPException:
|
||||
return HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=message)
|
||||
|
||||
|
||||
def _not_found(name: str) -> HTTPException:
|
||||
return HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Jail not found: {name!r}",
|
||||
)
|
||||
|
||||
@router.get(
|
||||
"",
|
||||
response_model=FilterListResponse,
|
||||
@@ -123,13 +96,7 @@ async def get_filter(
|
||||
HTTPException: 404 if the filter is not found in ``filter.d/``.
|
||||
HTTPException: 502 if fail2ban is unreachable.
|
||||
"""
|
||||
try:
|
||||
return await filter_config_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
|
||||
return await filter_config_service.get_filter(config_dir, socket_path, name)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -143,15 +110,6 @@ _FilterNamePath = Annotated[
|
||||
]
|
||||
|
||||
|
||||
def _filter_not_found(name: str) -> HTTPException:
|
||||
return HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Filter not found: {name!r}",
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
@router.put(
|
||||
"/{name}",
|
||||
response_model=FilterConfig,
|
||||
@@ -189,19 +147,7 @@ async def update_filter(
|
||||
HTTPException: 422 if any regex pattern fails to compile.
|
||||
HTTPException: 500 if writing the ``.local`` file fails.
|
||||
"""
|
||||
try:
|
||||
return await filter_config_service.update_filter(config_dir, socket_path, name, body, do_reload=reload)
|
||||
except FilterNameError as exc:
|
||||
raise _bad_request(str(exc)) from exc
|
||||
except FilterNotFoundError:
|
||||
raise _filter_not_found(name) from None
|
||||
except FilterInvalidRegexError as exc:
|
||||
raise _unprocessable(str(exc)) from exc
|
||||
except ConfigWriteError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to write filter override: {exc}",
|
||||
) from exc
|
||||
return await filter_config_service.update_filter(config_dir, socket_path, name, body, do_reload=reload)
|
||||
|
||||
|
||||
|
||||
@@ -241,22 +187,7 @@ async def create_filter(
|
||||
HTTPException: 422 if any regex pattern is invalid.
|
||||
HTTPException: 500 if writing fails.
|
||||
"""
|
||||
try:
|
||||
return await filter_config_service.create_filter(config_dir, socket_path, body, do_reload=reload)
|
||||
except FilterNameError as exc:
|
||||
raise _bad_request(str(exc)) from exc
|
||||
except FilterAlreadyExistsError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail=f"Filter {exc.name!r} already exists.",
|
||||
) from exc
|
||||
except FilterInvalidRegexError as exc:
|
||||
raise _unprocessable(str(exc)) from exc
|
||||
except ConfigWriteError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to write filter: {exc}",
|
||||
) from exc
|
||||
return await filter_config_service.create_filter(config_dir, socket_path, body, do_reload=reload)
|
||||
|
||||
|
||||
|
||||
@@ -290,22 +221,7 @@ async def delete_filter(
|
||||
HTTPException: 409 if the filter is a shipped default (conf-only).
|
||||
HTTPException: 500 if deletion fails.
|
||||
"""
|
||||
try:
|
||||
await filter_config_service.delete_filter(config_dir, name)
|
||||
except FilterNameError as exc:
|
||||
raise _bad_request(str(exc)) from exc
|
||||
except FilterNotFoundError:
|
||||
raise _filter_not_found(name) from None
|
||||
except FilterReadonlyError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail=str(exc),
|
||||
) from exc
|
||||
except ConfigWriteError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to delete filter: {exc}",
|
||||
) from exc
|
||||
await filter_config_service.delete_filter(config_dir, name)
|
||||
|
||||
|
||||
|
||||
@@ -314,18 +230,5 @@ async def delete_filter(
|
||||
# Action discovery endpoints (Task 3.1)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_ActionNamePath = Annotated[
|
||||
str,
|
||||
Path(description="Action base name, e.g. ``iptables`` or ``iptables.conf``."),
|
||||
]
|
||||
|
||||
|
||||
def _action_not_found(name: str) -> HTTPException:
|
||||
return HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Action not found: {name!r}",
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ from typing import TYPE_CHECKING, Annotated
|
||||
if TYPE_CHECKING:
|
||||
from app.services.jail_service import IpLookupResult
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Path, status
|
||||
from fastapi import APIRouter, Path
|
||||
|
||||
from app.dependencies import (
|
||||
AuthDep,
|
||||
@@ -21,7 +21,6 @@ from app.dependencies import (
|
||||
Fail2BanSocketDep,
|
||||
HttpSessionDep,
|
||||
)
|
||||
from app.exceptions import Fail2BanConnectionError
|
||||
from app.models.geo import GeoCacheStatsResponse, GeoReResolveResponse, IpLookupResponse
|
||||
from app.services import geo_service, jail_service
|
||||
|
||||
@@ -58,22 +57,11 @@ async def lookup_ip(
|
||||
HTTPException: 400 when *ip* is not a valid IP address.
|
||||
HTTPException: 502 when fail2ban is unreachable.
|
||||
"""
|
||||
try:
|
||||
result: IpLookupResult = await jail_service.lookup_ip(
|
||||
socket_path,
|
||||
ip,
|
||||
http_session=http_session,
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=str(exc),
|
||||
) from exc
|
||||
except Fail2BanConnectionError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||
detail=f"Cannot reach fail2ban: {exc}",
|
||||
) from exc
|
||||
result: IpLookupResult = await jail_service.lookup_ip(
|
||||
socket_path,
|
||||
ip,
|
||||
http_session=http_session,
|
||||
)
|
||||
|
||||
return IpLookupResponse(**result)
|
||||
|
||||
|
||||
@@ -57,48 +57,6 @@ router: APIRouter = APIRouter(prefix="/jails", tags=["Jail Config"])
|
||||
|
||||
_NamePath = Annotated[str, Path(description='Jail name as configured in fail2ban.')]
|
||||
|
||||
|
||||
def _not_found(name: str) -> HTTPException:
|
||||
return HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Jail not found: {name!r}",
|
||||
)
|
||||
|
||||
|
||||
def _bad_gateway(exc: Exception) -> HTTPException:
|
||||
return HTTPException(
|
||||
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||
detail=f"Cannot reach fail2ban: {exc}",
|
||||
)
|
||||
|
||||
|
||||
def _unprocessable(message: str) -> HTTPException:
|
||||
return HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
||||
detail=message,
|
||||
)
|
||||
|
||||
|
||||
def _bad_request(message: str) -> HTTPException:
|
||||
return HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=message,
|
||||
)
|
||||
|
||||
|
||||
def _filter_not_found(name: str) -> HTTPException:
|
||||
return HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Filter not found: {name!r}",
|
||||
)
|
||||
|
||||
|
||||
def _action_not_found(name: str) -> HTTPException:
|
||||
return HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Action not found: {name!r}",
|
||||
)
|
||||
|
||||
@router.get(
|
||||
"",
|
||||
response_model=JailConfigListResponse,
|
||||
@@ -121,10 +79,7 @@ async def get_jail_configs(
|
||||
Returns:
|
||||
:class:`~app.models.config.JailConfigListResponse`.
|
||||
"""
|
||||
try:
|
||||
return await config_service.list_jail_configs(socket_path)
|
||||
except Fail2BanConnectionError as exc:
|
||||
raise _bad_gateway(exc) from exc
|
||||
return await config_service.list_jail_configs(socket_path)
|
||||
|
||||
|
||||
|
||||
@@ -206,12 +161,7 @@ async def get_jail_config(
|
||||
HTTPException: 404 when the jail does not exist.
|
||||
HTTPException: 502 when fail2ban is unreachable.
|
||||
"""
|
||||
try:
|
||||
return await config_service.get_jail_config(socket_path, name)
|
||||
except JailNotFoundError:
|
||||
raise _not_found(name) from None
|
||||
except Fail2BanConnectionError as exc:
|
||||
raise _bad_gateway(exc) from exc
|
||||
return await config_service.get_jail_config(socket_path, name)
|
||||
|
||||
|
||||
|
||||
@@ -245,16 +195,7 @@ async def update_jail_config(
|
||||
HTTPException: 400 when a set command is rejected.
|
||||
HTTPException: 502 when fail2ban is unreachable.
|
||||
"""
|
||||
try:
|
||||
await config_service.update_jail_config(socket_path, name, body)
|
||||
except JailNotFoundError:
|
||||
raise _not_found(name) from None
|
||||
except ConfigValidationError as exc:
|
||||
raise _unprocessable(str(exc)) from exc
|
||||
except ConfigOperationError as exc:
|
||||
raise _bad_request(str(exc)) from exc
|
||||
except Fail2BanConnectionError as exc:
|
||||
raise _bad_gateway(exc) from exc
|
||||
await config_service.update_jail_config(socket_path, name, body)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -292,14 +233,7 @@ async def add_log_path(
|
||||
HTTPException: 400 when the command is rejected.
|
||||
HTTPException: 502 when fail2ban is unreachable.
|
||||
"""
|
||||
try:
|
||||
await config_service.add_log_path(socket_path, name, body)
|
||||
except JailNotFoundError:
|
||||
raise _not_found(name) from None
|
||||
except ConfigOperationError as exc:
|
||||
raise _bad_request(str(exc)) from exc
|
||||
except Fail2BanConnectionError as exc:
|
||||
raise _bad_gateway(exc) from exc
|
||||
await config_service.add_log_path(socket_path, name, body)
|
||||
|
||||
|
||||
|
||||
@@ -332,14 +266,7 @@ async def delete_log_path(
|
||||
HTTPException: 400 when the command is rejected.
|
||||
HTTPException: 502 when fail2ban is unreachable.
|
||||
"""
|
||||
try:
|
||||
await config_service.delete_log_path(socket_path, name, log_path)
|
||||
except JailNotFoundError:
|
||||
raise _not_found(name) from None
|
||||
except ConfigOperationError as exc:
|
||||
raise _bad_request(str(exc)) from exc
|
||||
except Fail2BanConnectionError as exc:
|
||||
raise _bad_gateway(exc) from exc
|
||||
await config_service.delete_log_path(socket_path, name, log_path)
|
||||
|
||||
|
||||
|
||||
@@ -381,24 +308,7 @@ async def activate_jail(
|
||||
"""
|
||||
req = body if body is not None else ActivateJailRequest()
|
||||
|
||||
try:
|
||||
result = await jail_config_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
|
||||
result = await jail_config_service.activate_jail(config_dir, socket_path, name, req)
|
||||
|
||||
if result.active:
|
||||
record_activation(app, name)
|
||||
@@ -438,25 +348,7 @@ async def deactivate_jail(
|
||||
HTTPException: 502 if fail2ban is unreachable.
|
||||
"""
|
||||
|
||||
try:
|
||||
result = await jail_config_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
|
||||
|
||||
result = await jail_config_service.deactivate_jail(config_dir, socket_path, name)
|
||||
return result
|
||||
|
||||
|
||||
@@ -494,24 +386,7 @@ async def delete_jail_local_override(
|
||||
HTTPException: 502 if fail2ban is unreachable.
|
||||
"""
|
||||
|
||||
try:
|
||||
await jail_config_service.delete_jail_local_override(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 JailAlreadyActiveError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail=f"Jail {name!r} is currently active; deactivate it first.",
|
||||
) from None
|
||||
except ConfigWriteError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to delete config override: {exc}",
|
||||
) from exc
|
||||
except Fail2BanConnectionError as exc:
|
||||
raise _bad_gateway(exc) from exc
|
||||
await jail_config_service.delete_jail_local_override(config_dir, socket_path, name)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -549,10 +424,7 @@ async def validate_jail(
|
||||
HTTPException: 400 if *name* contains invalid characters.
|
||||
HTTPException: 404 if *name* is not found in any config file.
|
||||
"""
|
||||
try:
|
||||
return await jail_config_service.validate_jail_config(config_dir, name)
|
||||
except JailNameError as exc:
|
||||
raise _bad_request(str(exc)) from exc
|
||||
return await jail_config_service.validate_jail_config(config_dir, name)
|
||||
|
||||
|
||||
@router.post(
|
||||
@@ -590,15 +462,7 @@ async def rollback_jail(
|
||||
"""
|
||||
start_cmd_parts: list[str] = start_cmd.split()
|
||||
|
||||
try:
|
||||
result = await jail_config_service.rollback_jail(config_dir, socket_path, name, start_cmd_parts)
|
||||
except JailNameError as exc:
|
||||
raise _bad_request(str(exc)) from exc
|
||||
except ConfigWriteError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to write config override: {exc}",
|
||||
) from exc
|
||||
result = await jail_config_service.rollback_jail(config_dir, socket_path, name, start_cmd_parts)
|
||||
|
||||
if result.fail2ban_running:
|
||||
clear_pending_recovery(app)
|
||||
@@ -638,22 +502,7 @@ async def assign_filter_to_jail(
|
||||
HTTPException: 404 if the jail or filter does not exist.
|
||||
HTTPException: 500 if writing fails.
|
||||
"""
|
||||
try:
|
||||
await filter_config_service.assign_filter_to_jail(config_dir, socket_path, name, body, do_reload=reload)
|
||||
except (JailNameError, FilterNameError) as exc:
|
||||
raise _bad_request(str(exc)) from exc
|
||||
except JailNotFoundInConfigError:
|
||||
raise _not_found(name) from None
|
||||
except FilterNotFoundError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Filter not found: {exc.name!r}",
|
||||
) from exc
|
||||
except ConfigWriteError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to write jail override: {exc}",
|
||||
) from exc
|
||||
await filter_config_service.assign_filter_to_jail(config_dir, socket_path, name, body, do_reload=reload)
|
||||
|
||||
|
||||
@router.post(
|
||||
@@ -688,22 +537,7 @@ async def assign_action_to_jail(
|
||||
HTTPException: 404 if the jail or action does not exist.
|
||||
HTTPException: 500 if writing fails.
|
||||
"""
|
||||
try:
|
||||
await action_config_service.assign_action_to_jail(config_dir, socket_path, name, body, do_reload=reload)
|
||||
except (JailNameError, ActionNameError) as exc:
|
||||
raise _bad_request(str(exc)) from exc
|
||||
except JailNotFoundInConfigError:
|
||||
raise _not_found(name) from None
|
||||
except ActionNotFoundError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Action not found: {exc.name!r}",
|
||||
) from exc
|
||||
except ConfigWriteError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to write jail override: {exc}",
|
||||
) from exc
|
||||
await action_config_service.assign_action_to_jail(config_dir, socket_path, name, body, do_reload=reload)
|
||||
|
||||
|
||||
@router.delete(
|
||||
@@ -737,23 +571,13 @@ async def remove_action_from_jail(
|
||||
HTTPException: 404 if the jail is not found in config files.
|
||||
HTTPException: 500 if writing fails.
|
||||
"""
|
||||
try:
|
||||
await action_config_service.remove_action_from_jail(
|
||||
config_dir,
|
||||
socket_path,
|
||||
name,
|
||||
action_name,
|
||||
do_reload=reload,
|
||||
)
|
||||
except (JailNameError, ActionNameError) as exc:
|
||||
raise _bad_request(str(exc)) from exc
|
||||
except JailNotFoundInConfigError:
|
||||
raise _not_found(name) from None
|
||||
except ConfigWriteError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to write jail override: {exc}",
|
||||
) from exc
|
||||
await action_config_service.remove_action_from_jail(
|
||||
config_dir,
|
||||
socket_path,
|
||||
name,
|
||||
action_name,
|
||||
do_reload=reload,
|
||||
)
|
||||
# ---------------------------------------------------------------------------
|
||||
# Filter discovery endpoints (Task 2.1)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -46,58 +46,8 @@ from app.services import jail_service
|
||||
|
||||
router: APIRouter = APIRouter(prefix="/api/jails", tags=["Jails"])
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_NamePath = Annotated[str, Path(description="Jail name as configured in fail2ban.")]
|
||||
|
||||
|
||||
def _not_found(name: str) -> HTTPException:
|
||||
"""Return a 404 response for an unknown jail.
|
||||
|
||||
Args:
|
||||
name: Jail name that was not found.
|
||||
|
||||
Returns:
|
||||
:class:`fastapi.HTTPException` with status 404.
|
||||
"""
|
||||
return HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Jail not found: {name!r}",
|
||||
)
|
||||
|
||||
|
||||
def _bad_gateway(exc: Exception) -> HTTPException:
|
||||
"""Return a 502 response when fail2ban is unreachable.
|
||||
|
||||
Args:
|
||||
exc: The underlying connection error.
|
||||
|
||||
Returns:
|
||||
:class:`fastapi.HTTPException` with status 502.
|
||||
"""
|
||||
return HTTPException(
|
||||
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||
detail=f"Cannot reach fail2ban: {exc}",
|
||||
)
|
||||
|
||||
|
||||
def _conflict(message: str) -> HTTPException:
|
||||
"""Return a 409 response for invalid jail state transitions.
|
||||
|
||||
Args:
|
||||
message: Human-readable description of the conflict.
|
||||
|
||||
Returns:
|
||||
:class:`fastapi.HTTPException` with status 409.
|
||||
"""
|
||||
return HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail=message,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Jail listing & detail
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -124,10 +74,7 @@ async def get_jails(
|
||||
Returns:
|
||||
:class:`~app.models.jail.JailListResponse` with all active jails.
|
||||
"""
|
||||
try:
|
||||
return await jail_service.list_jails(socket_path)
|
||||
except Fail2BanConnectionError as exc:
|
||||
raise _bad_gateway(exc) from exc
|
||||
return await jail_service.list_jails(socket_path)
|
||||
|
||||
|
||||
@router.get(
|
||||
@@ -157,12 +104,7 @@ async def get_jail(
|
||||
HTTPException: 404 when the jail does not exist.
|
||||
HTTPException: 502 when fail2ban is unreachable.
|
||||
"""
|
||||
try:
|
||||
return await jail_service.get_jail(socket_path, name)
|
||||
except JailNotFoundError:
|
||||
raise _not_found(name) from None
|
||||
except Fail2BanConnectionError as exc:
|
||||
raise _bad_gateway(exc) from exc
|
||||
return await jail_service.get_jail(socket_path, name)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -194,13 +136,8 @@ async def reload_all_jails(
|
||||
HTTPException: 502 when fail2ban is unreachable.
|
||||
HTTPException: 409 when fail2ban reports the operation failed.
|
||||
"""
|
||||
try:
|
||||
await jail_service.reload_all(socket_path)
|
||||
return JailCommandResponse(message="All jails reloaded successfully.", jail="*")
|
||||
except JailOperationError as exc:
|
||||
raise _conflict(str(exc)) from exc
|
||||
except Fail2BanConnectionError as exc:
|
||||
raise _bad_gateway(exc) from exc
|
||||
await jail_service.reload_all(socket_path)
|
||||
return JailCommandResponse(message="All jails reloaded successfully.", jail="*")
|
||||
|
||||
|
||||
@router.post(
|
||||
@@ -227,15 +164,8 @@ async def start_jail(
|
||||
HTTPException: 409 when fail2ban reports the operation failed.
|
||||
HTTPException: 502 when fail2ban is unreachable.
|
||||
"""
|
||||
try:
|
||||
await jail_service.start_jail(socket_path, name)
|
||||
return JailCommandResponse(message=f"Jail {name!r} started.", jail=name)
|
||||
except JailNotFoundError:
|
||||
raise _not_found(name) from None
|
||||
except JailOperationError as exc:
|
||||
raise _conflict(str(exc)) from exc
|
||||
except Fail2BanConnectionError as exc:
|
||||
raise _bad_gateway(exc) from exc
|
||||
await jail_service.start_jail(socket_path, name)
|
||||
return JailCommandResponse(message=f"Jail {name!r} started.", jail=name)
|
||||
|
||||
|
||||
@router.post(
|
||||
@@ -265,13 +195,8 @@ async def stop_jail(
|
||||
HTTPException: 409 when fail2ban reports the operation failed.
|
||||
HTTPException: 502 when fail2ban is unreachable.
|
||||
"""
|
||||
try:
|
||||
await jail_service.stop_jail(socket_path, name)
|
||||
return JailCommandResponse(message=f"Jail {name!r} stopped.", jail=name)
|
||||
except JailOperationError as exc:
|
||||
raise _conflict(str(exc)) from exc
|
||||
except Fail2BanConnectionError as exc:
|
||||
raise _bad_gateway(exc) from exc
|
||||
await jail_service.stop_jail(socket_path, name)
|
||||
return JailCommandResponse(message=f"Jail {name!r} stopped.", jail=name)
|
||||
|
||||
|
||||
@router.post(
|
||||
@@ -304,18 +229,11 @@ async def toggle_idle(
|
||||
HTTPException: 502 when fail2ban is unreachable.
|
||||
"""
|
||||
state_str = "on" if on else "off"
|
||||
try:
|
||||
await jail_service.set_idle(socket_path, name, on=on)
|
||||
return JailCommandResponse(
|
||||
message=f"Jail {name!r} idle mode turned {state_str}.",
|
||||
jail=name,
|
||||
)
|
||||
except JailNotFoundError:
|
||||
raise _not_found(name) from None
|
||||
except JailOperationError as exc:
|
||||
raise _conflict(str(exc)) from exc
|
||||
except Fail2BanConnectionError as exc:
|
||||
raise _bad_gateway(exc) from exc
|
||||
await jail_service.set_idle(socket_path, name, on=on)
|
||||
return JailCommandResponse(
|
||||
message=f"Jail {name!r} idle mode turned {state_str}.",
|
||||
jail=name,
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
@@ -342,15 +260,8 @@ async def reload_jail(
|
||||
HTTPException: 409 when fail2ban reports the operation failed.
|
||||
HTTPException: 502 when fail2ban is unreachable.
|
||||
"""
|
||||
try:
|
||||
await jail_service.reload_jail(socket_path, name)
|
||||
return JailCommandResponse(message=f"Jail {name!r} reloaded.", jail=name)
|
||||
except JailNotFoundError:
|
||||
raise _not_found(name) from None
|
||||
except JailOperationError as exc:
|
||||
raise _conflict(str(exc)) from exc
|
||||
except Fail2BanConnectionError as exc:
|
||||
raise _bad_gateway(exc) from exc
|
||||
await jail_service.reload_jail(socket_path, name)
|
||||
return JailCommandResponse(message=f"Jail {name!r} reloaded.", jail=name)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -389,12 +300,7 @@ async def get_ignore_list(
|
||||
HTTPException: 404 when the jail does not exist.
|
||||
HTTPException: 502 when fail2ban is unreachable.
|
||||
"""
|
||||
try:
|
||||
return await jail_service.get_ignore_list(socket_path, name)
|
||||
except JailNotFoundError:
|
||||
raise _not_found(name) from None
|
||||
except Fail2BanConnectionError as exc:
|
||||
raise _bad_gateway(exc) from exc
|
||||
return await jail_service.get_ignore_list(socket_path, name)
|
||||
|
||||
|
||||
@router.post(
|
||||
@@ -428,23 +334,11 @@ async def add_ignore_ip(
|
||||
HTTPException: 409 when fail2ban reports the operation failed.
|
||||
HTTPException: 502 when fail2ban is unreachable.
|
||||
"""
|
||||
try:
|
||||
await jail_service.add_ignore_ip(socket_path, name, body.ip)
|
||||
return JailCommandResponse(
|
||||
message=f"IP {body.ip!r} added to ignore list of jail {name!r}.",
|
||||
jail=name,
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=str(exc),
|
||||
) from exc
|
||||
except JailNotFoundError:
|
||||
raise _not_found(name) from None
|
||||
except JailOperationError as exc:
|
||||
raise _conflict(str(exc)) from exc
|
||||
except Fail2BanConnectionError as exc:
|
||||
raise _bad_gateway(exc) from exc
|
||||
await jail_service.add_ignore_ip(socket_path, name, body.ip)
|
||||
return JailCommandResponse(
|
||||
message=f"IP {body.ip!r} added to ignore list of jail {name!r}.",
|
||||
jail=name,
|
||||
)
|
||||
|
||||
|
||||
@router.delete(
|
||||
@@ -473,18 +367,11 @@ async def del_ignore_ip(
|
||||
HTTPException: 409 when fail2ban reports the operation failed.
|
||||
HTTPException: 502 when fail2ban is unreachable.
|
||||
"""
|
||||
try:
|
||||
await jail_service.del_ignore_ip(socket_path, name, body.ip)
|
||||
return JailCommandResponse(
|
||||
message=f"IP {body.ip!r} removed from ignore list of jail {name!r}.",
|
||||
jail=name,
|
||||
)
|
||||
except JailNotFoundError:
|
||||
raise _not_found(name) from None
|
||||
except JailOperationError as exc:
|
||||
raise _conflict(str(exc)) from exc
|
||||
except Fail2BanConnectionError as exc:
|
||||
raise _bad_gateway(exc) from exc
|
||||
await jail_service.del_ignore_ip(socket_path, name, body.ip)
|
||||
return JailCommandResponse(
|
||||
message=f"IP {body.ip!r} removed from ignore list of jail {name!r}.",
|
||||
jail=name,
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
@@ -517,18 +404,11 @@ async def toggle_ignore_self(
|
||||
HTTPException: 502 when fail2ban is unreachable.
|
||||
"""
|
||||
state_str = "enabled" if on else "disabled"
|
||||
try:
|
||||
await jail_service.set_ignore_self(socket_path, name, on=on)
|
||||
return JailCommandResponse(
|
||||
message=f"ignoreself {state_str} for jail {name!r}.",
|
||||
jail=name,
|
||||
)
|
||||
except JailNotFoundError:
|
||||
raise _not_found(name) from None
|
||||
except JailOperationError as exc:
|
||||
raise _conflict(str(exc)) from exc
|
||||
except Fail2BanConnectionError as exc:
|
||||
raise _bad_gateway(exc) from exc
|
||||
await jail_service.set_ignore_self(socket_path, name, on=on)
|
||||
return JailCommandResponse(
|
||||
message=f"ignoreself {state_str} for jail {name!r}.",
|
||||
jail=name,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -584,18 +464,13 @@ async def get_jail_banned_ips(
|
||||
detail="page_size must be between 1 and 100.",
|
||||
)
|
||||
|
||||
try:
|
||||
return await jail_service.get_jail_banned_ips(
|
||||
socket_path=socket_path,
|
||||
jail_name=name,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
search=search,
|
||||
geo_batch_lookup=geo_batch_lookup,
|
||||
http_session=http_session,
|
||||
app_db=db,
|
||||
)
|
||||
except JailNotFoundError:
|
||||
raise _not_found(name) from None
|
||||
except Fail2BanConnectionError as exc:
|
||||
raise _bad_gateway(exc) from exc
|
||||
return await jail_service.get_jail_banned_ips(
|
||||
socket_path=socket_path,
|
||||
jail_name=name,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
search=search,
|
||||
geo_batch_lookup=geo_batch_lookup,
|
||||
http_session=http_session,
|
||||
app_db=db,
|
||||
)
|
||||
|
||||
@@ -15,31 +15,11 @@ from fastapi import APIRouter, HTTPException, Request, status
|
||||
from app.dependencies import AuthDep, Fail2BanSocketDep
|
||||
from app.models.server import ServerSettingsResponse, ServerSettingsUpdate
|
||||
from app.services import server_service
|
||||
from app.exceptions import ServerOperationError
|
||||
from app.exceptions import Fail2BanConnectionError
|
||||
from app.exceptions import ServerOperationError, Fail2BanConnectionError
|
||||
|
||||
router: APIRouter = APIRouter(prefix="/api/server", tags=["Server"])
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _bad_gateway(exc: Exception) -> HTTPException:
|
||||
return HTTPException(
|
||||
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||
detail=f"Cannot reach fail2ban: {exc}",
|
||||
)
|
||||
|
||||
|
||||
def _bad_request(message: str) -> HTTPException:
|
||||
return HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=message,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Endpoints
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -70,10 +50,7 @@ async def get_server_settings(
|
||||
Raises:
|
||||
HTTPException: 502 when fail2ban is unreachable.
|
||||
"""
|
||||
try:
|
||||
return await server_service.get_settings(socket_path)
|
||||
except Fail2BanConnectionError as exc:
|
||||
raise _bad_gateway(exc) from exc
|
||||
return await server_service.get_settings(socket_path)
|
||||
|
||||
|
||||
@router.put(
|
||||
@@ -101,12 +78,7 @@ async def update_server_settings(
|
||||
HTTPException: 400 when a set command is rejected by fail2ban.
|
||||
HTTPException: 502 when fail2ban is unreachable.
|
||||
"""
|
||||
try:
|
||||
await server_service.update_settings(socket_path, body)
|
||||
except ServerOperationError as exc:
|
||||
raise _bad_request(str(exc)) from exc
|
||||
except Fail2BanConnectionError as exc:
|
||||
raise _bad_gateway(exc) from exc
|
||||
await server_service.update_settings(socket_path, body)
|
||||
|
||||
|
||||
@router.post(
|
||||
@@ -135,10 +107,5 @@ async def flush_logs(
|
||||
HTTPException: 400 when the command is rejected.
|
||||
HTTPException: 502 when fail2ban is unreachable.
|
||||
"""
|
||||
try:
|
||||
result = await server_service.flush_logs(socket_path)
|
||||
return {"message": result}
|
||||
except ServerOperationError as exc:
|
||||
raise _bad_request(str(exc)) from exc
|
||||
except Fail2BanConnectionError as exc:
|
||||
raise _bad_gateway(exc) from exc
|
||||
result = await server_service.flush_logs(socket_path)
|
||||
return {"message": result}
|
||||
|
||||
Reference in New Issue
Block a user