Fix config sub-router prefixes and router tags

This commit is contained in:
2026-04-14 10:25:36 +02:00
parent cee5372690
commit 0e84f1f60c
4 changed files with 198 additions and 183 deletions

View File

@@ -11,19 +11,16 @@ from app.exceptions import (
ActionNotFoundError,
ActionReadonlyError,
ConfigWriteError,
JailNameError,
JailNotFoundInConfigError,
)
from app.models.config import (
ActionConfig,
ActionCreateRequest,
ActionListResponse,
ActionUpdateRequest,
AssignActionRequest,
)
from app.services import action_config_service
router: APIRouter = APIRouter()
router: APIRouter = APIRouter(prefix="/actions", tags=["Action Config"])
_ActionNamePath = Annotated[
str,
@@ -54,7 +51,7 @@ def _not_found(name: str) -> HTTPException:
)
@router.get(
"/actions",
"/",
response_model=ActionListResponse,
summary="List all available actions with active/inactive status",
)
@@ -91,7 +88,7 @@ async def list_actions(
@router.get(
"/actions/{name}",
"/{name}",
response_model=ActionConfig,
summary="Return full parsed detail for a single action",
)
@@ -133,7 +130,7 @@ async def get_action(
@router.put(
"/actions/{name}",
"/{name}",
response_model=ActionConfig,
summary="Update an action's .local override with new lifecycle command values",
)
@@ -182,7 +179,7 @@ async def update_action(
@router.post(
"/actions",
"/",
response_model=ActionConfig,
status_code=status.HTTP_201_CREATED,
summary="Create a new user-defined action",
@@ -233,7 +230,7 @@ async def create_action(
@router.delete(
"/actions/{name}",
"/{name}",
status_code=status.HTTP_204_NO_CONTENT,
summary="Delete a user-created action's .local file",
)
@@ -279,108 +276,6 @@ async def delete_action(
@router.post(
"/jails/{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(
"/jails/{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
# ---------------------------------------------------------------------------
# fail2ban log viewer endpoints
# ---------------------------------------------------------------------------

View File

@@ -20,7 +20,7 @@ from app.models.config import (
from app.services import config_file_service, config_service, jail_service, log_service, setup_service
from app.utils.fail2ban_client import Fail2BanConnectionError
router: APIRouter = APIRouter()
router: APIRouter = APIRouter(tags=["Config Misc"])
def _bad_gateway(exc: Exception) -> HTTPException:

View File

@@ -12,11 +12,8 @@ from app.exceptions import (
FilterNameError,
FilterNotFoundError,
FilterReadonlyError,
JailNameError,
JailNotFoundInConfigError,
)
from app.models.config import (
AssignFilterRequest,
FilterConfig,
FilterCreateRequest,
FilterListResponse,
@@ -24,7 +21,7 @@ from app.models.config import (
)
from app.services import filter_config_service
router: APIRouter = APIRouter()
router: APIRouter = APIRouter(prefix="/filters", tags=["Filter Config"])
_NamePath = Annotated[
str,
@@ -59,7 +56,7 @@ def _not_found(name: str) -> HTTPException:
)
@router.get(
"/filters",
"/",
response_model=FilterListResponse,
summary="List all available filters with active/inactive status",
)
@@ -97,7 +94,7 @@ async def list_filters(
@router.get(
"/filters/{name}",
"/{name}",
response_model=FilterConfig,
summary="Return full parsed detail for a single filter",
)
@@ -156,7 +153,7 @@ def _filter_not_found(name: str) -> HTTPException:
@router.put(
"/filters/{name}",
"/{name}",
response_model=FilterConfig,
summary="Update a filter's .local override with new regex/pattern values",
)
@@ -210,7 +207,7 @@ async def update_filter(
@router.post(
"/filters",
"/",
response_model=FilterConfig,
status_code=status.HTTP_201_CREATED,
summary="Create a new user-defined filter",
@@ -265,7 +262,7 @@ async def create_filter(
@router.delete(
"/filters/{name}",
"/{name}",
status_code=status.HTTP_204_NO_CONTENT,
summary="Delete a user-created filter's .local file",
)
@@ -313,55 +310,6 @@ async def delete_filter(
@router.post(
"/jails/{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
# ---------------------------------------------------------------------------
# Action discovery endpoints (Task 3.1)
# ---------------------------------------------------------------------------

View File

@@ -13,9 +13,13 @@ from app.dependencies import (
PendingRecoveryDep,
)
from app.exceptions import (
ActionNameError,
ActionNotFoundError,
ConfigOperationError,
ConfigValidationError,
ConfigWriteError,
FilterNameError,
FilterNotFoundError,
JailAlreadyActiveError,
JailAlreadyInactiveError,
JailNameError,
@@ -25,6 +29,8 @@ from app.exceptions import (
from app.models.config import (
ActivateJailRequest,
AddLogPathRequest,
AssignActionRequest,
AssignFilterRequest,
InactiveJailListResponse,
JailActivationResponse,
JailConfigListResponse,
@@ -34,10 +40,15 @@ from app.models.config import (
PendingRecovery,
RollbackResponse,
)
from app.services import config_service, jail_config_service, jail_service
from app.services import (
action_config_service,
config_service,
filter_config_service,
jail_config_service,
)
from app.utils.fail2ban_client import Fail2BanConnectionError
router: APIRouter = APIRouter()
router: APIRouter = APIRouter(prefix="/jails", tags=["Jail Config"])
_NamePath = Annotated[str, Path(description='Jail name as configured in fail2ban.')]
@@ -69,8 +80,22 @@ def _bad_request(message: str) -> HTTPException:
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(
"/jails",
"/",
response_model=JailConfigListResponse,
summary="List configuration for all active jails",
)
@@ -100,7 +125,7 @@ async def get_jail_configs(
@router.get(
"/jails/inactive",
"/inactive",
response_model=InactiveJailListResponse,
summary="List all inactive jails discovered in config files",
)
@@ -129,7 +154,7 @@ async def get_inactive_jails(
@router.get(
"/jails/{name}",
"/{name}",
response_model=JailConfigResponse,
summary="Return configuration for a single jail",
)
@@ -164,7 +189,7 @@ async def get_jail_config(
@router.put(
"/jails/{name}",
"/{name}",
status_code=status.HTTP_204_NO_CONTENT,
summary="Update jail configuration",
)
@@ -212,7 +237,7 @@ async def update_jail_config(
@router.post(
"/jails/{name}/logpath",
"/{name}/logpath",
status_code=status.HTTP_204_NO_CONTENT,
summary="Add a log file path to an existing jail",
)
@@ -252,7 +277,7 @@ async def add_log_path(
@router.delete(
"/jails/{name}/logpath",
"/{name}/logpath",
status_code=status.HTTP_204_NO_CONTENT,
summary="Remove a monitored log path from a jail",
)
@@ -292,7 +317,7 @@ async def delete_log_path(
@router.post(
"/jails/{name}/activate",
"/{name}/activate",
response_model=JailActivationResponse,
summary="Activate an inactive jail",
)
@@ -353,7 +378,7 @@ async def activate_jail(
@router.post(
"/jails/{name}/deactivate",
"/{name}/deactivate",
response_model=JailActivationResponse,
summary="Deactivate an active jail",
)
@@ -409,7 +434,7 @@ async def deactivate_jail(
@router.delete(
"/jails/{name}/local",
"/{name}/local",
status_code=status.HTTP_204_NO_CONTENT,
summary="Delete the jail.d override file for an inactive jail",
)
@@ -468,7 +493,7 @@ async def delete_jail_local_override(
@router.post(
"/jails/{name}/validate",
"/{name}/validate",
response_model=JailValidationResult,
summary="Validate jail configuration before activation",
)
@@ -531,7 +556,7 @@ async def get_pending_recovery(
@router.post(
"/jails/{name}/rollback",
"/{name}/rollback",
response_model=RollbackResponse,
summary="Disable a bad jail config and restart fail2ban",
)
@@ -578,6 +603,153 @@ async def rollback_jail(
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)
# ---------------------------------------------------------------------------