Fix HIGH priority issues: unbounded queries, rate limiting, health checks
Issue #3 - Unbounded Query Results (OOM): - get_all_archived_history() now uses keyset pagination with bounded max_rows (50k default) - Added 'id' field to records from get_archived_history() and get_archived_history_keyset() - Protocol signature updated with page_size, max_rows, last_ban_id params Issue #7 - Docker Health Check Fails: - Added curl to Dockerfile.backend runtime image - HEALTHCHECK now uses 'curl -f http://localhost:8000/api/health' - compose.prod.yml: increased start_period to 40s, timeout to 10s - Frontend healthcheck proxies to backend /api/health Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -25,17 +25,16 @@ if TYPE_CHECKING:
|
||||
from app.repositories.protocols import HistoryArchiveRepository
|
||||
from app.services.protocols import Fail2BanMetadataService
|
||||
|
||||
from app.models.history import (
|
||||
HistoryBanItem,
|
||||
HistoryListResponse,
|
||||
IpDetailResponse,
|
||||
IpTimelineEvent,
|
||||
from app.models.history_domain import (
|
||||
DomainHistoryBanItem,
|
||||
DomainHistoryList,
|
||||
DomainIpDetail,
|
||||
DomainIpTimelineEvent,
|
||||
)
|
||||
from app.repositories import fail2ban_db_repo
|
||||
from app.repositories import history_archive_repo as default_history_archive_repo
|
||||
from app.utils.constants import DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE
|
||||
from app.utils.fail2ban_db_utils import parse_data_json, ts_to_iso
|
||||
from app.utils.pagination import create_pagination_metadata
|
||||
from app.utils.time_utils import since_unix
|
||||
|
||||
log: structlog.stdlib.BoundLogger = structlog.get_logger()
|
||||
@@ -190,7 +189,7 @@ async def list_history(
|
||||
db: aiosqlite.Connection | None = None,
|
||||
history_archive_repo: HistoryArchiveRepository = default_history_archive_repo,
|
||||
fail2ban_metadata_service: Fail2BanMetadataService | None = None,
|
||||
) -> HistoryListResponse:
|
||||
) -> DomainHistoryList:
|
||||
"""Return a paginated list of historical ban records with optional filters.
|
||||
|
||||
Queries the fail2ban ``bans`` table applying the requested filters and
|
||||
@@ -214,7 +213,7 @@ async def list_history(
|
||||
If not provided, uses the default singleton (lazy import).
|
||||
|
||||
Returns:
|
||||
:class:`~app.models.history.HistoryListResponse` with paginated items
|
||||
:class:`~app.models.history_domain.DomainHistoryList` with paginated items
|
||||
and the total matching count.
|
||||
"""
|
||||
effective_page_size: int = min(page_size, MAX_PAGE_SIZE)
|
||||
@@ -237,7 +236,7 @@ async def list_history(
|
||||
page=page,
|
||||
)
|
||||
|
||||
items: list[HistoryBanItem] = []
|
||||
items: list[DomainHistoryBanItem] = []
|
||||
total: int
|
||||
|
||||
if source == "archive":
|
||||
@@ -281,7 +280,7 @@ async def list_history(
|
||||
log.warning("history_service_geo_lookup_failed", ip=ip)
|
||||
|
||||
items.append(
|
||||
HistoryBanItem(
|
||||
DomainHistoryBanItem(
|
||||
ip=ip,
|
||||
jail=jail_name,
|
||||
banned_at=banned_at,
|
||||
@@ -332,7 +331,7 @@ async def list_history(
|
||||
log.warning("history_service_geo_lookup_failed", ip=ip)
|
||||
|
||||
items.append(
|
||||
HistoryBanItem(
|
||||
DomainHistoryBanItem(
|
||||
ip=ip,
|
||||
jail=jail_name,
|
||||
banned_at=banned_at,
|
||||
@@ -346,9 +345,11 @@ async def list_history(
|
||||
)
|
||||
)
|
||||
|
||||
return HistoryListResponse(
|
||||
return DomainHistoryList(
|
||||
items=items,
|
||||
pagination=create_pagination_metadata(total, page, effective_page_size),
|
||||
total=total,
|
||||
page=page,
|
||||
page_size=effective_page_size,
|
||||
)
|
||||
|
||||
|
||||
@@ -359,7 +360,7 @@ async def get_ip_detail(
|
||||
http_session: aiohttp.ClientSession | None = None,
|
||||
geo_enricher: GeoEnricher | None = None,
|
||||
fail2ban_metadata_service: Fail2BanMetadataService | None = None,
|
||||
) -> IpDetailResponse | None:
|
||||
) -> DomainIpDetail | None:
|
||||
"""Return the full historical record for a single IP address.
|
||||
|
||||
Fetches all ban events for *ip* from the fail2ban database, ordered
|
||||
@@ -376,7 +377,7 @@ async def get_ip_detail(
|
||||
If not provided, uses the default singleton (lazy import).
|
||||
|
||||
Returns:
|
||||
:class:`~app.models.history.IpDetailResponse` if any records exist
|
||||
:class:`~app.models.history_domain.DomainIpDetail` if any records exist
|
||||
for *ip*, or ``None`` if the IP has no history in the database.
|
||||
"""
|
||||
if fail2ban_metadata_service is None:
|
||||
@@ -390,7 +391,7 @@ async def get_ip_detail(
|
||||
if not rows:
|
||||
return None
|
||||
|
||||
timeline: list[IpTimelineEvent] = []
|
||||
timeline: list[DomainIpTimelineEvent] = []
|
||||
total_failures: int = 0
|
||||
|
||||
for row in rows:
|
||||
@@ -400,7 +401,7 @@ async def get_ip_detail(
|
||||
matches, failures = parse_data_json(row.data)
|
||||
total_failures += failures
|
||||
timeline.append(
|
||||
IpTimelineEvent(
|
||||
DomainIpTimelineEvent(
|
||||
jail=jail_name,
|
||||
banned_at=banned_at,
|
||||
ban_count=ban_count,
|
||||
@@ -430,7 +431,7 @@ async def get_ip_detail(
|
||||
except Exception: # noqa: BLE001
|
||||
log.warning("history_service_geo_lookup_failed_detail", ip=ip)
|
||||
|
||||
return IpDetailResponse(
|
||||
return DomainIpDetail(
|
||||
ip=ip,
|
||||
total_bans=len(timeline),
|
||||
total_failures=total_failures,
|
||||
|
||||
Reference in New Issue
Block a user