764 lines
23 KiB
Python
764 lines
23 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import Annotated
|
|
|
|
from fastapi import APIRouter, HTTPException, Path, Query, Request, status
|
|
|
|
from app.dependencies import (
|
|
AppDep,
|
|
AuthDep,
|
|
Fail2BanConfigDirDep,
|
|
Fail2BanSocketDep,
|
|
Fail2BanStartCommandDep,
|
|
PendingRecoveryDep,
|
|
)
|
|
from app.exceptions import (
|
|
ActionNameError,
|
|
ActionNotFoundError,
|
|
ConfigOperationError,
|
|
ConfigValidationError,
|
|
ConfigWriteError,
|
|
FilterNameError,
|
|
FilterNotFoundError,
|
|
JailAlreadyActiveError,
|
|
JailAlreadyInactiveError,
|
|
JailNameError,
|
|
JailNotFoundError,
|
|
JailNotFoundInConfigError,
|
|
)
|
|
from app.models.config import (
|
|
ActivateJailRequest,
|
|
AddLogPathRequest,
|
|
AssignActionRequest,
|
|
AssignFilterRequest,
|
|
InactiveJailListResponse,
|
|
JailActivationResponse,
|
|
JailConfigListResponse,
|
|
JailConfigResponse,
|
|
JailConfigUpdate,
|
|
JailValidationResult,
|
|
PendingRecovery,
|
|
RollbackResponse,
|
|
)
|
|
from app.services import (
|
|
action_config_service,
|
|
config_service,
|
|
filter_config_service,
|
|
jail_config_service,
|
|
)
|
|
from app.exceptions import Fail2BanConnectionError
|
|
from app.utils.runtime_state import (
|
|
clear_activation_record,
|
|
clear_pending_recovery,
|
|
create_pending_recovery,
|
|
record_activation,
|
|
)
|
|
|
|
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,
|
|
summary="List configuration for all active jails",
|
|
)
|
|
async def get_jail_configs(
|
|
request: Request,
|
|
_auth: AuthDep,
|
|
socket_path: Fail2BanSocketDep,
|
|
) -> JailConfigListResponse:
|
|
"""Return editable configuration for every active fail2ban jail.
|
|
|
|
Fetches ban time, find time, max retries, regex patterns, log paths,
|
|
date pattern, encoding, backend, and attached actions for all jails.
|
|
|
|
Args:
|
|
request: Incoming request (used to access ``app.state``).
|
|
_auth: Validated session — enforces authentication.
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
@router.get(
|
|
"/inactive",
|
|
response_model=InactiveJailListResponse,
|
|
summary="List all inactive jails discovered in config files",
|
|
)
|
|
async def get_inactive_jails(
|
|
request: Request,
|
|
_auth: AuthDep,
|
|
config_dir: Fail2BanConfigDirDep,
|
|
socket_path: Fail2BanSocketDep,
|
|
) -> InactiveJailListResponse:
|
|
"""Return all jails defined in fail2ban config files that are not running.
|
|
|
|
Parses ``jail.conf``, ``jail.local``, and ``jail.d/`` following the
|
|
fail2ban merge order. Jails that fail2ban currently reports as running
|
|
are excluded; only truly inactive entries are returned.
|
|
|
|
Args:
|
|
request: FastAPI request object.
|
|
_auth: Validated session — enforces authentication.
|
|
|
|
Returns:
|
|
:class:`~app.models.config.InactiveJailListResponse`.
|
|
"""
|
|
return await jail_config_service.list_inactive_jails(config_dir, socket_path)
|
|
|
|
|
|
@router.get(
|
|
"/pending-recovery",
|
|
response_model=PendingRecovery | None,
|
|
summary="Return active crash-recovery record if one exists",
|
|
)
|
|
async def get_pending_recovery(
|
|
_auth: AuthDep,
|
|
pending_recovery: PendingRecoveryDep,
|
|
) -> PendingRecovery | None:
|
|
"""Return the current :class:`~app.models.config.PendingRecovery` record.
|
|
|
|
A non-null response means fail2ban crashed shortly after a jail activation
|
|
and the user should be offered a rollback option. Returns ``null`` (HTTP
|
|
200 with ``null`` body) when no recovery is pending.
|
|
|
|
Args:
|
|
request: FastAPI request object.
|
|
_auth: Validated session.
|
|
|
|
Returns:
|
|
:class:`~app.models.config.PendingRecovery` or ``None``.
|
|
"""
|
|
return pending_recovery
|
|
|
|
|
|
@router.get(
|
|
"/{name}",
|
|
response_model=JailConfigResponse,
|
|
summary="Return configuration for a single jail",
|
|
)
|
|
async def get_jail_config(
|
|
request: Request,
|
|
_auth: AuthDep,
|
|
socket_path: Fail2BanSocketDep,
|
|
name: _NamePath,
|
|
) -> JailConfigResponse:
|
|
"""Return the full editable configuration for one fail2ban jail.
|
|
|
|
Args:
|
|
request: Incoming request.
|
|
_auth: Validated session.
|
|
name: Jail name.
|
|
|
|
Returns:
|
|
:class:`~app.models.config.JailConfigResponse`.
|
|
|
|
Raises:
|
|
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
|
|
|
|
|
|
|
|
|
|
@router.put(
|
|
"/{name}",
|
|
status_code=status.HTTP_204_NO_CONTENT,
|
|
summary="Update jail configuration",
|
|
)
|
|
async def update_jail_config(
|
|
request: Request,
|
|
_auth: AuthDep,
|
|
socket_path: Fail2BanSocketDep,
|
|
name: _NamePath,
|
|
body: JailConfigUpdate,
|
|
) -> None:
|
|
"""Update one or more configuration fields for an active fail2ban jail.
|
|
|
|
Regex patterns are validated before being sent to fail2ban. An invalid
|
|
pattern returns 422 with the regex error message.
|
|
|
|
Args:
|
|
request: Incoming request.
|
|
_auth: Validated session.
|
|
name: Jail name.
|
|
body: Partial update — only non-None fields are written.
|
|
|
|
Raises:
|
|
HTTPException: 404 when the jail does not exist.
|
|
HTTPException: 422 when a regex pattern fails to compile.
|
|
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
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Global configuration endpoints
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
@router.post(
|
|
"/{name}/logpath",
|
|
status_code=status.HTTP_204_NO_CONTENT,
|
|
summary="Add a log file path to an existing jail",
|
|
)
|
|
async def add_log_path(
|
|
request: Request,
|
|
_auth: AuthDep,
|
|
socket_path: Fail2BanSocketDep,
|
|
name: _NamePath,
|
|
body: AddLogPathRequest,
|
|
) -> None:
|
|
"""Register an additional log file for an existing jail to monitor.
|
|
|
|
Uses ``set <jail> addlogpath <path> <tail|head>`` to add the path
|
|
without requiring a daemon restart.
|
|
|
|
Args:
|
|
request: Incoming request.
|
|
_auth: Validated session.
|
|
name: Jail name.
|
|
body: Log path and tail/head preference.
|
|
|
|
Raises:
|
|
HTTPException: 404 when the jail does not exist.
|
|
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
|
|
|
|
|
|
|
|
|
|
@router.delete(
|
|
"/{name}/logpath",
|
|
status_code=status.HTTP_204_NO_CONTENT,
|
|
summary="Remove a monitored log path from a jail",
|
|
)
|
|
async def delete_log_path(
|
|
request: Request,
|
|
_auth: AuthDep,
|
|
socket_path: Fail2BanSocketDep,
|
|
name: _NamePath,
|
|
log_path: str = Query(..., description="Absolute path of the log file to stop monitoring."),
|
|
) -> None:
|
|
"""Stop a jail from monitoring the specified log file.
|
|
|
|
Uses ``set <jail> dellogpath <path>`` to remove the log path at runtime
|
|
without requiring a daemon restart.
|
|
|
|
Args:
|
|
request: Incoming request.
|
|
_auth: Validated session.
|
|
name: Jail name.
|
|
log_path: Absolute path to the log file to remove (query parameter).
|
|
|
|
Raises:
|
|
HTTPException: 404 when the jail does not exist.
|
|
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
|
|
|
|
|
|
|
|
|
|
@router.post(
|
|
"/{name}/activate",
|
|
response_model=JailActivationResponse,
|
|
summary="Activate an inactive jail",
|
|
)
|
|
async def activate_jail(
|
|
app: AppDep,
|
|
_auth: AuthDep,
|
|
config_dir: Fail2BanConfigDirDep,
|
|
socket_path: Fail2BanSocketDep,
|
|
name: _NamePath,
|
|
body: ActivateJailRequest | None = None,
|
|
) -> JailActivationResponse:
|
|
"""Enable an inactive jail and reload fail2ban.
|
|
|
|
Writes ``enabled = true`` (plus any override values from the request
|
|
body) to ``jail.d/{name}.local`` and triggers a full fail2ban reload so
|
|
the jail starts immediately.
|
|
|
|
Args:
|
|
app: FastAPI application instance.
|
|
_auth: Validated session.
|
|
name: Name of the jail to activate.
|
|
body: Optional override values (bantime, findtime, maxretry, port,
|
|
logpath).
|
|
|
|
Returns:
|
|
:class:`~app.models.config.JailActivationResponse`.
|
|
|
|
Raises:
|
|
HTTPException: 400 if *name* contains invalid characters.
|
|
HTTPException: 404 if *name* is not found in any config file.
|
|
HTTPException: 409 if the jail is already active.
|
|
HTTPException: 502 if fail2ban is unreachable.
|
|
"""
|
|
req = body if body is not None else ActivateJailRequest()
|
|
|
|
activation_time = record_activation(app, name)
|
|
|
|
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
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
@router.post(
|
|
"/{name}/deactivate",
|
|
response_model=JailActivationResponse,
|
|
summary="Deactivate an active jail",
|
|
)
|
|
async def deactivate_jail(
|
|
_auth: AuthDep,
|
|
config_dir: Fail2BanConfigDirDep,
|
|
socket_path: Fail2BanSocketDep,
|
|
name: _NamePath,
|
|
) -> JailActivationResponse:
|
|
"""Disable an active jail and reload fail2ban.
|
|
|
|
Writes ``enabled = false`` to ``jail.d/{name}.local`` and triggers a
|
|
full fail2ban reload so the jail stops immediately.
|
|
|
|
Args:
|
|
_auth: Validated session.
|
|
name: Name of the jail to deactivate.
|
|
|
|
Returns:
|
|
:class:`~app.models.config.JailActivationResponse`.
|
|
|
|
Raises:
|
|
HTTPException: 400 if *name* contains invalid characters.
|
|
HTTPException: 404 if *name* is not found in any config file.
|
|
HTTPException: 409 if the jail is already inactive.
|
|
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
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
@router.delete(
|
|
"/{name}/local",
|
|
status_code=status.HTTP_204_NO_CONTENT,
|
|
summary="Delete the jail.d override file for an inactive jail",
|
|
)
|
|
async def delete_jail_local_override(
|
|
request: Request,
|
|
_auth: AuthDep,
|
|
config_dir: Fail2BanConfigDirDep,
|
|
socket_path: Fail2BanSocketDep,
|
|
name: _NamePath,
|
|
) -> None:
|
|
"""Remove the ``jail.d/{name}.local`` override file for an inactive jail.
|
|
|
|
This endpoint is the clean-up action for inactive jails that still carry
|
|
a ``.local`` override file (e.g. one written with ``enabled = false`` by a
|
|
previous deactivation). The file is deleted without modifying fail2ban's
|
|
running state, since the jail is already inactive.
|
|
|
|
Args:
|
|
request: FastAPI request object.
|
|
_auth: Validated session.
|
|
name: Name of the jail whose ``.local`` file should be removed.
|
|
|
|
Raises:
|
|
HTTPException: 400 if *name* contains invalid characters.
|
|
HTTPException: 404 if *name* is not found in any config file.
|
|
HTTPException: 409 if the jail is currently active.
|
|
HTTPException: 500 if the file cannot be deleted.
|
|
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
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Jail validation & rollback endpoints (Task 3)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
@router.post(
|
|
"/{name}/validate",
|
|
response_model=JailValidationResult,
|
|
summary="Validate jail configuration before activation",
|
|
)
|
|
async def validate_jail(
|
|
request: Request,
|
|
_auth: AuthDep,
|
|
config_dir: Fail2BanConfigDirDep,
|
|
name: _NamePath,
|
|
) -> JailValidationResult:
|
|
"""Run pre-activation validation checks on a jail configuration.
|
|
|
|
Validates filter and action file existence, regex pattern compilation, and
|
|
log path existence without modifying any files or reloading fail2ban.
|
|
|
|
Args:
|
|
request: FastAPI request object.
|
|
_auth: Validated session.
|
|
name: Jail name to validate.
|
|
|
|
Returns:
|
|
:class:`~app.models.config.JailValidationResult` with any issues found.
|
|
|
|
Raises:
|
|
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
|
|
|
|
|
|
@router.post(
|
|
"/{name}/rollback",
|
|
response_model=RollbackResponse,
|
|
summary="Disable a bad jail config and restart fail2ban",
|
|
)
|
|
async def rollback_jail(
|
|
_auth: AuthDep,
|
|
app: AppDep,
|
|
config_dir: Fail2BanConfigDirDep,
|
|
socket_path: Fail2BanSocketDep,
|
|
start_cmd: Fail2BanStartCommandDep,
|
|
name: _NamePath,
|
|
) -> RollbackResponse:
|
|
"""Disable the specified jail and attempt to restart fail2ban.
|
|
|
|
Writes ``enabled = false`` to ``jail.d/{name}.local`` (works even when
|
|
fail2ban is down — no socket is needed), then runs the configured start
|
|
command and waits up to ten seconds for the daemon to come back online.
|
|
|
|
On success, clears the :class:`~app.models.config.PendingRecovery` record.
|
|
|
|
Args:
|
|
_auth: Validated session.
|
|
app: FastAPI application instance.
|
|
name: Jail name to disable and roll back.
|
|
|
|
Returns:
|
|
:class:`~app.models.config.RollbackResponse`.
|
|
|
|
Raises:
|
|
HTTPException: 400 if *name* contains invalid characters.
|
|
HTTPException: 500 if writing the .local override file fails.
|
|
"""
|
|
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
|
|
|
|
if result.fail2ban_running:
|
|
clear_pending_recovery(app)
|
|
clear_activation_record(app)
|
|
|
|
return result
|
|
|
|
|
|
@router.post(
|
|
"/{name}/filter",
|
|
status_code=status.HTTP_204_NO_CONTENT,
|
|
summary="Assign a filter to a jail",
|
|
)
|
|
async def assign_filter_to_jail(
|
|
request: Request,
|
|
_auth: AuthDep,
|
|
config_dir: Fail2BanConfigDirDep,
|
|
socket_path: Fail2BanSocketDep,
|
|
name: _NamePath,
|
|
body: AssignFilterRequest,
|
|
reload: bool = Query(default=False, description="Reload fail2ban after assigning."),
|
|
) -> None:
|
|
"""Write ``filter = {filter_name}`` to the jail's ``.local`` config.
|
|
|
|
Existing keys in the jail's ``.local`` file are preserved. If the file
|
|
does not exist it is created.
|
|
|
|
Args:
|
|
request: FastAPI request object.
|
|
_auth: Validated session.
|
|
name: Jail name.
|
|
body: Filter to assign.
|
|
reload: When ``true``, trigger a fail2ban reload after writing.
|
|
|
|
Raises:
|
|
HTTPException: 400 if *name* or *filter_name* contain invalid characters.
|
|
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
|
|
|
|
|
|
@router.post(
|
|
"/{name}/action",
|
|
status_code=status.HTTP_204_NO_CONTENT,
|
|
summary="Add an action to a jail",
|
|
)
|
|
async def assign_action_to_jail(
|
|
request: Request,
|
|
_auth: AuthDep,
|
|
config_dir: Fail2BanConfigDirDep,
|
|
socket_path: Fail2BanSocketDep,
|
|
name: _NamePath,
|
|
body: AssignActionRequest,
|
|
reload: bool = Query(default=False, description="Reload fail2ban after assigning."),
|
|
) -> None:
|
|
"""Append an action entry to the jail's ``.local`` config.
|
|
|
|
Existing keys in the jail's ``.local`` file are preserved. If the file
|
|
does not exist it is created. The action is not duplicated if it is
|
|
already present.
|
|
|
|
Args:
|
|
request: FastAPI request object.
|
|
_auth: Validated session.
|
|
name: Jail name.
|
|
body: Action to add plus optional per-jail parameters.
|
|
reload: When ``true``, trigger a fail2ban reload after writing.
|
|
|
|
Raises:
|
|
HTTPException: 400 if *name* or *action_name* contain invalid characters.
|
|
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
|
|
|
|
|
|
@router.delete(
|
|
"/{name}/action/{action_name}",
|
|
status_code=status.HTTP_204_NO_CONTENT,
|
|
summary="Remove an action from a jail",
|
|
)
|
|
async def remove_action_from_jail(
|
|
request: Request,
|
|
_auth: AuthDep,
|
|
config_dir: Fail2BanConfigDirDep,
|
|
socket_path: Fail2BanSocketDep,
|
|
name: _NamePath,
|
|
action_name: Annotated[str, Path(description="Action base name to remove.")],
|
|
reload: bool = Query(default=False, description="Reload fail2ban after removing."),
|
|
) -> None:
|
|
"""Remove an action from the jail's ``.local`` config.
|
|
|
|
If the jail has no ``.local`` file or the action is not listed there,
|
|
the call is silently idempotent.
|
|
|
|
Args:
|
|
request: FastAPI request object.
|
|
_auth: Validated session.
|
|
name: Jail name.
|
|
action_name: Base name of the action to remove.
|
|
reload: When ``true``, trigger a fail2ban reload after writing.
|
|
|
|
Raises:
|
|
HTTPException: 400 if *name* or *action_name* contain invalid characters.
|
|
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
|
|
# ---------------------------------------------------------------------------
|
|
# Filter discovery endpoints (Task 2.1)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|