TASK-015: Add validation for GlobalConfigUpdate.log_target and log_level

- Add LogLevel Literal type: CRITICAL, ERROR, WARNING, NOTICE, INFO, DEBUG
- Add log_target validation to accept special values (STDOUT, STDERR, SYSLOG)
  or validated file paths within allowed directories
- Update GlobalConfigResponse to use LogLevel type
- Add field_validator for log_target in both GlobalConfigUpdate and
  GlobalConfigResponse following the same pattern as AddLogPathRequest
- Add @autouse fixture to test_config_service.py to mock get_settings
- Update existing tests to use uppercase log level values
- Add 12 comprehensive tests for new validation in test_models.py
- Update Features.md to document valid log_target and log_level values
- Add section to Backend-Development.md documenting Literal types and
  field_validator patterns with examples

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-04-26 13:57:22 +02:00
parent b9e046bd66
commit d66493f135
6 changed files with 306 additions and 45 deletions

View File

@@ -191,6 +191,56 @@ class BanResponse(BaseModel):
ban_count: int = Field(..., ge=1, description="Number of times this IP was banned")
```
### Using `Literal` Types for Constrained Strings
When a field should only accept a small set of predefined values, use `Literal` to enforce this at the type level:
```python
from typing import Literal
from pydantic import BaseModel, Field
LogLevel = Literal["CRITICAL", "ERROR", "WARNING", "NOTICE", "INFO", "DEBUG"]
class GlobalConfigUpdate(BaseModel):
log_level: LogLevel | None = Field(
default=None,
description="Log level: CRITICAL, ERROR, WARNING, NOTICE, INFO, or DEBUG.",
)
```
This provides:
- **Type safety** — IDEs and type checkers enforce valid values.
- **API documentation** — OpenAPI docs automatically list all allowed values.
- **Validation** — Pydantic rejects invalid values and provides a clear error message.
### Custom Field Validators
For fields that require complex validation (e.g., file paths that must be within allowed directories), use `@field_validator`:
```python
from pydantic import field_validator
class AddLogPathRequest(BaseModel):
log_path: str = Field(..., description="Absolute path to the log file to monitor.")
@field_validator("log_path", mode="after")
@classmethod
def validate_log_path(cls, value: str) -> str:
"""Validate that the log path is within allowed directories."""
settings = get_settings()
resolved_path = Path(value).resolve()
for allowed_dir in settings.allowed_log_dirs:
if resolved_path.is_relative_to(Path(allowed_dir).resolve()):
return value
raise ValueError(f"Path {value!r} is outside allowed directories")
```
**Key points:**
- Use `mode="after"` to validate after Pydantic's basic type coercion.
- Raise `ValueError` if validation fails; Pydantic converts it to an HTTP 400 response.
- **Never use string prefix matching** for path validation (e.g., `path.startswith("/var/log")`). Use `Path.is_relative_to()` to avoid bypasses like `/var/log_evil/file.log`.
- Resolve symlinks before validating to prevent symlink-based escapes.
---
## 6. Async Rules

View File

@@ -228,8 +228,10 @@ A page to inspect and modify the fail2ban configuration without leaving the web
### Server Settings
- View and change the fail2ban log level (e.g. Critical, Error, Warning, Info, Debug).
- View and change the log target (file path, stdout, stderr, syslog, systemd journal).
- View and change the fail2ban log level using valid values: `CRITICAL`, `ERROR`, `WARNING`, `NOTICE`, `INFO`, `DEBUG`.
- View and change the log target, which can be:
- Special values: `STDOUT`, `STDERR`, `SYSLOG`
- A file path that resolves to one of the configured safe log directories (default: `/var/log` and `/config/log`). Symlinks are resolved to their canonical targets before validation.
- View and change the syslog socket if syslog is used.
- Flush and re-open log files (useful after log rotation).
- View and change the fail2ban database file location.
@@ -264,7 +266,7 @@ A page to inspect and modify the fail2ban configuration without leaving the web
- **Auto-refresh** toggle with interval selector (5 s / 10 s / 30 s) for live monitoring.
- Truncation notice when the total log file line count exceeds the requested tail limit.
- Container automatically scrolls to the bottom after each data update.
- When fail2ban is configured to log to a non-file target (STDOUT, STDERR, SYSLOG, SYSTEMD-JOURNAL), an informational banner explains that file-based log viewing is unavailable.
- When fail2ban is configured to log to a non-file target (`STDOUT`, `STDERR`, or `SYSLOG`), an informational banner explains that file-based log viewing is unavailable.
- Log file paths are validated against a configurable allowlist of safe directories on the backend to prevent unauthorized reads of sensitive system files.
---

View File

@@ -1,38 +1,3 @@
## TASK-014 — `add_log_path` passes arbitrary paths to fail2ban — no allowlist
**Severity:** High
### Where found
`backend/app/services/config_service.py``add_log_path()`. `backend/app/models/config.py``AddLogPathRequest.log_path: str`.
### Why this is needed
An authenticated user can instruct fail2ban to monitor any file path on the system (e.g., `log_path: "/etc/shadow"`). fail2ban runs as root and opens the file for reading. Even if fail2ban cannot meaningfully parse it, repeated log monitoring of sensitive files can leak their contents via fail2ban's own logging, and the feature represents an unintended read primitive into arbitrary root-readable files.
### Goal
Restrict monitored log paths to a configurable set of safe directories.
### What to do
1. Add a `@field_validator("log_path")` to `AddLogPathRequest` that:
- Calls `Path(log_path).resolve()` to canonicalize.
- Checks `resolved.is_relative_to(Path("/var/log"))` or any path in `settings.allowed_log_dirs` (a new configurable list).
- Raises `ValueError` if the path is outside allowed prefixes.
2. Add `BANGUI_ALLOWED_LOG_DIRS` to `Settings` as `list[str]` defaulting to `["/var/log", "/config/log"]`.
3. Note: use `is_relative_to()`, not `startswith()` — the latter is bypassable with `/var/log_evil`.
### Possible traps and issues
- The validator runs on the Pydantic model before the service is called — the resolved path check happens at request time, not at the OS level. The allowed list must match the actual Docker volume mount paths.
- Custom log file locations (e.g., `/home/app/logs`) need to be added to `BANGUI_ALLOWED_LOG_DIRS`.
### Docs changes needed
- `Features.md` — document the log path restrictions.
- `Backend-Development.md` — input validation for path parameters.
### Doc references
- [Features.md](Features.md) — jail log monitoring
- [Backend-Development.md](Backend-Development.md) — path validation
---
## TASK-015 — `GlobalConfigUpdate.log_target`/`log_level` have no validation
**Severity:** High