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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user