Files
BanGUI/backend/app/routers/action_config.py
Lukas 5480dce221 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>
2026-04-23 16:00:37 +02:00

227 lines
6.8 KiB
Python

from __future__ import annotations
from typing import Annotated
from fastapi import APIRouter, HTTPException, Path, Query, Request, status
from app.dependencies import AuthDep, Fail2BanConfigDirDep, Fail2BanSocketDep
from app.exceptions import (
ActionAlreadyExistsError,
ActionNameError,
ActionNotFoundError,
ActionReadonlyError,
ConfigWriteError,
)
from app.models.config import (
ActionConfig,
ActionCreateRequest,
ActionListResponse,
ActionUpdateRequest,
)
from app.services import action_config_service
router: APIRouter = APIRouter(prefix="/actions", tags=["Action Config"])
_ActionNamePath = Annotated[
str,
Path(description='Action base name, e.g. ``iptables`` or ``iptables.conf``.'),
]
_NamePath = Annotated[
str,
Path(description='Jail name as configured in fail2ban.'),
]
@router.get(
"",
response_model=ActionListResponse,
summary="List all available actions with active/inactive status",
)
async def list_actions(
request: Request,
_auth: AuthDep,
config_dir: Fail2BanConfigDirDep,
socket_path: Fail2BanSocketDep,
) -> ActionListResponse:
"""Return all actions discovered in ``action.d/`` with active/inactive status.
Scans ``{config_dir}/action.d/`` for ``.conf`` files, merges any
corresponding ``.local`` overrides, and cross-references each action's
name against the ``action`` fields of currently running jails to determine
whether it is active.
Active actions (those used by at least one running jail) are sorted to the
top of the list; inactive actions follow. Both groups are sorted
alphabetically within themselves.
Args:
request: FastAPI request object.
_auth: Validated session — enforces authentication.
Returns:
:class:`~app.models.config.ActionListResponse` with all discovered
actions.
"""
result = await action_config_service.list_actions(config_dir, socket_path)
result.actions.sort(key=lambda a: (not a.active, a.name.lower()))
return result
@router.get(
"/{name}",
response_model=ActionConfig,
summary="Return full parsed detail for a single action",
)
async def get_action(
request: Request,
_auth: AuthDep,
config_dir: Fail2BanConfigDirDep,
socket_path: Fail2BanSocketDep,
name: _ActionNamePath,
) -> ActionConfig:
"""Return the full parsed configuration and active/inactive status for one action.
Reads ``{config_dir}/action.d/{name}.conf``, merges any corresponding
``.local`` override, and annotates the result with ``active``,
``used_by_jails``, ``source_file``, and ``has_local_override``.
Args:
request: FastAPI request object.
_auth: Validated session — enforces authentication.
name: Action base name (with or without ``.conf`` extension).
Returns:
:class:`~app.models.config.ActionConfig`.
Raises:
HTTPException: 404 if the action is not found in ``action.d/``.
"""
return await action_config_service.get_action(config_dir, socket_path, name)
# ---------------------------------------------------------------------------
# Action write endpoints (Task 3.2)
# ---------------------------------------------------------------------------
@router.put(
"/{name}",
response_model=ActionConfig,
summary="Update an action's .local override with new lifecycle command values",
)
async def update_action(
request: Request,
_auth: AuthDep,
config_dir: Fail2BanConfigDirDep,
socket_path: Fail2BanSocketDep,
name: _ActionNamePath,
body: ActionUpdateRequest,
reload: bool = Query(default=False, description="Reload fail2ban after writing."),
) -> ActionConfig:
"""Update an action's ``[Definition]`` fields by writing a ``.local`` override.
Only non-``null`` fields in the request body are written. The original
``.conf`` file is never modified.
Args:
request: FastAPI request object.
_auth: Validated session.
name: Action base name (with or without ``.conf`` extension).
body: Partial update — lifecycle commands and ``[Init]`` parameters.
reload: When ``true``, trigger a fail2ban reload after writing.
Returns:
Updated :class:`~app.models.config.ActionConfig`.
Raises:
HTTPException: 400 if *name* contains invalid characters.
HTTPException: 404 if the action does not exist.
HTTPException: 500 if writing the ``.local`` file fails.
"""
return await action_config_service.update_action(config_dir, socket_path, name, body, do_reload=reload)
@router.post(
"",
response_model=ActionConfig,
status_code=status.HTTP_201_CREATED,
summary="Create a new user-defined action",
)
async def create_action(
request: Request,
_auth: AuthDep,
config_dir: Fail2BanConfigDirDep,
socket_path: Fail2BanSocketDep,
body: ActionCreateRequest,
reload: bool = Query(default=False, description="Reload fail2ban after creating."),
) -> ActionConfig:
"""Create a new user-defined action at ``action.d/{name}.local``.
Returns 409 if a ``.conf`` or ``.local`` for the requested name already
exists.
Args:
request: FastAPI request object.
_auth: Validated session.
body: Action name and ``[Definition]`` lifecycle fields.
reload: When ``true``, trigger a fail2ban reload after creating.
Returns:
:class:`~app.models.config.ActionConfig` for the new action.
Raises:
HTTPException: 400 if the name contains invalid characters.
HTTPException: 409 if the action already exists.
HTTPException: 500 if writing fails.
"""
return await action_config_service.create_action(config_dir, socket_path, body, do_reload=reload)
@router.delete(
"/{name}",
status_code=status.HTTP_204_NO_CONTENT,
summary="Delete a user-created action's .local file",
)
async def delete_action(
request: Request,
_auth: AuthDep,
config_dir: Fail2BanConfigDirDep,
name: _ActionNamePath,
) -> None:
"""Delete a user-created action's ``.local`` override file.
Shipped ``.conf``-only actions cannot be deleted (returns 409). When
both a ``.conf`` and a ``.local`` exist, only the ``.local`` is removed.
Args:
request: FastAPI request object.
_auth: Validated session.
name: Action base name.
Raises:
HTTPException: 400 if *name* contains invalid characters.
HTTPException: 404 if the action does not exist.
HTTPException: 409 if the action is a shipped default (conf-only).
HTTPException: 500 if deletion fails.
"""
await action_config_service.delete_action(config_dir, name)
# ---------------------------------------------------------------------------
# fail2ban log viewer endpoints
# ---------------------------------------------------------------------------