From 5480dce22163b912b69c0086a1ace8a448c2c2b6 Mon Sep 17 00:00:00 2001 From: Lukas Date: Thu, 23 Apr 2026 16:00:37 +0200 Subject: [PATCH] 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> --- Docs/Backend-Development.md | 27 +++ Docs/Tasks.md | 24 --- backend/app/main.py | 67 +++++++ backend/app/routers/action_config.py | 67 +------ backend/app/routers/bans.py | 101 +++-------- backend/app/routers/config_misc.py | 76 ++------ backend/app/routers/file_config.py | 251 +++------------------------ backend/app/routers/filter_config.py | 105 +---------- backend/app/routers/geo.py | 24 +-- backend/app/routers/jail_config.py | 214 ++--------------------- backend/app/routers/jails.py | 207 +++++----------------- backend/app/routers/server.py | 43 +---- 12 files changed, 229 insertions(+), 977 deletions(-) diff --git a/Docs/Backend-Development.md b/Docs/Backend-Development.md index 74c4aae..42dd0a3 100644 --- a/Docs/Backend-Development.md +++ b/Docs/Backend-Development.md @@ -251,6 +251,33 @@ async def jail_not_found_handler(request: Request, exc: JailNotFoundError) -> JS return JSONResponse(status_code=404, content={"detail": f"Jail '{exc.name}' not found"}) ``` +### Routers and Exception Propagation + +- **Routers must NOT construct `HTTPException` for domain errors** — let domain exceptions propagate. +- Routers should never have helper functions like `_bad_gateway()`, `_not_found()`, `_conflict()` etc. that convert domain exceptions to `HTTPException`. +- All domain exception types must have corresponding handlers registered in `main.py` via `app.add_exception_handler()`. +- Exception handlers are registered in order from most specific to least specific — FastAPI evaluates them in registration order. + +```python +# ❌ BAD — routers constructing HTTPException for domain exceptions +@router.get("/{name}") +async def get_jail(name: str, socket_path: Fail2BanSocketDep) -> JailDetailResponse: + try: + return await jail_service.get_jail(socket_path, name) + except JailNotFoundError: + raise HTTPException(status_code=404, detail=f"Jail not found: {name!r}") from None + +# ✅ GOOD — domain exception propagates to global handler +@router.get("/{name}") +async def get_jail(name: str, socket_path: Fail2BanSocketDep) -> JailDetailResponse: + return await jail_service.get_jail(socket_path, name) +``` + +All domain exceptions raised by services propagate to handlers in `main.py`, ensuring: +1. Consistent error response format across the entire API. +2. No duplicated exception-to-HTTP-status mapping logic. +3. Easy to audit all error codes — they are all in one place. + --- ## 9. Testing diff --git a/Docs/Tasks.md b/Docs/Tasks.md index 5ee3a47..2ad5153 100644 --- a/Docs/Tasks.md +++ b/Docs/Tasks.md @@ -1,27 +1,3 @@ -### T-01 · Extract `_ok()` / `_to_dict()` into shared util module - -**Where found:** `backend/app/services/ban_service.py`, `jail_service.py`, `config_service.py`, `health_service.py`, `server_service.py`, `log_service.py`, `utils/config_file_utils.py` - -**Why this is needed:** The same two helper functions are copy-pasted across 6–7 service modules. `config_service.py` even has a comment admitting it: *"mirrored from jail_service for isolation"*. Any bug fix or behavioural change requires touching every copy independently. - -**Goal:** Single authoritative implementation. All services import from `app/utils/fail2ban_response.py`. - -**What to do:** -1. Create `backend/app/utils/fail2ban_response.py` with `ok()`, `to_dict()`, `ensure_list()`, `is_not_found_error()`. -2. Delete local definitions in all service files. -3. Import from `fail2ban_response` in each service. - -**Possible traps and issues:** -- `config_file_utils.py` defines `_to_dict_inner` as a nested function inside another function — needs to be unwrapped. -- `ban_service._ok` uses `response` typed as `object`; `server_service._ok` types it as `Fail2BanResponse`. Unify the signature. -- Run all service tests after the change to confirm no subtle type differences. - -**Docs changes needed:** Update `Docs/Backend-Development.md` to document `fail2ban_response.py` as the canonical response-parsing utility. - -**Doc references:** `Docs/Backend-Development.md`, `Docs/Architekture.md` - ---- - ### T-02 · Remove duplicate router-level exception helpers — use global handlers only **Where found:** `backend/app/routers/jails.py`, `bans.py`, `jail_config.py`, `server.py`, `config_misc.py` each define `_bad_gateway()`, `_not_found()`, `_conflict()`. Global handlers for the same exceptions exist in `backend/app/main.py`. diff --git a/backend/app/main.py b/backend/app/main.py index a9c2afe..ae51e19 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -31,23 +31,31 @@ from app import __version__ from app.config import Settings, get_settings from app.exceptions import ( ActionAlreadyExistsError, + ActionNameError, ActionNotFoundError, ActionReadonlyError, + ConfigDirError, ConfigFileExistsError, ConfigFileNameError, ConfigFileNotFoundError, + ConfigFileWriteError, ConfigOperationError, ConfigValidationError, ConfigWriteError, Fail2BanConnectionError, Fail2BanProtocolError, FilterAlreadyExistsError, + FilterInvalidRegexError, + FilterNameError, FilterNotFoundError, FilterReadonlyError, JailAlreadyActiveError, JailAlreadyInactiveError, + JailNameError, JailNotFoundError, + JailNotFoundInConfigError, JailOperationError, + ServerOperationError, ) from app.routers import ( auth, @@ -308,6 +316,56 @@ async def _domain_error_handler( ) +async def _value_error_handler( + request: Request, + exc: ValueError, +) -> JSONResponse: + """Return a ``400 Bad Request`` response for validation and value errors. + + Args: + request: The incoming FastAPI request. + exc: The :class:`ValueError`. + + Returns: + A :class:`fastapi.responses.JSONResponse` with status 400. + """ + log.warning( + "value_error", + path=request.url.path, + method=request.method, + error=str(exc), + ) + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content={"detail": str(exc)}, + ) + + +async def _service_unavailable_handler( + request: Request, + exc: Exception, +) -> JSONResponse: + """Return a ``503 Service Unavailable`` response for infrastructure errors. + + Args: + request: The incoming FastAPI request. + exc: The infrastructure exception (e.g., ConfigDirError). + + Returns: + A :class:`fastapi.responses.JSONResponse` with status 503. + """ + log.warning( + "service_unavailable", + path=request.url.path, + method=request.method, + error=str(exc), + ) + return JSONResponse( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + content={"detail": str(exc)}, + ) + + # --------------------------------------------------------------------------- # Setup-redirect middleware # --------------------------------------------------------------------------- @@ -426,12 +484,19 @@ def create_app(settings: Settings | None = None) -> FastAPI: app.add_exception_handler(Fail2BanConnectionError, _fail2ban_connection_handler) # type: ignore[arg-type] app.add_exception_handler(Fail2BanProtocolError, _fail2ban_protocol_handler) # type: ignore[arg-type] app.add_exception_handler(JailNotFoundError, _not_found_handler) # type: ignore[arg-type] + app.add_exception_handler(JailNotFoundInConfigError, _not_found_handler) # type: ignore[arg-type] app.add_exception_handler(FilterNotFoundError, _not_found_handler) # type: ignore[arg-type] app.add_exception_handler(ActionNotFoundError, _not_found_handler) # type: ignore[arg-type] app.add_exception_handler(ConfigFileNotFoundError, _not_found_handler) # type: ignore[arg-type] app.add_exception_handler(ConfigValidationError, _bad_request_handler) # type: ignore[arg-type] app.add_exception_handler(ConfigFileNameError, _bad_request_handler) # type: ignore[arg-type] app.add_exception_handler(ConfigOperationError, _bad_request_handler) # type: ignore[arg-type] + app.add_exception_handler(ServerOperationError, _bad_request_handler) # type: ignore[arg-type] + app.add_exception_handler(ActionNameError, _bad_request_handler) # type: ignore[arg-type] + app.add_exception_handler(FilterNameError, _bad_request_handler) # type: ignore[arg-type] + app.add_exception_handler(JailNameError, _bad_request_handler) # type: ignore[arg-type] + app.add_exception_handler(FilterInvalidRegexError, _bad_request_handler) # type: ignore[arg-type] + app.add_exception_handler(ValueError, _value_error_handler) # type: ignore[arg-type] app.add_exception_handler(JailOperationError, _conflict_handler) # type: ignore[arg-type] app.add_exception_handler(JailAlreadyActiveError, _conflict_handler) # type: ignore[arg-type] app.add_exception_handler(JailAlreadyInactiveError, _conflict_handler) # type: ignore[arg-type] @@ -441,6 +506,8 @@ def create_app(settings: Settings | None = None) -> FastAPI: app.add_exception_handler(ActionReadonlyError, _conflict_handler) # type: ignore[arg-type] app.add_exception_handler(ConfigFileExistsError, _conflict_handler) # type: ignore[arg-type] app.add_exception_handler(ConfigWriteError, _domain_error_handler) # type: ignore[arg-type] + app.add_exception_handler(ConfigDirError, _service_unavailable_handler) # type: ignore[arg-type] + app.add_exception_handler(ConfigFileWriteError, _bad_request_handler) # type: ignore[arg-type] app.add_exception_handler(Exception, _unhandled_exception_handler) # --- Routers --- diff --git a/backend/app/routers/action_config.py b/backend/app/routers/action_config.py index 28bef3a..4124c85 100644 --- a/backend/app/routers/action_config.py +++ b/backend/app/routers/action_config.py @@ -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) diff --git a/backend/app/routers/bans.py b/backend/app/routers/bans.py index 1621b59..2e7cc17 100644 --- a/backend/app/routers/bans.py +++ b/backend/app/routers/bans.py @@ -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, + ) diff --git a/backend/app/routers/config_misc.py b/backend/app/routers/config_misc.py index bdf12cb..1091db9 100644 --- a/backend/app/routers/config_misc.py +++ b/backend/app/routers/config_misc.py @@ -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, + ) diff --git a/backend/app/routers/file_config.py b/backend/app/routers/file_config.py index 5086a68..57bc023 100644 --- a/backend/app/routers/file_config.py +++ b/backend/app/routers/file_config.py @@ -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) diff --git a/backend/app/routers/filter_config.py b/backend/app/routers/filter_config.py index d8b2be3..b1f2eef 100644 --- a/backend/app/routers/filter_config.py +++ b/backend/app/routers/filter_config.py @@ -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}", - ) - - diff --git a/backend/app/routers/geo.py b/backend/app/routers/geo.py index bef5d29..fd483e2 100644 --- a/backend/app/routers/geo.py +++ b/backend/app/routers/geo.py @@ -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) diff --git a/backend/app/routers/jail_config.py b/backend/app/routers/jail_config.py index de4a9fc..466e064 100644 --- a/backend/app/routers/jail_config.py +++ b/backend/app/routers/jail_config.py @@ -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) # --------------------------------------------------------------------------- diff --git a/backend/app/routers/jails.py b/backend/app/routers/jails.py index 46c7de4..f5ab79b 100644 --- a/backend/app/routers/jails.py +++ b/backend/app/routers/jails.py @@ -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, + ) diff --git a/backend/app/routers/server.py b/backend/app/routers/server.py index dd088f2..e058b96 100644 --- a/backend/app/routers/server.py +++ b/backend/app/routers/server.py @@ -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}