diff --git a/Docs/Tasks.md b/Docs/Tasks.md index 52693a9..00561bd 100644 --- a/Docs/Tasks.md +++ b/Docs/Tasks.md @@ -1,59 +1,3 @@ -### Issue #67: LOW - Default Page Size Inconsistently Applied Across Routers - -**Where found**: -- `backend/app/routers/history.py:80-84` – uses `DEFAULT_PAGE_SIZE` constant -- Multiple other routers – may hardcode page size values - -**Why this is needed**: -Endpoints with different default page sizes create an inconsistent API experience and make it hard to reason about server load. A client that does not pass `page_size` gets different result counts from different endpoints. - -**Goal**: -All paginated endpoints use the same default page size driven by a single constant. - -**What to do**: -1. Audit all `page_size` Query parameters across routers. -2. Replace all hardcoded defaults with `DEFAULT_PAGE_SIZE` from `constants.py`. -3. Add a linting check or unit test that asserts no hardcoded page size defaults exist in routers. - -**Possible traps and issues**: -- Some endpoints may intentionally use a different page size for performance reasons; document exceptions explicitly. - -**Docs changes needed**: -- API reference: document the default page size and how to override it. - -**Doc references**: -- `backend/app/utils/constants.py` – `DEFAULT_PAGE_SIZE` - ---- - -### Issue #68: LOW - No Reserved Keyword Validation for Jail Names - -**Where found**: -- `backend/app/models/jail.py` – jail name validated against alphanumeric regex only -- `backend/app/routers/jail_config.py` - -**Why this is needed**: -Fail2ban uses reserved jail names and command keywords (e.g., `all`, `status`, `purge`). A user-created jail with a reserved name could shadow fail2ban built-in commands or produce confusing behavior when management commands are issued. - -**Goal**: -Reject jail names that conflict with fail2ban reserved words at model validation time. - -**What to do**: -1. Define a `FAIL2BAN_RESERVED_JAIL_NAMES` set in `constants.py`. -2. Add a Pydantic validator on the jail name field that rejects reserved words. -3. Return a 422 with a descriptive error message. - -**Possible traps and issues**: -- The reserved word list may change across fail2ban versions; source it from fail2ban documentation and version-gate if necessary. - -**Docs changes needed**: -- API reference: document the list of reserved jail names. - -**Doc references**: -- Fail2ban documentation on reserved jail identifiers - ---- - ### Issue #69: LOW - Jail Names Echoed in Error Messages Without Sanitization **Where found**: diff --git a/backend/app/models/file_config.py b/backend/app/models/file_config.py index 2cb87b4..eaee729 100644 --- a/backend/app/models/file_config.py +++ b/backend/app/models/file_config.py @@ -4,10 +4,10 @@ Covers jail config files (``jail.d/``), filter definitions (``filter.d/``), and action definitions (``action.d/``). """ -from pydantic import Field +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) @@ -82,3 +82,15 @@ class ConfFileCreateRequest(BanGuiBaseModel): "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 diff --git a/backend/app/utils/constants.py b/backend/app/utils/constants.py index a6366f2..8d9e968 100644 --- a/backend/app/utils/constants.py +++ b/backend/app/utils/constants.py @@ -156,3 +156,26 @@ RATE_LIMIT_JAIL_ACTIVATE_REQUESTS: Final[int] = 100 RATE_LIMIT_JAIL_DEACTIVATE_REQUESTS: Final[int] = 100 """Max jail deactivation requests per IP per minute.""" + +# --------------------------------------------------------------------------- +# Jail configuration +# --------------------------------------------------------------------------- + +FAIL2BAN_RESERVED_JAIL_NAMES: Final[frozenset[str]] = frozenset( + { + "all", + "status", + "purge", + "start", + "stop", + "reload", + "restart", + "ban", + "unban", + "add", + "del", + "set", + "get", + } +) +"""fail2ban reserved jail names. Users cannot create jails with these names.""" diff --git a/backend/tests/test_models.py b/backend/tests/test_models.py index 43567e9..39a122f 100644 --- a/backend/tests/test_models.py +++ b/backend/tests/test_models.py @@ -347,3 +347,38 @@ def test_ban_country_empty_string_coerced_to_none() -> None: country="", ) assert ban.country is None + + +# --------------------------------------------------------------------------- +# ConfFileCreateRequest reserved jail name validation +# --------------------------------------------------------------------------- + + +def test_conffile_create_request_rejects_reserved_jail_name() -> None: + """ConfFileCreateRequest rejects fail2ban reserved jail names.""" + from app.models.file_config import ConfFileCreateRequest + + for reserved in ["all", "status", "purge", "start", "stop", "reload", "restart"]: + with pytest.raises(ValidationError) as exc_info: + ConfFileCreateRequest(name=reserved, content="[my jail]") + assert "reserved" in str(exc_info.value).lower() + + +def test_conffile_create_request_accepts_valid_jail_name() -> None: + """ConfFileCreateRequest accepts valid non-reserved jail names.""" + from app.models.file_config import ConfFileCreateRequest + + req = ConfFileCreateRequest(name="sshd", content="[my jail]") + assert req.name == "sshd" + + req = ConfFileCreateRequest(name="nginx-http-auth", content="[my jail]") + assert req.name == "nginx-http-auth" + + +def test_conffile_create_request_rejects_ban_and_unban() -> None: + """ConfFileCreateRequest rejects 'ban' and 'unban' as jail names.""" + from app.models.file_config import ConfFileCreateRequest + + for name in ["ban", "unban", "add", "del", "set", "get"]: + with pytest.raises(ValidationError): + ConfFileCreateRequest(name=name, content="[my jail]")