diff --git a/backend/app/routers/action_config.py b/backend/app/routers/action_config.py index 63e411a..007a87f 100644 --- a/backend/app/routers/action_config.py +++ b/backend/app/routers/action_config.py @@ -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 # --------------------------------------------------------------------------- diff --git a/backend/app/routers/config_misc.py b/backend/app/routers/config_misc.py index 483a4af..02a450c 100644 --- a/backend/app/routers/config_misc.py +++ b/backend/app/routers/config_misc.py @@ -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: diff --git a/backend/app/routers/filter_config.py b/backend/app/routers/filter_config.py index 4acecde..0ad577a 100644 --- a/backend/app/routers/filter_config.py +++ b/backend/app/routers/filter_config.py @@ -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) # --------------------------------------------------------------------------- diff --git a/backend/app/routers/jail_config.py b/backend/app/routers/jail_config.py index c18abcc..b085693 100644 --- a/backend/app/routers/jail_config.py +++ b/backend/app/routers/jail_config.py @@ -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) # ---------------------------------------------------------------------------