refactoring-backend #3
@@ -85,7 +85,7 @@ backend/
|
||||
│ ├── `main.py` # FastAPI app factory, lifespan, exception handlers
|
||||
│ ├── `config.py` # Pydantic settings (env vars, .env loading)
|
||||
│ ├── `db.py` # Database connection and initialization
|
||||
│ ├── `exceptions.py` # Shared domain exception classes
|
||||
│ ├── `exceptions.py` # Shared domain exception classes; all services and routers import from here
|
||||
│ ├── `dependencies.py` # FastAPI Depends() providers (DB, services, auth)
|
||||
│ ├── `models/` # Pydantic schemas
|
||||
│ │ ├── auth.py # Login request/response, session models
|
||||
|
||||
@@ -90,6 +90,8 @@ Any config file write error in a filter operation (bad content, permission denie
|
||||
|
||||
### Task 4 — Consolidate all exception classes into app.exceptions
|
||||
|
||||
**Status:** Completed
|
||||
|
||||
**Severity:** High
|
||||
|
||||
**Where:**
|
||||
|
||||
@@ -51,3 +51,83 @@ class ConfigWriteError(Exception):
|
||||
def __init__(self, message: str) -> None:
|
||||
self.message = message
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
class JailNameError(Exception):
|
||||
"""Raised when a jail name contains invalid characters."""
|
||||
|
||||
|
||||
class JailAlreadyActiveError(Exception):
|
||||
"""Raised when trying to activate a jail that is already active."""
|
||||
|
||||
def __init__(self, name: str) -> None:
|
||||
self.name = name
|
||||
super().__init__(f"Jail is already active: {name!r}")
|
||||
|
||||
|
||||
class JailAlreadyInactiveError(Exception):
|
||||
"""Raised when trying to deactivate a jail that is already inactive."""
|
||||
|
||||
def __init__(self, name: str) -> None:
|
||||
self.name = name
|
||||
super().__init__(f"Jail is already inactive: {name!r}")
|
||||
|
||||
|
||||
class FilterNotFoundError(Exception):
|
||||
"""Raised when the requested filter name is not found."""
|
||||
|
||||
def __init__(self, name: str) -> None:
|
||||
self.name = name
|
||||
super().__init__(f"Filter not found: {name!r}")
|
||||
|
||||
|
||||
class FilterAlreadyExistsError(Exception):
|
||||
"""Raised when trying to create a filter whose `.conf` or `.local` already exists."""
|
||||
|
||||
def __init__(self, name: str) -> None:
|
||||
self.name = name
|
||||
super().__init__(f"Filter already exists: {name!r}")
|
||||
|
||||
|
||||
class FilterNameError(Exception):
|
||||
"""Raised when a filter name contains invalid characters."""
|
||||
|
||||
|
||||
class FilterReadonlyError(Exception):
|
||||
"""Raised when trying to delete a shipped `.conf` filter with no `.local` override."""
|
||||
|
||||
def __init__(self, name: str) -> None:
|
||||
self.name = name
|
||||
super().__init__(
|
||||
f"Filter {name!r} is a shipped default (.conf only); only user-created .local files can be deleted."
|
||||
)
|
||||
|
||||
|
||||
class ActionNotFoundError(Exception):
|
||||
"""Raised when the requested action name is not found."""
|
||||
|
||||
def __init__(self, name: str) -> None:
|
||||
self.name = name
|
||||
super().__init__(f"Action not found: {name!r}")
|
||||
|
||||
|
||||
class ActionAlreadyExistsError(Exception):
|
||||
"""Raised when trying to create an action whose `.conf` or `.local` already exists."""
|
||||
|
||||
def __init__(self, name: str) -> None:
|
||||
self.name = name
|
||||
super().__init__(f"Action already exists: {name!r}")
|
||||
|
||||
|
||||
class ActionNameError(Exception):
|
||||
"""Raised when an action name contains invalid characters."""
|
||||
|
||||
|
||||
class ActionReadonlyError(Exception):
|
||||
"""Raised when trying to delete a shipped `.conf` action with no `.local` override."""
|
||||
|
||||
def __init__(self, name: str) -> None:
|
||||
self.name = name
|
||||
super().__init__(
|
||||
f"Action {name!r} is a shipped default (.conf only); only user-created .local files can be deleted."
|
||||
)
|
||||
|
||||
@@ -2,10 +2,18 @@ from __future__ import annotations
|
||||
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Path, Query, status
|
||||
from fastapi import APIRouter, HTTPException, Path, Query, Request, status
|
||||
|
||||
from app.dependencies import AuthDep, Fail2BanConfigDirDep, Fail2BanSocketDep
|
||||
from app.exceptions import ConfigWriteError
|
||||
from app.exceptions import (
|
||||
ActionAlreadyExistsError,
|
||||
ActionNameError,
|
||||
ActionNotFoundError,
|
||||
ActionReadonlyError,
|
||||
ConfigWriteError,
|
||||
JailNameError,
|
||||
JailNotFoundInConfigError,
|
||||
)
|
||||
from app.models.config import (
|
||||
ActionConfig,
|
||||
ActionCreateRequest,
|
||||
@@ -14,13 +22,6 @@ from app.models.config import (
|
||||
AssignActionRequest,
|
||||
)
|
||||
from app.services import action_config_service
|
||||
from app.services.action_config_service import (
|
||||
ActionAlreadyExistsError,
|
||||
ActionNameError,
|
||||
ActionNotFoundError,
|
||||
ActionReadonlyError,
|
||||
)
|
||||
from app.services.jail_config_service import JailNameError, JailNotFoundInConfigError
|
||||
|
||||
router: APIRouter = APIRouter()
|
||||
|
||||
@@ -29,6 +30,11 @@ _ActionNamePath = Annotated[
|
||||
Path(description='Action base name, e.g. ``iptables`` or ``iptables.conf``.'),
|
||||
]
|
||||
|
||||
_NamePath = Annotated[
|
||||
str,
|
||||
Path(description='Jail name as configured in fail2ban.'),
|
||||
]
|
||||
|
||||
|
||||
def _action_not_found(name: str) -> HTTPException:
|
||||
return HTTPException(
|
||||
@@ -36,6 +42,17 @@ def _action_not_found(name: str) -> HTTPException:
|
||||
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(
|
||||
"/actions",
|
||||
response_model=ActionListResponse,
|
||||
|
||||
@@ -2,26 +2,27 @@ from __future__ import annotations
|
||||
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Path, Query, status
|
||||
from fastapi import APIRouter, HTTPException, Path, Query, Request, status
|
||||
|
||||
from app.dependencies import AuthDep, Fail2BanConfigDirDep, Fail2BanSocketDep
|
||||
from app.exceptions import ConfigWriteError
|
||||
from app.models.config import (
|
||||
AssignFilterRequest,
|
||||
FilterConfig,
|
||||
FilterConfigUpdate,
|
||||
FilterCreateRequest,
|
||||
FilterListResponse,
|
||||
)
|
||||
from app.services import filter_config_service
|
||||
from app.services.filter_config_service import (
|
||||
from app.exceptions import (
|
||||
ConfigWriteError,
|
||||
FilterAlreadyExistsError,
|
||||
FilterInvalidRegexError,
|
||||
FilterNameError,
|
||||
FilterNotFoundError,
|
||||
FilterReadonlyError,
|
||||
JailNameError,
|
||||
JailNotFoundInConfigError,
|
||||
)
|
||||
from app.services.jail_config_service import JailNameError, JailNotFoundInConfigError
|
||||
from app.models.config import (
|
||||
AssignFilterRequest,
|
||||
FilterConfig,
|
||||
FilterCreateRequest,
|
||||
FilterListResponse,
|
||||
FilterUpdateRequest,
|
||||
)
|
||||
from app.services import filter_config_service
|
||||
|
||||
router: APIRouter = APIRouter()
|
||||
|
||||
@@ -42,6 +43,21 @@ def _filter_not_found(name: str) -> HTTPException:
|
||||
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(
|
||||
"/filters",
|
||||
response_model=FilterListResponse,
|
||||
|
||||
@@ -15,7 +15,12 @@ from app.dependencies import (
|
||||
from app.exceptions import (
|
||||
ConfigOperationError,
|
||||
ConfigValidationError,
|
||||
ConfigWriteError,
|
||||
JailAlreadyActiveError,
|
||||
JailAlreadyInactiveError,
|
||||
JailNameError,
|
||||
JailNotFoundError,
|
||||
JailNotFoundInConfigError,
|
||||
)
|
||||
from app.models.config import (
|
||||
ActivateJailRequest,
|
||||
@@ -30,13 +35,6 @@ from app.models.config import (
|
||||
RollbackResponse,
|
||||
)
|
||||
from app.services import config_service, jail_config_service, jail_service
|
||||
from app.services.jail_config_service import (
|
||||
ConfigWriteError,
|
||||
JailAlreadyActiveError,
|
||||
JailAlreadyInactiveError,
|
||||
JailNameError,
|
||||
JailNotFoundInConfigError,
|
||||
)
|
||||
from app.utils.fail2ban_client import Fail2BanConnectionError
|
||||
|
||||
router: APIRouter = APIRouter()
|
||||
|
||||
@@ -7,7 +7,6 @@ for fail2ban action configurations.
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from app.utils.async_utils import run_blocking
|
||||
import configparser
|
||||
import contextlib
|
||||
import io
|
||||
@@ -18,7 +17,15 @@ from pathlib import Path
|
||||
|
||||
import structlog
|
||||
|
||||
from app.exceptions import ConfigWriteError, JailNotFoundInConfigError
|
||||
from app.exceptions import (
|
||||
ActionAlreadyExistsError,
|
||||
ActionNameError,
|
||||
ActionNotFoundError,
|
||||
ActionReadonlyError,
|
||||
ConfigWriteError,
|
||||
JailNameError,
|
||||
JailNotFoundInConfigError,
|
||||
)
|
||||
from app.helpers.config_file_helpers import (
|
||||
_get_active_jail_names,
|
||||
_parse_jails_sync,
|
||||
@@ -33,59 +40,10 @@ from app.models.config import (
|
||||
AssignActionRequest,
|
||||
)
|
||||
from app.utils import conffile_parser
|
||||
from app.utils.async_utils import run_blocking
|
||||
|
||||
log: structlog.stdlib.BoundLogger = structlog.get_logger()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Custom exceptions
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class ActionNotFoundError(Exception):
|
||||
"""Raised when the requested action name is not found in ``action.d/``."""
|
||||
|
||||
def __init__(self, name: str) -> None:
|
||||
"""Initialise with the action name that was not found.
|
||||
|
||||
Args:
|
||||
name: The action name that could not be located.
|
||||
"""
|
||||
self.name: str = name
|
||||
super().__init__(f"Action not found: {name!r}")
|
||||
|
||||
|
||||
class ActionAlreadyExistsError(Exception):
|
||||
"""Raised when trying to create an action whose ``.conf`` or ``.local`` already exists."""
|
||||
|
||||
def __init__(self, name: str) -> None:
|
||||
"""Initialise with the action name that already exists.
|
||||
|
||||
Args:
|
||||
name: The action name that already exists.
|
||||
"""
|
||||
self.name: str = name
|
||||
super().__init__(f"Action already exists: {name!r}")
|
||||
|
||||
|
||||
class ActionReadonlyError(Exception):
|
||||
"""Raised when trying to delete a shipped ``.conf`` action with no ``.local`` override."""
|
||||
|
||||
def __init__(self, name: str) -> None:
|
||||
"""Initialise with the action name that cannot be deleted.
|
||||
|
||||
Args:
|
||||
name: The action name that is read-only (shipped ``.conf`` only).
|
||||
"""
|
||||
self.name: str = name
|
||||
super().__init__(
|
||||
f"Action {name!r} is a shipped default (.conf only); only user-created .local files can be deleted."
|
||||
)
|
||||
|
||||
|
||||
class ActionNameError(Exception):
|
||||
"""Raised when an action name contains invalid characters."""
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Constants
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -108,15 +66,6 @@ _TRUE_VALUES: frozenset[str] = frozenset({"true", "yes", "1"})
|
||||
_FALSE_VALUES: frozenset[str] = frozenset({"false", "no", "0"})
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helper exceptions
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class JailNameError(Exception):
|
||||
"""Raised when a jail name contains invalid characters."""
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Internal helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -601,7 +550,6 @@ async def list_actions(
|
||||
``used_by_jails`` lists.
|
||||
"""
|
||||
action_d = Path(config_dir) / "action.d"
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
raw_actions: list[tuple[str, str, str, bool, str]] = await run_blocking( _parse_actions_sync, action_d)
|
||||
|
||||
@@ -676,7 +624,6 @@ async def get_action(
|
||||
action_d = Path(config_dir) / "action.d"
|
||||
conf_path = action_d / f"{base_name}.conf"
|
||||
local_path = action_d / f"{base_name}.local"
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
def _read() -> tuple[str, bool, str]:
|
||||
"""Read action content and return (content, has_local_override, source_path)."""
|
||||
@@ -787,7 +734,6 @@ async def update_action(
|
||||
content = conffile_parser.serialize_action_config(merged)
|
||||
|
||||
action_d = Path(config_dir) / "action.d"
|
||||
loop = asyncio.get_event_loop()
|
||||
await run_blocking( _write_action_local_sync, action_d, base_name, content)
|
||||
|
||||
if do_reload:
|
||||
@@ -840,7 +786,6 @@ async def create_action(
|
||||
if conf_path.is_file() or local_path.is_file():
|
||||
raise ActionAlreadyExistsError(req.name)
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
await run_blocking( _check_not_exists)
|
||||
|
||||
cfg = ActionConfig(
|
||||
@@ -903,7 +848,6 @@ async def delete_action(
|
||||
conf_path = action_d / f"{base_name}.conf"
|
||||
local_path = action_d / f"{base_name}.local"
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
def _delete() -> None:
|
||||
has_conf = conf_path.is_file()
|
||||
@@ -957,7 +901,6 @@ async def assign_action_to_jail(
|
||||
_safe_jail_name(jail_name)
|
||||
_safe_action_name(req.action_name)
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
all_jails, _src = await run_blocking( _parse_jails_sync, Path(config_dir))
|
||||
if jail_name not in all_jails:
|
||||
@@ -1035,7 +978,6 @@ async def remove_action_from_jail(
|
||||
_safe_jail_name(jail_name)
|
||||
_safe_action_name(action_name)
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
all_jails, _src = await run_blocking( _parse_jails_sync, Path(config_dir))
|
||||
if jail_name not in all_jails:
|
||||
|
||||
@@ -21,7 +21,6 @@ directory traversal.
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from app.utils.async_utils import run_blocking
|
||||
import configparser
|
||||
import contextlib
|
||||
import io
|
||||
@@ -33,7 +32,23 @@ from typing import cast
|
||||
|
||||
import structlog
|
||||
|
||||
from app.exceptions import FilterInvalidRegexError, JailNotFoundError
|
||||
from app.exceptions import (
|
||||
ActionAlreadyExistsError,
|
||||
ActionNameError,
|
||||
ActionNotFoundError,
|
||||
ActionReadonlyError,
|
||||
ConfigWriteError,
|
||||
FilterAlreadyExistsError,
|
||||
FilterInvalidRegexError,
|
||||
FilterNameError,
|
||||
FilterNotFoundError,
|
||||
FilterReadonlyError,
|
||||
JailAlreadyActiveError,
|
||||
JailAlreadyInactiveError,
|
||||
JailNameError,
|
||||
JailNotFoundError,
|
||||
JailNotFoundInConfigError,
|
||||
)
|
||||
from app.helpers.jail_helpers import reload_jails
|
||||
from app.models.config import (
|
||||
ActionConfig,
|
||||
@@ -58,6 +73,7 @@ from app.models.config import (
|
||||
RollbackResponse,
|
||||
)
|
||||
from app.utils import conffile_parser
|
||||
from app.utils.async_utils import run_blocking
|
||||
from app.utils.fail2ban_client import (
|
||||
Fail2BanClient,
|
||||
Fail2BanConnectionError,
|
||||
@@ -120,90 +136,6 @@ _TRUE_VALUES: frozenset[str] = frozenset({"true", "yes", "1"})
|
||||
_FALSE_VALUES: frozenset[str] = frozenset({"false", "no", "0"})
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Custom exceptions
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class JailNotFoundInConfigError(Exception):
|
||||
"""Raised when the requested jail name is not defined in any config file."""
|
||||
|
||||
def __init__(self, name: str) -> None:
|
||||
"""Initialise with the jail name that was not found.
|
||||
|
||||
Args:
|
||||
name: The jail name that could not be located.
|
||||
"""
|
||||
self.name: str = name
|
||||
super().__init__(f"Jail not found in config files: {name!r}")
|
||||
|
||||
|
||||
class JailAlreadyActiveError(Exception):
|
||||
"""Raised when trying to activate a jail that is already active."""
|
||||
|
||||
def __init__(self, name: str) -> None:
|
||||
"""Initialise with the jail name.
|
||||
|
||||
Args:
|
||||
name: The jail that is already active.
|
||||
"""
|
||||
self.name: str = name
|
||||
super().__init__(f"Jail is already active: {name!r}")
|
||||
|
||||
|
||||
class JailAlreadyInactiveError(Exception):
|
||||
"""Raised when trying to deactivate a jail that is already inactive."""
|
||||
|
||||
def __init__(self, name: str) -> None:
|
||||
"""Initialise with the jail name.
|
||||
|
||||
Args:
|
||||
name: The jail that is already inactive.
|
||||
"""
|
||||
self.name: str = name
|
||||
super().__init__(f"Jail is already inactive: {name!r}")
|
||||
|
||||
|
||||
class JailNameError(Exception):
|
||||
"""Raised when a jail name contains invalid characters."""
|
||||
|
||||
|
||||
class ConfigWriteError(Exception):
|
||||
"""Raised when writing a ``.local`` override file fails."""
|
||||
|
||||
|
||||
class FilterNameError(Exception):
|
||||
"""Raised when a filter name contains invalid characters."""
|
||||
|
||||
|
||||
class FilterAlreadyExistsError(Exception):
|
||||
"""Raised when trying to create a filter whose ``.conf`` or ``.local`` already exists."""
|
||||
|
||||
def __init__(self, name: str) -> None:
|
||||
"""Initialise with the filter name that already exists.
|
||||
|
||||
Args:
|
||||
name: The filter name that already exists.
|
||||
"""
|
||||
self.name: str = name
|
||||
super().__init__(f"Filter already exists: {name!r}")
|
||||
|
||||
|
||||
class FilterReadonlyError(Exception):
|
||||
"""Raised when trying to delete a shipped ``.conf`` filter with no ``.local`` override."""
|
||||
|
||||
def __init__(self, name: str) -> None:
|
||||
"""Initialise with the filter name that cannot be deleted.
|
||||
|
||||
Args:
|
||||
name: The filter name that is read-only (shipped ``.conf`` only).
|
||||
"""
|
||||
self.name: str = name
|
||||
super().__init__(
|
||||
f"Filter {name!r} is a shipped default (.conf only); only user-created .local files can be deleted."
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Internal helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -1098,8 +1030,9 @@ async def list_inactive_jails(
|
||||
:class:`~app.models.config.InactiveJailListResponse` with all
|
||||
inactive jails.
|
||||
"""
|
||||
loop = asyncio.get_event_loop()
|
||||
parsed_result: tuple[dict[str, dict[str, str]], dict[str, str]] = await run_blocking(_parse_jails_sync, Path(config_dir)
|
||||
parsed_result: tuple[dict[str, dict[str, str]], dict[str, str]] = await run_blocking(
|
||||
_parse_jails_sync,
|
||||
Path(config_dir),
|
||||
)
|
||||
all_jails, source_files = parsed_result
|
||||
active_names: set[str] = await _get_active_jail_names(socket_path)
|
||||
@@ -1155,7 +1088,6 @@ async def activate_jail(
|
||||
"""
|
||||
_safe_jail_name(name)
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
all_jails, _source_files = await run_blocking( _parse_jails_sync, Path(config_dir))
|
||||
|
||||
if name not in all_jails:
|
||||
@@ -1207,7 +1139,8 @@ async def activate_jail(
|
||||
# we can restore it if activation fails. #
|
||||
# ---------------------------------------------------------------------- #
|
||||
local_path = Path(config_dir) / "jail.d" / f"{name}.local"
|
||||
original_content: bytes | None = await run_blocking(lambda: local_path.read_bytes() if local_path.exists() else None,
|
||||
original_content: bytes | None = await run_blocking(
|
||||
lambda: local_path.read_bytes() if local_path.exists() else None,
|
||||
)
|
||||
|
||||
await run_blocking(_write_local_override_sync,
|
||||
@@ -1351,7 +1284,6 @@ async def _rollback_activation_async(
|
||||
``True`` if fail2ban is responsive again after the rollback, ``False``
|
||||
if recovery also failed.
|
||||
"""
|
||||
loop = asyncio.get_event_loop()
|
||||
local_path = Path(config_dir) / "jail.d" / f"{name}.local"
|
||||
|
||||
# Step 1 — restore original file (or delete it).
|
||||
@@ -1411,7 +1343,6 @@ async def deactivate_jail(
|
||||
"""
|
||||
_safe_jail_name(name)
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
all_jails, _source_files = await run_blocking( _parse_jails_sync, Path(config_dir))
|
||||
|
||||
if name not in all_jails:
|
||||
@@ -1467,7 +1398,6 @@ async def delete_jail_local_override(
|
||||
"""
|
||||
_safe_jail_name(name)
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
all_jails, _source_files = await run_blocking( _parse_jails_sync, Path(config_dir))
|
||||
|
||||
if name not in all_jails:
|
||||
@@ -1507,7 +1437,6 @@ async def validate_jail_config(
|
||||
JailNameError: If *name* contains invalid characters.
|
||||
"""
|
||||
_safe_jail_name(name)
|
||||
loop = asyncio.get_event_loop()
|
||||
return await run_blocking(_validate_jail_config_sync,
|
||||
Path(config_dir),
|
||||
name,
|
||||
@@ -1542,7 +1471,6 @@ async def rollback_jail(
|
||||
"""
|
||||
_safe_jail_name(name)
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
# Write enabled=false — this must succeed even when fail2ban is down.
|
||||
await run_blocking(_write_local_override_sync,
|
||||
@@ -1596,19 +1524,6 @@ async def rollback_jail(
|
||||
_SAFE_FILTER_NAME_RE: re.Pattern[str] = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$")
|
||||
|
||||
|
||||
class FilterNotFoundError(Exception):
|
||||
"""Raised when the requested filter name is not found in ``filter.d/``."""
|
||||
|
||||
def __init__(self, name: str) -> None:
|
||||
"""Initialise with the filter name that was not found.
|
||||
|
||||
Args:
|
||||
name: The filter name that could not be located.
|
||||
"""
|
||||
self.name: str = name
|
||||
super().__init__(f"Filter not found: {name!r}")
|
||||
|
||||
|
||||
def _extract_filter_base_name(filter_raw: str) -> str:
|
||||
"""Extract the base filter name from a raw fail2ban filter string.
|
||||
|
||||
@@ -1778,7 +1693,6 @@ async def list_filters(
|
||||
``used_by_jails`` lists.
|
||||
"""
|
||||
filter_d = Path(config_dir) / "filter.d"
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
# Run the synchronous scan in a thread-pool executor.
|
||||
raw_filters: list[tuple[str, str, str, bool, str]] = await run_blocking( _parse_filters_sync, filter_d)
|
||||
@@ -1855,7 +1769,6 @@ async def get_filter(
|
||||
filter_d = Path(config_dir) / "filter.d"
|
||||
conf_path = filter_d / f"{base_name}.conf"
|
||||
local_path = filter_d / f"{base_name}.local"
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
def _read() -> tuple[str, bool, str]:
|
||||
"""Read filter content and return (content, has_local_override, source_path)."""
|
||||
@@ -1976,7 +1889,6 @@ async def update_filter(
|
||||
content = conffile_parser.serialize_filter_config(merged)
|
||||
|
||||
filter_d = Path(config_dir) / "filter.d"
|
||||
loop = asyncio.get_event_loop()
|
||||
await run_blocking( _write_filter_local_sync, filter_d, base_name, content)
|
||||
|
||||
if do_reload:
|
||||
@@ -2033,7 +1945,6 @@ async def create_filter(
|
||||
if conf_path.is_file() or local_path.is_file():
|
||||
raise FilterAlreadyExistsError(req.name)
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
await run_blocking( _check_not_exists)
|
||||
|
||||
# Validate regex patterns.
|
||||
@@ -2099,7 +2010,6 @@ async def delete_filter(
|
||||
conf_path = filter_d / f"{base_name}.conf"
|
||||
local_path = filter_d / f"{base_name}.local"
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
def _delete() -> None:
|
||||
has_conf = conf_path.is_file()
|
||||
@@ -2154,7 +2064,6 @@ async def assign_filter_to_jail(
|
||||
_safe_jail_name(jail_name)
|
||||
_safe_filter_name(req.filter_name)
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
# Verify the jail exists in config.
|
||||
all_jails, _src = await run_blocking( _parse_jails_sync, Path(config_dir))
|
||||
@@ -2206,51 +2115,6 @@ async def assign_filter_to_jail(
|
||||
_SAFE_ACTION_NAME_RE: re.Pattern[str] = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$")
|
||||
|
||||
|
||||
class ActionNotFoundError(Exception):
|
||||
"""Raised when the requested action name is not found in ``action.d/``."""
|
||||
|
||||
def __init__(self, name: str) -> None:
|
||||
"""Initialise with the action name that was not found.
|
||||
|
||||
Args:
|
||||
name: The action name that could not be located.
|
||||
"""
|
||||
self.name: str = name
|
||||
super().__init__(f"Action not found: {name!r}")
|
||||
|
||||
|
||||
class ActionAlreadyExistsError(Exception):
|
||||
"""Raised when trying to create an action whose ``.conf`` or ``.local`` already exists."""
|
||||
|
||||
def __init__(self, name: str) -> None:
|
||||
"""Initialise with the action name that already exists.
|
||||
|
||||
Args:
|
||||
name: The action name that already exists.
|
||||
"""
|
||||
self.name: str = name
|
||||
super().__init__(f"Action already exists: {name!r}")
|
||||
|
||||
|
||||
class ActionReadonlyError(Exception):
|
||||
"""Raised when trying to delete a shipped ``.conf`` action with no ``.local`` override."""
|
||||
|
||||
def __init__(self, name: str) -> None:
|
||||
"""Initialise with the action name that cannot be deleted.
|
||||
|
||||
Args:
|
||||
name: The action name that is read-only (shipped ``.conf`` only).
|
||||
"""
|
||||
self.name: str = name
|
||||
super().__init__(
|
||||
f"Action {name!r} is a shipped default (.conf only); only user-created .local files can be deleted."
|
||||
)
|
||||
|
||||
|
||||
class ActionNameError(Exception):
|
||||
"""Raised when an action name contains invalid characters."""
|
||||
|
||||
|
||||
def _safe_action_name(name: str) -> str:
|
||||
"""Validate *name* and return it unchanged or raise :class:`ActionNameError`.
|
||||
|
||||
@@ -2642,7 +2506,6 @@ async def list_actions(
|
||||
``used_by_jails`` lists.
|
||||
"""
|
||||
action_d = Path(config_dir) / "action.d"
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
raw_actions: list[tuple[str, str, str, bool, str]] = await run_blocking( _parse_actions_sync, action_d)
|
||||
|
||||
@@ -2717,7 +2580,6 @@ async def get_action(
|
||||
action_d = Path(config_dir) / "action.d"
|
||||
conf_path = action_d / f"{base_name}.conf"
|
||||
local_path = action_d / f"{base_name}.local"
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
def _read() -> tuple[str, bool, str]:
|
||||
"""Read action content and return (content, has_local_override, source_path)."""
|
||||
@@ -2828,7 +2690,6 @@ async def update_action(
|
||||
content = conffile_parser.serialize_action_config(merged)
|
||||
|
||||
action_d = Path(config_dir) / "action.d"
|
||||
loop = asyncio.get_event_loop()
|
||||
await run_blocking( _write_action_local_sync, action_d, base_name, content)
|
||||
|
||||
if do_reload:
|
||||
@@ -2881,7 +2742,6 @@ async def create_action(
|
||||
if conf_path.is_file() or local_path.is_file():
|
||||
raise ActionAlreadyExistsError(req.name)
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
await run_blocking( _check_not_exists)
|
||||
|
||||
cfg = ActionConfig(
|
||||
@@ -2944,7 +2804,6 @@ async def delete_action(
|
||||
conf_path = action_d / f"{base_name}.conf"
|
||||
local_path = action_d / f"{base_name}.local"
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
def _delete() -> None:
|
||||
has_conf = conf_path.is_file()
|
||||
@@ -2999,7 +2858,6 @@ async def assign_action_to_jail(
|
||||
_safe_jail_name(jail_name)
|
||||
_safe_action_name(req.action_name)
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
all_jails, _src = await run_blocking( _parse_jails_sync, Path(config_dir))
|
||||
if jail_name not in all_jails:
|
||||
@@ -3077,7 +2935,6 @@ async def remove_action_from_jail(
|
||||
_safe_jail_name(jail_name)
|
||||
_safe_action_name(action_name)
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
all_jails, _src = await run_blocking( _parse_jails_sync, Path(config_dir))
|
||||
if jail_name not in all_jails:
|
||||
|
||||
@@ -18,7 +18,16 @@ from pathlib import Path
|
||||
|
||||
import structlog
|
||||
|
||||
from app.exceptions import ConfigWriteError, FilterInvalidRegexError, JailNotFoundInConfigError
|
||||
from app.exceptions import (
|
||||
ConfigWriteError,
|
||||
FilterAlreadyExistsError,
|
||||
FilterInvalidRegexError,
|
||||
FilterNameError,
|
||||
FilterNotFoundError,
|
||||
FilterReadonlyError,
|
||||
JailNameError,
|
||||
JailNotFoundInConfigError,
|
||||
)
|
||||
from app.helpers.config_file_helpers import (
|
||||
_get_active_jail_names,
|
||||
_parse_jails_sync,
|
||||
@@ -37,65 +46,11 @@ from app.utils import conffile_parser
|
||||
|
||||
log: structlog.stdlib.BoundLogger = structlog.get_logger()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Custom exceptions
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class FilterNotFoundError(Exception):
|
||||
"""Raised when the requested filter name is not found in ``filter.d/``."""
|
||||
|
||||
def __init__(self, name: str) -> None:
|
||||
"""Initialise with the filter name that was not found.
|
||||
|
||||
Args:
|
||||
name: The filter name that could not be located.
|
||||
"""
|
||||
self.name: str = name
|
||||
super().__init__(f"Filter not found: {name!r}")
|
||||
|
||||
|
||||
class FilterAlreadyExistsError(Exception):
|
||||
"""Raised when trying to create a filter whose ``.conf`` or ``.local`` already exists."""
|
||||
|
||||
def __init__(self, name: str) -> None:
|
||||
"""Initialise with the filter name that already exists.
|
||||
|
||||
Args:
|
||||
name: The filter name that already exists.
|
||||
"""
|
||||
self.name: str = name
|
||||
super().__init__(f"Filter already exists: {name!r}")
|
||||
|
||||
|
||||
class FilterReadonlyError(Exception):
|
||||
"""Raised when trying to delete a shipped ``.conf`` filter with no ``.local`` override."""
|
||||
|
||||
def __init__(self, name: str) -> None:
|
||||
"""Initialise with the filter name that cannot be deleted.
|
||||
|
||||
Args:
|
||||
name: The filter name that is read-only (shipped ``.conf`` only).
|
||||
"""
|
||||
self.name: str = name
|
||||
super().__init__(
|
||||
f"Filter {name!r} is a shipped default (.conf only); only user-created .local files can be deleted."
|
||||
)
|
||||
|
||||
|
||||
class FilterNameError(Exception):
|
||||
"""Raised when a filter name contains invalid characters."""
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Additional helper functions for this service
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class JailNameError(Exception):
|
||||
"""Raised when a jail name contains invalid characters."""
|
||||
|
||||
|
||||
_SAFE_FILTER_NAME_RE: re.Pattern[str] = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$")
|
||||
_SAFE_JAIL_NAME_RE: re.Pattern[str] = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$")
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ overrides in jail.d/*.local files.
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from app.utils.async_utils import run_blocking
|
||||
import configparser
|
||||
import contextlib
|
||||
import io
|
||||
@@ -21,7 +20,14 @@ from typing import TYPE_CHECKING, cast
|
||||
|
||||
import structlog
|
||||
|
||||
from app.exceptions import JailNotFoundError
|
||||
from app.exceptions import (
|
||||
ConfigWriteError,
|
||||
JailAlreadyActiveError,
|
||||
JailAlreadyInactiveError,
|
||||
JailNameError,
|
||||
JailNotFoundError,
|
||||
JailNotFoundInConfigError,
|
||||
)
|
||||
from app.helpers.config_file_helpers import (
|
||||
_build_inactive_jail,
|
||||
_get_active_jail_names,
|
||||
@@ -38,6 +44,7 @@ from app.models.config import (
|
||||
RollbackResponse,
|
||||
)
|
||||
from app.tasks.health_check import run_probe
|
||||
from app.utils.async_utils import run_blocking
|
||||
from app.utils.fail2ban_client import Fail2BanClient
|
||||
from app.utils.runtime_state import (
|
||||
clear_activation_record,
|
||||
@@ -76,58 +83,6 @@ _POST_RELOAD_PROBE_INTERVAL: float = 2.0
|
||||
_POST_RELOAD_MAX_ATTEMPTS: int = 4
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Custom exceptions
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class JailNotFoundInConfigError(Exception):
|
||||
"""Raised when the requested jail name is not defined in any config file."""
|
||||
|
||||
def __init__(self, name: str) -> None:
|
||||
"""Initialise with the jail name that was not found.
|
||||
|
||||
Args:
|
||||
name: The jail name that could not be located.
|
||||
"""
|
||||
self.name: str = name
|
||||
super().__init__(f"Jail not found in config files: {name!r}")
|
||||
|
||||
|
||||
class JailAlreadyActiveError(Exception):
|
||||
"""Raised when trying to activate a jail that is already active."""
|
||||
|
||||
def __init__(self, name: str) -> None:
|
||||
"""Initialise with the jail name.
|
||||
|
||||
Args:
|
||||
name: The jail that is already active.
|
||||
"""
|
||||
self.name: str = name
|
||||
super().__init__(f"Jail is already active: {name!r}")
|
||||
|
||||
|
||||
class JailAlreadyInactiveError(Exception):
|
||||
"""Raised when trying to deactivate a jail that is already inactive."""
|
||||
|
||||
def __init__(self, name: str) -> None:
|
||||
"""Initialise with the jail name.
|
||||
|
||||
Args:
|
||||
name: The jail that is already inactive.
|
||||
"""
|
||||
self.name: str = name
|
||||
super().__init__(f"Jail is already inactive: {name!r}")
|
||||
|
||||
|
||||
class JailNameError(Exception):
|
||||
"""Raised when a jail name contains invalid characters."""
|
||||
|
||||
|
||||
class ConfigWriteError(Exception):
|
||||
"""Raised when writing a ``.local`` override file fails."""
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Internal helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -502,8 +457,9 @@ async def list_inactive_jails(
|
||||
:class:`~app.models.config.InactiveJailListResponse` with all
|
||||
inactive jails.
|
||||
"""
|
||||
loop = asyncio.get_event_loop()
|
||||
parsed_result: tuple[dict[str, dict[str, str]], dict[str, str]] = await run_blocking(_parse_jails_sync, Path(config_dir)
|
||||
parsed_result: tuple[dict[str, dict[str, str]], dict[str, str]] = await run_blocking(
|
||||
_parse_jails_sync,
|
||||
Path(config_dir),
|
||||
)
|
||||
all_jails, source_files = parsed_result
|
||||
active_names: set[str] = await _get_active_jail_names(socket_path)
|
||||
@@ -587,7 +543,6 @@ async def _activate_jail(
|
||||
"""
|
||||
_safe_jail_name(name)
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
all_jails, _source_files = await run_blocking( _parse_jails_sync, Path(config_dir))
|
||||
|
||||
if name not in all_jails:
|
||||
@@ -639,7 +594,8 @@ async def _activate_jail(
|
||||
# we can restore it if activation fails. #
|
||||
# ---------------------------------------------------------------------- #
|
||||
local_path = Path(config_dir) / "jail.d" / f"{name}.local"
|
||||
original_content: bytes | None = await run_blocking(lambda: local_path.read_bytes() if local_path.exists() else None,
|
||||
original_content: bytes | None = await run_blocking(
|
||||
lambda: local_path.read_bytes() if local_path.exists() else None,
|
||||
)
|
||||
|
||||
await run_blocking(_write_local_override_sync,
|
||||
@@ -783,7 +739,6 @@ async def _rollback_activation_async(
|
||||
``True`` if fail2ban is responsive again after the rollback, ``False``
|
||||
if recovery also failed.
|
||||
"""
|
||||
loop = asyncio.get_event_loop()
|
||||
local_path = Path(config_dir) / "jail.d" / f"{name}.local"
|
||||
|
||||
# Step 1 — restore original file (or delete it).
|
||||
@@ -860,7 +815,6 @@ async def _deactivate_jail(
|
||||
"""
|
||||
_safe_jail_name(name)
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
all_jails, _source_files = await run_blocking( _parse_jails_sync, Path(config_dir))
|
||||
|
||||
if name not in all_jails:
|
||||
@@ -916,7 +870,6 @@ async def delete_jail_local_override(
|
||||
"""
|
||||
_safe_jail_name(name)
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
all_jails, _source_files = await run_blocking( _parse_jails_sync, Path(config_dir))
|
||||
|
||||
if name not in all_jails:
|
||||
@@ -956,7 +909,6 @@ async def validate_jail_config(
|
||||
JailNameError: If *name* contains invalid characters.
|
||||
"""
|
||||
_safe_jail_name(name)
|
||||
loop = asyncio.get_event_loop()
|
||||
return await run_blocking(_validate_jail_config_sync,
|
||||
Path(config_dir),
|
||||
name,
|
||||
@@ -1008,7 +960,6 @@ async def _rollback_jail(
|
||||
"""
|
||||
_safe_jail_name(name)
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
# Write enabled=false — this must succeed even when fail2ban is down.
|
||||
await run_blocking(_write_local_override_sync,
|
||||
|
||||
Reference in New Issue
Block a user