feat(backend): add raw file write endpoints for jail, filter, and action configs

Add PUT endpoints for overwriting raw content of jail.d, filter.d, and
action.d config files. Mirrors the existing GET endpoints so the frontend
can show an editable raw-text view of each config file.
This commit is contained in:
2026-03-13 14:34:41 +01:00
parent 44f3fb8718
commit cf2336c0bc
2 changed files with 84 additions and 0 deletions

View File

@@ -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,

View File

@@ -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
# ---------------------------------------------------------------------------