"""Pydantic models for file-based fail2ban configuration management. Covers jail config files (``jail.d/``), filter definitions (``filter.d/``), and action definitions (``action.d/``). """ from pydantic import Field, field_validator from app.models.response import BanGuiBaseModel from app.utils.constants import FAIL2BAN_RESERVED_JAIL_NAMES # --------------------------------------------------------------------------- # Jail config file models (Task 4a) # --------------------------------------------------------------------------- class JailConfigFile(BanGuiBaseModel): """Metadata for a single jail configuration file in ``jail.d/``.""" name: str = Field(..., description="Jail name (file stem, e.g. ``sshd``).") filename: str = Field(..., description="Actual filename (e.g. ``sshd.conf``).") enabled: bool = Field( ..., description=( "Whether the jail is enabled. Derived from the ``enabled`` key " "inside the file; defaults to ``true`` when the key is absent." ), ) class JailConfigFilesResponse(BanGuiBaseModel): """Response for ``GET /api/config/jail-files``.""" files: list[JailConfigFile] = Field(default_factory=list) total: int = Field(..., ge=0) class JailConfigFileContent(BanGuiBaseModel): """Single jail config file with its raw content.""" name: str = Field(..., description="Jail name (file stem).") filename: str = Field(..., description="Actual filename.") enabled: bool = Field(..., description="Whether the jail is enabled.") content: str = Field(..., description="Raw file content.") class JailConfigFileEnabledUpdate(BanGuiBaseModel): """Payload for ``PUT /api/config/jail-files/{filename}/enabled``.""" enabled: bool = Field(..., description="New enabled state for this jail.") # --------------------------------------------------------------------------- # Generic conf-file entry (shared by filter.d and action.d) # --------------------------------------------------------------------------- class ConfFileEntry(BanGuiBaseModel): """Metadata for a single ``.conf`` or ``.local`` file.""" name: str = Field(..., description="Base name without extension (e.g. ``sshd``).") filename: str = Field(..., description="Actual filename (e.g. ``sshd.conf``).") class ConfFilesResponse(BanGuiBaseModel): """Response for list endpoints (``GET /api/config/filters`` and ``GET /api/config/actions``).""" files: list[ConfFileEntry] = Field(default_factory=list) total: int = Field(..., ge=0) class ConfFileContent(BanGuiBaseModel): """A conf file with its raw text content.""" name: str = Field(..., description="Base name without extension.") filename: str = Field(..., description="Actual filename.") content: str = Field(..., description="Raw file content.") class ConfFileUpdateRequest(BanGuiBaseModel): """Payload for ``PUT /api/config/filters/{name}`` and ``PUT /api/config/actions/{name}``.""" content: str = Field(..., description="New raw file content (must not exceed 512 KB).") class ConfFileCreateRequest(BanGuiBaseModel): """Payload for ``POST /api/config/filters`` and ``POST /api/config/actions``.""" name: str = Field( ..., description="New file base name (without extension). Must contain only " "alphanumeric characters, hyphens, underscores, and dots.", ) content: str = Field(..., description="Initial raw file content (must not exceed 512 KB).") @field_validator("name", mode="after") @classmethod def _reject_reserved_jail_name(cls, v: str) -> str: """Reject fail2ban reserved jail names.""" if v in FAIL2BAN_RESERVED_JAIL_NAMES: valid_names = ", ".join(sorted(FAIL2BAN_RESERVED_JAIL_NAMES)) raise ValueError( f"Jail name {v!r} is reserved by fail2ban ({valid_names})." ) return v