diff --git a/backend/app/routers/file_config.py b/backend/app/routers/file_config.py index 43fd808..57a199d 100644 --- a/backend/app/routers/file_config.py +++ b/backend/app/routers/file_config.py @@ -6,6 +6,7 @@ files directly on the filesystem (``jail.d/``, ``filter.d/``, ``action.d/``). Endpoints: * ``GET /api/config/jail-files`` — list all jail config files * ``GET /api/config/jail-files/{filename}`` — get one jail config file (with content) +* ``PUT /api/config/jail-files/{filename}`` — overwrite a jail config file * ``PUT /api/config/jail-files/{filename}/enabled`` — enable/disable a jail config * ``GET /api/config/filters`` — list all filter files * ``GET /api/config/filters/{name}`` — get one filter file (with content) @@ -169,6 +170,46 @@ async def get_jail_config_file( raise _service_unavailable(str(exc)) from exc +@router.put( + "/jail-files/{filename}", + status_code=status.HTTP_204_NO_CONTENT, + summary="Overwrite a jail.d config file with new raw content", +) +async def write_jail_config_file( + request: Request, + _auth: AuthDep, + filename: _FilenamePath, + body: ConfFileUpdateRequest, +) -> None: + """Overwrite the raw content of an existing jail.d config file. + + The change is written directly to disk. You must reload fail2ban + (``POST /api/config/reload``) separately for the change to take effect. + + Args: + request: Incoming request. + _auth: Validated session. + filename: Filename of the jail config file (e.g. ``sshd.conf``). + body: New raw file content. + + Raises: + HTTPException: 400 if *filename* is unsafe or content is invalid. + HTTPException: 404 if the file does not exist. + HTTPException: 503 if the config directory is unavailable. + """ + config_dir: str = request.app.state.settings.fail2ban_config_dir + try: + await file_config_service.write_jail_config_file(config_dir, filename, body) + except ConfigFileNameError as exc: + raise _bad_request(str(exc)) from exc + except ConfigFileNotFoundError: + raise _not_found(filename) from None + except ConfigFileWriteError as exc: + raise _bad_request(str(exc)) from exc + except ConfigDirError as exc: + raise _service_unavailable(str(exc)) from exc + + @router.put( "/jail-files/{filename}/enabled", status_code=status.HTTP_204_NO_CONTENT, diff --git a/backend/app/services/file_config_service.py b/backend/app/services/file_config_service.py index ad0462a..271cbc8 100644 --- a/backend/app/services/file_config_service.py +++ b/backend/app/services/file_config_service.py @@ -418,6 +418,49 @@ async def create_jail_config_file( return await asyncio.get_event_loop().run_in_executor(None, _do) +async def write_jail_config_file( + config_dir: str, + filename: str, + req: ConfFileUpdateRequest, +) -> None: + """Overwrite an existing jail.d config file with new raw content. + + Args: + config_dir: Path to the fail2ban configuration directory. + filename: Filename including extension (e.g. ``sshd.conf``). + req: :class:`~app.models.file_config.ConfFileUpdateRequest` with new + content. + + Raises: + ConfigFileNotFoundError: If the file does not exist. + ConfigFileNameError: If *filename* is unsafe or has a bad extension. + ConfigFileWriteError: If the file cannot be written. + ConfigDirError: If *config_dir* does not exist. + """ + + def _do() -> None: + jail_d = _resolve_subdir(config_dir, "jail.d").resolve() + if not jail_d.is_dir(): + raise ConfigFileNotFoundError(filename) + path = (jail_d / filename).resolve() + _assert_within(jail_d, path) + if path.suffix not in _CONF_EXTENSIONS: + raise ConfigFileNameError( + f"Only .conf and .local files are supported, got {filename!r}." + ) + if not path.is_file(): + raise ConfigFileNotFoundError(filename) + try: + path.write_text(req.content, encoding="utf-8") + except OSError as exc: + raise ConfigFileWriteError( + f"Cannot write {filename!r}: {exc}" + ) from exc + log.info("jail_config_file_written", filename=filename) + + await asyncio.get_event_loop().run_in_executor(None, _do) + + # --------------------------------------------------------------------------- # Internal helpers — generic conf file listing / reading / writing # ---------------------------------------------------------------------------